diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js index b5c1801..6272f8c 100644 --- a/www/html/Admin/admin.js +++ b/www/html/Admin/admin.js @@ -2022,13 +2022,16 @@ var QB_LABELS_ABC = ['A', 'B', 'C']; var QUIZ_BATTLE_CATEGORIES = [ - { id: 'cybercrime', labelTh: 'อาชญากรรมออนไลน์', labelEn: 'Cybercrime' }, - { id: 'cyber_law', labelTh: 'กฎหมายไซเบอร์', labelEn: 'Cyber law' }, - { id: 'privacy', labelTh: 'ความเป็นส่วนตัวและข้อมูลส่วนบุคคล', labelEn: 'Privacy & PDPA' }, - { id: 'digital_security', labelTh: 'ความปลอดภัยดิจิทัล', labelEn: 'Digital security' }, - { id: 'social_media', labelTh: 'สื่อสังคมออนไลน์', labelEn: 'Social media' }, - { id: 'ethics', labelTh: 'จริยธรรมดิจิทัล', labelEn: 'Digital ethics' }, - { id: 'ai_data', labelTh: 'AI และข้อมูล', labelEn: 'AI & data' }, + { id: 'topic1', labelTh: 'ห้อง 1 — กฎหมายใกล้ตัว', labelEn: 'Room 1' }, + { id: 'topic2', labelTh: 'ห้อง 2 — กฎหมายสิทธิ์พื้นฐาน', labelEn: 'Room 2' }, + { id: 'topic3', labelTh: 'ห้อง 3 — กฎหมายจราจร', labelEn: 'Room 3' }, + { id: 'topic4', labelTh: 'ห้อง 4 — คดีเกี่ยวกับทรัพย์', labelEn: 'Room 4' }, + { id: 'topic5', labelTh: 'ห้อง 5 — คดีหมิ่นประมาท', labelEn: 'Room 5' }, + { id: 'topic6', labelTh: 'ห้อง 6 — คดีทางเพศ', labelEn: 'Room 6' }, + { id: 'topic7', labelTh: 'ห้อง 7 — คดีอาชญากรรม', labelEn: 'Room 7' }, + { id: 'topic8', labelTh: 'ห้อง 8 — อาชญากรรมออนไลน์', labelEn: 'Room 8' }, + { id: 'topic9', labelTh: 'ห้อง 9 — กระบวนการยุติธรรม', labelEn: 'Room 9' }, + { id: 'topic10', labelTh: 'ห้อง 10 — งานบริการกระทรวงยุติธรรม', labelEn: 'Room 10' }, ]; function qbBattleCategoryLabel(catId) { diff --git a/www/html/Admin/admin.js.bak-20260607-114128 b/www/html/Admin/admin.js.bak-20260607-114128 new file mode 100644 index 0000000..b5c1801 --- /dev/null +++ b/www/html/Admin/admin.js.bak-20260607-114128 @@ -0,0 +1,4915 @@ +(function () { + 'use strict'; + + var API = '/Admin/api/'; + /** ผ่าน PHP Admin เพื่อให้ PUT ถึง Node แม้ nginx ไม่ proxy /Game/api/ ไปยัง Node */ + var GAME_QUIZ_API = '/Admin/api/game-quiz-settings.php'; + var GAME_TIMING_API = '/Game/api/game-timing'; + var GAME_GAUNTLET_ASSETS_API = '/Game/api/gauntlet-assets'; + /** ลำดับอัปโหลดจริงอยู่ที่ quizCarryPlaqueUploadWithFallback — Node ก่อน แล้วค่อย PHP */ + var GAME_QUIZ_CARRY_PLAQUE_UPLOAD_NODE = '/Game/api/quiz-carry-plaque-upload'; + var GAME_QUIZ_CARRY_PLAQUE_UPLOAD_PHP = '/Admin/api/game-quiz-carry-plaque-upload.php'; + /** >0 ระหว่างอัปโหลดรูปป้าย — ห้ามบันทึกก่อนจบไม่งั้น plaqueImageUrl ว่างถูกเขียนลง JSON */ + var quizCarryPlaqueUploadPending = 0; + /** epoch ms เมื่อเริ่มอัปโหลดล่าสุด — กันสถานะค้างบล็อกบันทึกตลอด */ + var quizCarryPlaqueUploadStartMs = 0; + var quizCarrySaveInFlight = false; + /** ซิงก์ range ↔ ตัวเลข carryChoicePlaqueMapScale (wire ครั้งเดียว) */ + var quizCarryPlaqueScaleControlsWired = false; + /** สำรอง URL หลังอัปโหลดสำเร็จต่อช่อง — กัน race ที่ช่อง text ว่างตอน buildForSave แต่รูปอัปโหลดแล้วจริง */ + var quizCarryPlaquePendingUrlBySlot = {}; + var stackGameSaveInFlight = false; + var megaVirusSaveInFlight = false; + /** โหมดแทนที่รูป: ชื่อไฟล์เป้าหมายก่อนเปิด file picker */ + var gauntletReplaceTarget = null; + var cred = { credentials: 'include' }; + + function el(id) { return document.getElementById(id); } + + function api(path, opts) { + opts = opts || {}; + opts.credentials = 'include'; + if (!opts.headers) opts.headers = {}; + if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) { + opts.headers['Content-Type'] = 'application/json'; + opts.body = JSON.stringify(opts.body); + } + return fetch(API + path, opts).then(function (r) { + return r.text().then(function (text) { + var j = {}; + try { + j = text ? JSON.parse(text) : {}; + } catch (e) { + if (!r.ok) throw new Error(text || r.statusText); + throw e; + } + if (!r.ok) throw new Error(j.error || r.statusText || 'Request failed'); + return j; + }); + }); + } + + function setMsg(id, text, cls) { + var n = el(id); + if (!n) return; + n.textContent = text || ''; + n.className = 'msg' + (cls ? ' ' + cls : ''); + } + + function gameQuizFetch(method, body) { + var m = method || 'GET'; + var url = GAME_QUIZ_API; + if (m === 'GET' && body == null) { + url += (GAME_QUIZ_API.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now(); + } + var opts = { method: m, credentials: 'include', cache: 'no-store' }; + if (body != null) { + opts.headers = { 'Content-Type': 'application/json' }; + opts.body = JSON.stringify(body); + } + return fetch(url, opts).then(function (r) { + return r.text().then(function (text) { + var j = {}; + try { + j = text ? JSON.parse(text) : {}; + } catch (e) { + if (!r.ok) { + throw new Error((text || '').slice(0, 280) || r.statusText || 'Request failed'); + } + throw new Error('คำตอบไม่ใช่ JSON (HTTP ' + r.status + ') — ตรวจ nginx / PHP proxy'); + } + if (!r.ok) { + throw new Error(j.error || ('HTTP ' + r.status + ' ' + r.statusText)); + } + return j; + }); + }); + } + + function gameTimingFetch(method, body) { + var m = method || 'GET'; + var url = GAME_TIMING_API; + if (m === 'GET' && body == null) { + url += (GAME_TIMING_API.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now(); + } + var opts = { method: m, credentials: 'include', cache: 'no-store' }; + if (body != null) { + opts.headers = { 'Content-Type': 'application/json' }; + opts.body = JSON.stringify(body); + } + return fetch(url, opts).then(function (r) { + return r.text().then(function (text) { + var j = {}; + try { + j = text ? JSON.parse(text) : {}; + } catch (e) { + if (!r.ok) { + var plain = (text || r.statusText || 'Request failed').trim(); + if (r.status === 404) { + plain += ' — รีสตาร์ท Node ที่รัน Game/server.js (pm2 / systemd) หลัง deploy · Restart Game Node after deploy'; + } + throw new Error(plain); + } + throw e; + } + if (!r.ok) { + var base = j.error || r.statusText || 'Request failed'; + if (r.status === 404) { + base += ' — รีสตาร์ท Node ที่รัน Game/server.js (pm2 / systemd) หลัง deploy · Restart Game Node after deploy'; + } + throw new Error(base); + } + return j; + }); + }); + } + + function addQuizAdminRow(text, answerTrue) { + var list = el('quiz-admin-questions-list'); + if (!list) return; + var row = document.createElement('div'); + row.className = 'quiz-admin-q-row'; + + var lab1 = document.createElement('label'); + lab1.className = 'quiz-admin-q-label'; + lab1.appendChild(document.createTextNode('คำถาม ')); + var inp = document.createElement('input'); + inp.type = 'text'; + inp.className = 'quiz-admin-q-text'; + inp.placeholder = 'พิมพ์คำถาม...'; + inp.maxLength = 500; + inp.value = text || ''; + lab1.appendChild(inp); + + var lab2 = document.createElement('label'); + lab2.className = 'quiz-admin-ans-label'; + lab2.appendChild(document.createTextNode('คำตอบถูก ')); + var sel = document.createElement('select'); + sel.className = 'quiz-admin-q-ans'; + [['true', 'จริง (ถูก)'], ['false', 'เท็จ (ผิด)']].forEach(function (pair) { + var o = document.createElement('option'); + o.value = pair[0]; + o.textContent = pair[1]; + var isTrue = answerTrue !== false; + if (pair[0] === 'true' ? isTrue : !isTrue) o.selected = true; + sel.appendChild(o); + }); + lab2.appendChild(sel); + + var rm = document.createElement('button'); + rm.type = 'button'; + rm.className = 'btn btn-danger btn-sm quiz-admin-q-remove'; + rm.setAttribute('aria-label', 'ลบข้อนี้'); + rm.textContent = 'ลบ'; + rm.addEventListener('click', function () { + row.remove(); + if (!list.querySelector('.quiz-admin-q-row')) addQuizAdminRow('', true); + }); + + row.appendChild(lab1); + row.appendChild(lab2); + row.appendChild(rm); + list.appendChild(row); + } + + function renderQuizAdminQuestions(arr) { + var list = el('quiz-admin-questions-list'); + if (!list) return; + list.innerHTML = ''; + var rows = (arr && arr.length) ? arr : [{ text: '', answerTrue: true }]; + rows.forEach(function (q) { + addQuizAdminRow(q.text || '', q.answerTrue !== false); + }); + } + + function loadQuizSettingsPanel() { + gameQuizFetch('GET').then(function (data) { + el('quiz-read-sec').value = String(Math.round((data.readMs || 10000) / 1000)); + el('quiz-answer-sec').value = String(Math.round((data.answerMs || 5000) / 1000)); + var bMs = data.betweenMs != null ? data.betweenMs : 3500; + el('quiz-between-sec').value = String(Math.max(0, Math.round(bMs / 1000))); + var rqIn = el('quiz-round-q-count'); + if (rqIn) { + var rq = data.quizRoundQuestionCount != null ? parseInt(data.quizRoundQuestionCount, 10) : 10; + if (Number.isNaN(rq) || rq < 1) rq = 10; + rq = Math.min(50, rq); + rqIn.value = String(rq); + } + renderQuizAdminQuestions(data.questions); + if (data.quizMapPanelTheme && typeof data.quizMapPanelTheme === 'object') { + quizTfFillFormFromTheme(data.quizMapPanelTheme); + } else { + quizTfFillFormFromTheme({}); + } + setMsg('quiz-settings-msg', '', ''); + }).catch(function (e) { + setMsg('quiz-settings-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + } + + function saveQuizSettingsPanel() { + var readS = parseInt(el('quiz-read-sec').value, 10); + var ansS = parseInt(el('quiz-answer-sec').value, 10); + var betS = parseInt(el('quiz-between-sec').value, 10); + if (Number.isNaN(readS) || readS < 1) readS = 10; + if (Number.isNaN(ansS) || ansS < 1) ansS = 5; + if (Number.isNaN(betS) || betS < 0) betS = 3; + readS = Math.min(300, readS); + ansS = Math.min(300, ansS); + betS = Math.min(120, betS); + + var roundQEl = el('quiz-round-q-count'); + var roundQ = roundQEl ? parseInt(roundQEl.value, 10) : 10; + if (Number.isNaN(roundQ) || roundQ < 1) roundQ = 10; + roundQ = Math.min(50, roundQ); + + var questions = []; + document.querySelectorAll('#quiz-admin-questions-list .quiz-admin-q-row').forEach(function (row) { + var inp = row.querySelector('.quiz-admin-q-text'); + var sel = row.querySelector('.quiz-admin-q-ans'); + var t = inp && inp.value ? String(inp.value).trim() : ''; + if (!t) return; + questions.push({ text: t, answerTrue: !!(sel && sel.value === 'true') }); + }); + + gameQuizFetch('PUT', { + readMs: readS * 1000, + answerMs: ansS * 1000, + betweenMs: betS * 1000, + quizRoundQuestionCount: roundQ, + questions: questions, + quizMapPanelTheme: quizTfBuildThemeForSave(), + }).then(function () { + setMsg('quiz-settings-msg', 'บันทึกแล้ว', 'ok'); + }).catch(function (e) { + setMsg('quiz-settings-msg', e.message || 'บันทึกไม่ได้', 'error'); + }); + } + + /* ===== ชุดคำถามพิเศษ (Level×Case = 15 ชุด) รับการ์ดพิเศษ ===== */ + var SPECIAL_CARD_RARITIES = ['common', 'rare', 'legendary']; + var SPECIAL_CARD_ASSET_BASE = '/Game/img/special-cards/'; + var SPECIAL_CARD_CATALOG = [ + { id: 1, en: 'Time Dilation', th: 'เพิ่มเวลาเล่นมินิเกมทันที (+10 วินาที)' }, + { id: 2, en: 'Extension', th: 'เพิ่มเวลาในห้องพิจารณาคดี (+10 วิ FINAL VOTE)' }, + { id: 3, en: 'Ban Card', th: 'ห้ามเล่นมินิเกม 1 ตา' }, + { id: 4, en: 'Silence Card', th: 'การ์ดปิดปาก' }, + { id: 5, en: 'Bail Coin', th: 'เหรียญประกันตัว' }, + { id: 6, en: 'Fund', th: 'รับ coin เพิ่ม' }, + { id: 7, en: 'Free Evidence', th: 'รับหลักฐานฟรี' }, + ]; + var specialQuizSetsState = {}; + var specialQuizCurrentKey = 'L1_C1'; + var specialCardPickerBuilt = false; + + function specialCardTxtFilename(id) { + return id === 7 ? 'card--7txt.png' : 'card-' + id + '-txt.png'; + } + + function specialCardImageUrl(id) { + return SPECIAL_CARD_ASSET_BASE + 'card-' + id + '.png'; + } + + function specialCardTxtImageUrl(id) { + return SPECIAL_CARD_ASSET_BASE + specialCardTxtFilename(id); + } + + function specialCardCatalogEntry(id) { + for (var i = 0; i < SPECIAL_CARD_CATALOG.length; i++) { + if (SPECIAL_CARD_CATALOG[i].id === id) return SPECIAL_CARD_CATALOG[i]; + } + return null; + } + + function resolveSpecialCardIdFromSaved(card) { + if (!card || typeof card !== 'object') return null; + var cid = Number(card.cardId); + if (cid >= 1 && cid <= 7) return cid; + var url = String(card.imageUrl || ''); + var m = url.match(/\/card-(\d)\.png/i); + return m ? Number(m[1]) : null; + } + + function updateSpecialCardPreview() { + var prev = el('special-card-preview'); + var img = el('special-card-preview-img'); + var cid = parseInt(el('special-card-id') && el('special-card-id').value, 10); + if (!prev || !img) return; + if (!(cid >= 1 && cid <= 7)) { + prev.hidden = true; + img.removeAttribute('src'); + return; + } + img.src = specialCardImageUrl(cid); + img.alt = (specialCardCatalogEntry(cid) || {}).en || ('Card ' + cid); + prev.hidden = false; + } + + function setSpecialCardPickerSelection(cardId) { + var picker = el('special-card-picker'); + if (!picker) return; + picker.querySelectorAll('.sq-card-pick').forEach(function (btn) { + var on = String(btn.getAttribute('data-card-id')) === String(cardId); + btn.classList.toggle('is-selected', on); + btn.setAttribute('aria-checked', on ? 'true' : 'false'); + }); + } + + function selectSpecialCard(cardId, fillMeta) { + var idInp = el('special-card-id'); + var imgInp = el('special-card-image'); + var txtInp = el('special-card-txt-image'); + if (!(cardId >= 1 && cardId <= 7)) { + if (idInp) idInp.value = ''; + if (imgInp) imgInp.value = ''; + if (txtInp) txtInp.value = ''; + setSpecialCardPickerSelection(null); + updateSpecialCardPreview(); + return; + } + var entry = specialCardCatalogEntry(cardId); + if (idInp) idInp.value = String(cardId); + if (imgInp) imgInp.value = specialCardImageUrl(cardId); + if (txtInp) txtInp.value = specialCardTxtImageUrl(cardId); + if (fillMeta && entry) { + if (el('special-card-th')) el('special-card-th').value = entry.th; + if (el('special-card-en')) el('special-card-en').value = entry.en; + } + setSpecialCardPickerSelection(cardId); + updateSpecialCardPreview(); + } + + function ensureSpecialCardPicker() { + if (specialCardPickerBuilt) return; + var picker = el('special-card-picker'); + if (!picker) return; + picker.innerHTML = ''; + SPECIAL_CARD_CATALOG.forEach(function (entry) { + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'sq-card-pick'; + btn.setAttribute('role', 'radio'); + btn.setAttribute('aria-checked', 'false'); + btn.setAttribute('data-card-id', String(entry.id)); + btn.title = entry.en + ' — ' + entry.th; + var img = document.createElement('img'); + img.src = specialCardImageUrl(entry.id); + img.alt = entry.en; + img.width = 72; + img.height = 96; + img.decoding = 'async'; + img.loading = 'lazy'; + var cap = document.createElement('span'); + cap.className = 'sq-card-pick-cap'; + cap.textContent = entry.en; + btn.appendChild(img); + btn.appendChild(cap); + btn.addEventListener('click', function () { + selectSpecialCard(entry.id, true); + }); + picker.appendChild(btn); + }); + specialCardPickerBuilt = true; + } + + function specialQuizSelectorKey() { + var L = el('special-quiz-level'); + var C = el('special-quiz-case'); + return 'L' + ((L && L.value) || '1') + '_C' + ((C && C.value) || '1'); + } + + function updateSpecialQuizCount() { + var c = el('special-quiz-count'); + if (!c) return; + var n = document.querySelectorAll('#special-quiz-questions-list .sq-mcq-row').length; + c.textContent = '(' + n + ' ข้อ)'; + } + + var SPECIAL_QUIZ_CHOICE_COUNT = 4; + var SPECIAL_QUIZ_LETTERS = ['A', 'B', 'C', 'D', 'E', 'F']; + + /** คำถามตัวเลือก A/B/C/D — เลือกคำตอบถูก 1 ข้อ */ + function addSpecialQuizRow(q) { + q = q || {}; + var list = el('special-quiz-questions-list'); + if (!list) return; + var choices = Array.isArray(q.choices) ? q.choices : []; + var correctIndex = Number(q.correctIndex); + if (!(correctIndex >= 0 && correctIndex < SPECIAL_QUIZ_CHOICE_COUNT)) correctIndex = 0; + + var row = document.createElement('div'); + row.className = 'quiz-admin-q-row sq-mcq-row'; + + var qLab = document.createElement('label'); + qLab.className = 'sq-mcq-q'; + qLab.appendChild(document.createTextNode('คำถาม')); + var qInp = document.createElement('input'); + qInp.type = 'text'; + qInp.className = 'sq-mcq-text'; + qInp.maxLength = 500; + qInp.placeholder = 'พิมพ์คำถาม...'; + qInp.value = q.text || ''; + qLab.appendChild(qInp); + + var grid = document.createElement('div'); + grid.className = 'sq-mcq-choices'; + for (var i = 0; i < SPECIAL_QUIZ_CHOICE_COUNT; i++) { + var cl = document.createElement('label'); + cl.className = 'sq-mcq-choice'; + var badge = document.createElement('span'); + badge.className = 'sq-mcq-letter'; + badge.textContent = SPECIAL_QUIZ_LETTERS[i]; + var ci = document.createElement('input'); + ci.type = 'text'; + ci.className = 'sq-mcq-choice-input'; + ci.maxLength = 200; + ci.placeholder = 'ตัวเลือก ' + SPECIAL_QUIZ_LETTERS[i]; + ci.value = choices[i] || ''; + ci.setAttribute('data-choice-index', String(i)); + cl.appendChild(badge); + cl.appendChild(ci); + grid.appendChild(cl); + } + + var foot = document.createElement('div'); + foot.className = 'sq-mcq-foot'; + var ansLab = document.createElement('label'); + ansLab.className = 'sq-mcq-ans'; + ansLab.appendChild(document.createTextNode('คำตอบที่ถูก ')); + var ansSel = document.createElement('select'); + ansSel.className = 'sq-mcq-correct'; + for (var j = 0; j < SPECIAL_QUIZ_CHOICE_COUNT; j++) { + var o = document.createElement('option'); + o.value = String(j); + o.textContent = SPECIAL_QUIZ_LETTERS[j]; + if (j === correctIndex) o.selected = true; + ansSel.appendChild(o); + } + ansLab.appendChild(ansSel); + + var rm = document.createElement('button'); + rm.type = 'button'; + rm.className = 'btn btn-danger btn-sm quiz-admin-q-remove'; + rm.setAttribute('aria-label', 'ลบข้อนี้'); + rm.textContent = 'ลบ'; + rm.addEventListener('click', function () { + row.remove(); + updateSpecialQuizCount(); + }); + foot.appendChild(ansLab); + foot.appendChild(rm); + + row.appendChild(qLab); + row.appendChild(grid); + row.appendChild(foot); + list.appendChild(row); + } + + function renderSpecialQuizSet(key) { + ensureSpecialCardPicker(); + var set = specialQuizSetsState[key] || { questions: [], specialCard: {} }; + var card = (set.specialCard && typeof set.specialCard === 'object') ? set.specialCard : {}; + var cardId = resolveSpecialCardIdFromSaved(card); + selectSpecialCard(cardId, false); + if (el('special-card-th')) el('special-card-th').value = card.th || (cardId ? (specialCardCatalogEntry(cardId) || {}).th : '') || ''; + if (el('special-card-en')) el('special-card-en').value = card.en || (cardId ? (specialCardCatalogEntry(cardId) || {}).en : '') || ''; + if (el('special-card-rarity')) { + el('special-card-rarity').value = SPECIAL_CARD_RARITIES.indexOf(card.rarity) >= 0 ? card.rarity : 'rare'; + } + if (cardId && el('special-card-image')) el('special-card-image').value = card.imageUrl || specialCardImageUrl(cardId); + if (cardId && el('special-card-txt-image')) el('special-card-txt-image').value = card.txtImageUrl || specialCardTxtImageUrl(cardId); + var list = el('special-quiz-questions-list'); + if (list) { + list.innerHTML = ''; + var qs = (set.questions && set.questions.length) + ? set.questions + : [{ type: 'mcq', text: '', choices: ['', '', '', ''], correctIndex: 0 }]; + qs.forEach(addSpecialQuizRow); + } + var tag = el('special-quiz-set-tag'); + if (tag) tag.textContent = 'ชุดที่แก้: ' + key; + updateSpecialQuizCount(); + } + + function captureSpecialQuizForm(key) { + var questions = []; + document.querySelectorAll('#special-quiz-questions-list .sq-mcq-row').forEach(function (row) { + var inp = row.querySelector('.sq-mcq-text'); + var t = inp && inp.value ? String(inp.value).trim() : ''; + if (!t) return; + var choices = []; + row.querySelectorAll('.sq-mcq-choice-input').forEach(function (ci) { + choices.push(String(ci.value || '').trim()); + }); + if (choices.filter(function (c) { return c; }).length < 2) return; + var sel = row.querySelector('.sq-mcq-correct'); + var correctIndex = sel ? parseInt(sel.value, 10) : 0; + if (!(correctIndex >= 0 && correctIndex < choices.length) || !choices[correctIndex]) { + correctIndex = choices.findIndex(function (c) { return c; }); + if (correctIndex < 0) correctIndex = 0; + } + questions.push({ type: 'mcq', text: t, choices: choices, correctIndex: correctIndex }); + }); + var rar = el('special-card-rarity') ? el('special-card-rarity').value : 'rare'; + var cardId = parseInt(el('special-card-id') && el('special-card-id').value, 10); + if (!(cardId >= 1 && cardId <= 7)) cardId = null; + specialQuizSetsState[key] = { + questions: questions, + specialCard: { + cardId: cardId, + th: el('special-card-th') ? String(el('special-card-th').value || '').trim() : '', + en: el('special-card-en') ? String(el('special-card-en').value || '').trim() : '', + rarity: SPECIAL_CARD_RARITIES.indexOf(rar) >= 0 ? rar : 'rare', + imageUrl: el('special-card-image') ? String(el('special-card-image').value || '').trim() : '', + txtImageUrl: el('special-card-txt-image') ? String(el('special-card-txt-image').value || '').trim() : '', + }, + }; + } + + function onSpecialQuizSelectorChange() { + captureSpecialQuizForm(specialQuizCurrentKey); + specialQuizCurrentKey = specialQuizSelectorKey(); + renderSpecialQuizSet(specialQuizCurrentKey); + } + + function loadSpecialQuizPanel() { + setMsg('special-quiz-msg', 'กำลังโหลด...', ''); + gameQuizFetch('GET').then(function (data) { + specialQuizSetsState = (data.specialQuizSets && typeof data.specialQuizSets === 'object') ? data.specialQuizSets : {}; + specialQuizCurrentKey = specialQuizSelectorKey(); + renderSpecialQuizSet(specialQuizCurrentKey); + setMsg('special-quiz-msg', '', ''); + }).catch(function (e) { + setMsg('special-quiz-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + } + + function saveSpecialQuizPanel() { + captureSpecialQuizForm(specialQuizCurrentKey); + setMsg('special-quiz-msg', 'กำลังบันทึก...', ''); + gameQuizFetch('PUT', { specialQuizSets: specialQuizSetsState }).then(function (res) { + if (res && res.specialQuizSets && typeof res.specialQuizSets === 'object') { + specialQuizSetsState = res.specialQuizSets; + } + setMsg('special-quiz-msg', 'บันทึกแล้ว (15 ชุด)', 'ok'); + renderSpecialQuizSet(specialQuizCurrentKey); + }).catch(function (e) { + setMsg('special-quiz-msg', e.message || 'บันทึกไม่ได้', 'error'); + }); + } + + function quizTfThemeHiddenId(kind) { + if (kind === 'bg') return 'quiz-tf-theme-bg'; + if (kind === 'border') return 'quiz-tf-theme-border'; + return 'quiz-tf-theme-text'; + } + + function quizTfRowPrefix(kind) { + return 'quiz-tf-theme-' + kind; + } + + function quizTfSetPickersFromColorString(kind, cssStr) { + var fb = QUIZ_CARRY_THEME_DEFAULTS[kind]; + var o = quizCarryParseCssColor(cssStr, fb); + var pref = quizTfRowPrefix(kind); + var sw = el(pref + '-swatch'); + var ra = el(pref + '-alpha'); + var out = el(pref + '-alpha-out'); + var hi = el(quizTfThemeHiddenId(kind)); + var pr = el(pref + '-preview'); + if (!sw || !ra || !hi) return; + sw.value = quizCarryHexFromRgb(o.r, o.g, o.b); + var pct = Math.max(0, Math.min(100, Math.round(Number(o.a) * 100))); + ra.value = String(pct); + if (out) out.textContent = pct + '%'; + hi.value = quizCarryRgbToRgbaString(o); + if (pr) pr.textContent = hi.value; + } + + function quizTfComposeFromPickers(kind) { + var pref = quizTfRowPrefix(kind); + var sw = el(pref + '-swatch'); + var ra = el(pref + '-alpha'); + if (!sw || !ra) return ''; + var pr = quizCarryParseHexToRgb(sw.value); + if (!pr) return ''; + var pct = parseInt(String(ra.value), 10); + if (!Number.isFinite(pct)) pct = 100; + pct = Math.max(0, Math.min(100, pct)); + return quizCarryRgbToRgbaString({ r: pr.r, g: pr.g, b: pr.b, a: pct / 100 }); + } + + function quizTfSyncRowOutput(kind) { + var s = quizTfComposeFromPickers(kind); + var hi = el(quizTfThemeHiddenId(kind)); + var pref = quizTfRowPrefix(kind); + var ra = el(pref + '-alpha'); + var out = el(pref + '-alpha-out'); + var pr = el(pref + '-preview'); + if (hi) hi.value = s; + if (out && ra) out.textContent = String(Math.max(0, Math.min(100, parseInt(ra.value, 10) || 0))) + '%'; + if (pr && hi) pr.textContent = hi.value; + } + + function quizTfBindThemePickersOnce() { + if (quizTfBindThemePickersOnce._done) return; + ['bg', 'border', 'text'].forEach(function (kind) { + var pref = quizTfRowPrefix(kind); + var sw = el(pref + '-swatch'); + var ra = el(pref + '-alpha'); + if (!sw || !ra) return; + if (ra.getAttribute('data-tf-bound') === '1') return; + ra.setAttribute('data-tf-bound', '1'); + function sync() { + quizTfSyncRowOutput(kind); + } + sw.addEventListener('input', sync); + sw.addEventListener('change', sync); + ra.addEventListener('input', sync); + ra.addEventListener('change', sync); + }); + quizTfBindThemePickersOnce._done = true; + } + + function quizTfBuildThemeForSave() { + quizTfBindThemePickersOnce(); + ['bg', 'border', 'text'].forEach(function (k) { + quizTfSyncRowOutput(k); + }); + var fbBg = QUIZ_CARRY_THEME_DEFAULTS.bg; + var fbBr = QUIZ_CARRY_THEME_DEFAULTS.border; + var fbTx = QUIZ_CARRY_THEME_DEFAULTS.text; + var sBg = quizTfComposeFromPickers('bg'); + var sBr = quizTfComposeFromPickers('border'); + var sTx = quizTfComposeFromPickers('text'); + var bwEl = el('quiz-tf-theme-border-w'); + var bw = bwEl ? parseInt(String(bwEl.value || '2'), 10) : 2; + if (!Number.isFinite(bw)) bw = 2; + bw = Math.max(0, Math.min(12, Math.round(bw))); + var qfMinEl = el('quiz-tf-theme-qfont-min'); + var qfMaxEl = el('quiz-tf-theme-qfont-max'); + var qMin = qfMinEl ? parseInt(String(qfMinEl.value || '10'), 10) : 10; + var qMax = qfMaxEl ? parseInt(String(qfMaxEl.value || '24'), 10) : 24; + if (!Number.isFinite(qMin)) qMin = 10; + if (!Number.isFinite(qMax)) qMax = 24; + qMin = Math.max(10, Math.min(40, Math.round(qMin))); + qMax = Math.max(14, Math.min(56, Math.round(qMax))); + if (qMax < qMin) { + var swap = qMin; + qMin = qMax; + qMax = swap; + } + return { + panelBg: (sBg && sBg.trim()) ? sBg.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBg), + panelBorder: (sBr && sBr.trim()) ? sBr.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBr), + textColor: (sTx && sTx.trim()) ? sTx.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbTx), + borderWidthPx: bw, + questionFontMinPx: qMin, + questionFontMaxPx: qMax, + }; + } + + function quizTfFillFormFromTheme(th) { + quizTfBindThemePickersOnce(); + var t = th && typeof th === 'object' ? th : {}; + quizTfSetPickersFromColorString('bg', t.panelBg); + quizTfSetPickersFromColorString('border', t.panelBorder); + quizTfSetPickersFromColorString('text', t.textColor); + var bwEl = el('quiz-tf-theme-border-w'); + var bw = parseInt(String(t.borderWidthPx), 10); + if (bwEl) bwEl.value = String(Number.isFinite(bw) && bw >= 0 ? Math.min(12, bw) : 2); + var qfMinEl = el('quiz-tf-theme-qfont-min'); + var qfMaxEl = el('quiz-tf-theme-qfont-max'); + var qMin = parseInt(String(t.questionFontMinPx), 10); + var qMax = parseInt(String(t.questionFontMaxPx), 10); + if (qfMinEl) qfMinEl.value = String(Number.isFinite(qMin) ? Math.max(10, Math.min(40, qMin)) : 10); + if (qfMaxEl) qfMaxEl.value = String(Number.isFinite(qMax) ? Math.max(14, Math.min(56, qMax)) : 24); + } + + var QUIZ_CARRY_MAX_SLOTS = 16; + + function rebuildQuizCarryCorrectSelect(row) { + var sel = row.querySelector('.quiz-carry-correct-sel'); + if (!sel) return; + var inputs = row.querySelectorAll('.quiz-carry-slot-inp'); + var prev = sel.value; + sel.innerHTML = ''; + var has = false; + for (var i = 0; i < inputs.length; i++) { + var t = inputs[i].value ? String(inputs[i].value).trim() : ''; + if (!t) continue; + has = true; + var o = document.createElement('option'); + o.value = String(i); + var preview = t.length > 48 ? t.slice(0, 45) + '…' : t; + o.textContent = String(i + 1) + '. ' + preview; + sel.appendChild(o); + } + if (!has) { + var ox = document.createElement('option'); + ox.value = ''; + ox.textContent = '— กรอกตัวเลือกอย่างน้อย 2 ช่อง —'; + sel.appendChild(ox); + sel.disabled = true; + return; + } + sel.disabled = false; + var ok = false; + for (var j = 0; j < sel.options.length; j++) { + if (sel.options[j].value === prev) { + sel.selectedIndex = j; + ok = true; + break; + } + } + if (!ok) sel.selectedIndex = 0; + } + + function bindQuizCarryRowInputs(row) { + row.querySelectorAll('.quiz-carry-slot-inp').forEach(function (inp) { + inp.addEventListener('input', function () { + rebuildQuizCarryCorrectSelect(row); + }); + }); + } + + function addQuizCarryAdminRow(q) { + var list = el('quiz-carry-admin-list'); + if (!list) return; + q = q || {}; + var choices = Array.isArray(q.choices) ? q.choices : []; + var choiceImages = Array.isArray(q.choiceImageUrls) ? q.choiceImageUrls : []; + var block = document.createElement('div'); + block.className = 'quiz-carry-admin-block'; + + var head = document.createElement('div'); + head.className = 'quiz-carry-admin-block-head'; + + var labQ = document.createElement('label'); + labQ.className = 'quiz-carry-q-label'; + labQ.appendChild(document.createTextNode('คำถาม')); + var inpQ = document.createElement('input'); + inpQ.type = 'text'; + inpQ.className = 'quiz-carry-q-text'; + inpQ.placeholder = 'พิมพ์คำถาม…'; + inpQ.maxLength = 500; + inpQ.value = q.text ? String(q.text) : ''; + labQ.appendChild(inpQ); + + var rm = document.createElement('button'); + rm.type = 'button'; + rm.className = 'btn btn-danger btn-sm quiz-carry-block-remove'; + rm.setAttribute('aria-label', 'ลบข้อนี้'); + rm.textContent = 'ลบข้อ'; + rm.addEventListener('click', function () { + block.remove(); + if (!list.querySelector('.quiz-carry-admin-block')) addQuizCarryAdminRow(null); + }); + + head.appendChild(labQ); + head.appendChild(rm); + block.appendChild(head); + + var grid = document.createElement('div'); + grid.className = 'quiz-carry-choice-grid'; + for (var s = 0; s < QUIZ_CARRY_MAX_SLOTS; s++) { + var lab = document.createElement('label'); + lab.className = 'quiz-carry-slot-label'; + lab.appendChild(document.createTextNode('ตัวเลือก ' + (s + 1))); + var inp = document.createElement('input'); + inp.type = 'text'; + inp.className = 'quiz-carry-slot-inp'; + inp.placeholder = s < 2 ? 'จำเป็น' : 'ไม่บังคับ'; + inp.maxLength = 160; + inp.value = choices[s] != null ? String(choices[s]) : ''; + lab.appendChild(inp); + var imgInp = document.createElement('input'); + imgInp.type = 'url'; + imgInp.className = 'quiz-carry-slot-img'; + imgInp.placeholder = 'รูป (URL — ไม่บังคับ)'; + imgInp.maxLength = 512; + imgInp.autocomplete = 'off'; + imgInp.value = choiceImages[s] != null ? String(choiceImages[s]) : ''; + imgInp.setAttribute('aria-label', 'URL รูปตัวเลือก ' + (s + 1)); + lab.appendChild(imgInp); + grid.appendChild(lab); + } + block.appendChild(grid); + + var cw = document.createElement('div'); + cw.className = 'quiz-carry-correct-wrap'; + var labC = document.createElement('label'); + labC.className = 'quiz-carry-correct-label'; + labC.appendChild(document.createTextNode('ข้อที่ถูกต้อง')); + var sel = document.createElement('select'); + sel.className = 'quiz-carry-correct-sel'; + sel.setAttribute('aria-label', 'เลือกข้อที่ถูก'); + labC.appendChild(sel); + cw.appendChild(labC); + block.appendChild(cw); + + list.appendChild(block); + bindQuizCarryRowInputs(block); + rebuildQuizCarryCorrectSelect(block); + var ci = Number(q.correctIndex); + if (Number.isFinite(ci) && ci >= 0) { + var targetSlot = ci; + var opts = sel.querySelectorAll('option'); + for (var k = 0; k < opts.length; k++) { + if (opts[k].value === String(targetSlot)) { + sel.selectedIndex = k; + break; + } + } + } + } + + function renderQuizCarryAdminList(arr) { + var list = el('quiz-carry-admin-list'); + if (!list) return; + list.innerHTML = ''; + var rows = arr && arr.length ? arr : [null]; + rows.forEach(function (q) { + addQuizCarryAdminRow(q); + }); + } + + /** ค่าเริ่มต้น RGBA สำหรับแผงคำถามบนแผนที่ (quiz_carry) */ + var QUIZ_CARRY_THEME_DEFAULTS = { + bg: { r: 12, g: 14, b: 28, a: 0.88 }, + border: { r: 255, g: 214, b: 102, a: 0.7 }, + text: { r: 241, g: 245, b: 249, a: 1 }, + }; + + function quizCarryClampByte(n) { + return Math.max(0, Math.min(255, Math.round(Number(n)) || 0)); + } + + function quizCarryHexFromRgb(r, g, b) { + return '#' + [r, g, b].map(function (x) { + var h = quizCarryClampByte(x).toString(16); + return h.length === 1 ? '0' + h : h; + }).join(''); + } + + function quizCarryParseHexToRgb(hex) { + var h = String(hex || '').trim().replace(/^#/, ''); + if (!h) return null; + if (h.length === 3) { + return { + r: parseInt(h[0] + h[0], 16), + g: parseInt(h[1] + h[1], 16), + b: parseInt(h[2] + h[2], 16), + alpha01: null, + }; + } + if (h.length === 6) { + return { + r: parseInt(h.slice(0, 2), 16), + g: parseInt(h.slice(2, 4), 16), + b: parseInt(h.slice(4, 6), 16), + alpha01: null, + }; + } + if (h.length === 8) { + return { + r: parseInt(h.slice(0, 2), 16), + g: parseInt(h.slice(2, 4), 16), + b: parseInt(h.slice(4, 6), 16), + alpha01: parseInt(h.slice(6, 8), 16) / 255, + }; + } + return null; + } + + function quizCarryParseCssColor(str, fallback) { + var s = String(str == null ? '' : str).trim(); + if (!s) return fallback; + var m = /^rgba?\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i.exec(s); + if (m) { + var a = m[4] != null && m[4] !== '' ? parseFloat(m[4]) : 1; + if (!Number.isFinite(a)) a = 1; + return { + r: quizCarryClampByte(parseInt(m[1], 10)), + g: quizCarryClampByte(parseInt(m[2], 10)), + b: quizCarryClampByte(parseInt(m[3], 10)), + a: Math.max(0, Math.min(1, a)), + }; + } + if (/^#/.test(s)) { + var pr = quizCarryParseHexToRgb(s); + if (!pr) return fallback; + var a = pr.alpha01 != null ? pr.alpha01 : 1; + return { r: pr.r, g: pr.g, b: pr.b, a: Math.max(0, Math.min(1, a)) }; + } + return fallback; + } + + function quizCarryRgbToRgbaString(o) { + var a = Math.max(0, Math.min(1, Number(o.a))); + var t = Math.round(a * 1000) / 1000; + return 'rgba(' + quizCarryClampByte(o.r) + ', ' + quizCarryClampByte(o.g) + ', ' + quizCarryClampByte(o.b) + ', ' + t + ')'; + } + + function quizCarryRowPrefix(kind) { + return 'quiz-carry-theme-' + kind; + } + + function quizCarryThemeHiddenId(kind) { + if (kind === 'bg') return 'quiz-carry-theme-bg'; + if (kind === 'border') return 'quiz-carry-theme-border'; + return 'quiz-carry-theme-text'; + } + + function quizCarrySetPickersFromColorString(kind, cssStr) { + var fb = QUIZ_CARRY_THEME_DEFAULTS[kind]; + var o = quizCarryParseCssColor(cssStr, fb); + var pref = quizCarryRowPrefix(kind); + var sw = el(pref + '-swatch'); + var ra = el(pref + '-alpha'); + var out = el(pref + '-alpha-out'); + var hi = el(quizCarryThemeHiddenId(kind)); + var pr = el(pref + '-preview'); + if (!sw || !ra || !hi) return; + sw.value = quizCarryHexFromRgb(o.r, o.g, o.b); + var pct = Math.max(0, Math.min(100, Math.round(Number(o.a) * 100))); + ra.value = String(pct); + if (out) out.textContent = pct + '%'; + hi.value = quizCarryRgbToRgbaString(o); + if (pr) pr.textContent = hi.value; + } + + function quizCarryComposeFromPickers(kind) { + var pref = quizCarryRowPrefix(kind); + var sw = el(pref + '-swatch'); + var ra = el(pref + '-alpha'); + if (!sw || !ra) return ''; + var pr = quizCarryParseHexToRgb(sw.value); + if (!pr) return ''; + var pct = parseInt(String(ra.value), 10); + if (!Number.isFinite(pct)) pct = 100; + pct = Math.max(0, Math.min(100, pct)); + return quizCarryRgbToRgbaString({ r: pr.r, g: pr.g, b: pr.b, a: pct / 100 }); + } + + function quizCarrySyncRowOutput(kind) { + var s = quizCarryComposeFromPickers(kind); + var hi = el(quizCarryThemeHiddenId(kind)); + var pref = quizCarryRowPrefix(kind); + var ra = el(pref + '-alpha'); + var out = el(pref + '-alpha-out'); + var pr = el(pref + '-preview'); + if (hi) hi.value = s; + if (out && ra) out.textContent = String(Math.max(0, Math.min(100, parseInt(ra.value, 10) || 0))) + '%'; + if (pr && hi) pr.textContent = hi.value; + } + + /** อ่านค่าจาก color + range โดยตรง (ไม่พึ่ง hidden ที่อาจว่าง) ก่อน PUT */ + function quizCarryBuildThemeForSave() { + quizCarryBindThemePickersOnce(); + ['bg', 'border', 'text'].forEach(function (k) { + quizCarrySyncRowOutput(k); + }); + var fbBg = QUIZ_CARRY_THEME_DEFAULTS.bg; + var fbBr = QUIZ_CARRY_THEME_DEFAULTS.border; + var fbTx = QUIZ_CARRY_THEME_DEFAULTS.text; + var sBg = quizCarryComposeFromPickers('bg'); + var sBr = quizCarryComposeFromPickers('border'); + var sTx = quizCarryComposeFromPickers('text'); + var bwEl = el('quiz-carry-theme-border-w'); + var bw = bwEl ? parseInt(String(bwEl.value || '2'), 10) : 2; + if (!Number.isFinite(bw)) bw = 2; + bw = Math.max(0, Math.min(12, Math.round(bw))); + var qfMinEl = el('quiz-carry-theme-qfont-min'); + var qfMaxEl = el('quiz-carry-theme-qfont-max'); + var qMin = qfMinEl ? parseInt(String(qfMinEl.value || '10'), 10) : 10; + var qMax = qfMaxEl ? parseInt(String(qfMaxEl.value || '24'), 10) : 24; + if (!Number.isFinite(qMin)) qMin = 10; + if (!Number.isFinite(qMax)) qMax = 24; + qMin = Math.max(10, Math.min(40, Math.round(qMin))); + qMax = Math.max(14, Math.min(56, Math.round(qMax))); + if (qMax < qMin) { + var swap = qMin; + qMin = qMax; + qMax = swap; + } + return { + panelBg: (sBg && sBg.trim()) ? sBg.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBg), + panelBorder: (sBr && sBr.trim()) ? sBr.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBr), + textColor: (sTx && sTx.trim()) ? sTx.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbTx), + borderWidthPx: bw, + questionFontMinPx: qMin, + questionFontMaxPx: qMax, + }; + } + + function quizCarryBindThemePickersOnce() { + if (quizCarryBindThemePickersOnce._done) return; + ['bg', 'border', 'text'].forEach(function (kind) { + var pref = quizCarryRowPrefix(kind); + var sw = el(pref + '-swatch'); + var ra = el(pref + '-alpha'); + if (!sw || !ra) return; + if (ra.getAttribute('data-bound') === '1') return; + ra.setAttribute('data-bound', '1'); + function sync() { + quizCarrySyncRowOutput(kind); + } + sw.addEventListener('input', sync); + sw.addEventListener('change', sync); + ra.addEventListener('input', sync); + ra.addEventListener('change', sync); + }); + quizCarryBindThemePickersOnce._done = true; + } + + /** ธีมนับ 3-2-1 embed — สีม่าน / กล่อง / ขนาด (carryEmbedCountdownTheme) */ + var QUIZ_CARRY_ECD_COLS = [ + { sw: 'quiz-carry-ecd-overlay-swatch', ra: 'quiz-carry-ecd-overlay-alpha', out: 'quiz-carry-ecd-overlay-alpha-out', hi: 'quiz-carry-ecd-overlay-val', pr: 'quiz-carry-ecd-overlay-preview' }, + { sw: 'quiz-carry-ecd-inner-bg-swatch', ra: 'quiz-carry-ecd-inner-bg-alpha', out: 'quiz-carry-ecd-inner-bg-alpha-out', hi: 'quiz-carry-ecd-inner-bg-val', pr: 'quiz-carry-ecd-inner-bg-preview' }, + { sw: 'quiz-carry-ecd-inner-border-swatch', ra: 'quiz-carry-ecd-inner-border-alpha', out: 'quiz-carry-ecd-inner-border-alpha-out', hi: 'quiz-carry-ecd-inner-border-val', pr: 'quiz-carry-ecd-inner-border-preview' }, + ]; + var QUIZ_CARRY_ECD_DEFAULTS_RGBA = { + overlay: { r: 8, g: 10, b: 20, a: 0.42 }, + innerBg: { r: 12, g: 14, b: 28, a: 0.82 }, + innerBorder: { r: 122, g: 162, b: 247, a: 0.45 }, + }; + + function quizCarryEcdColKey(idx) { + if (idx === 0) return 'overlay'; + if (idx === 1) return 'innerBg'; + return 'innerBorder'; + } + + function quizCarryEcdSyncRow(idx) { + var row = QUIZ_CARRY_ECD_COLS[idx]; + var sw = el(row.sw); + var ra = el(row.ra); + var hi = el(row.hi); + var pr = el(row.pr); + var out = el(row.out); + if (!sw || !ra || !hi) return; + var prRgb = quizCarryParseHexToRgb(sw.value); + if (!prRgb) return; + var pct = parseInt(String(ra.value), 10); + if (!Number.isFinite(pct)) pct = 100; + pct = Math.max(0, Math.min(100, pct)); + var s = quizCarryRgbToRgbaString({ r: prRgb.r, g: prRgb.g, b: prRgb.b, a: pct / 100 }); + hi.value = s; + if (out) out.textContent = pct + '%'; + if (pr) pr.textContent = s; + } + + function quizCarryEcdSetRowFromRgba(idx, cssStr) { + var row = QUIZ_CARRY_ECD_COLS[idx]; + var key = quizCarryEcdColKey(idx); + var fb = QUIZ_CARRY_ECD_DEFAULTS_RGBA[key]; + var o = quizCarryParseCssColor(cssStr, fb); + var sw = el(row.sw); + var ra = el(row.ra); + var out = el(row.out); + var hi = el(row.hi); + var pr = el(row.pr); + if (!sw || !ra || !hi) return; + sw.value = quizCarryHexFromRgb(o.r, o.g, o.b); + var pct = Math.max(0, Math.min(100, Math.round(Number(o.a) * 100))); + ra.value = String(pct); + if (out) out.textContent = pct + '%'; + hi.value = quizCarryRgbToRgbaString(o); + if (pr) pr.textContent = hi.value; + } + + function quizCarryEcdBindPickersOnce() { + if (quizCarryEcdBindPickersOnce._done) return; + for (var i = 0; i < QUIZ_CARRY_ECD_COLS.length; i++) { + (function (idx) { + var row = QUIZ_CARRY_ECD_COLS[idx]; + var sw = el(row.sw); + var ra = el(row.ra); + if (!sw || !ra) return; + if (ra.getAttribute('data-ecd-bound') === '1') return; + ra.setAttribute('data-ecd-bound', '1'); + function sync() { + quizCarryEcdSyncRow(idx); + } + sw.addEventListener('input', sync); + sw.addEventListener('change', sync); + ra.addEventListener('input', sync); + ra.addEventListener('change', sync); + })(i); + } + quizCarryEcdBindPickersOnce._done = true; + } + + function quizCarryEcdBuildForSave() { + quizCarryEcdBindPickersOnce(); + for (var i = 0; i < QUIZ_CARRY_ECD_COLS.length; i++) quizCarryEcdSyncRow(i); + var o0 = el('quiz-carry-ecd-overlay-val'); + var o1 = el('quiz-carry-ecd-inner-bg-val'); + var o2 = el('quiz-carry-ecd-inner-border-val'); + var bwEl = el('quiz-carry-ecd-inner-border-w'); + var radEl = el('quiz-carry-ecd-inner-radius'); + var dig = el('quiz-carry-ecd-digit-swatch'); + var m1 = el('quiz-carry-ecd-map-cqmin'); + var m2 = el('quiz-carry-ecd-map-cqh'); + var m3 = el('quiz-carry-ecd-map-max-px'); + var sv = el('quiz-carry-ecd-screen-vw'); + var sm = el('quiz-carry-ecd-screen-max-px'); + var bw = bwEl ? parseInt(String(bwEl.value || '1'), 10) : 1; + if (!Number.isFinite(bw)) bw = 1; + bw = Math.max(0, Math.min(12, Math.round(bw))); + var rad = radEl ? parseInt(String(radEl.value || '12'), 10) : 12; + if (!Number.isFinite(rad)) rad = 12; + rad = Math.max(0, Math.min(32, Math.round(rad))); + var cqmin = m1 ? parseInt(String(m1.value || '78'), 10) : 78; + var cqh = m2 ? parseInt(String(m2.value || '82'), 10) : 82; + var mpx = m3 ? parseInt(String(m3.value || '200'), 10) : 200; + cqmin = Math.max(35, Math.min(100, cqmin)); + cqh = Math.max(35, Math.min(100, cqh)); + mpx = Math.max(48, Math.min(400, mpx)); + var vw = sv ? parseInt(String(sv.value || '28'), 10) : 28; + var smx = sm ? parseInt(String(sm.value || '132'), 10) : 132; + vw = Math.max(6, Math.min(44, vw)); + smx = Math.max(48, Math.min(220, smx)); + var digitHex = dig && /^#[0-9a-fA-F]{6}$/.test(String(dig.value || '').trim()) ? String(dig.value).trim() : '#ffe066'; + return { + overlayBackdrop: (o0 && o0.value.trim()) ? o0.value.trim().slice(0, 120) : quizCarryRgbToRgbaString(QUIZ_CARRY_ECD_DEFAULTS_RGBA.overlay), + innerBg: (o1 && o1.value.trim()) ? o1.value.trim().slice(0, 120) : quizCarryRgbToRgbaString(QUIZ_CARRY_ECD_DEFAULTS_RGBA.innerBg), + innerBorder: (o2 && o2.value.trim()) ? o2.value.trim().slice(0, 120) : quizCarryRgbToRgbaString(QUIZ_CARRY_ECD_DEFAULTS_RGBA.innerBorder), + innerBorderWpx: bw, + innerRadiusPx: rad, + digitColor: digitHex.slice(0, 32), + mapDigitCqmin: cqmin, + mapDigitCqh: cqh, + mapDigitMaxPx: mpx, + screenDigitVw: vw, + screenDigitMaxPx: smx, + }; + } + + function quizCarryEcdFillForm(th) { + quizCarryEcdBindPickersOnce(); + var t = th && typeof th === 'object' ? th : {}; + quizCarryEcdSetRowFromRgba(0, t.overlayBackdrop); + quizCarryEcdSetRowFromRgba(1, t.innerBg); + quizCarryEcdSetRowFromRgba(2, t.innerBorder); + var bwEl = el('quiz-carry-ecd-inner-border-w'); + var radEl = el('quiz-carry-ecd-inner-radius'); + var dig = el('quiz-carry-ecd-digit-swatch'); + var m1 = el('quiz-carry-ecd-map-cqmin'); + var m2 = el('quiz-carry-ecd-map-cqh'); + var m3 = el('quiz-carry-ecd-map-max-px'); + var sv = el('quiz-carry-ecd-screen-vw'); + var sm = el('quiz-carry-ecd-screen-max-px'); + var bw = parseInt(String(t.innerBorderWpx), 10); + if (bwEl) bwEl.value = String(Number.isFinite(bw) && bw >= 0 ? Math.min(12, bw) : 1); + var rad = parseInt(String(t.innerRadiusPx), 10); + if (radEl) radEl.value = String(Number.isFinite(rad) && rad >= 0 ? Math.min(32, rad) : 12); + if (dig && typeof t.digitColor === 'string' && /^#[0-9a-fA-F]{6}$/.test(t.digitColor.trim())) dig.value = t.digitColor.trim(); + var cqmin = parseInt(String(t.mapDigitCqmin), 10); + var cqh = parseInt(String(t.mapDigitCqh), 10); + var mpx = parseInt(String(t.mapDigitMaxPx), 10); + if (m1) m1.value = String(Number.isFinite(cqmin) ? Math.max(35, Math.min(100, cqmin)) : 78); + if (m2) m2.value = String(Number.isFinite(cqh) ? Math.max(35, Math.min(100, cqh)) : 82); + if (m3) m3.value = String(Number.isFinite(mpx) ? Math.max(48, Math.min(400, mpx)) : 200); + var vw = parseInt(String(t.screenDigitVw), 10); + var smx = parseInt(String(t.screenDigitMaxPx), 10); + if (sv) sv.value = String(Number.isFinite(vw) ? Math.max(6, Math.min(44, vw)) : 28); + if (sm) sm.value = String(Number.isFinite(smx) ? Math.max(48, Math.min(220, smx)) : 132); + } + + var QUIZ_CARRY_PLAQUE_SLOT_COUNT = 16; + var QUIZ_CARRY_PLAQUE_KINDS = ['fixed', 'fill', 'text']; + var QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA = { + fixed: { r: 122, g: 200, b: 255, a: 0.9 }, + fill: { r: 12, g: 10, b: 20, a: 0.88 }, + text: { r: 248, g: 249, b: 255, a: 1 }, + }; + + function quizCarryPlaqueSlotPrefix(si) { + return 'quiz-carry-plaque-s' + si + '-'; + } + + /** + * อัปโหลดป้าย: ลอง POST ตรง Node (/Game/api/ — nginx proxy 20M) ก่อน แล้วค่อย PHP ถ้าล้ม + * เดิมลอง PHP ก่อนทำให้พังง่ายเมื่อ php-fpm ต่อ 127.0.0.1:3001 ไม่ได้หรือ session ผิด แม้เกมจะรันอยู่ + */ + function quizCarryPlaqueUploadWithFallback(dataUrl) { + var body = JSON.stringify({ imageDataUrl: dataUrl }); + var opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body }; + return gauntletAssetsJsonFetch(GAME_QUIZ_CARRY_PLAQUE_UPLOAD_NODE, opts).catch(function (errFirst) { + var msg = String((errFirst && errFirst.message) || ''); + if (/\b401\b|Unauthorized|หมดเซสชัน/i.test(msg)) { + return Promise.reject(errFirst); + } + var tryPhp = + /\b404\b|\b405\b|Not Found|ไม่พบ|502|503|504|Bad Gateway|Failed to fetch|NetworkError|Load failed|ECONNREFUSED/i.test( + msg, + ) || + /\b413\b|Request Entity Too Large|Payload too large|รับ body ไม่ได้|too large/i.test(msg); + if (!tryPhp) { + return Promise.reject(errFirst); + } + return gauntletAssetsJsonFetch(GAME_QUIZ_CARRY_PLAQUE_UPLOAD_PHP, opts).then(function (res) { + try { + setMsg( + 'quiz-carry-settings-msg', + 'อัปโหลดสำเร็จผ่าน PHP (รองจาก /Game/api/) · Upload OK via PHP fallback', + 'ok', + ); + } catch (e1) { /* ignore */ } + return res; + }); + }); + } + + /** URL สำหรับ — path ที่ขึ้นต้นด้วย / ให้ต่อ origin (รองรับ Admin ใต้โดเมนเดียวกับ /Game/) */ + function quizCarryPlaqueAbsMediaUrl(path) { + var s = String(path || '').trim(); + if (!s) return ''; + if (/^https?:\/\//i.test(s)) return s; + if (s.charAt(0) === '/') return String(window.location.origin || '') + s; + return ''; + } + + function quizCarryPlaqueUpdateImagePreview(si) { + var pfx = quizCarryPlaqueSlotPrefix(si); + var inp = el(pfx + 'plaque-image-url'); + var img = el(pfx + 'plaque-preview-img'); + var errEl = el(pfx + 'plaque-preview-err'); + if (!img) return; + var raw = inp && inp.value ? String(inp.value).trim() : ''; + if (errEl) errEl.textContent = ''; + if (!raw) { + img.style.display = 'none'; + img.removeAttribute('src'); + return; + } + var abs = quizCarryPlaqueAbsMediaUrl(raw); + if (!abs) { + img.style.display = 'none'; + if (errEl) errEl.textContent = 'รูปแบบ URL ไม่รองรับ'; + return; + } + img.onerror = function () { + img.style.display = 'none'; + if (errEl) errEl.textContent = 'โหลดตัวอย่างไม่ได้ — ตรวจ path / สิทธิ์ไฟล์ / รีสตาร์ท Node'; + }; + img.onload = function () { + try { + img.removeAttribute('width'); + img.removeAttribute('height'); + } catch (e) { /* ignore */ } + img.style.display = 'block'; + if (errEl) errEl.textContent = ''; + }; + var sep = abs.indexOf('?') >= 0 ? '&' : '?'; + img.src = abs + sep + '_pv=' + Date.now(); + } + + function quizCarryPlaqueUpdateNeonUi(si) { + var modeEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-mode'); + var grid = modeEl && modeEl.closest ? modeEl.closest('.quiz-carry-theme-grid') : null; + if (!grid) return; + if (modeEl && modeEl.value === 'fixed') grid.classList.remove('quiz-carry-plaque-grid--neon'); + else grid.classList.add('quiz-carry-plaque-grid--neon'); + var hint = grid.querySelector('.quiz-carry-plaque-neon-hint'); + if (hint) hint.style.display = modeEl && modeEl.value === 'fixed' ? 'none' : ''; + } + + function quizCarryPlaqueRowIds(si, kind) { + var seg = kind === 'fixed' ? 'fixed' : kind === 'fill' ? 'fill' : 'text'; + var p = quizCarryPlaqueSlotPrefix(si); + return { + sw: p + seg + '-swatch', + ra: p + seg + '-alpha', + out: p + seg + '-alpha-out', + hi: p + seg + '-val', + }; + } + + function quizCarryPlaqueEnsureMounted() { + var root = el('quiz-carry-plaque-slots-root'); + if (!root || root.getAttribute('data-mounted') === '1') return; + for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) { + var det = document.createElement('details'); + det.className = 'quiz-carry-plaque-slot-details'; + if (si === 0) det.open = true; + var summ = document.createElement('summary'); + summ.textContent = 'ตัวเลือก ' + (si + 1); + det.appendChild(summ); + var grid = document.createElement('div'); + grid.className = 'quiz-carry-theme-grid quiz-carry-plaque-grid'; + var rowMode = document.createElement('div'); + rowMode.className = 'quiz-carry-theme-row quiz-carry-theme-row--narrow'; + rowMode.style.marginBottom = '0.45rem'; + var labMode = document.createElement('label'); + labMode.className = 'admin-field quiz-carry-theme-label'; + labMode.appendChild(document.createTextNode('โหมดขอบป้าย')); + var selMode = document.createElement('select'); + selMode.id = quizCarryPlaqueSlotPrefix(si) + 'border-mode'; + selMode.setAttribute('aria-label', 'โหมดขอบป้ายช่อง ' + (si + 1)); + [['neon', 'เรืองตามช่อง (neon)'], ['fixed', 'สีขอบคงที่']].forEach(function (opt) { + var o = document.createElement('option'); + o.value = opt[0]; + o.textContent = opt[1]; + selMode.appendChild(o); + }); + labMode.appendChild(selMode); + rowMode.appendChild(labMode); + grid.appendChild(rowMode); + selMode.addEventListener('change', function () { + quizCarryPlaqueUpdateNeonUi(si); + }); + + var hintNeon = document.createElement('p'); + hintNeon.className = 'muted quiz-carry-plaque-neon-hint'; + hintNeon.style.cssText = 'margin:0 0 0.5rem;font-size:0.78rem;line-height:1.45'; + hintNeon.innerHTML = + 'Neon / เรืองตามช่อง: ขอบบนแมปใช้สีตามเลขช่อง ไม่ใช้ «สีขอบ (fixed)» ด้านล่าง — ต้องการขอบตาม picker ให้เลือก สีขอบคงที่ · ' + + 'English: In Neon mode the map uses per-slot hue; Fixed border uses the fixed-border pickers.'; + grid.appendChild(hintNeon); + + function addColorRow(labelText, kind, defHex, defAlpha) { + var ids = quizCarryPlaqueRowIds(si, kind); + var row = document.createElement('div'); + row.className = 'quiz-carry-theme-row'; + row.setAttribute('data-quiz-carry-plaque-color-row', kind); + var lbl = document.createElement('span'); + lbl.className = 'quiz-carry-theme-label'; + lbl.textContent = labelText; + row.appendChild(lbl); + var controls = document.createElement('div'); + controls.className = 'quiz-carry-color-controls'; + var sw = document.createElement('input'); + sw.type = 'color'; + sw.id = ids.sw; + sw.value = defHex; + sw.title = labelText; + controls.appendChild(sw); + var labA = document.createElement('label'); + labA.className = 'quiz-carry-alpha-label'; + var spanA = document.createElement('span'); + spanA.textContent = 'โปร่ง A'; + labA.appendChild(spanA); + var ra = document.createElement('input'); + ra.type = 'range'; + ra.id = ids.ra; + ra.min = '0'; + ra.max = '100'; + ra.step = '1'; + ra.value = String(defAlpha); + labA.appendChild(ra); + var out = document.createElement('output'); + out.id = ids.out; + out.className = 'quiz-carry-alpha-out'; + out.setAttribute('for', ids.ra); + out.textContent = defAlpha + '%'; + labA.appendChild(out); + controls.appendChild(labA); + var hi = document.createElement('input'); + hi.type = 'hidden'; + hi.id = ids.hi; + controls.appendChild(hi); + row.appendChild(controls); + grid.appendChild(row); + } + addColorRow('สีขอบ (fixed)', 'fixed', '#7ac8ff', 90); + addColorRow('พื้นหลังกล่อง', 'fill', '#0c0a14', 88); + addColorRow('สีตัวอักษร', 'text', '#f8f9ff', 100); + var rowW = document.createElement('div'); + rowW.className = 'quiz-carry-theme-row quiz-carry-theme-row--narrow'; + var labW = document.createElement('label'); + labW.className = 'admin-field quiz-carry-theme-label'; + labW.appendChild(document.createTextNode('ความหนาขอบ (px)')); + var inpW = document.createElement('input'); + inpW.type = 'number'; + inpW.className = 'admin-inp-num'; + inpW.id = quizCarryPlaqueSlotPrefix(si) + 'border-w'; + inpW.min = '0'; + inpW.max = '8'; + inpW.step = '0.5'; + inpW.value = '2.5'; + labW.appendChild(inpW); + rowW.appendChild(labW); + grid.appendChild(rowW); + + var rowImg = document.createElement('div'); + /* ห้ามใช้ quiz-carry-theme-row--narrow — มี max-width:12rem ทำช่อง URL แคบมาก ดูเหมือนอัปโหลดไม่ทำงาน */ + rowImg.className = 'quiz-carry-theme-row quiz-carry-plaque-image-row'; + rowImg.style.cssText = 'flex-wrap:wrap;gap:0.65rem;align-items:flex-end'; + var labUrl = document.createElement('label'); + labUrl.className = 'admin-field'; + labUrl.style.cssText = 'flex:1;min-width:200px;margin:0'; + labUrl.appendChild(document.createTextNode('รูปบนป้าย (ช่องนี้)')); + var inpImgUrl = document.createElement('input'); + inpImgUrl.type = 'text'; + inpImgUrl.className = 'admin-inp-text'; + inpImgUrl.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-image-url'; + inpImgUrl.placeholder = '/Game/img/quiz-carry-plaque-assets/... หรือ https://...'; + inpImgUrl.setAttribute('aria-label', 'URL รูปป้ายช่อง ' + (si + 1)); + labUrl.appendChild(document.createElement('br')); + labUrl.appendChild(inpImgUrl); + var prevErr = document.createElement('span'); + prevErr.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-preview-err'; + prevErr.className = 'muted quiz-carry-plaque-preview-err'; + prevErr.style.cssText = 'display:block;font-size:0.72rem;margin:0.25rem 0 0.15rem;min-height:1em'; + labUrl.appendChild(prevErr); + var prevImg = document.createElement('img'); + prevImg.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-preview-img'; + prevImg.alt = 'พรีวิวรูปป้ายช่อง ' + (si + 1); + prevImg.className = 'quiz-carry-plaque-preview-img'; + /* ห้าม width/height = 0 — บางเบราว์เซอร์แสดงกล่อง 0×0 แม้ onload แล้ว · Don't use 0×0 attrs or preview stays invisible */ + prevImg.style.cssText = 'display:none;max-height:120px;max-width:min(100%,320px);width:auto;height:auto;border-radius:8px;border:1px solid var(--border);object-fit:contain;background:var(--card)'; + labUrl.appendChild(prevImg); + inpImgUrl.addEventListener('input', function () { + var v = String(inpImgUrl.value || '').trim(); + if (!v) delete quizCarryPlaquePendingUrlBySlot[si]; + quizCarryPlaqueUpdateImagePreview(si); + }); + inpImgUrl.addEventListener('change', function () { + var v2 = String(inpImgUrl.value || '').trim(); + if (!v2) delete quizCarryPlaquePendingUrlBySlot[si]; + quizCarryPlaqueUpdateImagePreview(si); + }); + rowImg.appendChild(labUrl); + var upWrap = document.createElement('div'); + upWrap.className = 'admin-field'; + upWrap.style.margin = '0'; + var fileInp = document.createElement('input'); + fileInp.type = 'file'; + fileInp.accept = 'image/png,image/jpeg,image/webp,image/gif'; + fileInp.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-image-file'; + fileInp.style.fontSize = '0.82rem'; + var upMsg = document.createElement('span'); + upMsg.className = 'muted'; + upMsg.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-image-msg'; + upMsg.style.cssText = 'display:block;font-size:0.75rem;margin-top:0.25rem'; + fileInp.addEventListener('change', function () { + while (upMsg.firstChild) upMsg.removeChild(upMsg.firstChild); + upMsg.textContent = ''; + if (!fileInp.files || !fileInp.files[0]) return; + upMsg.textContent = 'กำลังอ่านไฟล์และอัปโหลด… · Reading file & uploading…'; + try { + upMsg.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } catch (scrollErr) { /* ignore */ } + quizCarryPlaqueUploadPending++; + quizCarryPlaqueUploadStartMs = Date.now(); + readFileAsDataURL(fileInp.files[0]) + .then(function (dataUrl) { + return quizCarryPlaqueUploadWithFallback(dataUrl); + }) + .then(function (res) { + if (res && res.url) { + var uOk = String(res.url).trim(); + inpImgUrl.value = uOk; + quizCarryPlaquePendingUrlBySlot[si] = uOk; + quizCarryPlaqueUpdateImagePreview(si); + upMsg.textContent = ''; + var ok1 = document.createElement('span'); + ok1.textContent = 'อัปโหลดแล้ว · path ที่ได้: '; + upMsg.appendChild(ok1); + var codeU = document.createElement('code'); + codeU.style.cssText = 'font-size:0.78rem;word-break:break-all'; + codeU.textContent = res.url; + upMsg.appendChild(codeU); + var br = document.createElement('br'); + upMsg.appendChild(br); + var ok2 = document.createElement('span'); + ok2.textContent = 'กด «บันทึก» ด้านล่างเพื่อเขียนลง quiz-settings.json · Then click Save below'; + upMsg.appendChild(ok2); + } else { + upMsg.textContent = (res && res.error) || 'ตอบกลับไม่มี url — ตรวจ Node /Game/api/quiz-carry-plaque-upload'; + } + }) + .catch(function (e) { + upMsg.textContent = e.message || 'อัปโหลดไม่สำเร็จ'; + setMsg('quiz-carry-settings-msg', e.message || 'อัปโหลดรูปป้ายไม่สำเร็จ · Plaque upload failed', 'error'); + try { + upMsg.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + var smUp = el('quiz-carry-settings-msg'); + if (smUp) smUp.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } catch (scrollE) { /* ignore */ } + }) + .finally(function () { + fileInp.value = ''; + quizCarryPlaqueUploadPending = Math.max(0, quizCarryPlaqueUploadPending - 1); + if (quizCarryPlaqueUploadPending === 0) quizCarryPlaqueUploadStartMs = 0; + }); + }); + upWrap.appendChild(document.createTextNode('อัปโหลด')); + upWrap.appendChild(document.createElement('br')); + upWrap.appendChild(fileInp); + upWrap.appendChild(upMsg); + rowImg.appendChild(upWrap); + grid.appendChild(rowImg); + + det.appendChild(grid); + root.appendChild(det); + quizCarryPlaqueUpdateNeonUi(si); + } + root.setAttribute('data-mounted', '1'); + } + + function quizCarryPlaqueComposeFromPickers(si, kind) { + var ids = quizCarryPlaqueRowIds(si, kind); + var sw = el(ids.sw); + var ra = el(ids.ra); + var fb = QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA[kind] || QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA.fill; + if (!sw || !ra) return quizCarryRgbToRgbaString(fb); + var pr = quizCarryParseHexToRgb(sw.value); + if (!pr) return quizCarryRgbToRgbaString(fb); + var pct = parseInt(String(ra.value), 10); + if (!Number.isFinite(pct)) pct = Math.round((fb.a != null ? fb.a : 1) * 100); + pct = Math.max(0, Math.min(100, pct)); + return quizCarryRgbToRgbaString({ r: pr.r, g: pr.g, b: pr.b, a: pct / 100 }); + } + + function quizCarryPlaqueSetPickersFromString(si, kind, cssStr) { + var ids = quizCarryPlaqueRowIds(si, kind); + var fb = QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA[kind] || QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA.fill; + var o = quizCarryParseCssColor(cssStr, fb); + var sw = el(ids.sw); + var ra = el(ids.ra); + var hi = el(ids.hi); + var out = el(ids.out); + if (sw) sw.value = quizCarryHexFromRgb(o.r, o.g, o.b); + if (ra) ra.value = String(Math.round((o.a != null ? o.a : 1) * 100)); + if (out) out.textContent = ra ? ra.value + '%' : ''; + if (hi) hi.value = quizCarryRgbToRgbaString(o); + } + + function quizCarryPlaqueSyncRow(si, kind) { + var s = quizCarryPlaqueComposeFromPickers(si, kind); + var ids = quizCarryPlaqueRowIds(si, kind); + var hi = el(ids.hi); + var out = el(ids.out); + var ra = el(ids.ra); + if (hi) hi.value = s; + if (out && ra) out.textContent = ra.value + '%'; + } + + var quizCarryPlaqueBindPickersDone = false; + function quizCarryPlaqueBindPickersOnce() { + if (quizCarryPlaqueBindPickersDone) return; + quizCarryPlaqueEnsureMounted(); + for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) { + QUIZ_CARRY_PLAQUE_KINDS.forEach(function (kind) { + var ids = quizCarryPlaqueRowIds(si, kind); + var sw = el(ids.sw); + var ra = el(ids.ra); + if (sw) { + sw.addEventListener('input', function () { + quizCarryPlaqueSyncRow(si, kind); + }); + } + if (ra) { + ra.addEventListener('input', function () { + quizCarryPlaqueSyncRow(si, kind); + }); + } + }); + } + quizCarryPlaqueBindPickersDone = true; + } + + function quizCarryPlaqueBuildForSave() { + quizCarryPlaqueEnsureMounted(); + quizCarryPlaqueBindPickersOnce(); + var arr = []; + for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) { + for (var ki = 0; ki < QUIZ_CARRY_PLAQUE_KINDS.length; ki++) { + quizCarryPlaqueSyncRow(si, QUIZ_CARRY_PLAQUE_KINDS[ki]); + } + var modeEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-mode'); + var mode = modeEl && modeEl.value === 'fixed' ? 'fixed' : 'neon'; + var bwEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-w'); + var bw = bwEl ? parseFloat(String(bwEl.value)) : 2.5; + if (!Number.isFinite(bw)) bw = 2.5; + bw = Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10; + var imgUrlEl = el(quizCarryPlaqueSlotPrefix(si) + 'plaque-image-url'); + var plaqueImageUrl = imgUrlEl ? String(imgUrlEl.value || '').trim() : ''; + if (!plaqueImageUrl && quizCarryPlaquePendingUrlBySlot[si]) { + plaqueImageUrl = String(quizCarryPlaquePendingUrlBySlot[si] || '').trim(); + if (imgUrlEl && plaqueImageUrl) imgUrlEl.value = plaqueImageUrl; + } + plaqueImageUrl = plaqueImageUrl.slice(0, 512); + arr.push({ + borderMode: mode, + fixedBorder: quizCarryPlaqueComposeFromPickers(si, 'fixed'), + fillBg: quizCarryPlaqueComposeFromPickers(si, 'fill'), + textColor: quizCarryPlaqueComposeFromPickers(si, 'text'), + borderWidthPx: bw, + plaqueImageUrl: plaqueImageUrl, + }); + } + return arr; + } + + function quizCarryPlaqueFillSlot(si, t) { + var obj = t && typeof t === 'object' ? t : {}; + var modeEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-mode'); + if (modeEl) modeEl.value = String(obj.borderMode || '').toLowerCase() === 'fixed' ? 'fixed' : 'neon'; + quizCarryPlaqueSetPickersFromString(si, 'fixed', obj.fixedBorder); + quizCarryPlaqueSetPickersFromString(si, 'fill', obj.fillBg); + quizCarryPlaqueSetPickersFromString(si, 'text', obj.textColor); + var bwEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-w'); + var bw = parseFloat(String(obj.borderWidthPx)); + if (bwEl) { + bwEl.value = String(Number.isFinite(bw) ? Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10 : 2.5); + } + var imgEl = el(quizCarryPlaqueSlotPrefix(si) + 'plaque-image-url'); + if (imgEl) { + imgEl.value = obj.plaqueImageUrl != null ? String(obj.plaqueImageUrl) : ''; + } + var srvU = obj.plaqueImageUrl != null ? String(obj.plaqueImageUrl).trim() : ''; + if (srvU) quizCarryPlaquePendingUrlBySlot[si] = srvU.slice(0, 512); + var msgEl = el(quizCarryPlaqueSlotPrefix(si) + 'plaque-image-msg'); + if (msgEl) msgEl.textContent = ''; + quizCarryPlaqueUpdateNeonUi(si); + quizCarryPlaqueUpdateImagePreview(si); + } + + function quizCarryPlaqueFillAllFromApi(data) { + quizCarryPlaqueBindPickersOnce(); + var arr = null; + if (data && Array.isArray(data.carryChoicePlaqueThemes) && data.carryChoicePlaqueThemes.length) { + arr = data.carryChoicePlaqueThemes; + } else if (data && data.carryChoicePlaqueTheme && typeof data.carryChoicePlaqueTheme === 'object') { + arr = []; + for (var u = 0; u < QUIZ_CARRY_PLAQUE_SLOT_COUNT; u++) arr.push(data.carryChoicePlaqueTheme); + } + for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) { + var th = (arr && arr[si]) ? arr[si] : {}; + quizCarryPlaqueFillSlot(si, th); + quizCarryPlaqueUpdateImagePreview(si); + } + } + + function quizCarrySetPlaqueMapScaleInputs(scale) { + var pms = Number(scale); + if (!Number.isFinite(pms)) pms = 1.25; + pms = Math.max(0.85, Math.min(2.5, pms)); + var cent = Math.round(pms * 100); + cent = Math.max(85, Math.min(250, cent)); + var num = el('quiz-carry-plaque-map-scale'); + var rng = el('quiz-carry-plaque-map-scale-range'); + var out = el('quiz-carry-plaque-map-scale-out'); + if (num) num.value = String(Math.round((cent / 100) * 100) / 100); + if (rng) rng.value = String(cent); + if (out) out.textContent = '× ' + (num ? num.value : String(cent / 100)); + } + + function wireQuizCarryPlaqueMapScaleControls() { + if (quizCarryPlaqueScaleControlsWired) return; + var num = el('quiz-carry-plaque-map-scale'); + var rng = el('quiz-carry-plaque-map-scale-range'); + if (!num || !rng) return; + quizCarryPlaqueScaleControlsWired = true; + function fromRange() { + var c = parseInt(String(rng.value), 10); + if (!Number.isFinite(c)) c = 125; + c = Math.max(85, Math.min(250, c)); + rng.value = String(c); + var sc = c / 100; + num.value = String(Math.round(sc * 100) / 100); + var out = el('quiz-carry-plaque-map-scale-out'); + if (out) out.textContent = '× ' + num.value; + } + function fromNum() { + var p = parseFloat(String(num.value || '').replace(',', '.')); + if (!Number.isFinite(p)) p = 1.25; + p = Math.max(0.85, Math.min(2.5, p)); + rng.value = String(Math.round(p * 100)); + num.value = String(Math.round(p * 100) / 100); + var out = el('quiz-carry-plaque-map-scale-out'); + if (out) out.textContent = '× ' + num.value; + } + rng.addEventListener('input', fromRange); + rng.addEventListener('change', fromRange); + num.addEventListener('input', fromNum); + num.addEventListener('change', fromNum); + } + + function loadQuizCarryPanel(opts) { + opts = opts || {}; + var delay = Number(opts.fetchDelayMs) || 0; + var runLoad = function () { + gameQuizFetch('GET').then(function (data) { + if (opts.themeOverride && typeof opts.themeOverride === 'object') { + var ov = opts.themeOverride; + if ( + ov.panelBg != null || + ov.borderWidthPx != null || + ov.panelBorder != null || + ov.textColor != null || + ov.questionFontMinPx != null || + ov.questionFontMaxPx != null + ) { + data.carryMapPanelTheme = ov; + } else if (ov.carryMapPanelTheme && typeof ov.carryMapPanelTheme === 'object') { + data.carryMapPanelTheme = ov.carryMapPanelTheme; + } + if (ov.carryEmbedCountdownTheme && typeof ov.carryEmbedCountdownTheme === 'object') { + data.carryEmbedCountdownTheme = ov.carryEmbedCountdownTheme; + } + if (ov.carryChoicePlaqueThemes && Array.isArray(ov.carryChoicePlaqueThemes)) { + data.carryChoicePlaqueThemes = ov.carryChoicePlaqueThemes; + if (ov.carryChoicePlaqueThemes[0] && typeof ov.carryChoicePlaqueThemes[0] === 'object') { + data.carryChoicePlaqueTheme = ov.carryChoicePlaqueThemes[0]; + } + } else if (ov.carryChoicePlaqueTheme && typeof ov.carryChoicePlaqueTheme === 'object') { + /* legacy อ็อบเจ็กต์เดียว — ต้องตั้งทั้งอาร์เรย์ ไม่งั้น Fill ยังใช้ carryChoicePlaqueThemes จาก GET เก่า (URL ป้ายหายหลังบันทึก) */ + data.carryChoicePlaqueTheme = ov.carryChoicePlaqueTheme; + var expPlaque = []; + for (var exi = 0; exi < QUIZ_CARRY_PLAQUE_SLOT_COUNT; exi++) { + expPlaque.push(ov.carryChoicePlaqueTheme); + } + data.carryChoicePlaqueThemes = expPlaque; + } + } + if (opts.timingOverride && typeof opts.timingOverride === 'object') { + var t = opts.timingOverride; + if (t.carryReadMs != null) data.carryReadMs = t.carryReadMs; + if (t.carryAnswerMs != null) data.carryAnswerMs = t.carryAnswerMs; + if (t.carrySessionLength != null) data.carrySessionLength = t.carrySessionLength; + if (t.carryWalkSpeedMultForMapId !== undefined) data.carryWalkSpeedMultForMapId = t.carryWalkSpeedMultForMapId; + if (t.carryWalkSpeedMult !== undefined) data.carryWalkSpeedMult = t.carryWalkSpeedMult; + } + renderQuizCarryAdminList(data.carryQuestions || []); + var readSec = el('quiz-carry-read-sec'); + var ansSec = el('quiz-carry-answer-sec'); + if (readSec) { + var rms = Number(data.carryReadMs); + readSec.value = String(Number.isFinite(rms) ? Math.round(rms / 1000) : 3); + } + if (ansSec) { + var ams = Number(data.carryAnswerMs); + ansSec.value = String(Number.isFinite(ams) ? Math.round(ams / 1000) : 5); + } + var sessLen = el('quiz-carry-session-len'); + if (sessLen) { + var sl = parseInt(String(data.carrySessionLength), 10); + sessLen.value = String(Number.isFinite(sl) && sl >= 0 ? sl : 0); + } + var walkMapIdInp = el('quiz-carry-walk-map-id'); + if (walkMapIdInp) { + walkMapIdInp.value = data.carryWalkSpeedMultForMapId ? String(data.carryWalkSpeedMultForMapId).trim() : ''; + } + var walkMultInp = el('quiz-carry-walk-mult'); + if (walkMultInp) { + var wmv = Number(data.carryWalkSpeedMult); + walkMultInp.value = String(Number.isFinite(wmv) ? wmv : 1.42); + } + quizCarrySetPlaqueMapScaleInputs(data.carryChoicePlaqueMapScale); + quizCarryBindThemePickersOnce(); + var th = data.carryMapPanelTheme && typeof data.carryMapPanelTheme === 'object' ? data.carryMapPanelTheme : {}; + quizCarrySetPickersFromColorString('bg', th.panelBg); + quizCarrySetPickersFromColorString('border', th.panelBorder); + quizCarrySetPickersFromColorString('text', th.textColor); + var bwInp = el('quiz-carry-theme-border-w'); + if (bwInp) { + var bw = parseInt(String(th.borderWidthPx), 10); + bwInp.value = String(Number.isFinite(bw) && bw >= 0 ? Math.min(12, bw) : 2); + } + var qfMinInp = el('quiz-carry-theme-qfont-min'); + var qfMaxInp = el('quiz-carry-theme-qfont-max'); + if (qfMinInp) { + var qm = parseInt(String(th.questionFontMinPx), 10); + qfMinInp.value = String(Number.isFinite(qm) ? Math.max(10, Math.min(40, qm)) : 10); + } + if (qfMaxInp) { + var qx = parseInt(String(th.questionFontMaxPx), 10); + qfMaxInp.value = String(Number.isFinite(qx) ? Math.max(14, Math.min(56, qx)) : 24); + } + quizCarryEcdFillForm(data.carryEmbedCountdownTheme || {}); + quizCarryPlaqueFillAllFromApi(data); + if (opts.clearMsg !== false) setMsg('quiz-carry-settings-msg', '', ''); + }).catch(function (e) { + setMsg('quiz-carry-settings-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + }; + if (delay > 0) setTimeout(runLoad, delay); + else runLoad(); + } + + /** รอจน plaque upload จบ (pending=0) — กันกดบันทึกทันทีหลังเลือกไฟล์แล้ว build ได้ URL ว่าง · returns false if timeout */ + function waitUntilQuizCarryUploadsDone(maxMs) { + maxMs = maxMs || 120000; + return new Promise(function (resolve) { + if (quizCarryPlaqueUploadPending <= 0) return resolve(true); + var started = Date.now(); + var iv = setInterval(function () { + if (quizCarryPlaqueUploadPending <= 0) { + clearInterval(iv); + resolve(true); + } else if (Date.now() - started >= maxMs) { + clearInterval(iv); + resolve(false); + } + }, 40); + }); + } + + function saveQuizCarryPanel() { + var list = el('quiz-carry-admin-list'); + var saveBtn = el('btn-quiz-carry-save'); + if (!list) { + setMsg('quiz-carry-settings-msg', 'ไม่พบรายการคำถามในแท็บนี้ — รีเฟรชหน้า · quiz-carry-admin-list missing', 'error'); + return; + } + if (quizCarryPlaqueUploadPending > 0 && quizCarryPlaqueUploadStartMs > 0 && Date.now() - quizCarryPlaqueUploadStartMs > 120000) { + quizCarryPlaqueUploadPending = 0; + quizCarryPlaqueUploadStartMs = 0; + setMsg( + 'quiz-carry-settings-msg', + 'ยกเลิกสถานะอัปโหลดค้าง (>2 นาที) แล้วดำเนินการบันทึก — ถ้ายังต้องการรูปให้อัปโหลดใหม่ · Stuck upload cleared; re-upload if needed', + 'error', + ); + } + if (quizCarrySaveInFlight) { + setMsg('quiz-carry-settings-msg', 'กำลังบันทึกอยู่แล้ว — รอสักครู่ · Save already in progress', 'error'); + return; + } + var carryQuestions = []; + list.querySelectorAll('.quiz-carry-admin-block').forEach(function (block) { + var inpQ = block.querySelector('.quiz-carry-q-text'); + var text = inpQ && inpQ.value ? String(inpQ.value).trim() : ''; + if (!text) return; + var slots = []; + block.querySelectorAll('.quiz-carry-slot-label').forEach(function (lab, idx) { + var ti = lab.querySelector('.quiz-carry-slot-inp'); + var ii = lab.querySelector('.quiz-carry-slot-img'); + var tv = ti && ti.value ? String(ti.value).trim() : ''; + var iv = ii && ii.value ? String(ii.value).trim().slice(0, 512) : ''; + if (tv) slots.push({ idx: idx, text: tv, imageUrl: iv }); + }); + if (slots.length < 2) return; + var sel = block.querySelector('.quiz-carry-correct-sel'); + var slotStr = sel && !sel.disabled && sel.value !== '' ? sel.value : null; + if (slotStr == null) return; + var slotNum = parseInt(slotStr, 10); + var correctIndex = -1; + for (var i = 0; i < slots.length; i++) { + if (slots[i].idx === slotNum) { + correctIndex = i; + break; + } + } + if (correctIndex < 0) return; + var rowOut = { + text: text, + choices: slots.map(function (s) { + return s.text; + }), + correctIndex: correctIndex, + }; + var urls = slots.map(function (s) { + return s.imageUrl || ''; + }); + if (urls.some(function (u) { + return u; + })) rowOut.choiceImageUrls = urls; + carryQuestions.push(rowOut); + }); + + var readSecEl = el('quiz-carry-read-sec'); + var ansSecEl = el('quiz-carry-answer-sec'); + var readSec = readSecEl ? parseInt(String(readSecEl.value || '3'), 10) : 3; + var ansSec = ansSecEl ? parseInt(String(ansSecEl.value || '5'), 10) : 5; + if (!Number.isFinite(readSec) || readSec < 0) readSec = 0; + if (readSec > 120) readSec = 120; + if (!Number.isFinite(ansSec) || ansSec < 1) ansSec = 1; + if (ansSec > 300) ansSec = 300; + var carryReadMs = readSec * 1000; + var carryAnswerMs = ansSec * 1000; + var sessEl = el('quiz-carry-session-len'); + var carrySessionLength = sessEl ? parseInt(String(sessEl.value || '0'), 10) : 0; + if (!Number.isFinite(carrySessionLength) || carrySessionLength < 0) carrySessionLength = 0; + if (carrySessionLength > 500) carrySessionLength = 500; + + var carryMapPanelTheme = quizCarryBuildThemeForSave(); + + var blockEls = list.querySelectorAll('.quiz-carry-admin-block'); + var hasAnyQuestionText = false; + for (var bi = 0; bi < blockEls.length; bi++) { + var qn = blockEls[bi].querySelector('.quiz-carry-q-text'); + if (qn && String(qn.value || '').trim()) { + hasAnyQuestionText = true; + break; + } + } + if (carryQuestions.length === 0 && hasAnyQuestionText) { + setMsg( + 'quiz-carry-settings-msg', + 'ยังบันทึกชุดคำถามไม่ได้: แต่ละข้อต้องมีคำถาม ตัวเลือกอย่างน้อย 2 ช่องที่กรอกแล้ว และเลือก «ข้อที่ถูกต้อง» — ตรวจแถวที่มีข้อความค้าง', + 'error', + ); + return; + } + + quizCarrySaveInFlight = true; + if (saveBtn) { + saveBtn.disabled = true; + saveBtn.setAttribute('aria-busy', 'true'); + } + var waitLine = + quizCarryPlaqueUploadPending > 0 + ? 'กำลังรอให้อัปโหลดรูปป้ายจบ แล้วจะบันทึกอัตโนมัติ (ไม่ต้องกดซ้ำ)… · Waiting for plaque upload, then save…' + : 'กำลังบันทึก… · Saving…'; + setMsg('quiz-carry-settings-msg', waitLine, ''); + try { + var smEl = el('quiz-carry-settings-msg'); + if (smEl) smEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } catch (scrollSave) { /* ignore */ } + + waitUntilQuizCarryUploadsDone(120000).then(function (uploadsOk) { + if (!uploadsOk) { + quizCarrySaveInFlight = false; + if (saveBtn) { + saveBtn.disabled = false; + saveBtn.removeAttribute('aria-busy'); + } + setMsg( + 'quiz-carry-settings-msg', + 'รออัปโหลดนานเกิน 2 นาที — ตรวจเครือข่าย/เซิร์ฟเกมแล้วลองใหม่ · Upload wait timeout', + 'error', + ); + return; + } + var plaqueScaleEl = el('quiz-carry-plaque-map-scale'); + var plaqueMapScale = 1.25; + if (plaqueScaleEl) { + var psn = parseFloat(String(plaqueScaleEl.value || '').replace(',', '.')); + if (Number.isFinite(psn)) plaqueMapScale = Math.max(0.85, Math.min(2.5, psn)); + } + var walkMapIdRaw = el('quiz-carry-walk-map-id') && el('quiz-carry-walk-map-id').value ? String(el('quiz-carry-walk-map-id').value).trim() : ''; + var walkMapId = walkMapIdRaw.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64); + var walkMultRaw = el('quiz-carry-walk-mult') ? parseFloat(String(el('quiz-carry-walk-mult').value || '').replace(',', '.')) : NaN; + var putBody = { + carryReadMs: carryReadMs, + carryAnswerMs: carryAnswerMs, + carrySessionLength: carrySessionLength, + carryMapPanelTheme: carryMapPanelTheme, + carryEmbedCountdownTheme: quizCarryEcdBuildForSave(), + carryChoicePlaqueThemes: quizCarryPlaqueBuildForSave(), + carryChoicePlaqueMapScale: plaqueMapScale, + }; + if (walkMapId) { + putBody.carryWalkSpeedMultForMapId = walkMapId; + putBody.carryWalkSpeedMult = Number.isFinite(walkMultRaw) ? Math.max(0.5, Math.min(3, walkMultRaw)) : 1.42; + } else { + putBody.carryWalkSpeedMultForMapId = ''; + putBody.carryWalkSpeedMult = null; + } + if (carryQuestions.length > 0) { + putBody.carryQuestions = carryQuestions; + } + + gameQuizFetch('PUT', putBody).then(function (res) { + var tail = carryQuestions.length ? ' · คำถาม ' + carryQuestions.length + ' ข้อ' : ' · คงชุดคำถามเดิม (อัปเดตเวลา/ธีมอย่างเดียว)'; + var themeFromRes = res && res.carryMapPanelTheme && typeof res.carryMapPanelTheme === 'object' ? res.carryMapPanelTheme : null; + var ecdFromRes = res && res.carryEmbedCountdownTheme && typeof res.carryEmbedCountdownTheme === 'object' ? res.carryEmbedCountdownTheme : null; + var plaqueFromRes = null; + if (res && Array.isArray(res.carryChoicePlaqueThemes) && res.carryChoicePlaqueThemes.length) { + plaqueFromRes = res.carryChoicePlaqueThemes; + } else if (res && res.carryChoicePlaqueTheme && typeof res.carryChoicePlaqueTheme === 'object') { + plaqueFromRes = []; + for (var pxi = 0; pxi < QUIZ_CARRY_PLAQUE_SLOT_COUNT; pxi++) { + plaqueFromRes.push(res.carryChoicePlaqueTheme); + } + } + if (!plaqueFromRes) { + plaqueFromRes = quizCarryPlaqueBuildForSave(); + } + var themeOk = !!themeFromRes || !!ecdFromRes || !!plaqueFromRes; + var themeTail = themeOk ? ' · ธีมแผง/ป้าย/นับถอยหลังบันทึกแล้ว' : ''; + setMsg('quiz-carry-settings-msg', 'บันทึกแล้ว' + tail + themeTail, 'ok'); + quizCarryPlaquePendingUrlBySlot = {}; + loadQuizCarryPanel({ + clearMsg: false, + themeOverride: { + carryMapPanelTheme: themeFromRes || carryMapPanelTheme, + carryEmbedCountdownTheme: ecdFromRes || quizCarryEcdBuildForSave(), + carryChoicePlaqueThemes: Array.isArray(plaqueFromRes) ? plaqueFromRes : null, + carryChoicePlaqueTheme: + Array.isArray(plaqueFromRes) && plaqueFromRes[0] && typeof plaqueFromRes[0] === 'object' + ? plaqueFromRes[0] + : null, + }, + timingOverride: { + carryReadMs: carryReadMs, + carryAnswerMs: carryAnswerMs, + carrySessionLength: carrySessionLength, + carryWalkSpeedMultForMapId: walkMapId, + carryWalkSpeedMult: walkMapId ? (Number.isFinite(walkMultRaw) ? Math.max(0.5, Math.min(3, walkMultRaw)) : 1.42) : null, + }, + fetchDelayMs: 120, + }); + }).catch(function (e) { + setMsg('quiz-carry-settings-msg', e.message || 'บันทึกไม่ได้', 'error'); + }).finally(function () { + quizCarrySaveInFlight = false; + if (saveBtn) { + saveBtn.disabled = false; + saveBtn.removeAttribute('aria-busy'); + } + }); + }); + } + + /** หมวด Quiz Battle — ชื่อไทยให้สอดคล้องธีมการศึกษาไซเบอร์ / หน้า Quiz-Battle */ + var QB_LABELS_ABC = ['A', 'B', 'C']; + + var QUIZ_BATTLE_CATEGORIES = [ + { id: 'cybercrime', labelTh: 'อาชญากรรมออนไลน์', labelEn: 'Cybercrime' }, + { id: 'cyber_law', labelTh: 'กฎหมายไซเบอร์', labelEn: 'Cyber law' }, + { id: 'privacy', labelTh: 'ความเป็นส่วนตัวและข้อมูลส่วนบุคคล', labelEn: 'Privacy & PDPA' }, + { id: 'digital_security', labelTh: 'ความปลอดภัยดิจิทัล', labelEn: 'Digital security' }, + { id: 'social_media', labelTh: 'สื่อสังคมออนไลน์', labelEn: 'Social media' }, + { id: 'ethics', labelTh: 'จริยธรรมดิจิทัล', labelEn: 'Digital ethics' }, + { id: 'ai_data', labelTh: 'AI และข้อมูล', labelEn: 'AI & data' }, + ]; + + function qbBattleCategoryLabel(catId) { + for (var i = 0; i < QUIZ_BATTLE_CATEGORIES.length; i++) { + if (QUIZ_BATTLE_CATEGORIES[i].id === catId) return QUIZ_BATTLE_CATEGORIES[i].labelTh; + } + return 'Quiz Battle'; + } + + function fillQbBattleFilterOptions() { + var sel = el('qb-battle-filter-cat'); + if (!sel) return; + var keep = sel.value; + while (sel.options.length > 1) sel.remove(1); + QUIZ_BATTLE_CATEGORIES.forEach(function (c) { + var o = document.createElement('option'); + o.value = c.id; + o.textContent = c.labelTh; + sel.appendChild(o); + }); + if (keep) { + for (var oi = 0; oi < sel.options.length; oi++) { + if (sel.options[oi].value === keep) { + sel.value = keep; + break; + } + } + } + } + + function applyQbBattleFilter() { + var list = el('qb-battle-admin-list'); + var f = el('qb-battle-filter-cat'); + if (!list || !f) return; + var fv = f.value || ''; + list.querySelectorAll('.qb-battle-block').forEach(function (row) { + var catInp = row.querySelector('.qb-battle-cat'); + var cat = catInp && catInp.value ? catInp.value : ''; + row.hidden = !!(fv && cat !== fv); + }); + refreshQbBattlePreview(); + } + + function getFirstVisibleQbBattleBlock() { + var list = el('qb-battle-admin-list'); + if (!list) return null; + var rows = list.querySelectorAll('.qb-battle-block'); + for (var i = 0; i < rows.length; i++) { + if (rows[i].hidden) continue; + var q = rows[i].querySelector('.qb-battle-q-text'); + var t = q && q.value ? String(q.value).trim() : ''; + if (t) return rows[i]; + } + for (var j = 0; j < rows.length; j++) { + if (!rows[j].hidden) return rows[j]; + } + return null; + } + + function readQbBattleBlockData(row) { + if (!row) return null; + var catSel = row.querySelector('.qb-battle-cat'); + var qInp = row.querySelector('.qb-battle-q-text'); + var a0 = row.querySelector('.qb-battle-a'); + var a1 = row.querySelector('.qb-battle-b'); + var a2 = row.querySelector('.qb-battle-c'); + var radios = row.querySelectorAll('input[type="radio"][name^="qb-correct-"]'); + var ci = 0; + for (var i = 0; i < radios.length; i++) { + if (radios[i].checked) { + ci = parseInt(radios[i].value, 10); + if (!Number.isFinite(ci)) ci = 0; + break; + } + } + return { + categoryId: catSel && catSel.value ? catSel.value : 'cybercrime', + text: qInp && qInp.value ? String(qInp.value).trim() : '', + choices: [ + a0 && a0.value ? String(a0.value).trim() : '', + a1 && a1.value ? String(a1.value).trim() : '', + a2 && a2.value ? String(a2.value).trim() : '', + ], + correctIndex: Math.max(0, Math.min(2, ci)), + }; + } + + function refreshQbBattlePreview() { + var hudCat = el('qb-preview-hud-cat'); + var qtext = el('qb-preview-modal-q'); + var c0 = el('qb-preview-c0'); + var c1 = el('qb-preview-c1'); + var c2 = el('qb-preview-c2'); + var qModal = el('qb-cyber-modal-question'); + var sModal = el('qb-cyber-modal-summary'); + var layer = el('qb-modal-layer'); + if (!hudCat || !qtext || !c0 || !c1 || !c2 || !qModal || !sModal) return; + + var modeBtn = document.querySelector('.qb-preview-mode-btn.is-active'); + var mode = modeBtn && modeBtn.getAttribute('data-qb-mode') ? modeBtn.getAttribute('data-qb-mode') : 'question'; + + var row = getFirstVisibleQbBattleBlock(); + var d = readQbBattleBlockData(row); + var defQ = 'การกระทำใดผิดกฎหมายไซเบอร์?'; + var defC = [ + 'การเจาะระบบเข้าสู่ข้อมูลส่วนบุคคล', + 'การใช้ WI-FI สาธารณะเพื่อดูข่าว', + 'การอัปเดตซอฟต์แวร์เพื่อความปลอดภัย', + ]; + hudCat.textContent = d ? qbBattleCategoryLabel(d.categoryId) : qbBattleCategoryLabel('cybercrime'); + qtext.textContent = d && d.text ? d.text : defQ; + var ch = d ? d.choices : defC; + for (var s = 0; s < 3; s++) { + var eln = s === 0 ? c0 : s === 1 ? c1 : c2; + var txt = ch[s] ? ch[s] : defC[s]; + eln.textContent = QB_LABELS_ABC[s] + ' : ' + txt; + } + + var choiceEls = document.querySelectorAll('#qb-preview-choices .qb-cyber-choice'); + choiceEls.forEach(function (node) { + node.classList.remove('qb-cyber-choice--correct', 'qb-cyber-choice--wrong', 'qb-cyber-choice--neutral'); + var mark = node.querySelector('.qb-cyber-choice-mark'); + if (mark) mark.textContent = ''; + }); + + if (mode === 'summary') { + qModal.hidden = true; + sModal.hidden = false; + if (layer) layer.classList.add('qb-modal-layer--summary'); + } else { + qModal.hidden = false; + sModal.hidden = true; + if (layer) layer.classList.remove('qb-modal-layer--summary'); + var ci = d ? d.correctIndex : 0; + if (mode === 'revealed') { + var wrongPick = (ci + 1) % 3; + choiceEls.forEach(function (node, idx) { + var mark = node.querySelector('.qb-cyber-choice-mark'); + if (idx === ci) { + node.classList.add('qb-cyber-choice--correct'); + if (mark) mark.textContent = '✓'; + } else if (idx === wrongPick) { + node.classList.add('qb-cyber-choice--wrong'); + if (mark) mark.textContent = '✕'; + } else { + node.classList.add('qb-cyber-choice--neutral'); + } + }); + } + } + } + + function addQbBattleRow(q) { + var list = el('qb-battle-admin-list'); + if (!list) return; + q = q || {}; + var uid = 'r' + Math.random().toString(36).slice(2, 10); + var block = document.createElement('div'); + block.className = 'qb-battle-block'; + block.setAttribute('data-category', q.categoryId || 'cybercrime'); + + var head = document.createElement('div'); + head.className = 'qb-battle-block-head'; + + var labCat = document.createElement('label'); + labCat.className = 'qb-battle-cat-label'; + labCat.appendChild(document.createTextNode('หมวด')); + var selCat = document.createElement('select'); + selCat.className = 'qb-battle-cat'; + QUIZ_BATTLE_CATEGORIES.forEach(function (c) { + var o = document.createElement('option'); + o.value = c.id; + o.textContent = c.labelTh; + if ((q.categoryId || 'cybercrime') === c.id) o.selected = true; + selCat.appendChild(o); + }); + selCat.addEventListener('change', function () { + block.setAttribute('data-category', selCat.value); + applyQbBattleFilter(); + }); + labCat.appendChild(selCat); + + var rm = document.createElement('button'); + rm.type = 'button'; + rm.className = 'btn btn-danger btn-sm qb-battle-remove'; + rm.textContent = 'ลบข้อ'; + rm.addEventListener('click', function () { + block.remove(); + if (!list.querySelector('.qb-battle-block')) addQbBattleRow(null); + applyQbBattleFilter(); + }); + + head.appendChild(labCat); + head.appendChild(rm); + block.appendChild(head); + + var labQ = document.createElement('label'); + labQ.className = 'qb-battle-q-label'; + labQ.appendChild(document.createTextNode('คำถาม')); + var inpQ = document.createElement('input'); + inpQ.type = 'text'; + inpQ.className = 'qb-battle-q-text'; + inpQ.maxLength = 500; + inpQ.placeholder = 'พิมพ์คำถาม…'; + inpQ.value = q.text ? String(q.text) : ''; + labQ.appendChild(inpQ); + block.appendChild(labQ); + + var choices = Array.isArray(q.choices) ? q.choices : []; + var grid = document.createElement('div'); + grid.className = 'qb-battle-abc-grid'; + [['A', 'a', 0], ['B', 'b', 1], ['C', 'c', 2]].forEach(function (tri) { + var lab = document.createElement('label'); + lab.className = 'qb-battle-opt-label'; + lab.appendChild(document.createTextNode('ตัวเลือก ' + tri[0])); + var inp = document.createElement('input'); + inp.type = 'text'; + inp.className = 'qb-battle-' + tri[1]; + inp.maxLength = 160; + inp.placeholder = tri[0]; + inp.value = choices[tri[2]] != null ? String(choices[tri[2]]) : ''; + lab.appendChild(inp); + grid.appendChild(lab); + }); + block.appendChild(grid); + + var ci = Number(q.correctIndex); + if (!Number.isFinite(ci)) ci = 0; + ci = Math.max(0, Math.min(2, Math.floor(ci))); + + var fs = document.createElement('fieldset'); + fs.className = 'qb-battle-correct-fs'; + var leg = document.createElement('legend'); + leg.textContent = 'ข้อที่ถูกต้อง'; + fs.appendChild(leg); + var rg = document.createElement('div'); + rg.className = 'qb-battle-correct-rg'; + for (var r = 0; r < 3; r++) { + var labR = document.createElement('label'); + labR.className = 'qb-battle-radio-label'; + var radio = document.createElement('input'); + radio.type = 'radio'; + radio.name = 'qb-correct-' + uid; + radio.value = String(r); + if (r === ci) radio.checked = true; + labR.appendChild(radio); + labR.appendChild(document.createTextNode(' ' + QB_LABELS_ABC[r])); + rg.appendChild(labR); + } + fs.appendChild(rg); + block.appendChild(fs); + + function onFieldInput() { + refreshQbBattlePreview(); + } + inpQ.addEventListener('input', onFieldInput); + grid.querySelectorAll('input').forEach(function (inp) { + inp.addEventListener('input', onFieldInput); + }); + rg.querySelectorAll('input[type="radio"]').forEach(function (rdo) { + rdo.addEventListener('change', onFieldInput); + }); + + list.appendChild(block); + applyQbBattleFilter(); + } + + function renderQbBattleList(arr, clearMsg) { + var list = el('qb-battle-admin-list'); + if (!list) return; + list.innerHTML = ''; + fillQbBattleFilterOptions(); + var rows = arr && arr.length ? arr : [null]; + rows.forEach(function (q) { + addQbBattleRow(q); + }); + if (clearMsg !== false) setMsg('qb-battle-settings-msg', '', ''); + } + + function loadQbBattlePanel(keepSavedMsg) { + gameQuizFetch('GET').then(function (data) { + renderQbBattleList(data.battleQuizMcq || [], !keepSavedMsg); + refreshQbBattlePreview(); + }).catch(function (e) { + setMsg('qb-battle-settings-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + } + + function classifyQbBattleRow(block) { + var d = readQbBattleBlockData(block); + if (!d) return { kind: 'empty', d: null }; + var hasQ = !!d.text; + var filled = 0; + for (var i = 0; i < d.choices.length; i++) { + if (d.choices[i]) filled++; + } + if (!hasQ && filled === 0) return { kind: 'empty', d: d }; + if (hasQ && filled === 3) return { kind: 'ok', d: d }; + return { kind: 'partial', d: d }; + } + + function saveQbBattlePanel() { + var list = el('qb-battle-admin-list'); + if (!list) return; + var battleQuizMcq = []; + var partialLines = []; + var blocks = list.querySelectorAll('.qb-battle-block'); + for (var bi = 0; bi < blocks.length; bi++) { + var block = blocks[bi]; + var st = classifyQbBattleRow(block); + if (st.kind === 'empty') continue; + if (st.kind === 'partial') { + partialLines.push('แถวที่ ' + (bi + 1) + ': ต้องกรอกคำถามและตัวเลือก A, B, C ให้ครบทุกช่อง'); + continue; + } + battleQuizMcq.push({ + categoryId: st.d.categoryId, + text: st.d.text, + choices: st.d.choices, + correctIndex: st.d.correctIndex, + }); + } + if (partialLines.length) { + setMsg( + 'qb-battle-settings-msg', + 'ยังบันทึกไม่ได้ — ' + partialLines.join(' · ') + ' · (Empty question/options are skipped by the server; fill all fields.)', + 'error' + ); + return; + } + + gameQuizFetch('PUT', { battleQuizMcq: battleQuizMcq }).then(function (res) { + var n = battleQuizMcq.length; + var savedN = res && typeof res.battleQuizMcqSaved === 'number' ? res.battleQuizMcqSaved : n; + if (n > 0 && savedN !== n) { + setMsg( + 'qb-battle-settings-msg', + 'เซิร์ฟเวอร์รับเพียง ' + savedN + ' จาก ' + n + ' ข้อ (ข้อที่ไม่ผ่านกฎถูกตัด) · Server kept ' + savedN + '/' + n + ' — ตรวจหมวด/คำถาม/A B C ให้ครบ · Restart Node (Game/server.js) ถ้ายังผิดปกติ', + 'error' + ); + } else { + setMsg( + 'qb-battle-settings-msg', + n ? ('บันทึกแล้ว ' + n + ' ข้อ (ดิสก์ + Node) · Saved ' + n + ' question(s)') : 'บันทึกแล้ว (0 ข้อ) · Saved empty set', + 'ok' + ); + } + return loadQbBattlePanel(true); + }).catch(function (e) { + setMsg('qb-battle-settings-msg', e.message || 'บันทึกไม่ได้', 'error'); + }); + } + + function laneUrlsToText(arr) { + if (!arr || !arr.length) return ''; + return arr.map(function (u) { return String(u || '').trim(); }).filter(Boolean).join('\n'); + } + + function parseLaneUrlsTextarea() { + var raw = el('game-timing-lane-urls') && el('game-timing-lane-urls').value ? String(el('game-timing-lane-urls').value) : ''; + var lines = raw.split(/[\n\r]+/); + var out = []; + for (var i = 0; i < lines.length && out.length < 24; i++) { + var t = lines[i].trim(); + if (t) out.push(t); + } + return out; + } + + function moveLaneUrl(index, dir) { + var urls = parseLaneUrlsTextarea(); + var j = index + dir; + if (j < 0 || j >= urls.length) return; + var t = urls[index]; + urls[index] = urls[j]; + urls[j] = t; + var ta = el('game-timing-lane-urls'); + if (ta) ta.value = urls.join('\n'); + renderGameTimingLaneVisuals(); + } + + function renderGameTimingLaneVisuals() { + var wrap = el('game-timing-lane-visual-list'); + if (!wrap) return; + wrap.innerHTML = ''; + var urls = parseLaneUrlsTextarea(); + if (!urls.length) { + var ph = document.createElement('p'); + ph.className = 'muted'; + ph.style.margin = '0'; + ph.textContent = 'ยังไม่มีรูป — กด «Lane» ที่การ์ดในคลัง · No lane images yet'; + wrap.appendChild(ph); + return; + } + urls.forEach(function (u, i) { + var card = document.createElement('div'); + card.className = 'game-timing-visual-card'; + card.setAttribute('role', 'listitem'); + var img = document.createElement('img'); + img.className = 'game-timing-visual-card-img'; + img.src = u; + img.alt = ''; + img.referrerPolicy = 'no-referrer'; + img.onerror = function () { img.classList.add('is-broken'); }; + var actions = document.createElement('div'); + actions.className = 'game-timing-visual-card-actions'; + function mkBtn(cls, text, fn) { + var b = document.createElement('button'); + b.type = 'button'; + b.className = 'btn btn-sm ' + cls; + b.textContent = text; + b.addEventListener('click', fn); + return b; + } + actions.appendChild(mkBtn('btn-ghost', 'ดู', function () { + window.open(u, '_blank', 'noopener,noreferrer'); + })); + actions.appendChild(mkBtn('btn-ghost', '←', function () { + moveLaneUrl(i, -1); + })); + actions.appendChild(mkBtn('btn-ghost', '→', function () { + moveLaneUrl(i, 1); + })); + actions.appendChild(mkBtn('btn-danger', 'ลบ', function () { + var next = urls.filter(function (_, j) { return j !== i; }); + var ta = el('game-timing-lane-urls'); + if (ta) ta.value = next.join('\n'); + renderGameTimingLaneVisuals(); + setGauntletUploadMsg('เอาออกจาก Lane แล้ว (ไฟล์ในคลังยังอยู่) · Removed from lane', 'ok'); + })); + card.appendChild(img); + card.appendChild(actions); + wrap.appendChild(card); + }); + } + + /** พรีวิวคอลัมน์ beam — สูงแนวตั้ง + สีเติม/กรอบตามช่อง (คล้ายในเกม) */ + function styleGameTimingLaserBeamPreviewShell() { + var prev = el('game-timing-laser-line-preview'); + if (!prev) return; + var fillIn = el('game-timing-laser-fill'); + var strokeIn = el('game-timing-laser-stroke'); + var lwIn = el('game-timing-laser-line-width'); + var fill = fillIn && fillIn.value ? String(fillIn.value).trim() : ''; + var stroke = strokeIn && strokeIn.value ? String(strokeIn.value).trim() : ''; + var lw = lwIn ? parseInt(lwIn.value, 10) : 2; + if (Number.isNaN(lw)) lw = 2; + lw = Math.max(0, Math.min(24, lw)); + prev.style.backgroundColor = fill || ''; + if (lw > 0 && stroke) { + prev.style.boxShadow = 'inset 0 0 0 ' + Math.min(8, lw) + 'px ' + stroke; + } else { + prev.style.boxShadow = ''; + } + } + + function renderGameTimingLaserVisuals() { + var slots = [ + { inpId: 'game-timing-laser-top-url', prevId: 'game-timing-laser-top-preview' }, + { inpId: 'game-timing-laser-bottom-url', prevId: 'game-timing-laser-bottom-preview' }, + { inpId: 'game-timing-laser-line-url', prevId: 'game-timing-laser-line-preview' }, + ]; + slots.forEach(function (s) { + var inp = el(s.inpId); + var prev = el(s.prevId); + if (!prev) return; + prev.innerHTML = ''; + var url = inp && inp.value ? String(inp.value).trim() : ''; + if (!url) { + var ph = document.createElement('div'); + ph.className = 'game-timing-laser-placeholder'; + ph.textContent = 'ยังไม่ตั้ง · Not set'; + prev.appendChild(ph); + return; + } + var img = document.createElement('img'); + img.className = 'game-timing-laser-preview-img'; + img.src = url; + img.alt = ''; + img.referrerPolicy = 'no-referrer'; + img.onerror = function () { img.classList.add('is-broken'); }; + prev.appendChild(img); + }); + styleGameTimingLaserBeamPreviewShell(); + } + + function clamp255(x) { + return Math.max(0, Math.min(255, Math.round(Number(x)))); + } + + function parseCssColorToRgba(str) { + str = String(str || '').trim(); + if (!str) return null; + var m = str.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i); + if (m) { + var r = clamp255(m[1]); + var g = clamp255(m[2]); + var b = clamp255(m[3]); + var a = + m[4] !== undefined && m[4] !== '' && !Number.isNaN(Number(m[4])) + ? Math.max(0, Math.min(1, Number(m[4]))) + : 1; + return { r: r, g: g, b: b, a: a }; + } + m = str.match(/^#([\da-f]{3}|[\da-f]{6})$/i); + if (m) { + var h = m[1]; + var r2; + var g2; + var b2; + if (h.length === 3) { + r2 = parseInt(h[0] + h[0], 16); + g2 = parseInt(h[1] + h[1], 16); + b2 = parseInt(h[2] + h[2], 16); + } else { + r2 = parseInt(h.slice(0, 2), 16); + g2 = parseInt(h.slice(2, 4), 16); + b2 = parseInt(h.slice(4, 6), 16); + } + if (Number.isNaN(r2) || Number.isNaN(g2) || Number.isNaN(b2)) return null; + return { r: r2, g: g2, b: b2, a: 1 }; + } + var probe = document.createElement('span'); + probe.style.cssText = 'position:absolute;visibility:hidden;left:0;top:0;background-color:' + str; + document.body.appendChild(probe); + var bg = getComputedStyle(probe).backgroundColor; + document.body.removeChild(probe); + m = bg && bg.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (!m) return null; + r2 = clamp255(m[1]); + g2 = clamp255(m[2]); + b2 = clamp255(m[3]); + var a2 = m[4] !== undefined && !Number.isNaN(Number(m[4])) ? Math.max(0, Math.min(1, Number(m[4]))) : 1; + return { r: r2, g: g2, b: b2, a: a2 }; + } + + function rgbToHex6(r, g, b) { + function hx(n) { + var h = clamp255(n).toString(16); + return h.length === 1 ? '0' + h : h; + } + return '#' + hx(r) + hx(g) + hx(b); + } + + function laserHexFromTextOrFallback(text, fallbackHex) { + var c = parseCssColorToRgba(text); + if (!c) return fallbackHex; + return rgbToHex6(c.r, c.g, c.b); + } + + function formatLaserRgba(r, g, b, a) { + var aa = Math.round(Math.max(0, Math.min(1, a)) * 100) / 100; + return 'rgba(' + clamp255(r) + ',' + clamp255(g) + ',' + clamp255(b) + ',' + aa + ')'; + } + + function syncGameTimingLaserColorSwatches() { + function apply(swatchId, textVal) { + var sw = el(swatchId); + if (!sw) return; + var v = textVal != null ? String(textVal).trim() : ''; + sw.style.backgroundColor = v || ''; + sw.classList.toggle('is-empty', !v); + sw.title = v || 'ว่าง — พิมพ์ค่า CSS หรือกดสี่เหลี่ยมเพื่อเลือกสี'; + } + var fillIn = el('game-timing-laser-fill'); + var strokeIn = el('game-timing-laser-stroke'); + apply('game-timing-laser-fill-swatch', fillIn && fillIn.value); + apply('game-timing-laser-stroke-swatch', strokeIn && strokeIn.value); + var fillPk = el('game-timing-laser-fill-picker'); + var strokePk = el('game-timing-laser-stroke-picker'); + if (fillPk && fillIn) { + var hf = laserHexFromTextOrFallback(fillIn.value, '#ef4444'); + if (fillPk.value.toLowerCase() !== hf.toLowerCase()) fillPk.value = hf; + } + if (strokePk && strokeIn) { + var hs = laserHexFromTextOrFallback(strokeIn.value, '#fca5a5'); + if (strokePk.value.toLowerCase() !== hs.toLowerCase()) strokePk.value = hs; + } + styleGameTimingLaserBeamPreviewShell(); + } + + function bindGameTimingLaserColorSwatches() { + var fillIn = el('game-timing-laser-fill'); + var strokeIn = el('game-timing-laser-stroke'); + if (fillIn) { + fillIn.addEventListener('input', syncGameTimingLaserColorSwatches); + fillIn.addEventListener('change', syncGameTimingLaserColorSwatches); + } + if (strokeIn) { + strokeIn.addEventListener('input', syncGameTimingLaserColorSwatches); + strokeIn.addEventListener('change', syncGameTimingLaserColorSwatches); + } + function wirePicker(pickerId, textEl, defaultAlpha) { + var pk = el(pickerId); + var tx = textEl; + if (!pk || !tx) return; + function applyHex() { + var hex = String(pk.value || '').trim(); + if (!/^#[\da-f]{6}$/i.test(hex)) return; + var r = parseInt(hex.slice(1, 3), 16); + var g = parseInt(hex.slice(3, 5), 16); + var b = parseInt(hex.slice(5, 7), 16); + var prev = parseCssColorToRgba(tx.value); + var a = prev && typeof prev.a === 'number' ? prev.a : defaultAlpha; + tx.value = formatLaserRgba(r, g, b, a); + syncGameTimingLaserColorSwatches(); + } + pk.addEventListener('input', applyHex); + pk.addEventListener('change', applyHex); + } + wirePicker('game-timing-laser-fill-picker', fillIn, 0.35); + wirePicker('game-timing-laser-stroke-picker', strokeIn, 0.95); + var lwIn = el('game-timing-laser-line-width'); + if (lwIn) { + lwIn.addEventListener('input', styleGameTimingLaserBeamPreviewShell); + lwIn.addEventListener('change', styleGameTimingLaserBeamPreviewShell); + } + syncGameTimingLaserColorSwatches(); + } + + function bindGameTimingAssignedVisuals() { + function wire(which, viewId, clearId) { + var inpId = 'game-timing-laser-' + which + '-url'; + var vBtn = el(viewId); + var cBtn = el(clearId); + if (vBtn) vBtn.addEventListener('click', function () { + var inp = el(inpId); + var u = inp && inp.value ? String(inp.value).trim() : ''; + if (!u) return; + window.open(u, '_blank', 'noopener,noreferrer'); + }); + if (cBtn) cBtn.addEventListener('click', function () { + var inp = el(inpId); + if (inp) inp.value = ''; + renderGameTimingLaserVisuals(); + setGauntletUploadMsg('ล้างช่อง Laser แล้ว · Laser slot cleared', 'ok'); + }); + } + wire('top', 'btn-game-timing-laser-top-view', 'btn-game-timing-laser-top-clear'); + wire('bottom', 'btn-game-timing-laser-bottom-view', 'btn-game-timing-laser-bottom-clear'); + wire('line', 'btn-game-timing-laser-line-view', 'btn-game-timing-laser-line-clear'); + } + + function gauntletAssetsJsonFetch(url, opts) { + opts = opts || {}; + opts.credentials = 'include'; + return fetch(url, opts).then(function (r) { + return r.text().then(function (text) { + var j = {}; + var parsed = false; + try { + j = text ? JSON.parse(text) : {}; + parsed = true; + } catch (e) { + if (!r.ok) { + var snippet = (text || '').replace(/\s+/g, ' ').trim().slice(0, 220); + var tail = ''; + if (r.status === 413) { + tail = + ' · คำขอใหญ่เกิน (nginx client_max_body_size / PHP post_max_size) — รัน deploy-nginx และตั้ง post_max_size ≥20M · Payload too large'; + } else if (r.status === 401) { + tail = ' · ล็อกอิน Admin ใหม่ · Re-login to Admin'; + } else if (r.status === 502 || r.status === 503 || r.status === 504) { + tail = ' · เซิร์ฟเกม/PHP upstream ล่ม — ตรวจ Node + php-fpm · Upstream error'; + } else if (r.status === 404) { + tail = ' · ตรวจ path ไฟล์ / รีสตาร์ทบริการหลัง deploy · Not found'; + } + throw new Error((snippet || r.statusText || 'Request failed') + tail + ' (HTTP ' + r.status + ')'); + } + throw e; + } + if (!r.ok) { + var plain = (j && j.error) || r.statusText || 'Request failed'; + if (r.status === 413) { + plain = + (j && j.error) || + 'ไฟล์/คำขอใหญ่เกินขีดจำกัด (nginx หรือ PHP) — ตั้ง client_max_body_size (server หรือ /Admin/api/) และ post_max_size ≥20M · Payload too large'; + } + if (r.status === 401) { + plain = + (j && j.error) || + 'หมดเซสชันแอดมิน — ล็อกอินใหม่แล้วลองอัปโหลดอีกครั้ง · Re-login to Admin and retry upload'; + } + if (r.status === 404) { + plain += ' — รีสตาร์ท Node ที่รัน Game/server.js หลัง deploy · Restart Game Node'; + } + if (r.status === 502 || r.status === 503 || r.status === 504) { + plain += + ' — PHP/Node upstream ล่ม — ตรวจ game-zep (Node) และ php-fpm · Check Node + PHP-FPM'; + } + if (parsed && (!j || Object.keys(j).length === 0)) { + plain = (text || '').replace(/\s+/g, ' ').trim().slice(0, 200) || plain; + if (r.status) plain += ' (HTTP ' + r.status + ')'; + } + throw new Error(plain); + } + return j; + }); + }); + } + + function setGauntletUploadMsg(text, cls) { + setMsg('gauntlet-asset-upload-msg', text, cls); + } + + function readFileAsDataURL(file) { + return new Promise(function (resolve, reject) { + var fr = new FileReader(); + fr.onload = function () { resolve(fr.result); }; + fr.onerror = function () { reject(new Error('อ่านไฟล์ไม่ได้ · Cannot read file')); }; + fr.readAsDataURL(file); + }); + } + + function appendLaneUrlUnique(url) { + var ta = el('game-timing-lane-urls'); + if (!ta || !url) return; + var lines = parseLaneUrlsTextarea(); + if (lines.indexOf(url) >= 0) { + setGauntletUploadMsg('มี URL นี้ใน Lane แล้ว · Already in lane list', 'ok'); + return; + } + lines.push(url); + ta.value = lines.join('\n'); + renderGameTimingLaneVisuals(); + setGauntletUploadMsg('เพิ่มใน Lane แล้ว · Added to lane URLs', 'ok'); + } + + function setLaserUrlField(which, url) { + var ids = { top: 'game-timing-laser-top-url', bottom: 'game-timing-laser-bottom-url', line: 'game-timing-laser-line-url' }; + var inp = el(ids[which]); + if (!inp) return; + inp.value = url; + renderGameTimingLaserVisuals(); + setGauntletUploadMsg('ตั้งค่า Laser แล้ว · Laser field updated', 'ok'); + } + + function uploadGauntletDataUrls(fileArr) { + var files = [].slice.call(fileArr || []); + if (!files.length) return Promise.resolve(); + var rf = gauntletReplaceTarget; + gauntletReplaceTarget = null; + if (rf) files = files.slice(0, 1); + setGauntletUploadMsg('กำลังอัปโหลด… · Uploading…', ''); + var idx = 0; + function step() { + if (idx >= files.length) { + setGauntletUploadMsg(rf ? 'แทนที่รูปแล้ว · Image replaced' : ('อัปโหลดแล้ว ' + files.length + ' ไฟล์ · Uploaded'), 'ok'); + return refreshGauntletAssetLibrary(); + } + var file = files[idx++]; + return readFileAsDataURL(file).then(function (dataUrl) { + var body = { imageDataUrl: dataUrl }; + if (rf) body.replaceFilename = rf; + else if (file.name) body.label = String(file.name).replace(/\.[^.]+$/, '').slice(0, 120); + return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + }).then(step).catch(function (e) { + setGauntletUploadMsg(e.message || 'อัปโหลดไม่สำเร็จ · Upload failed', 'error'); + return refreshGauntletAssetLibrary(); + }); + } + return step(); + } + + function refreshGauntletAssetLibrary() { + var listEl = el('gauntlet-asset-list'); + if (!listEl) return Promise.resolve(); + return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API, { method: 'GET' }) + .then(function (data) { + listEl.innerHTML = ''; + var items = data.items || []; + if (!items.length) { + var empty = document.createElement('p'); + empty.className = 'muted'; + empty.style.margin = '0'; + empty.textContent = 'ยังไม่มีรูปในคลัง — อัปโหลดด้านบน · No assets yet — upload above'; + listEl.appendChild(empty); + return; + } + items.forEach(function (it, ix) { + var card = document.createElement('article'); + card.className = 'gauntlet-asset-card'; + + var img = document.createElement('img'); + img.className = 'gauntlet-asset-card-preview'; + img.src = it.url + '?t=' + (it.mtime || 0); + img.alt = it.label || ''; + + var labelWrap = document.createElement('div'); + labelWrap.className = 'gauntlet-asset-card-label-wrap'; + var labelInp = document.createElement('input'); + labelInp.type = 'text'; + labelInp.maxLength = 120; + labelInp.value = it.label || ''; + var lid = 'gauntlet-asset-label-' + ix; + labelInp.id = lid; + var lab = document.createElement('label'); + lab.htmlFor = lid; + lab.textContent = 'ชื่อเรียก · Label'; + labelWrap.appendChild(lab); + labelWrap.appendChild(labelInp); + + var urlP = document.createElement('p'); + urlP.className = 'gauntlet-asset-card-url'; + urlP.textContent = it.url; + + var actions = document.createElement('div'); + actions.className = 'gauntlet-asset-card-actions'; + + function mkBtn(cls, text, handler) { + var b = document.createElement('button'); + b.type = 'button'; + b.className = 'btn ' + cls; + b.textContent = text; + b.addEventListener('click', handler); + return b; + } + + actions.appendChild(mkBtn('btn-ghost', 'ดูรูป', function () { + window.open(it.url, '_blank', 'noopener,noreferrer'); + })); + actions.appendChild(mkBtn('btn-ghost', 'บันทึกชื่อ', function () { + gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename: it.filename, label: labelInp.value }), + }) + .then(function () { + setGauntletUploadMsg('บันทึกชื่อแล้ว · Label saved', 'ok'); + return refreshGauntletAssetLibrary(); + }) + .catch(function (e) { + setGauntletUploadMsg(e.message, 'error'); + }); + })); + actions.appendChild(mkBtn('btn-ghost', 'แทนที่รูป', function () { + gauntletReplaceTarget = it.filename; + var finp = el('gauntlet-asset-file-input'); + if (finp) finp.click(); + })); + actions.appendChild(mkBtn('btn-ghost', 'Lane', function () { + appendLaneUrlUnique(it.url); + })); + actions.appendChild(mkBtn('btn-ghost', 'Laser บน', function () { + setLaserUrlField('top', it.url); + })); + actions.appendChild(mkBtn('btn-ghost', 'Laser ล่าง', function () { + setLaserUrlField('bottom', it.url); + })); + actions.appendChild(mkBtn('btn-ghost', 'Laser เส้น', function () { + setLaserUrlField('line', it.url); + })); + actions.appendChild(mkBtn('btn-danger', 'ลบ', function () { + if (!confirm('ลบไฟล์ ' + it.filename + ' จากเซิร์ฟเวอร์? · Delete from server?')) return; + gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '?file=' + encodeURIComponent(it.filename), { method: 'DELETE' }) + .then(function () { + setGauntletUploadMsg('ลบแล้ว · Deleted', 'ok'); + return refreshGauntletAssetLibrary(); + }) + .catch(function (e) { + setGauntletUploadMsg(e.message, 'error'); + }); + })); + + card.appendChild(img); + card.appendChild(labelWrap); + card.appendChild(urlP); + card.appendChild(actions); + listEl.appendChild(card); + }); + }) + .catch(function (e) { + listEl.innerHTML = ''; + var p = document.createElement('p'); + p.className = 'msg error'; + p.textContent = e.message || 'โหลดคลังไม่ได้'; + listEl.appendChild(p); + }); + } + + function bindGauntletAssetLibrary() { + var drop = el('gauntlet-asset-drop'); + var finp = el('gauntlet-asset-file-input'); + if (!drop || !finp) return; + + drop.addEventListener('click', function () { + finp.click(); + }); + drop.addEventListener('keydown', function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + finp.click(); + } + }); + + ['dragenter', 'dragover'].forEach(function (ev) { + drop.addEventListener(ev, function (e) { + e.preventDefault(); + e.stopPropagation(); + drop.classList.add('is-dragover'); + }); + }); + ['dragleave', 'drop'].forEach(function (ev) { + drop.addEventListener(ev, function (e) { + e.preventDefault(); + e.stopPropagation(); + if (ev !== 'drop') drop.classList.remove('is-dragover'); + }); + }); + drop.addEventListener('drop', function (e) { + drop.classList.remove('is-dragover'); + var files = e.dataTransfer && e.dataTransfer.files; + if (!files || !files.length) return; + var arr = []; + for (var i = 0; i < files.length; i++) arr.push(files[i]); + uploadGauntletDataUrls(arr); + }); + + finp.addEventListener('change', function () { + if (!finp.files || !finp.files.length) return; + var arr = []; + for (var i = 0; i < finp.files.length; i++) arr.push(finp.files[i]); + finp.value = ''; + uploadGauntletDataUrls(arr); + }); + } + + /** ว่าง / อัตโนมัติ / auto → null · ตัวเลข 0.85–3.2 tile */ + function parseStackBlockWidthForSave(raw) { + var t = String(raw == null ? '' : raw).trim(); + if (!t) return { ok: true, value: null }; + var low = t.toLowerCase(); + if (t === 'อัตโนมัติ' || low === 'auto' || low === 'automatic') return { ok: true, value: null }; + var w = parseFloat(t.replace(',', '.')); + if (!Number.isFinite(w)) { + return { ok: false, error: 'กว้างบล็อก: ใส่ตัวเลข 0.85–3.2 หรือปล่อยว่างเพื่ออัตโนมัติ' }; + } + if (w < 0.85 || w > 3.2) { + return { ok: false, error: 'กว้างบล็อกต้องอยู่ระหว่าง 0.85–3.2 tile หรือปล่อยว่าง' }; + } + return { ok: true, value: Math.round(Math.max(0.85, Math.min(3.2, w)) * 100) / 100 }; + } + + /** ช่อง Hz ใช้ type=text — ปล่อยว่าง = 0.55 */ + function parseStackSwingHzForSave(raw) { + var t = String(raw == null ? '' : raw).trim().replace(',', '.'); + if (!t) return { ok: true, value: 0.55 }; + var h = parseFloat(t); + if (!Number.isFinite(h)) { + return { ok: false, error: 'สวิง Hz: ใส่ตัวเลข (เช่น 0.58) หรือปล่อยว่างให้เป็น 0.55' }; + } + h = Math.max(0.08, Math.min(2.8, h)); + return { ok: true, value: Math.round(h * 100) / 100 }; + } + + /** ซิงก์ช่อง Stack จากข้อมูลเซิร์ฟเวอร์ (ช่องอยู่ใน DOM แม้แท็บอื่นเปิดอยู่ — ต้องไม่ปล่อยให้ค้างค่า default จาก HTML) */ + function applyGameTimingDataToStackInputs(data) { + if (!data || typeof data !== 'object') return; + if (el('stack-swing-hz')) { + var hz = data.stackSwingHz != null && data.stackSwingHz !== '' ? Number(data.stackSwingHz) : NaN; + if (!Number.isFinite(hz)) hz = 0.55; + hz = Math.max(0.08, Math.min(2.8, hz)); + el('stack-swing-hz').value = String(Math.round(hz * 100) / 100); + } + if (el('stack-block-width-tiles')) { + var rawW = data.stackBlockWidthTiles; + var bw = rawW != null && rawW !== '' ? Number(rawW) : NaN; + el('stack-block-width-tiles').value = Number.isFinite(bw) && bw >= 0.85 ? String(Math.round(bw * 100) / 100) : ''; + } + if (el('stack-tower-mission-sec')) { + var st = Number(data.stackTowerMissionTimeSec); + el('stack-tower-mission-sec').value = String(Number.isFinite(st) && st > 0 ? Math.floor(Math.max(10, Math.min(7200, st))) : 90); + } + if (el('stack-team-misses-max')) { + var sm = Number(data.stackTeamMissesMax); + el('stack-team-misses-max').value = String(Number.isFinite(sm) ? Math.floor(Math.max(1, Math.min(20, sm))) : 3); + } + if (el('stack-tower-progress-blocks')) { + var pb = Number(data.stackTowerProgressBlocks); + el('stack-tower-progress-blocks').value = String(Number.isFinite(pb) ? Math.floor(Math.max(1, Math.min(500, pb))) : 50); + } + if (el('stack-heavy-block-percent')) { + var shp = Number(data.stackHeavyBlockPercent); + el('stack-heavy-block-percent').value = String(Number.isFinite(shp) ? Math.max(0, Math.min(100, Math.floor(shp))) : 35); + } + var normArr = Array.isArray(data.stackBlockNormalImageUrls) ? data.stackBlockNormalImageUrls : []; + var heavyArr = Array.isArray(data.stackBlockHeavyImageUrls) ? data.stackBlockHeavyImageUrls : []; + for (var si = 1; si <= 6; si++) { + var nin = el('stack-block-normal-url-' + si); + var hin = el('stack-block-heavy-url-' + si); + if (nin) nin.value = normArr[si - 1] != null ? String(normArr[si - 1]) : ''; + if (hin) hin.value = heavyArr[si - 1] != null ? String(heavyArr[si - 1]) : ''; + updateStackBlockSeatPreview(si); + } + } + + function readStackBlockNormalUrlsFromForm() { + var out = []; + for (var i = 1; i <= 6; i++) { + var inp = el('stack-block-normal-url-' + i); + out.push(inp && inp.value ? String(inp.value).trim() : ''); + } + return out; + } + + function readStackBlockHeavyUrlsFromForm() { + var out = []; + for (var j = 1; j <= 6; j++) { + var inp = el('stack-block-heavy-url-' + j); + out.push(inp && inp.value ? String(inp.value).trim() : ''); + } + return out; + } + + function updateStackBlockSeatPreview(seat) { + ['normal', 'heavy'].forEach(function (kind) { + var inp = el('stack-block-' + kind + '-url-' + seat); + var img = el('stack-block-' + kind + '-prev-' + seat); + if (!img) return; + var v = inp && inp.value ? String(inp.value).trim() : ''; + if (!v) { + img.removeAttribute('src'); + img.alt = ''; + return; + } + if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, ''); + img.alt = (kind === 'heavy' ? 'Heavy ' : 'Normal ') + 'P' + seat; + img.src = v; + }); + } + + function bindStackBlockVisualInputs() { + for (var s = 1; s <= 6; s++) { + (function (seat) { + var nIn = el('stack-block-normal-url-' + seat); + var hIn = el('stack-block-heavy-url-' + seat); + if (nIn) { + nIn.addEventListener('change', function () { updateStackBlockSeatPreview(seat); }); + nIn.addEventListener('blur', function () { updateStackBlockSeatPreview(seat); }); + } + if (hIn) { + hIn.addEventListener('change', function () { updateStackBlockSeatPreview(seat); }); + hIn.addEventListener('blur', function () { updateStackBlockSeatPreview(seat); }); + } + var bn = el('btn-stack-block-normal-clear-' + seat); + if (bn) { + bn.addEventListener('click', function () { + var i = el('stack-block-normal-url-' + seat); + if (i) i.value = ''; + updateStackBlockSeatPreview(seat); + }); + } + var bh = el('btn-stack-block-heavy-clear-' + seat); + if (bh) { + bh.addEventListener('click', function () { + var i = el('stack-block-heavy-url-' + seat); + if (i) i.value = ''; + updateStackBlockSeatPreview(seat); + }); + } + })(s); + } + } + + function loadStackGamePanel() { + gameTimingFetch('GET') + .then(function (data) { + applyGameTimingDataToStackInputs(data); + setMsg('stack-game-msg', '', ''); + }) + .catch(function (e) { + setMsg('stack-game-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + } + + function saveStackGamePanel() { + if (stackGameSaveInFlight) return; + var hzParsed = parseStackSwingHzForSave(el('stack-swing-hz') ? el('stack-swing-hz').value : ''); + if (!hzParsed.ok) { + setMsg('stack-game-msg', hzParsed.error, 'error'); + return; + } + var hzIn = hzParsed.value; + var bwParsed = parseStackBlockWidthForSave(el('stack-block-width-tiles') ? el('stack-block-width-tiles').value : ''); + if (!bwParsed.ok) { + setMsg('stack-game-msg', bwParsed.error, 'error'); + return; + } + var blockW = bwParsed.value; + var secIn = el('stack-tower-mission-sec') ? parseInt(String(el('stack-tower-mission-sec').value), 10) : 90; + if (!Number.isFinite(secIn) || secIn < 10 || secIn > 7200) { + setMsg('stack-game-msg', 'เวลารอบต้องอยู่ระหว่าง 10–7200 วินาที', 'error'); + return; + } + var missIn = el('stack-team-misses-max') ? parseInt(String(el('stack-team-misses-max').value), 10) : 3; + if (!Number.isFinite(missIn) || missIn < 1 || missIn > 20) { + setMsg('stack-game-msg', 'โอกาสพลาดต้องอยู่ระหว่าง 1–20 ครั้ง', 'error'); + return; + } + var progBlocksIn = el('stack-tower-progress-blocks') ? parseInt(String(el('stack-tower-progress-blocks').value), 10) : 50; + if (!Number.isFinite(progBlocksIn) || progBlocksIn < 1 || progBlocksIn > 500) { + setMsg('stack-game-msg', 'Progress เต็ม 100%: จำนวนชั้นต้องอยู่ระหว่าง 1–500', 'error'); + return; + } + var heavyPctIn = el('stack-heavy-block-percent') ? parseInt(String(el('stack-heavy-block-percent').value), 10) : 35; + if (!Number.isFinite(heavyPctIn) || heavyPctIn < 0 || heavyPctIn > 100) { + setMsg('stack-game-msg', 'โอกาสบล็อกใหญ่ (%) ต้องอยู่ระหว่าง 0–100', 'error'); + return; + } + var btn = el('btn-stack-game-save'); + stackGameSaveInFlight = true; + if (btn) { + btn.disabled = true; + if (!btn.dataset.prevLabel) btn.dataset.prevLabel = btn.textContent; + btn.textContent = 'กำลังบันทึก…'; + } + gameTimingFetch('GET') + .then(function (data) { + data.stackSwingHz = hzIn; + data.stackBlockWidthTiles = blockW; + data.stackTowerMissionTimeSec = secIn; + data.stackTeamMissesMax = missIn; + data.stackTowerProgressBlocks = progBlocksIn; + data.stackHeavyBlockPercent = heavyPctIn; + data.stackBlockNormalImageUrls = readStackBlockNormalUrlsFromForm(); + data.stackBlockHeavyImageUrls = readStackBlockHeavyUrlsFromForm(); + return gameTimingFetch('PUT', data); + }) + .then(function () { + return gameTimingFetch('GET'); + }) + .then(function (fresh) { + applyGameTimingDataToStackInputs(fresh); + var hz = fresh && fresh.stackSwingHz != null ? Number(fresh.stackSwingHz) : hzIn; + if (!Number.isFinite(hz)) hz = hzIn; + hz = Math.round(hz * 100) / 100; + var wLabel = fresh && fresh.stackBlockWidthTiles != null && Number.isFinite(Number(fresh.stackBlockWidthTiles)) + ? String(Math.round(Number(fresh.stackBlockWidthTiles) * 100) / 100) + ' tile' + : 'อัตโนมัติ (จากแผนที่)'; + var stf = fresh && fresh.stackTowerMissionTimeSec != null ? Number(fresh.stackTowerMissionTimeSec) : secIn; + var msf = fresh && fresh.stackTeamMissesMax != null ? Number(fresh.stackTeamMissesMax) : missIn; + var pbf = fresh && fresh.stackTowerProgressBlocks != null ? Number(fresh.stackTowerProgressBlocks) : progBlocksIn; + setMsg('stack-game-msg', 'บันทึกสำเร็จ — สวิง ' + hz + ' Hz · กว้าง ' + wLabel + ' · เวลา ' + stf + 's · พลาด ' + msf + ' ครั้ง · Progress 100% ที่ ' + pbf + ' ชั้น · รูปบล็อก P1–P6 + ใหญ่ ' + (fresh && fresh.stackHeavyBlockPercent != null ? fresh.stackHeavyBlockPercent : heavyPctIn) + '% · โหลดยืนยันจากเซิร์ฟเวอร์แล้ว · ผู้เล่นต้องรีเฟรชหน้าเล่นหรือเข้าห้องใหม่', 'ok'); + }) + .catch(function (e) { + setMsg('stack-game-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็คว่า Node รัน Game/server.js และ URL /Game/api/game-timing ไม่ 404', 'error'); + }) + .then(function () { + stackGameSaveInFlight = false; + if (btn) { + btn.disabled = false; + btn.textContent = btn.dataset.prevLabel || 'บันทึก'; + } + }); + } + + function loadGameTimingPanel() { + gameTimingFetch('GET') + .then(function (data) { + el('game-timing-tick-ms').value = String(data.gauntletTickMs != null ? data.gauntletTickMs : 220); + el('game-timing-jump-ticks').value = String(data.gauntletJumpTicks != null ? data.gauntletJumpTicks : 16); + el('game-timing-limit-sec').value = String(data.gauntletTimeLimitSec != null ? data.gauntletTimeLimitSec : 0); + if (el('game-timing-lane-urls')) el('game-timing-lane-urls').value = laneUrlsToText(data.gauntletLaneImageUrls); + if (el('game-timing-laser-top-url')) el('game-timing-laser-top-url').value = data.gauntletLaserTopUrl != null ? String(data.gauntletLaserTopUrl) : ''; + if (el('game-timing-laser-bottom-url')) el('game-timing-laser-bottom-url').value = data.gauntletLaserBottomUrl != null ? String(data.gauntletLaserBottomUrl) : ''; + if (el('game-timing-laser-line-url')) el('game-timing-laser-line-url').value = data.gauntletLaserLineUrl != null ? String(data.gauntletLaserLineUrl) : ''; + if (el('game-timing-laser-fill')) el('game-timing-laser-fill').value = data.gauntletLaserFillColor != null ? String(data.gauntletLaserFillColor) : ''; + if (el('game-timing-laser-stroke')) el('game-timing-laser-stroke').value = data.gauntletLaserStrokeColor != null ? String(data.gauntletLaserStrokeColor) : ''; + var lw = data.gauntletLaserLineWidthPx != null ? Number(data.gauntletLaserLineWidthPx) : 2; + if (el('game-timing-laser-line-width')) el('game-timing-laser-line-width').value = String(Number.isFinite(lw) ? lw : 2); + renderGameTimingLaneVisuals(); + renderGameTimingLaserVisuals(); + syncGameTimingLaserColorSwatches(); + applyGameTimingDataToStackInputs(data); + setMsg('game-timing-msg', '', ''); + return refreshGauntletAssetLibrary(); + }) + .catch(function (e) { + setMsg('game-timing-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + } + + function normalizeJumpSurvivePlatformTileUrlInput(v) { + var s = String(v || '').trim().slice(0, 500); + if (!s) return ''; + if (!/^https?:\/\//i.test(s) && s.charAt(0) !== '/') s = '/' + s.replace(/^\/+/, ''); + return s; + } + + function updateJumpSurvivePlatformTilePreview(slot) { + var img = el('jump-survive-platform-tile-prev-' + slot); + var inp = el('jump-survive-platform-tile-url-' + slot); + if (!img) return; + var v = inp && inp.value ? normalizeJumpSurvivePlatformTileUrlInput(inp.value) : ''; + if (!v) { + img.removeAttribute('src'); + img.alt = ''; + return; + } + img.alt = 'Jump platform tile ' + slot + ' preview'; + img.src = v; + } + + function readJumpSurvivePlatformTilesFromForm() { + var out = []; + for (var s = 1; s <= 3; s++) { + var inpU = el('jump-survive-platform-tile-url-' + s); + var url = inpU ? normalizeJumpSurvivePlatformTileUrlInput(inpU.value) : ''; + var inpW = el('jump-survive-platform-tile-w-' + s); + var inpH = el('jump-survive-platform-tile-h-' + s); + var w = inpW ? parseInt(String(inpW.value), 10) : 0; + var h = inpH ? parseInt(String(inpH.value), 10) : 0; + if (!Number.isFinite(w) || w < 0) w = 0; + if (!Number.isFinite(h) || h < 0) h = 0; + out.push({ url: url, w: w, h: h }); + } + return out; + } + + function bindJumpSurvivePlatformTileInputs() { + for (var s = 1; s <= 3; s++) { + (function (slot) { + var inp = el('jump-survive-platform-tile-url-' + slot); + var btn = el('btn-jump-survive-platform-tile-clear-' + slot); + if (inp) { + inp.addEventListener('change', function () { updateJumpSurvivePlatformTilePreview(slot); }); + inp.addEventListener('blur', function () { updateJumpSurvivePlatformTilePreview(slot); }); + } + if (btn) { + btn.addEventListener('click', function () { + var i = el('jump-survive-platform-tile-url-' + slot); + if (i) i.value = ''; + updateJumpSurvivePlatformTilePreview(slot); + }); + } + })(s); + } + } + + function loadJumpSurviveTimingPanel() { + gameTimingFetch('GET') + .then(function (data) { + var inp = el('jump-survive-height-mult'); + if (inp) { + var jsm = Number(data.jumpSurviveJumpHeightMult); + inp.value = String(Number.isFinite(jsm) ? Math.round(Math.max(0.5, Math.min(4, jsm)) * 100) / 100 : 1.5); + } + var inpT = el('jump-survive-mission-sec'); + if (inpT) { + var jss = Number(data.jumpSurviveMissionTimeSec); + inpT.value = String(Number.isFinite(jss) && jss > 0 ? Math.floor(jss) : 0); + } + var tiles = Array.isArray(data.jumpSurvivePlatformTiles) ? data.jumpSurvivePlatformTiles : []; + var legacy = typeof data.jumpSurvivePlatformTileUrl === 'string' ? data.jumpSurvivePlatformTileUrl : ''; + for (var s = 1; s <= 3; s++) { + var o = tiles[s - 1] && typeof tiles[s - 1] === 'object' ? tiles[s - 1] : {}; + var url = typeof o.url === 'string' ? o.url : ''; + if (s === 1 && !url && legacy) url = legacy; + var inpU = el('jump-survive-platform-tile-url-' + s); + if (inpU) inpU.value = url; + var w = Number(o.w); + var h = Number(o.h); + var inpW = el('jump-survive-platform-tile-w-' + s); + var inpH = el('jump-survive-platform-tile-h-' + s); + if (inpW) inpW.value = String(Number.isFinite(w) && w > 0 ? Math.floor(w) : 0); + if (inpH) inpH.value = String(Number.isFinite(h) && h > 0 ? Math.floor(h) : 0); + updateJumpSurvivePlatformTilePreview(s); + } + setMsg('jump-survive-timing-msg', '', ''); + }) + .catch(function (e) { + setMsg('jump-survive-timing-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + } + + function updateSpaceShooterShipPreview(slot) { + var img = el('space-shooter-ship-prev-' + slot); + var inp = el('space-shooter-ship-url-' + slot); + if (!img) return; + var v = inp && inp.value ? String(inp.value).trim() : ''; + if (!v) { + img.removeAttribute('src'); + img.alt = ''; + return; + } + if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, ''); + img.alt = 'Ship slot ' + slot; + img.src = v; + } + + function bindSpaceShooterShipUrlInputs() { + for (var s = 1; s <= 6; s++) { + (function (slot) { + var inp = el('space-shooter-ship-url-' + slot); + var btn = el('btn-space-shooter-ship-clear-' + slot); + if (inp) { + inp.addEventListener('change', function () { updateSpaceShooterShipPreview(slot); }); + inp.addEventListener('blur', function () { updateSpaceShooterShipPreview(slot); }); + } + if (btn) { + btn.addEventListener('click', function () { + var i = el('space-shooter-ship-url-' + slot); + if (i) i.value = ''; + updateSpaceShooterShipPreview(slot); + }); + } + })(s); + } + } + + function readSpaceShooterShipImageUrlsFromForm() { + var urls = []; + for (var i = 1; i <= 6; i++) { + var inp = el('space-shooter-ship-url-' + i); + urls.push(inp && inp.value ? String(inp.value).trim() : ''); + } + return urls; + } + + function updateSpaceShooterDamageOverlayPreview(hit) { + var img = el('space-shooter-damage-prev-' + hit); + var inp = el('space-shooter-damage-url-' + hit); + if (!img) return; + var v = inp && inp.value ? String(inp.value).trim() : ''; + if (!v) { + img.removeAttribute('src'); + img.alt = ''; + return; + } + if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, ''); + img.alt = 'Damage overlay hit ' + hit; + img.src = v; + } + + function readSpaceShooterShipDamageOverlayUrlsFromForm() { + var urls = []; + for (var h = 1; h <= 3; h++) { + var inp = el('space-shooter-damage-url-' + h); + urls.push(inp && inp.value ? String(inp.value).trim() : ''); + } + return urls; + } + + function bindSpaceShooterDamageOverlayPanel() { + for (var h = 1; h <= 3; h++) { + (function (hit) { + var inp = el('space-shooter-damage-url-' + hit); + var btnClr = el('btn-space-shooter-damage-clear-' + hit); + var btnUp = el('btn-space-shooter-damage-upload-' + hit); + var fileInp = el('space-shooter-damage-file-' + hit); + if (inp) { + inp.addEventListener('change', function () { updateSpaceShooterDamageOverlayPreview(hit); }); + inp.addEventListener('blur', function () { updateSpaceShooterDamageOverlayPreview(hit); }); + } + if (btnClr) { + btnClr.addEventListener('click', function () { + var i = el('space-shooter-damage-url-' + hit); + if (i) i.value = ''; + if (fileInp) fileInp.value = ''; + updateSpaceShooterDamageOverlayPreview(hit); + }); + } + if (btnUp && fileInp) { + btnUp.addEventListener('click', function () { + if (!fileInp.files || !fileInp.files[0]) { + setMsg('space-shooter-timing-msg', 'เลือกไฟล์รูปก่อน · Pick an image file first', 'error'); + return; + } + readFileAsDataURL(fileInp.files[0]) + .then(function (dataUrl) { + return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + imageDataUrl: dataUrl, + label: 'space-shooter-damage-hit-' + hit, + }), + }); + }) + .then(function (j) { + if (!j || !j.ok || !j.url) { + throw new Error((j && j.error) || 'อัปโหลดไม่สำเร็จ · Upload failed'); + } + var inpU = el('space-shooter-damage-url-' + hit); + if (inpU) inpU.value = String(j.url).trim(); + updateSpaceShooterDamageOverlayPreview(hit); + setMsg( + 'space-shooter-timing-msg', + 'อัปโหลด overlay ช่อง ' + hit + ' แล้ว — กดบันทึกด้านล่างเพื่อเก็บลง game-timing.json · Uploaded; click Save', + 'ok' + ); + }) + .catch(function (e) { + setMsg('space-shooter-timing-msg', e.message || 'อัปโหลดไม่สำเร็จ', 'error'); + }); + }); + } + })(h); + } + } + + function updateSpaceShooterAstFallPreview() { + var img = el('space-shooter-ast-fall-prev'); + var inp = el('space-shooter-ast-fall-url'); + if (!img) return; + var v = inp && inp.value ? String(inp.value).trim() : ''; + if (!v) { + img.removeAttribute('src'); + img.alt = ''; + return; + } + if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, ''); + img.alt = 'Asteroid falling'; + img.src = v; + } + + function updateSpaceShooterAstExplodeRowPreview(row) { + if (!row) return; + var img = row.querySelector('.space-shooter-ast-explode-prev'); + var inp = row.querySelector('.space-shooter-ast-explode-url'); + if (!img) return; + var v = inp && inp.value ? String(inp.value).trim() : ''; + if (!v) { + img.removeAttribute('src'); + img.alt = ''; + return; + } + if (!/^https?:\/\//i.test(v) && v.charAt(0) !== '/') v = '/' + v.replace(/^\/+/, ''); + img.alt = 'Asteroid explode frame'; + img.src = v; + } + + function renumberSpaceShooterAstExplodeRows() { + var wrap = el('space-shooter-ast-explode-rows'); + if (!wrap) return; + var rows = wrap.querySelectorAll('.space-shooter-ast-explode-row'); + rows.forEach(function (row, i) { + var slot = row.querySelector('.space-shooter-ship-slot'); + if (slot) slot.textContent = String(i + 1); + }); + } + + function addSpaceShooterAstExplodeRow(initialUrl) { + var wrap = el('space-shooter-ast-explode-rows'); + if (!wrap) return; + var rows = wrap.querySelectorAll('.space-shooter-ast-explode-row'); + if (rows.length >= 31) { + setMsg('space-shooter-timing-msg', 'เฟรมแตกสูงสุด 31 แถว (รวมรูปตกแล้วไม่เกิน 32 URL) · Max 31 explosion rows', 'error'); + return; + } + var ix = rows.length + 1; + var row = document.createElement('div'); + row.className = 'space-shooter-ship-row space-shooter-ast-explode-row'; + row.innerHTML = + '' + + ix + + '' + + '' + + '' + + '' + + '' + + ''; + var inp = row.querySelector('.space-shooter-ast-explode-url'); + if (inp && initialUrl != null && String(initialUrl).trim()) inp.value = String(initialUrl).trim(); + if (inp) { + inp.addEventListener('change', function () { + updateSpaceShooterAstExplodeRowPreview(row); + }); + inp.addEventListener('blur', function () { + updateSpaceShooterAstExplodeRowPreview(row); + }); + } + wrap.appendChild(row); + updateSpaceShooterAstExplodeRowPreview(row); + } + + function clearSpaceShooterAstExplodeRows() { + var wrap = el('space-shooter-ast-explode-rows'); + if (wrap) wrap.innerHTML = ''; + } + + function readSpaceShooterAsteroidSpriteUrlsFromForm() { + var out = []; + var fallInp = el('space-shooter-ast-fall-url'); + var fall = fallInp && fallInp.value ? String(fallInp.value).trim() : ''; + if (fall) out.push(fall); + var wrap = el('space-shooter-ast-explode-rows'); + if (wrap) { + var rows = wrap.querySelectorAll('.space-shooter-ast-explode-row'); + for (var ri = 0; ri < rows.length && out.length < 32; ri++) { + var inp = rows[ri].querySelector('.space-shooter-ast-explode-url'); + var u = inp && inp.value ? String(inp.value).trim() : ''; + if (u) out.push(u); + } + } + return out; + } + + function bindSpaceShooterAsteroidSpritePanel() { + var wrap = el('space-shooter-ast-explode-rows'); + if (!wrap || wrap.getAttribute('data-bound') === '1') return; + wrap.setAttribute('data-bound', '1'); + wrap.addEventListener('click', function (ev) { + var t = ev.target; + if (!t || !t.closest) return; + var rm = t.closest('.btn-space-shooter-ast-explode-remove'); + if (rm) { + var row = rm.closest('.space-shooter-ast-explode-row'); + if (row && row.parentNode) row.parentNode.removeChild(row); + renumberSpaceShooterAstExplodeRows(); + return; + } + var clr = t.closest('.btn-space-shooter-ast-explode-clear'); + if (clr) { + var rowC = clr.closest('.space-shooter-ast-explode-row'); + if (!rowC) return; + var iu = rowC.querySelector('.space-shooter-ast-explode-url'); + var fi = rowC.querySelector('.space-shooter-ast-explode-file'); + if (iu) iu.value = ''; + if (fi) fi.value = ''; + updateSpaceShooterAstExplodeRowPreview(rowC); + return; + } + var up = t.closest('.btn-space-shooter-ast-explode-upload'); + if (!up) return; + var rowU = up.closest('.space-shooter-ast-explode-row'); + if (!rowU) return; + var fileInp = rowU.querySelector('.space-shooter-ast-explode-file'); + if (!fileInp || !fileInp.files || !fileInp.files[0]) { + setMsg('space-shooter-timing-msg', 'เลือกไฟล์รูปก่อน · Pick an image file first', 'error'); + return; + } + readFileAsDataURL(fileInp.files[0]) + .then(function (dataUrl) { + return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + imageDataUrl: dataUrl, + label: 'space-shooter-ast-explode', + }), + }); + }) + .then(function (j) { + if (!j || !j.ok || !j.url) { + throw new Error((j && j.error) || 'อัปโหลดไม่สำเร็จ · Upload failed'); + } + var inpU = rowU.querySelector('.space-shooter-ast-explode-url'); + if (inpU) inpU.value = String(j.url).trim(); + updateSpaceShooterAstExplodeRowPreview(rowU); + setMsg( + 'space-shooter-timing-msg', + 'อัปโหลดเฟรมแตกแล้ว — กดบันทึกด้านล่างเพื่อเก็บ · Uploaded; click Save', + 'ok' + ); + }) + .catch(function (e) { + setMsg('space-shooter-timing-msg', e.message || 'อัปโหลดไม่สำเร็จ', 'error'); + }); + }); + var btnAdd = el('btn-space-shooter-ast-add-explode'); + if (btnAdd) { + btnAdd.addEventListener('click', function () { + setMsg('space-shooter-timing-msg', '', ''); + addSpaceShooterAstExplodeRow(''); + }); + } + var fallInp = el('space-shooter-ast-fall-url'); + var fallFile = el('space-shooter-ast-fall-file'); + var fallUp = el('btn-space-shooter-ast-fall-upload'); + var fallClr = el('btn-space-shooter-ast-fall-clear'); + if (fallInp) { + fallInp.addEventListener('change', updateSpaceShooterAstFallPreview); + fallInp.addEventListener('blur', updateSpaceShooterAstFallPreview); + } + if (fallClr) { + fallClr.addEventListener('click', function () { + if (fallInp) fallInp.value = ''; + if (fallFile) fallFile.value = ''; + updateSpaceShooterAstFallPreview(); + }); + } + if (fallUp && fallFile) { + fallUp.addEventListener('click', function () { + if (!fallFile.files || !fallFile.files[0]) { + setMsg('space-shooter-timing-msg', 'เลือกไฟล์รูปก่อน · Pick an image file first', 'error'); + return; + } + readFileAsDataURL(fallFile.files[0]) + .then(function (dataUrl) { + return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + imageDataUrl: dataUrl, + label: 'space-shooter-ast-fall', + }), + }); + }) + .then(function (j) { + if (!j || !j.ok || !j.url) { + throw new Error((j && j.error) || 'อัปโหลดไม่สำเร็จ · Upload failed'); + } + if (fallInp) fallInp.value = String(j.url).trim(); + updateSpaceShooterAstFallPreview(); + setMsg( + 'space-shooter-timing-msg', + 'อัปโหลดรูปตกแล้ว — กดบันทึกด้านล่างเพื่อเก็บ · Uploaded; click Save', + 'ok' + ); + }) + .catch(function (e) { + setMsg('space-shooter-timing-msg', e.message || 'อัปโหลดไม่สำเร็จ', 'error'); + }); + }); + } + } + + function loadSpaceShooterTimingPanel() { + gameTimingFetch('GET') + .then(function (data) { + var inpT = el('space-shooter-mission-sec'); + if (inpT) { + var ss = Number(data.spaceShooterMissionTimeSec); + inpT.value = String(Number.isFinite(ss) && ss > 0 ? Math.floor(ss) : 0); + } + var inpIv = el('space-shooter-asteroid-interval-ms'); + if (inpIv) { + var iv = Number(data.spaceShooterAsteroidIntervalMs); + inpIv.value = String(Number.isFinite(iv) && iv >= 200 ? Math.min(10000, Math.floor(iv)) : 1040); + } + var urls = data.spaceShooterShipImageUrls; + if (!Array.isArray(urls)) urls = []; + for (var k = 1; k <= 6; k++) { + var inpU = el('space-shooter-ship-url-' + k); + if (inpU) inpU.value = urls[k - 1] != null ? String(urls[k - 1]) : ''; + updateSpaceShooterShipPreview(k); + } + var fallInpL = el('space-shooter-ast-fall-url'); + if (fallInpL) { + var astArr = data.spaceShooterAsteroidSpriteUrls; + if (!Array.isArray(astArr)) astArr = []; + fallInpL.value = astArr[0] != null ? String(astArr[0]) : ''; + updateSpaceShooterAstFallPreview(); + clearSpaceShooterAstExplodeRows(); + for (var ai = 1; ai < astArr.length && ai < 32; ai++) { + addSpaceShooterAstExplodeRow(astArr[ai]); + } + } + var inpFms = el('space-shooter-asteroid-explode-ms'); + if (inpFms) { + var ef = Number(data.spaceShooterAsteroidExplodeFrameMs); + inpFms.value = String(Number.isFinite(ef) ? Math.max(30, Math.min(500, Math.round(ef))) : 70); + } + var dmg = data.spaceShooterShipDamageOverlayUrls; + if (!Array.isArray(dmg)) dmg = []; + for (var dh = 1; dh <= 3; dh++) { + var inpD = el('space-shooter-damage-url-' + dh); + if (inpD) inpD.value = dmg[dh - 1] != null ? String(dmg[dh - 1]) : ''; + updateSpaceShooterDamageOverlayPreview(dh); + } + setMsg('space-shooter-timing-msg', '', ''); + }) + .catch(function (e) { + setMsg('space-shooter-timing-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + } + + function saveSpaceShooterTimingPanel() { + var limSs = el('space-shooter-mission-sec') ? parseInt(String(el('space-shooter-mission-sec').value), 10) : 0; + if (Number.isNaN(limSs) || limSs < 0) limSs = 0; + limSs = limSs <= 0 ? 0 : Math.max(10, Math.min(7200, limSs)); + var shipUrls = readSpaceShooterShipImageUrlsFromForm(); + var damageUrls = readSpaceShooterShipDamageOverlayUrlsFromForm(); + var astUrls = readSpaceShooterAsteroidSpriteUrlsFromForm(); + var inpFms = el('space-shooter-asteroid-explode-ms'); + var explodeMs = inpFms ? parseInt(String(inpFms.value), 10) : 70; + if (Number.isNaN(explodeMs)) explodeMs = 70; + explodeMs = Math.max(30, Math.min(500, explodeMs)); + var inpIv = el('space-shooter-asteroid-interval-ms'); + var asteroidIv = inpIv ? parseInt(String(inpIv.value), 10) : 1040; + if (Number.isNaN(asteroidIv)) asteroidIv = 1040; + asteroidIv = Math.max(200, Math.min(10000, asteroidIv)); + gameTimingFetch('GET') + .then(function (data) { + data.spaceShooterMissionTimeSec = limSs; + data.spaceShooterShipImageUrls = shipUrls; + data.spaceShooterShipDamageOverlayUrls = damageUrls; + data.spaceShooterAsteroidSpriteUrls = astUrls; + data.spaceShooterAsteroidExplodeFrameMs = explodeMs; + data.spaceShooterAsteroidIntervalMs = asteroidIv; + return gameTimingFetch('PUT', data); + }) + .then(function () { + setMsg('space-shooter-timing-msg', 'บันทึกแล้ว — เวลา / ความถี่เกิดอุกาบาต / รูปยาน / overlay / อุกาบาต มีผลเมื่อรีเฟรชหน้าเล่นหรือโหลด /api/game-timing', 'ok'); + }) + .catch(function (e) { + setMsg('space-shooter-timing-msg', e.message || 'บันทึกไม่ได้', 'error'); + }); + } + + function encodeSpacesInUrlPath(t) { + var s = String(t || ''); + var q = s.indexOf('?'); + var pathPart = q === -1 ? s : s.slice(0, q); + var query = q === -1 ? '' : s.slice(q); + return pathPart.replace(/ /g, '%20') + query; + } + + function decodeAssetUrlPercentRuns(t) { + var s = String(t || ''); + var q = s.indexOf('?'); + var base = q === -1 ? s : s.slice(0, q); + var query = q === -1 ? '' : s.slice(q); + for (var i = 0; i < 2; i++) { + try { + var d = decodeURIComponent(base); + if (d === base) break; + base = d; + } catch (e) { + break; + } + } + return base + query; + } + + function normalizeGameAssetUrlForWebAdmin(t) { + var s = String(t || ''); + s = s.replace(/^\/Game\/public\/img\//i, '/Game/img/'); + s = s.replace(/^Game\/public\/img\//i, 'Game/img/'); + return s; + } + + /** ให้สอดคล้องกับ server sanitizeGauntletAssetUrl + กัน placeholder .... */ + function normalizeMegaVirusAssetUrl(v) { + var t = v != null ? normalizeGameAssetUrlForWebAdmin(decodeAssetUrlPercentRuns(String(v).trim())).replace(/\/+$/, '') : ''; + if (/^\/Game\/img\/MegaVirus\/Artboard$/i.test(t) || /^Game\/img\/MegaVirus\/Artboard$/i.test(t)) { + t = '/Game/img/MegaVirus/Artboard%209.png'; + } + if (!t || t.length > 500) return ''; + if (/\.{4,}/.test(t)) return ''; + if (/[\t\n\r<>"'`]/.test(t)) return ''; + if (/^https?:\/\//i.test(t)) return encodeSpacesInUrlPath(t); + if (/^Game\//i.test(t)) return encodeSpacesInUrlPath('/' + t.replace(/^\/+/, '')); + if (t.charAt(0) === '/') return encodeSpacesInUrlPath(t); + return ''; + } + + function updateMegaVirusBossPreview() { + var img = el('mega-virus-boss-prev'); + var inp = el('mega-virus-boss-url'); + if (!img) return; + var v = normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : ''); + if (!v) { + img.removeAttribute('src'); + img.alt = ''; + return; + } + img.alt = 'Boss preview'; + img.src = v; + } + + function updateMegaVirusBalloonPreview(slot) { + var img = el('mega-virus-balloon-prev-' + slot); + var inp = el('mega-virus-balloon-url-' + slot); + if (!img) return; + var v = normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : ''); + if (!v) { + img.removeAttribute('src'); + img.alt = ''; + return; + } + img.alt = 'Balloon P' + slot; + img.src = v; + } + + function updateMegaVirusBalloonFallbackPreview() { + var img = el('mega-virus-balloon-fallback-prev'); + var inp = el('mega-virus-balloon-fallback-url'); + if (!img) return; + var v = normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : ''); + if (!v) { + img.removeAttribute('src'); + img.alt = ''; + return; + } + img.alt = 'Fallback balloon'; + img.src = v; + } + + function readMegaVirusBalloonUrlsFromForm() { + var urls = []; + for (var i = 1; i <= 6; i++) { + var inp = el('mega-virus-balloon-url-' + i); + urls.push(normalizeMegaVirusAssetUrl(inp && inp.value ? String(inp.value).trim() : '')); + } + return urls; + } + + function bindMegaVirusPanel() { + var formMv = el('form-mega-virus-timing'); + if (formMv) { + formMv.addEventListener('submit', function (e) { + e.preventDefault(); + saveMegaVirusTimingPanel(); + }); + } + var bossInp = el('mega-virus-boss-url'); + if (bossInp) { + bossInp.addEventListener('change', updateMegaVirusBossPreview); + bossInp.addEventListener('blur', updateMegaVirusBossPreview); + } + var bossClr = el('btn-mega-virus-boss-clear'); + if (bossClr) { + bossClr.addEventListener('click', function () { + var i = el('mega-virus-boss-url'); + if (i) i.value = ''; + updateMegaVirusBossPreview(); + }); + } + for (var s = 1; s <= 6; s++) { + (function (slot) { + var inp = el('mega-virus-balloon-url-' + slot); + var btn = el('btn-mega-virus-balloon-clear-' + slot); + if (inp) { + inp.addEventListener('change', function () { updateMegaVirusBalloonPreview(slot); }); + inp.addEventListener('blur', function () { updateMegaVirusBalloonPreview(slot); }); + } + if (btn) { + btn.addEventListener('click', function () { + var i = el('mega-virus-balloon-url-' + slot); + if (i) i.value = ''; + updateMegaVirusBalloonPreview(slot); + }); + } + })(s); + } + var fbInp = el('mega-virus-balloon-fallback-url'); + if (fbInp) { + fbInp.addEventListener('change', updateMegaVirusBalloonFallbackPreview); + fbInp.addEventListener('blur', updateMegaVirusBalloonFallbackPreview); + } + var fbClr = el('btn-mega-virus-balloon-fallback-clear'); + if (fbClr) { + fbClr.addEventListener('click', function () { + var i = el('mega-virus-balloon-fallback-url'); + if (i) i.value = ''; + updateMegaVirusBalloonFallbackPreview(); + }); + } + } + + function applyMegaVirusPanelFromTimingData(data) { + if (!data || typeof data !== 'object') return; + var inpT = el('mega-virus-mission-sec'); + if (inpT && Object.prototype.hasOwnProperty.call(data, 'balloonBossMissionTimeSec')) { + var bb = Number(data.balloonBossMissionTimeSec); + inpT.value = String(Number.isFinite(bb) && bb > 0 ? Math.floor(bb) : 0); + } + var bossInp = el('mega-virus-boss-url'); + if (bossInp && Object.prototype.hasOwnProperty.call(data, 'balloonBossBossImageUrl')) { + bossInp.value = data.balloonBossBossImageUrl != null ? String(data.balloonBossBossImageUrl) : ''; + updateMegaVirusBossPreview(); + } + if (Array.isArray(data.balloonBossPlayerBalloonImageUrls)) { + var urls = data.balloonBossPlayerBalloonImageUrls; + for (var k = 1; k <= 6; k++) { + var inpU = el('mega-virus-balloon-url-' + k); + if (inpU) inpU.value = urls[k - 1] != null ? String(urls[k - 1]) : ''; + updateMegaVirusBalloonPreview(k); + } + } + var fb = el('mega-virus-balloon-fallback-url'); + if (fb && Object.prototype.hasOwnProperty.call(data, 'balloonBossPlayerBalloonFallbackUrl')) { + fb.value = data.balloonBossPlayerBalloonFallbackUrl != null ? String(data.balloonBossPlayerBalloonFallbackUrl) : ''; + updateMegaVirusBalloonFallbackPreview(); + } + } + + function loadMegaVirusPanel() { + gameTimingFetch('GET') + .then(function (data) { + applyMegaVirusPanelFromTimingData(data); + setMsg('mega-virus-timing-msg', '', ''); + }) + .catch(function (e) { + setMsg('mega-virus-timing-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + } + + function saveMegaVirusTimingPanel() { + if (megaVirusSaveInFlight) return; + megaVirusSaveInFlight = true; + var btn = el('btn-mega-virus-save'); + if (btn) btn.disabled = true; + var limSs = el('mega-virus-mission-sec') ? parseInt(String(el('mega-virus-mission-sec').value), 10) : 0; + if (Number.isNaN(limSs) || limSs < 0) limSs = 0; + limSs = limSs <= 0 ? 0 : Math.max(10, Math.min(7200, limSs)); + var bossUrl = normalizeMegaVirusAssetUrl(el('mega-virus-boss-url') && el('mega-virus-boss-url').value ? String(el('mega-virus-boss-url').value).trim() : ''); + var balloonUrls = readMegaVirusBalloonUrlsFromForm(); + var fbUrl = normalizeMegaVirusAssetUrl(el('mega-virus-balloon-fallback-url') && el('mega-virus-balloon-fallback-url').value + ? String(el('mega-virus-balloon-fallback-url').value).trim() + : ''); + gameTimingFetch('GET') + .then(function (data) { + data.balloonBossMissionTimeSec = limSs; + data.balloonBossBossImageUrl = bossUrl; + data.balloonBossPlayerBalloonImageUrls = balloonUrls; + data.balloonBossPlayerBalloonFallbackUrl = fbUrl; + return gameTimingFetch('PUT', data); + }) + .then(function (res) { + if (res && typeof res === 'object') applyMegaVirusPanelFromTimingData(res); + setMsg('mega-virus-timing-msg', 'บันทึกแล้ว — รีเฟรชหน้าเล่น (หรือเปิดใหม่) เพื่อโหลดรูป · Saved; hard-refresh play.', 'ok'); + }) + .catch(function (e) { + setMsg('mega-virus-timing-msg', e.message || 'บันทึกไม่ได้', 'error'); + }) + .then(function () { + megaVirusSaveInFlight = false; + if (btn) btn.disabled = false; + }); + } + + function saveJumpSurviveTimingPanel() { + var inp = el('jump-survive-height-mult'); + var jumpSurvMult = inp ? parseFloat(String(inp.value)) : 1.5; + if (Number.isNaN(jumpSurvMult)) jumpSurvMult = 1.5; + jumpSurvMult = Math.round(Math.max(0.5, Math.min(4, jumpSurvMult)) * 100) / 100; + var limJump = el('jump-survive-mission-sec') ? parseInt(String(el('jump-survive-mission-sec').value), 10) : 0; + if (Number.isNaN(limJump) || limJump < 0) limJump = 0; + limJump = limJump <= 0 ? 0 : Math.max(10, Math.min(7200, limJump)); + var platTiles = readJumpSurvivePlatformTilesFromForm(); + gameTimingFetch('GET') + .then(function (data) { + data.jumpSurviveJumpHeightMult = jumpSurvMult; + data.jumpSurviveMissionTimeSec = limJump; + delete data.jumpSurvivePlatformTileUrl; + data.jumpSurvivePlatformTiles = platTiles; + return gameTimingFetch('PUT', data); + }) + .then(function () { + setMsg('jump-survive-timing-msg', 'บันทึกแล้ว — ผู้เล่นกระโดดให้รอดได้ค่าใหม่เมื่อรีเฟรชหน้าเล่นหรือ sync จากเซิร์ฟเวอร์', 'ok'); + }) + .catch(function (e) { + setMsg('jump-survive-timing-msg', e.message || 'บันทึกไม่ได้', 'error'); + }); + } + + function saveGameTimingPanel() { + var tickMs = parseInt(el('game-timing-tick-ms').value, 10); + var jumpT = parseInt(el('game-timing-jump-ticks').value, 10); + var limS = parseInt(el('game-timing-limit-sec').value, 10); + if (Number.isNaN(tickMs)) tickMs = 220; + if (Number.isNaN(jumpT)) jumpT = 16; + if (Number.isNaN(limS) || limS < 0) limS = 0; + tickMs = Math.max(80, Math.min(800, tickMs)); + jumpT = Math.max(4, Math.min(40, jumpT)); + limS = limS <= 0 ? 0 : Math.max(10, Math.min(7200, limS)); + var lwPx = el('game-timing-laser-line-width') ? parseInt(el('game-timing-laser-line-width').value, 10) : 2; + if (Number.isNaN(lwPx)) lwPx = 2; + lwPx = Math.max(0, Math.min(24, lwPx)); + /** แท็บเวลาเกม = เฉพาะ Gauntlet — อย่าส่ง stackSwingHz/stackBlockWidthTiles จากฟอร์ม (หลังรีเฟรชช่อง Stack ค้าง 0.55 จาก HTML จะทับค่าที่บันทึกไว้) */ + gameTimingFetch('GET') + .then(function (data) { + data.gauntletTickMs = tickMs; + data.gauntletJumpTicks = jumpT; + data.gauntletTimeLimitSec = limS; + data.gauntletLaneImageUrls = parseLaneUrlsTextarea(); + data.gauntletLaserTopUrl = el('game-timing-laser-top-url') ? String(el('game-timing-laser-top-url').value).trim() : ''; + data.gauntletLaserBottomUrl = el('game-timing-laser-bottom-url') ? String(el('game-timing-laser-bottom-url').value).trim() : ''; + data.gauntletLaserLineUrl = el('game-timing-laser-line-url') ? String(el('game-timing-laser-line-url').value).trim() : ''; + data.gauntletLaserFillColor = el('game-timing-laser-fill') ? String(el('game-timing-laser-fill').value).trim() : ''; + data.gauntletLaserStrokeColor = el('game-timing-laser-stroke') ? String(el('game-timing-laser-stroke').value).trim() : ''; + data.gauntletLaserLineWidthPx = lwPx; + return gameTimingFetch('PUT', data); + }) + .then(function () { + setMsg('game-timing-msg', 'บันทึกแล้ว — พรมแดง tick·กระโดด / รูปอุปสรรค์ · Minigame-5 / Minigame-6 ตั้งแท็บของแต่ละเกม · ค่า Tower block (Stack) แก้ที่แท็บ Minigame-3 เท่านั้น · จำกัดเวลาพรมแดงใช้กับรอบถัดไปที่โฮสต์เริ่มเกม', 'ok'); + }) + .catch(function (e) { + setMsg('game-timing-msg', e.message || 'บันทึกไม่ได้', 'error'); + }); + } + + function showPanel(name) { + ['panel-setup', 'panel-login', 'panel-app'].forEach(function (id) { + var p = el(id); + if (p) p.hidden = id !== name; + }); + el('top-actions').hidden = name !== 'panel-app'; + } + + // ===== Evidence Cards admin (รูปอย่างเดียว · 15 คดี · rarity จากเลขไฟล์) ===== + var EVIDENCE_CARDS_API = '/Game/api/evidence-cards'; + var EVIDENCE_IMAGES_API = '/Game/api/evidence-card-images'; + var EVIDENCE_CASE_COUNT = 15; + var EVIDENCE_DIRS = ['suspect-1', 'suspect-2', 'suspect-3']; + var evidenceData = null; + var evidencePools = null; // { 'suspect-1': {common,rare,legendary}, ... } + var evidenceCurrentCase = '1'; + + function evidencePopulateCaseSelect() { + var sel = el('evidence-case'); + if (!sel || sel.options.length) return; + for (var i = 1; i <= EVIDENCE_CASE_COUNT; i++) { + var o = document.createElement('option'); + o.value = String(i); o.textContent = 'คดี ' + i; + sel.appendChild(o); + } + } + + function loadEvidenceCardsPanel() { + evidencePopulateCaseSelect(); + if (evidencePools == null) { + fetch(EVIDENCE_IMAGES_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' }) + .then(function (r) { return r.json(); }) + .then(function (j) { evidencePools = (j && j.pools) || {}; renderEvidenceCase(evidenceCurrentCase); }) + .catch(function () { evidencePools = {}; }); + } + fetch(EVIDENCE_CARDS_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' }) + .then(function (r) { return r.json(); }) + .then(function (j) { + evidenceData = (j && j.cases) ? j : { cases: {} }; + var sel = el('evidence-case'); + if (sel) evidenceCurrentCase = sel.value || '1'; + renderEvidenceCase(evidenceCurrentCase); + }) + .catch(function (e) { + setMsg('evidence-cards-msg', (e.message || 'โหลดไม่ได้') + ' — เช็คว่า Node รัน Game/server.js และ /Game/api/evidence-cards ไม่ 404', 'error'); + }); + } + + function evidenceGetCase(cid) { + if (!evidenceData) evidenceData = { cases: {} }; + if (!evidenceData.cases) evidenceData.cases = {}; + if (!evidenceData.cases[cid]) evidenceData.cases[cid] = { suspects: [] }; + var cse = evidenceData.cases[cid]; + if (!Array.isArray(cse.suspects)) cse.suspects = []; + for (var si = 0; si < 3; si++) { + if (!cse.suspects[si]) cse.suspects[si] = {}; + var s = cse.suspects[si]; + if (!s.linkName) s.linkName = 'ผู้ต้องสงสัย ' + (si + 1); + if (EVIDENCE_DIRS.indexOf(s.imageDir) < 0) s.imageDir = EVIDENCE_DIRS[si] || EVIDENCE_DIRS[0]; + } + return cse; + } + + function evidencePoolBadge(dir) { + var p = (evidencePools && evidencePools[dir]) || null; + var box = document.createElement('div'); + box.className = 'ev-pool-counts'; + if (!p) { box.textContent = 'กำลังโหลดจำนวนรูป…'; return box; } + box.innerHTML = + 'ธรรมดา ' + (p.common || 0) + '' + + 'Rare ' + (p.rare || 0) + '' + + 'ชี้คนร้าย ' + (p.legendary || 0) + ''; + return box; + } + + function renderEvidenceCase(cid) { + evidenceCurrentCase = cid; + var tag = el('evidence-set-tag'); + if (tag) tag.textContent = 'กำลังแก้: คดี ' + cid; + var root = el('evidence-suspects-root'); + if (!root) return; + var cse = evidenceGetCase(cid); + root.innerHTML = ''; + cse.suspects.forEach(function (s, si) { + var block = document.createElement('div'); + block.className = 'ev-suspect-block'; + + var head = document.createElement('div'); + head.className = 'ev-suspect-head'; + head.innerHTML = 'ผู้ต้องสงสัย ' + (si + 1) + ''; + block.appendChild(head); + + var nameLabel = document.createElement('label'); + nameLabel.className = 'admin-field'; + nameLabel.textContent = 'ชื่อผู้ต้องสงสัย (Suspect Link)'; + var nameInp = document.createElement('input'); + nameInp.type = 'text'; + nameInp.maxLength = 60; + nameInp.value = s.linkName || ''; + nameInp.addEventListener('input', function () { s.linkName = nameInp.value; }); + nameLabel.appendChild(nameInp); + block.appendChild(nameLabel); + + var poolBox = evidencePoolBadge(s.imageDir); + + var dirLabel = document.createElement('label'); + dirLabel.className = 'admin-field'; + dirLabel.textContent = 'โฟลเดอร์รูปการ์ด (พูลที่ใช้สุ่ม)'; + var dirSel = document.createElement('select'); + dirSel.className = 'admin-inp-num'; + EVIDENCE_DIRS.forEach(function (d) { + var o = document.createElement('option'); + o.value = d; o.textContent = d.replace('suspect-', 'ผู้ต้องสงสัย-'); + if (d === s.imageDir) o.selected = true; + dirSel.appendChild(o); + }); + dirSel.addEventListener('change', function () { + s.imageDir = dirSel.value; + var fresh = evidencePoolBadge(s.imageDir); + poolBox.replaceWith(fresh); + poolBox = fresh; + }); + dirLabel.appendChild(dirSel); + block.appendChild(dirLabel); + block.appendChild(poolBox); + + root.appendChild(block); + }); + } + + function saveEvidenceCardsPanel() { + if (!evidenceData) return; + var btn = el('btn-evidence-save'); + if (btn) btn.disabled = true; + setMsg('evidence-cards-msg', 'กำลังบันทึก…', ''); + fetch(EVIDENCE_CARDS_API, { + method: 'PUT', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(evidenceData), + }).then(function (r) { + return r.text().then(function (t) { + var j = {}; + try { j = t ? JSON.parse(t) : {}; } catch (e) { /* ignore */ } + if (!r.ok || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status)); + return j; + }); + }).then(function (j) { + if (j && j.cases) evidenceData = { cases: j.cases }; + renderEvidenceCase(evidenceCurrentCase); + setMsg('evidence-cards-msg', 'บันทึกแล้ว — มีผลในเกมเมื่อรีเฟรช LobbyB', 'ok'); + }).catch(function (e) { + setMsg('evidence-cards-msg', e.message || 'บันทึกไม่ได้', 'error'); + }).then(function () { + if (btn) btn.disabled = false; + }); + } + + (function bindEvidenceCardsPanel() { + evidencePopulateCaseSelect(); + var caseSel = el('evidence-case'); + if (caseSel) caseSel.addEventListener('change', function () { renderEvidenceCase(caseSel.value || '1'); }); + var saveBtn = el('btn-evidence-save'); + if (saveBtn) saveBtn.addEventListener('click', saveEvidenceCardsPanel); + })(); + + /* ===== รูปคดี & Cutscene (LobbyA → LobbyB) ===== */ + var CASE_MEDIA_API = '/Game/api/case-media'; + var CASE_MEDIA_IMAGES_API = '/Game/api/case-media-images'; + var CASE_MEDIA_COUNT = 15; + var caseMediaData = null; + var caseMediaImages = null; // { case:[...], cutscene:[...] } + var caseMediaCurrent = '1'; + + function caseMediaPopulateSelect() { + var sel = el('case-media-case'); + if (!sel || sel.options.length) return; + for (var i = 1; i <= CASE_MEDIA_COUNT; i++) { + var o = document.createElement('option'); + o.value = String(i); o.textContent = 'คดี ' + i; + sel.appendChild(o); + } + } + + function loadCaseMediaPanel() { + caseMediaPopulateSelect(); + if (caseMediaImages == null) { + fetch(CASE_MEDIA_IMAGES_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' }) + .then(function (r) { return r.json(); }) + .then(function (j) { + caseMediaImages = (j && j.images) || { case: [], cutscene: [], 'suspect-pick': [], 'culprit-prison': [] }; + renderCaseMedia(caseMediaCurrent); + }) + .catch(function () { caseMediaImages = { case: [], cutscene: [], 'suspect-pick': [], 'culprit-prison': [] }; }); + } + fetch(CASE_MEDIA_API + '?_=' + Date.now(), { credentials: 'include', cache: 'no-store' }) + .then(function (r) { return r.json(); }) + .then(function (j) { + caseMediaData = (j && j.cases) ? j : { cases: {} }; + var sel = el('case-media-case'); + if (sel) caseMediaCurrent = sel.value || '1'; + renderCaseMedia(caseMediaCurrent); + }) + .catch(function (e) { + setMsg('case-media-msg', (e.message || 'โหลดไม่ได้') + ' — เช็คว่า Node รัน Game/server.js และ /Game/api/case-media ไม่ 404', 'error'); + }); + } + + function caseMediaGetCase(cid) { + if (!caseMediaData) caseMediaData = { cases: {} }; + if (!caseMediaData.cases) caseMediaData.cases = {}; + var n = parseInt(cid, 10) || 1; + if (!caseMediaData.cases[cid]) caseMediaData.cases[cid] = {}; + var c = caseMediaData.cases[cid]; + if (typeof c.name !== 'string') c.name = 'คดีที่ ' + n; + if (typeof c.art !== 'string') c.art = '/Main-Lobby/IMAGE/case/case-' + n + '.png'; + if (typeof c.detail !== 'string') c.detail = '/Main-Lobby/IMAGE/case/case-detail-' + n + '.png'; + if (!Array.isArray(c.story)) c.story = []; + if (typeof c.story[0] !== 'string') c.story[0] = '/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-1.png'; + if (typeof c.story[1] !== 'string') c.story[1] = '/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-2.png'; + if (!Array.isArray(c.suspects) || c.suspects.length < 3) { + c.suspects = [1, 2, 3].map(function (s) { return '/Main-Lobby/IMAGE/suspect-pick/case-' + n + '-suspect-' + s + '.png'; }); + } + if (!Array.isArray(c.culprits) || c.culprits.length < 3) { + c.culprits = [1, 2, 3].map(function (s) { return '/Main-Lobby/IMAGE/culprit-prison/case-' + n + '-suspect-' + s + '.png'; }); + } + return c; + } + + /** ตัวเลือกรูป (dropdown + พรีวิว) — kind = 'case' | 'cutscene' */ + function caseMediaImagePicker(label, kind, current, onChange) { + var wrap = document.createElement('div'); + wrap.className = 'cm-pick'; + var lab = document.createElement('div'); lab.className = 'cm-pick-label'; lab.textContent = label; + var prev = document.createElement('img'); prev.className = 'cm-pick-prev'; prev.alt = ''; prev.src = current; + prev.onerror = function () { this.style.opacity = '0.25'; }; + var sel = document.createElement('select'); sel.className = 'admin-inp-num'; + var list = (caseMediaImages && caseMediaImages[kind]) ? caseMediaImages[kind].slice() : []; + if (current && list.indexOf(current) < 0) list.unshift(current); + list.forEach(function (u) { + var o = document.createElement('option'); + o.value = u; o.textContent = u.split('/').pop(); + if (u === current) o.selected = true; + sel.appendChild(o); + }); + sel.addEventListener('change', function () { prev.src = sel.value; onChange(sel.value); }); + wrap.appendChild(lab); wrap.appendChild(prev); wrap.appendChild(sel); + return wrap; + } + + function renderCaseMedia(cid) { + caseMediaCurrent = cid; + var tag = el('case-media-set-tag'); + if (tag) tag.textContent = 'กำลังแก้: คดี ' + cid; + var root = el('case-media-root'); + if (!root) return; + var c = caseMediaGetCase(cid); + root.innerHTML = ''; + + var nameLabel = document.createElement('label'); + nameLabel.className = 'admin-field'; + nameLabel.textContent = 'ชื่อคดี'; + var nameInp = document.createElement('input'); + nameInp.type = 'text'; nameInp.maxLength = 80; nameInp.value = c.name || ''; + nameInp.addEventListener('input', function () { c.name = nameInp.value; }); + nameLabel.appendChild(nameInp); + root.appendChild(nameLabel); + + var grid = document.createElement('div'); + grid.className = 'cm-grid'; + grid.appendChild(caseMediaImagePicker('รูปเลือกคดี (การ์ด)', 'case', c.art, function (v) { c.art = v; })); + grid.appendChild(caseMediaImagePicker('รูปรายละเอียดคดี', 'case', c.detail, function (v) { c.detail = v; })); + grid.appendChild(caseMediaImagePicker('Cutscene รูปที่ 1', 'cutscene', c.story[0], function (v) { c.story[0] = v; })); + grid.appendChild(caseMediaImagePicker('Cutscene รูปที่ 2', 'cutscene', c.story[1], function (v) { c.story[1] = v; })); + root.appendChild(grid); + + var susHead = document.createElement('h3'); + susHead.className = 'cm-section-title'; + susHead.textContent = 'เลือกผู้ต้องสงสัย (3 คน) — LobbyB'; + root.appendChild(susHead); + var susGrid = document.createElement('div'); + susGrid.className = 'cm-grid'; + [0, 1, 2].forEach(function (si) { + susGrid.appendChild(caseMediaImagePicker('ผู้ต้องสงสัย ' + (si + 1), 'suspect-pick', c.suspects[si], function (v) { c.suspects[si] = v; })); + }); + root.appendChild(susGrid); + + var culHead = document.createElement('h3'); + culHead.className = 'cm-section-title'; + culHead.textContent = 'ส่งผู้ร้ายเข้าคุก (3 คน) — หน้าผลพิจารณาคดี'; + root.appendChild(culHead); + var culGrid = document.createElement('div'); + culGrid.className = 'cm-grid'; + [0, 1, 2].forEach(function (si) { + culGrid.appendChild(caseMediaImagePicker('คนร้ายในคุก ' + (si + 1), 'culprit-prison', c.culprits[si], function (v) { c.culprits[si] = v; })); + }); + root.appendChild(culGrid); + } + + function saveCaseMediaPanel() { + if (!caseMediaData) return; + var btn = el('btn-case-media-save'); + if (btn) btn.disabled = true; + setMsg('case-media-msg', 'กำลังบันทึก…', ''); + fetch(CASE_MEDIA_API, { + method: 'PUT', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(caseMediaData), + }).then(function (r) { + return r.text().then(function (t) { + var j = {}; + try { j = t ? JSON.parse(t) : {}; } catch (e) { /* ignore */ } + if (!r.ok || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status)); + return j; + }); + }).then(function (j) { + if (j && j.cases) caseMediaData = { cases: j.cases }; + renderCaseMedia(caseMediaCurrent); + setMsg('case-media-msg', 'บันทึกแล้ว — มีผลในเกมเมื่อรีเฟรช LobbyA/LobbyB', 'ok'); + }).catch(function (e) { + setMsg('case-media-msg', e.message || 'บันทึกไม่ได้', 'error'); + }).then(function () { + if (btn) btn.disabled = false; + }); + } + + (function bindCaseMediaPanel() { + caseMediaPopulateSelect(); + var caseSel = el('case-media-case'); + if (caseSel) caseSel.addEventListener('change', function () { renderCaseMedia(caseSel.value || '1'); }); + var saveBtn = el('btn-case-media-save'); + if (saveBtn) saveBtn.addEventListener('click', saveCaseMediaPanel); + })(); + + function setTab(name) { + document.querySelectorAll('.tab').forEach(function (t) { + var on = t.getAttribute('data-tab') === name; + t.classList.toggle('is-active', on); + t.setAttribute('aria-selected', on ? 'true' : 'false'); + }); + document.querySelectorAll('.tab-panel').forEach(function (p) { + p.hidden = !p.id || p.id !== 'tab-panel-' + name; + }); + var embedPanels = [ + { tab: 'map-editor', panel: 'tab-panel-map-editor', fsBtn: 'btn-map-editor-fullscreen' }, + { tab: 'characters', panel: 'tab-panel-characters', fsBtn: 'btn-character-fullscreen' }, + ]; + embedPanels.forEach(function (ep) { + if (name === ep.tab) return; + var pnl = el(ep.panel); + var fsBtn = el(ep.fsBtn); + if (pnl && pnl.classList.contains('is-fullscreen')) { + pnl.classList.remove('is-fullscreen'); + if (fsBtn) { + fsBtn.textContent = 'เต็มจอ'; + fsBtn.setAttribute('aria-pressed', 'false'); + } + } + }); + if (name === 'quiz') loadQuizSettingsPanel(); + if (name === 'special-quiz') loadSpecialQuizPanel(); + if (name === 'evidence-cards') loadEvidenceCardsPanel(); + if (name === 'case-media') loadCaseMediaPanel(); + if (name === 'quiz-carry') loadQuizCarryPanel(); + if (name === 'quiz-battle') loadQbBattlePanel(); + if (name === 'jump-survive') loadJumpSurviveTimingPanel(); + if (name === 'space-shooter') loadSpaceShooterTimingPanel(); + if (name === 'mega-virus') loadMegaVirusPanel(); + if (name === 'game-timing') loadGameTimingPanel(); + if (name === 'stack-game') loadStackGamePanel(); + } + + function boot() { + return api('session.php').then(function (s) { + if (s.needsSetup) { + showPanel('panel-setup'); + return null; + } + if (!s.loggedIn) { + showPanel('panel-login'); + return null; + } + showPanel('panel-app'); + el('admin-user-label').textContent = (s.admin && s.admin.username) || ''; + return s.admin; + }); + } + + el('form-setup').addEventListener('submit', function (e) { + e.preventDefault(); + var fd = new FormData(e.target); + var p1 = fd.get('password'); + var p2 = fd.get('password2'); + if (p1 !== p2) { + setMsg('setup-msg', 'รหัสผ่านไม่ตรงกัน', 'error'); + return; + } + api('setup.php', { method: 'POST', body: { password: p1 } }) + .then(function () { + setMsg('setup-msg', 'สร้างสำเร็จ — กรุณาล็อกอิน', 'ok'); + showPanel('panel-login'); + }) + .catch(function (err) { + setMsg('setup-msg', err.message || 'ผิดพลาด', 'error'); + }); + }); + + (function bindLoginPasswordToggle() { + var inp = el('login-password'); + var btn = el('login-password-toggle'); + if (!inp || !btn) return; + btn.addEventListener('click', function () { + var show = inp.type === 'password'; + inp.type = show ? 'text' : 'password'; + btn.setAttribute('aria-pressed', show ? 'true' : 'false'); + btn.setAttribute('aria-label', show ? 'ซ่อนรหัสผ่าน' : 'แสดงรหัสผ่าน'); + btn.textContent = show ? 'ซ่อน' : 'แสดง'; + }); + })(); + + el('form-login').addEventListener('submit', function (e) { + e.preventDefault(); + var fd = new FormData(e.target); + api('login.php', { + method: 'POST', + body: { + username: fd.get('username'), + password: fd.get('password'), + }, + }) + .then(function () { + setMsg('login-msg', '', ''); + return boot(); + }) + .then(function (admin) { + if (admin) loadAll(admin); + }) + .catch(function (err) { + setMsg('login-msg', err.message || 'เข้าไม่ได้', 'error'); + }); + }); + + el('btn-logout').addEventListener('click', function () { + api('logout.php', { method: 'POST' }) + .then(function () { + location.reload(); + }) + .catch(function () { + location.reload(); + }); + }); + + document.querySelectorAll('.tab').forEach(function (tab) { + tab.addEventListener('click', function () { + var name = tab.getAttribute('data-tab'); + if (name === 'change-password' && tab.classList.contains('is-active')) { + setTab('oauth'); + return; + } + setTab(name); + }); + }); + + function bindEmbedPanelFullscreen(btnId, panelId) { + var btn = el(btnId); + var panel = el(panelId); + if (!btn || !panel) return; + function setFs(on) { + panel.classList.toggle('is-fullscreen', on); + btn.textContent = on ? 'ออกจากเต็มจอ' : 'เต็มจอ'; + btn.setAttribute('aria-pressed', on ? 'true' : 'false'); + } + btn.addEventListener('click', function () { + setFs(!panel.classList.contains('is-fullscreen')); + }); + document.addEventListener('keydown', function (e) { + if (e.code !== 'Escape' || !panel.classList.contains('is-fullscreen')) return; + setFs(false); + }); + } + bindEmbedPanelFullscreen('btn-map-editor-fullscreen', 'tab-panel-map-editor'); + bindEmbedPanelFullscreen('btn-character-fullscreen', 'tab-panel-characters'); + + /* Test Mode toggle — เก็บใน localStorage (share ระหว่าง Admin กับ Game ภายใต้ origin เดียวกัน) */ + var TEST_MODE_KEY = 'justiceTestMode'; + function isTestModeOn() { + try { return localStorage.getItem(TEST_MODE_KEY) === '1'; } catch (e) { return false; } + } + function setTestMode(on) { + try { localStorage.setItem(TEST_MODE_KEY, on ? '1' : '0'); } catch (e) { /* ignore */ } + } + function syncTestModeUi() { + var chk = el('test-mode-toggle'); + var lbl = el('test-mode-state-label'); + var msg = el('test-mode-msg'); + var on = isTestModeOn(); + if (chk) chk.checked = on; + if (lbl) { + lbl.textContent = on ? 'เปิดอยู่ (ON)' : 'ปิดอยู่ (OFF)'; + lbl.style.color = on ? '#4ade80' : '#94a3b8'; + } + if (msg) { + msg.textContent = on + ? '✓ Test Mode เปิด — Shortcuts ใช้งานได้ในแท็บอื่นของเบราว์เซอร์นี้' + : '○ Test Mode ปิด — Shortcuts ไม่ทำงาน'; + msg.style.color = on ? '#4ade80' : '#94a3b8'; + } + } + var testModeChk = el('test-mode-toggle'); + if (testModeChk) { + testModeChk.addEventListener('change', function () { + setTestMode(!!testModeChk.checked); + syncTestModeUi(); + }); + } + syncTestModeUi(); + /* ฟัง storage event — sync เมื่อเปิดหลายแท็บ */ + window.addEventListener('storage', function (e) { + if (e && e.key === TEST_MODE_KEY) syncTestModeUi(); + }); + + var btnGameTimingSave = el('btn-game-timing-save'); + if (btnGameTimingSave) { + btnGameTimingSave.addEventListener('click', function () { + saveGameTimingPanel(); + }); + } + var btnJumpSurviveSave = el('btn-jump-survive-save'); + if (btnJumpSurviveSave) { + btnJumpSurviveSave.addEventListener('click', saveJumpSurviveTimingPanel); + } + bindJumpSurvivePlatformTileInputs(); + var btnSpaceShooterSave = el('btn-space-shooter-save'); + if (btnSpaceShooterSave) { + btnSpaceShooterSave.addEventListener('click', saveSpaceShooterTimingPanel); + } + bindSpaceShooterShipUrlInputs(); + bindStackBlockVisualInputs(); + bindSpaceShooterDamageOverlayPanel(); + bindSpaceShooterAsteroidSpritePanel(); + bindMegaVirusPanel(); + var btnStackGameSave = el('btn-stack-game-save'); + if (btnStackGameSave) { + btnStackGameSave.addEventListener('click', saveStackGamePanel); + } + bindGauntletAssetLibrary(); + bindGameTimingAssignedVisuals(); + bindGameTimingLaserColorSwatches(); + + var btnQuizAdd = el('btn-quiz-admin-add'); + var btnQuizSave = el('btn-quiz-admin-save'); + if (btnQuizAdd) { + btnQuizAdd.addEventListener('click', function () { + addQuizAdminRow('', true); + }); + } + if (btnQuizSave) btnQuizSave.addEventListener('click', saveQuizSettingsPanel); + + var btnSpecialQuizAdd = el('btn-special-quiz-add'); + var btnSpecialQuizSave = el('btn-special-quiz-save'); + if (btnSpecialQuizAdd) { + btnSpecialQuizAdd.addEventListener('click', function () { + addSpecialQuizRow({ type: 'tf', text: '', answerTrue: true }); + updateSpecialQuizCount(); + }); + } + if (btnSpecialQuizSave) btnSpecialQuizSave.addEventListener('click', saveSpecialQuizPanel); + var specialQuizLevelSel = el('special-quiz-level'); + var specialQuizCaseSel = el('special-quiz-case'); + if (specialQuizLevelSel) specialQuizLevelSel.addEventListener('change', onSpecialQuizSelectorChange); + if (specialQuizCaseSel) specialQuizCaseSel.addEventListener('change', onSpecialQuizSelectorChange); + + var btnQuizCarryAdd = el('btn-quiz-carry-add'); + var btnQuizCarrySave = el('btn-quiz-carry-save'); + if (btnQuizCarryAdd) { + btnQuizCarryAdd.addEventListener('click', function () { + addQuizCarryAdminRow(null); + }); + } + if (btnQuizCarrySave) btnQuizCarrySave.addEventListener('click', saveQuizCarryPanel); + wireQuizCarryPlaqueMapScaleControls(); + quizCarryBindThemePickersOnce(); + + var btnQbBattleAdd = el('btn-qb-battle-add'); + var btnQbBattleSave = el('btn-qb-battle-save'); + if (btnQbBattleAdd) { + btnQbBattleAdd.addEventListener('click', function () { + addQbBattleRow(null); + refreshQbBattlePreview(); + }); + } + if (btnQbBattleSave) btnQbBattleSave.addEventListener('click', saveQbBattlePanel); + + var qbFilterCat = el('qb-battle-filter-cat'); + if (qbFilterCat) qbFilterCat.addEventListener('change', applyQbBattleFilter); + + document.querySelectorAll('.qb-preview-mode-btn').forEach(function (b) { + b.addEventListener('click', function () { + document.querySelectorAll('.qb-preview-mode-btn').forEach(function (x) { + x.classList.toggle('is-active', x === b); + }); + refreshQbBattlePreview(); + }); + }); + + function loadOAuth() { + return api('oauth.php').then(function (r) { + var o = r.oauth || {}; + var f = el('form-oauth'); + ['facebookAppId', 'facebookAppSecret', 'facebookRedirectUri', 'googleClientId', 'googleClientSecret', 'googleRedirectUri'].forEach(function (k) { + var inp = f.querySelector('[name="' + k + '"]'); + if (inp) inp.value = o[k] || ''; + }); + }); + } + + el('form-oauth').addEventListener('submit', function (e) { + e.preventDefault(); + var fd = new FormData(e.target); + var body = {}; + fd.forEach(function (v, k) { + body[k] = v; + }); + api('oauth.php', { method: 'PUT', body: body }) + .then(function () { + setMsg('oauth-msg', 'บันทึกแล้ว', 'ok'); + }) + .catch(function (err) { + setMsg('oauth-msg', err.message || 'บันทึกไม่สำเร็จ', 'error'); + }); + }); + + function renderAccounts(list) { + var tb = el('table-accounts').querySelector('tbody'); + tb.innerHTML = ''; + var sumCoins = 0; + (list || []).forEach(function (a) { + var c = Math.max(0, parseInt(a.coins, 10) || 0); + sumCoins += c; + var tr = document.createElement('tr'); + tr.innerHTML = + '' + escapeHtml(a.displayName || '—') + '' + + '' + escapeHtml(a.email || '—') + '' + + '' + escapeHtml(a.loginType || '') + '' + + '' + escapeHtml(a.providerUserId || '—') + '' + + '' + + '' + + '' + + '' + + '' + (a.blocked ? 'บล็อก' : 'ปกติ') + '' + + ' ' + + ''; + tb.appendChild(tr); + }); + var sumEl = el('accounts-coins-summary'); + if (sumEl) { + sumEl.textContent = (list && list.length) ? ('ผู้ใช้ ' + list.length + ' บัญชี · รวม COINS ' + sumCoins) : 'ยังไม่มีบัญชี'; + } + tb.querySelectorAll('.btn-sm-del').forEach(function (b) { + b.addEventListener('click', function () { + if (!confirm('ลบบัญชีนี้?')) return; + api('accounts.php?id=' + encodeURIComponent(b.getAttribute('data-id')), { method: 'DELETE' }) + .then(loadAccounts) + .catch(function (err) { + setMsg('accounts-msg', err.message, 'error'); + }); + }); + }); + tb.querySelectorAll('.btn-sm-toggle').forEach(function (b) { + b.addEventListener('click', function () { + var id = b.getAttribute('data-id'); + var blocked = b.getAttribute('data-blocked') === '1'; + api('accounts.php', { + method: 'PATCH', + body: { id: id, blocked: !blocked }, + }) + .then(loadAccounts) + .catch(function (err) { + setMsg('accounts-msg', err.message, 'error'); + }); + }); + }); + tb.querySelectorAll('.btn-coins-save').forEach(function (b) { + b.addEventListener('click', function () { + var id = b.getAttribute('data-id'); + var row = b.closest('tr'); + var inp = row ? row.querySelector('input.input-coins') : null; + var coins = Math.max(0, parseInt(inp && inp.value, 10) || 0); + api('accounts.php', { method: 'PATCH', body: { id: id, coins: coins } }) + .then(function () { + setMsg('accounts-msg', 'อัปเดต COINS แล้ว', 'ok'); + return loadAccounts(); + }) + .catch(function (err) { + setMsg('accounts-msg', err.message, 'error'); + }); + }); + }); + } + + function escapeHtml(s) { + var d = document.createElement('div'); + d.textContent = s == null ? '' : String(s); + return d.innerHTML; + } + + function escapeAttr(s) { + return String(s || '').replace(/"/g, '"'); + } + + function loadAccounts() { + return api('accounts.php').then(function (r) { + renderAccounts(r.accounts); + setMsg('accounts-msg', '', ''); + }); + } + + el('form-account-add').addEventListener('submit', function (e) { + e.preventDefault(); + var fd = new FormData(e.target); + var body = { + email: fd.get('email'), + displayName: fd.get('displayName'), + loginType: fd.get('loginType'), + providerUserId: fd.get('providerUserId'), + notes: fd.get('notes'), + blocked: fd.get('blocked') === 'on', + coins: Math.max(0, parseInt(fd.get('coins'), 10) || 0), + }; + api('accounts.php', { method: 'POST', body: body }) + .then(function () { + e.target.reset(); + return loadAccounts(); + }) + .catch(function (err) { + setMsg('accounts-msg', err.message, 'error'); + }); + }); + + function renderAdmins(list, myId) { + var tb = el('table-admins').querySelector('tbody'); + tb.innerHTML = ''; + (list || []).forEach(function (a) { + var tr = document.createElement('tr'); + var delBtn = a.id === myId + ? '—' + : ''; + tr.innerHTML = + '' + escapeHtml(a.username) + '' + + '' + escapeHtml(a.role || 'admin') + '' + + '' + escapeHtml((a.createdAt || '').slice(0, 10)) + '' + + '' + delBtn + ''; + tb.appendChild(tr); + }); + tb.querySelectorAll('.btn-adm-del').forEach(function (b) { + b.addEventListener('click', function () { + if (!confirm('ลบแอดมินนี้?')) return; + api('admins.php?id=' + encodeURIComponent(b.getAttribute('data-id')), { method: 'DELETE' }) + .then(loadAdmins) + .catch(function (err) { + setMsg('admins-msg', err.message, 'error'); + }); + }); + }); + } + + function fillAdminResetSelect(admins, myId) { + var sel = el('select-admin-reset-target'); + if (!sel) return; + sel.innerHTML = ''; + (admins || []).forEach(function (a) { + if (!a.id || a.id === myId) return; + var opt = document.createElement('option'); + opt.value = a.id; + opt.textContent = a.username + ' (' + (a.role || 'admin') + ')'; + sel.appendChild(opt); + }); + } + + function loadAdmins() { + return api('admins.php').then(function (r) { + return api('session.php').then(function (s) { + var myId = s.admin && s.admin.id; + renderAdmins(r.admins, myId); + fillAdminResetSelect(r.admins, myId); + setMsg('admins-msg', '', ''); + }); + }); + } + + el('form-change-password').addEventListener('submit', function (e) { + e.preventDefault(); + var fd = new FormData(e.target); + var cur = fd.get('currentPassword'); + var n1 = fd.get('newPassword'); + var n2 = fd.get('newPassword2'); + if (n1 !== n2) { + setMsg('password-self-msg', 'รหัสใหม่ไม่ตรงกัน', 'error'); + return; + } + api('password.php', { + method: 'POST', + body: { currentPassword: cur, newPassword: n1 }, + }) + .then(function () { + e.target.reset(); + setMsg('password-self-msg', 'เปลี่ยนรหัสผ่านแล้ว', 'ok'); + }) + .catch(function (err) { + setMsg('password-self-msg', err.message, 'error'); + }); + }); + + el('form-admin-reset-other').addEventListener('submit', function (e) { + e.preventDefault(); + var fd = new FormData(e.target); + var tid = fd.get('targetId'); + var n1 = fd.get('newPassword'); + var n2 = fd.get('newPassword2'); + if (!tid) { + setMsg('admins-msg', 'เลือกแอดมิน', 'error'); + return; + } + if (n1 !== n2) { + setMsg('admins-msg', 'รหัสใหม่ไม่ตรงกัน', 'error'); + return; + } + api('admins.php', { + method: 'PATCH', + body: { id: tid, newPassword: n1 }, + }) + .then(function () { + e.target.reset(); + setMsg('admins-msg', 'ตั้งรหัสใหม่ให้แอดมินที่เลือกแล้ว', 'ok'); + }) + .catch(function (err) { + setMsg('admins-msg', err.message, 'error'); + }); + }); + + el('form-admin-add').addEventListener('submit', function (e) { + e.preventDefault(); + var fd = new FormData(e.target); + api('admins.php', { + method: 'POST', + body: { + username: fd.get('username'), + password: fd.get('password'), + role: fd.get('role'), + }, + }) + .then(function () { + e.target.reset(); + return loadAdmins(); + }) + .catch(function (err) { + setMsg('admins-msg', err.message, 'error'); + }); + }); + + function getDesiredAdminTab(admin) { + var want = null; + try { + want = new URL(window.location.href).searchParams.get('tab'); + } catch (e) { /* ignore */ } + var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'special-quiz': true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'space-shooter': true, 'mega-virus': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true }; + if (!want || !allowed[want]) return 'oauth'; + if (want === 'admins' && (!admin || admin.role !== 'super')) return 'oauth'; + return want; + } + + function loadAll(admin) { + loadOAuth().catch(function (e) { + setMsg('oauth-msg', e.message, 'error'); + }); + loadAccounts().catch(function (e) { + setMsg('accounts-msg', e.message, 'error'); + }); + var isSuper = admin && admin.role === 'super'; + el('tab-admins').hidden = !isSuper; + el('form-admin-add').hidden = !isSuper; + if (isSuper) { + loadAdmins().catch(function (e) { + setMsg('admins-msg', e.message, 'error'); + }); + } + setTab(getDesiredAdminTab(admin)); + /** อย่า prefetch ซิงก์ Stack ที่นี่ — GET ที่เริ่มก่อน PUT บันทึกอาจกลับช้ากว่าและทับช่องกว้าง/HZ ด้วยข้อมูลเก่า · โหลดจริงที่แท็บ Stack / เวลาเกมแทน */ + } + + boot().then(function (admin) { + if (admin) loadAll(admin); + }); +})(); diff --git a/www/html/Game/data/maps/qbroom1.json b/www/html/Game/data/maps/qbroom1.json index 8376772..20e2b76 100644 --- a/www/html/Game/data/maps/qbroom1.json +++ b/www/html/Game/data/maps/qbroom1.json @@ -1 +1 @@ -{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — กฎหมายใกล้ตัว","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-1.jpg","characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownArea":[],"carryEmbedCountdownHighlight":false,"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]} \ No newline at end of file +{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — กฎหมายใกล้ตัว","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-1.jpg","characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownArea":[],"carryEmbedCountdownHighlight":false,"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"quizBattleCategoryId":"topic1"} \ No newline at end of file diff --git a/www/html/Game/data/maps/qbroom10.json b/www/html/Game/data/maps/qbroom10.json index 58bdc7d..ed41a55 100644 --- a/www/html/Game/data/maps/qbroom10.json +++ b/www/html/Game/data/maps/qbroom10.json @@ -1 +1 @@ -{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":1,"y":1},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — งานบริการกระทรวงยุติธรรม","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-10.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":0,"blockPlayerSeparationH":0,"lobbySpawnMode":"random","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]} \ No newline at end of file +{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — งานบริการกระทรวงยุติธรรม","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-10.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"quizBattleCategoryId":"topic10"} \ No newline at end of file diff --git a/www/html/Game/data/maps/qbroom2.json b/www/html/Game/data/maps/qbroom2.json index 919d3ec..c322444 100644 --- a/www/html/Game/data/maps/qbroom2.json +++ b/www/html/Game/data/maps/qbroom2.json @@ -1 +1 @@ -{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":1,"y":1},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — กฎหมายสิทธิ์พื้นฐาน","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-2.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":0,"blockPlayerSeparationH":0,"lobbySpawnMode":"random","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]} \ No newline at end of file +{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — กฎหมายสิทธิ์พื้นฐาน","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-2.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"quizBattleCategoryId":"topic2"} \ No newline at end of file diff --git a/www/html/Game/data/maps/qbroom3.json b/www/html/Game/data/maps/qbroom3.json index 756d954..0dfd71a 100644 --- a/www/html/Game/data/maps/qbroom3.json +++ b/www/html/Game/data/maps/qbroom3.json @@ -1 +1 @@ -{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":1,"y":1},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — กฎหมายจราจร","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-3.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":0,"blockPlayerSeparationH":0,"lobbySpawnMode":"random","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]} \ No newline at end of file +{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — กฎหมายจราจร","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-3.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"quizBattleCategoryId":"topic3"} \ No newline at end of file diff --git a/www/html/Game/data/maps/qbroom4.json b/www/html/Game/data/maps/qbroom4.json index 07dd104..06cb4ac 100644 --- a/www/html/Game/data/maps/qbroom4.json +++ b/www/html/Game/data/maps/qbroom4.json @@ -1 +1 @@ -{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":1,"y":1},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — คดีเกี่ยวกับทรัพย์","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-4.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":0,"blockPlayerSeparationH":0,"lobbySpawnMode":"random","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]} \ No newline at end of file +{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — คดีเกี่ยวกับทรัพย์","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-4.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"quizBattleCategoryId":"topic4"} \ No newline at end of file diff --git a/www/html/Game/data/maps/qbroom5.json b/www/html/Game/data/maps/qbroom5.json index d8e29f4..c27aa8e 100644 --- a/www/html/Game/data/maps/qbroom5.json +++ b/www/html/Game/data/maps/qbroom5.json @@ -1 +1 @@ -{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":1,"y":1},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — คดีหมิ่นประมาท","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-5.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":0,"blockPlayerSeparationH":0,"lobbySpawnMode":"random","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]} \ No newline at end of file +{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — คดีหมิ่นประมาท","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-5.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"quizBattleCategoryId":"topic5"} \ No newline at end of file diff --git a/www/html/Game/data/maps/qbroom6.json b/www/html/Game/data/maps/qbroom6.json index 05cd67d..4d04bf5 100644 --- a/www/html/Game/data/maps/qbroom6.json +++ b/www/html/Game/data/maps/qbroom6.json @@ -1 +1 @@ -{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":1,"y":1},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — คดีทางเพศ","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-6.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":0,"blockPlayerSeparationH":0,"lobbySpawnMode":"random","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]} \ No newline at end of file +{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — คดีทางเพศ","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-6.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"quizBattleCategoryId":"topic6"} \ No newline at end of file diff --git a/www/html/Game/data/maps/qbroom7.json b/www/html/Game/data/maps/qbroom7.json index 00da3d2..2a71bef 100644 --- a/www/html/Game/data/maps/qbroom7.json +++ b/www/html/Game/data/maps/qbroom7.json @@ -1 +1 @@ -{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":1,"y":1},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — คดีอาชญากรรม","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-7.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":0,"blockPlayerSeparationH":0,"lobbySpawnMode":"random","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]} \ No newline at end of file +{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — คดีอาชญากรรม","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-7.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"quizBattleCategoryId":"topic7"} \ No newline at end of file diff --git a/www/html/Game/data/maps/qbroom8.json b/www/html/Game/data/maps/qbroom8.json index 129abd7..e0d92c5 100644 --- a/www/html/Game/data/maps/qbroom8.json +++ b/www/html/Game/data/maps/qbroom8.json @@ -1 +1 @@ -{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":1,"y":1},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — อาชญากรรมออนไลน์","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-8.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":0,"blockPlayerSeparationH":0,"lobbySpawnMode":"random","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]} \ No newline at end of file +{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — อาชญากรรมออนไลน์","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-8.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"quizBattleCategoryId":"topic8"} \ No newline at end of file diff --git a/www/html/Game/data/maps/qbroom9.json b/www/html/Game/data/maps/qbroom9.json index f6a26e6..ad13cf1 100644 --- a/www/html/Game/data/maps/qbroom9.json +++ b/www/html/Game/data/maps/qbroom9.json @@ -1 +1 @@ -{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":1,"y":1},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — กระบวนการยุติธรรม","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-9.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":0,"blockPlayerSeparationH":0,"lobbySpawnMode":"random","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]]} \ No newline at end of file +{"width":20,"height":40,"tileSize":22,"gameType":"quiz_battle","characterCells":1,"characterCellsW":1,"characterCellsH":1,"ground":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"objects":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1],[1,1,1,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],[1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1],[1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1],[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]],"blockPlayer":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"interactive":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"startGameArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawnArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"spawn":{"x":4,"y":4},"quizTrueArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizFalseArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizQuestions":[],"quizCarryHubArea":[],"quizCarryOptionArea":[],"stackReleaseArea":[],"stackLandArea":[],"jumpSurvivePlatforms":[],"jumpSurvivePlatformArea":[],"jumpSurviveRisePxPerSec":42,"jumpSurviveGravity":3200,"jumpSurviveJumpImpulse":0,"jumpSurviveMovePxPerSec":200,"name":"Quiz Battle — กระบวนการยุติธรรม","cellColors":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"showMapInGame":true,"quizBattleDomeArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0],[0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"shooterSpawnSlots":[],"balloonBossPlayerSlots":[],"balloonBossBossSpawn":null,"lanes":[],"gauntletPlayerSpawns":[],"backgroundImage":"/Game/img/qb-scenes/room-9.jpg","carryEmbedCountdownArea":[],"jumpSurvivePlatformVariantArea":[],"jumpSurviveHazardArea":[],"characterCollisionW":1,"characterCollisionH":1,"blockPlayerSeparationW":1,"blockPlayerSeparationH":1,"lobbySpawnMode":"slots6","lobbyPlayerSpawns":[null,null,null,null,null,null],"carryEmbedCountdownHighlight":false,"customizeSpot":null,"hostConsoleSpot":null,"gridImageLibrary":[],"gridImageSprites":[],"gridImageCells":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]],"quizBattleCategoryId":"topic9"} \ No newline at end of file diff --git a/www/html/Game/data/quiz-settings.json b/www/html/Game/data/quiz-settings.json index 429afb6..cb09517 100644 --- a/www/html/Game/data/quiz-settings.json +++ b/www/html/Game/data/quiz-settings.json @@ -278,214 +278,504 @@ ], "battleQuizMcq": [ { - "categoryId": "cybercrime", - "text": "การเข้าถึงระบบคอมพิวเตอร์ของผู้อื่นโดยไม่ได้รับอนุญาต ผิดกฎหมายใด?", + "categoryId": "topic1", + "text": "[กฎหมายใกล้ตัว] คำถามตัวอย่างที่ 1 (แก้ได้ใน Admin)", "choices": [ - "พ.ร.บ.จราจร", - "พ.ร.บ.คอมพิวเตอร์", - "กฎหมายแรงงาน" - ], - "correctIndex": 1 - }, - { - "categoryId": "cybercrime", - "text": "การหลอกให้เหยื่อโอนเงินผ่านเว็บ/ข้อความปลอม เรียกว่าอะไร?", - "choices": [ - "Phishing", - "Backup", - "Caching" + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" ], "correctIndex": 0 }, { - "categoryId": "cybercrime", - "text": "มัลแวร์ที่เข้ารหัสไฟล์แล้วเรียกค่าไถ่ คือ?", + "categoryId": "topic1", + "text": "[กฎหมายใกล้ตัว] คำถามตัวอย่างที่ 2 (แก้ได้ใน Admin)", "choices": [ - "Adware", - "Ransomware", - "Firewall" + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" ], "correctIndex": 1 }, { - "categoryId": "cyber_law", - "text": "การนำเข้าข้อมูลเท็จสู่ระบบคอมพิวเตอร์ที่ทำให้ผู้อื่นเสียหาย มีโทษตามกฎหมายใด?", + "categoryId": "topic1", + "text": "[กฎหมายใกล้ตัว] คำถามตัวอย่างที่ 3 (แก้ได้ใน Admin)", "choices": [ - "พ.ร.บ.คอมพิวเตอร์", - "พ.ร.บ.ลิขสิทธิ์", - "ไม่มีความผิด" - ], - "correctIndex": 0 - }, - { - "categoryId": "cyber_law", - "text": "ข้อมูล log การเข้าใช้งานระบบ จัดเป็นพยานหลักฐานประเภทใด?", - "choices": [ - "พยานบุคคล", - "พยานเอกสารอิเล็กทรอนิกส์", - "ไม่ถือเป็นหลักฐาน" - ], - "correctIndex": 1 - }, - { - "categoryId": "cyber_law", - "text": "ผู้ให้บริการต้องเก็บข้อมูลจราจรทางคอมพิวเตอร์ (log) อย่างน้อยกี่วันตาม พ.ร.บ.คอมพิวเตอร์?", - "choices": [ - "7 วัน", - "90 วัน", - "1 ปี" - ], - "correctIndex": 1 - }, - { - "categoryId": "privacy", - "text": "กฎหมายคุ้มครองข้อมูลส่วนบุคคลของไทย เรียกย่อว่าอะไร?", - "choices": [ - "PDPA", - "GDPR", - "DMCA" - ], - "correctIndex": 0 - }, - { - "categoryId": "privacy", - "text": "ก่อนเก็บข้อมูลส่วนบุคคล โดยทั่วไปต้องทำสิ่งใดก่อน?", - "choices": [ - "ขอความยินยอม", - "เผยแพร่ทันที", - "ขายต่อ" - ], - "correctIndex": 0 - }, - { - "categoryId": "privacy", - "text": "ข้อใดเป็น “ข้อมูลส่วนบุคคลอ่อนไหว”?", - "choices": [ - "ชื่อเล่น", - "ข้อมูลสุขภาพ", - "รหัสไปรษณีย์จังหวัด" - ], - "correctIndex": 1 - }, - { - "categoryId": "digital_security", - "text": "วิธีใดช่วยเพิ่มความปลอดภัยบัญชีได้ดีที่สุด?", - "choices": [ - "ใช้รหัสผ่านเดียวทุกที่", - "เปิดยืนยันตัวตน 2 ชั้น (2FA)", - "จดรหัสไว้หน้าจอ" - ], - "correctIndex": 1 - }, - { - "categoryId": "digital_security", - "text": "รหัสผ่านแบบใดปลอดภัยที่สุด?", - "choices": [ - "123456", - "วันเกิดตัวเอง", - "ยาว+ผสมอักขระหลายแบบ" + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" ], "correctIndex": 2 }, { - "categoryId": "digital_security", - "text": "ควรทำอย่างไรเมื่อได้รับลิงก์แปลกจากคนไม่รู้จัก?", + "categoryId": "topic1", + "text": "[กฎหมายใกล้ตัว] คำถามตัวอย่างที่ 4 (แก้ได้ใน Admin)", "choices": [ - "กดทันที", - "ไม่กดและตรวจสอบที่มา", - "ส่งต่อให้เพื่อน" - ], - "correctIndex": 1 - }, - { - "categoryId": "social_media", - "text": "การโพสต์ข้อความหมิ่นประมาทผู้อื่นบนโซเชียล มีผลอย่างไร?", - "choices": [ - "ไม่ผิดเพราะเป็นออนไลน์", - "อาจมีความผิดทางกฎหมาย", - "ผิดเฉพาะถ้าใช้ชื่อจริง" - ], - "correctIndex": 1 - }, - { - "categoryId": "social_media", - "text": "ก่อนแชร์ข่าว ควรทำสิ่งใด?", - "choices": [ - "ตรวจสอบความถูกต้องของแหล่งข่าว", - "แชร์ให้เร็วที่สุด", - "แชร์เฉพาะพาดหัว" + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" ], "correctIndex": 0 }, { - "categoryId": "social_media", - "text": "การตั้งค่าความเป็นส่วนตัวของโพสต์ช่วยเรื่องใด?", + "categoryId": "topic1", + "text": "[กฎหมายใกล้ตัว] คำถามตัวอย่างที่ 5 (แก้ได้ใน Admin)", "choices": [ - "ควบคุมว่าใครเห็นได้", - "เพิ่มความเร็วเน็ต", - "ลบไวรัส" + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic2", + "text": "[กฎหมายสิทธิ์พื้นฐาน] คำถามตัวอย่างที่ 1 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" ], "correctIndex": 0 }, { - "categoryId": "ethics", - "text": "การคัดลอกผลงานผู้อื่นมาเป็นของตนเรียกว่าอะไร?", + "categoryId": "topic2", + "text": "[กฎหมายสิทธิ์พื้นฐาน] คำถามตัวอย่างที่ 2 (แก้ได้ใน Admin)", "choices": [ - "การอ้างอิง", - "การลอกเลียน (plagiarism)", - "การสำรองข้อมูล" + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" ], "correctIndex": 1 }, { - "categoryId": "ethics", - "text": "พฤติกรรมใดถือว่า “กลั่นแกล้งทางออนไลน์” (cyberbullying)?", + "categoryId": "topic2", + "text": "[กฎหมายสิทธิ์พื้นฐาน] คำถามตัวอย่างที่ 3 (แก้ได้ใน Admin)", "choices": [ - "ให้กำลังใจเพื่อน", - "ส่งข้อความข่มขู่ซ้ำ ๆ", - "กดไลก์รูปภาพ" + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" ], - "correctIndex": 1 + "correctIndex": 2 }, { - "categoryId": "ethics", - "text": "การใช้ข้อมูลของผู้อื่นโดยไม่ขออนุญาตถือว่า?", + "categoryId": "topic2", + "text": "[กฎหมายสิทธิ์พื้นฐาน] คำถามตัวอย่างที่ 4 (แก้ได้ใน Admin)", "choices": [ - "ยอมรับได้เสมอ", - "ไม่เหมาะสมและอาจผิดกฎหมาย", - "เป็นมารยาทที่ดี" - ], - "correctIndex": 1 - }, - { - "categoryId": "ai_data", - "text": "ข้อมูลที่ใช้สอนโมเดล AI ควรมีลักษณะใด?", - "choices": [ - "มีอคติมาก ๆ", - "มีคุณภาพและได้รับอนุญาต", - "ละเมิดลิขสิทธิ์ก็ได้" - ], - "correctIndex": 1 - }, - { - "categoryId": "ai_data", - "text": "ผลลัพธ์จาก AI ควรถูกตรวจสอบโดยใคร?", - "choices": [ - "ไม่ต้องตรวจ", - "มนุษย์ผู้รับผิดชอบ", - "AI ตัวอื่นเท่านั้น" - ], - "correctIndex": 1 - }, - { - "categoryId": "ai_data", - "text": "Deepfake คือสิ่งใด?", - "choices": [ - "สื่อปลอมที่สร้างด้วย AI", - "โปรแกรมป้องกันไวรัส", - "ระบบสำรองข้อมูล" + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" ], "correctIndex": 0 + }, + { + "categoryId": "topic2", + "text": "[กฎหมายสิทธิ์พื้นฐาน] คำถามตัวอย่างที่ 5 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic3", + "text": "[กฎหมายจราจร] คำถามตัวอย่างที่ 1 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic3", + "text": "[กฎหมายจราจร] คำถามตัวอย่างที่ 2 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic3", + "text": "[กฎหมายจราจร] คำถามตัวอย่างที่ 3 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 2 + }, + { + "categoryId": "topic3", + "text": "[กฎหมายจราจร] คำถามตัวอย่างที่ 4 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic3", + "text": "[กฎหมายจราจร] คำถามตัวอย่างที่ 5 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic4", + "text": "[คดีเกี่ยวกับทรัพย์] คำถามตัวอย่างที่ 1 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic4", + "text": "[คดีเกี่ยวกับทรัพย์] คำถามตัวอย่างที่ 2 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic4", + "text": "[คดีเกี่ยวกับทรัพย์] คำถามตัวอย่างที่ 3 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 2 + }, + { + "categoryId": "topic4", + "text": "[คดีเกี่ยวกับทรัพย์] คำถามตัวอย่างที่ 4 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic4", + "text": "[คดีเกี่ยวกับทรัพย์] คำถามตัวอย่างที่ 5 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic5", + "text": "[คดีหมิ่นประมาท] คำถามตัวอย่างที่ 1 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic5", + "text": "[คดีหมิ่นประมาท] คำถามตัวอย่างที่ 2 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic5", + "text": "[คดีหมิ่นประมาท] คำถามตัวอย่างที่ 3 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 2 + }, + { + "categoryId": "topic5", + "text": "[คดีหมิ่นประมาท] คำถามตัวอย่างที่ 4 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic5", + "text": "[คดีหมิ่นประมาท] คำถามตัวอย่างที่ 5 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic6", + "text": "[คดีทางเพศ] คำถามตัวอย่างที่ 1 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic6", + "text": "[คดีทางเพศ] คำถามตัวอย่างที่ 2 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic6", + "text": "[คดีทางเพศ] คำถามตัวอย่างที่ 3 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 2 + }, + { + "categoryId": "topic6", + "text": "[คดีทางเพศ] คำถามตัวอย่างที่ 4 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic6", + "text": "[คดีทางเพศ] คำถามตัวอย่างที่ 5 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic7", + "text": "[คดีอาชญากรรม] คำถามตัวอย่างที่ 1 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic7", + "text": "[คดีอาชญากรรม] คำถามตัวอย่างที่ 2 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic7", + "text": "[คดีอาชญากรรม] คำถามตัวอย่างที่ 3 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 2 + }, + { + "categoryId": "topic7", + "text": "[คดีอาชญากรรม] คำถามตัวอย่างที่ 4 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic7", + "text": "[คดีอาชญากรรม] คำถามตัวอย่างที่ 5 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic8", + "text": "[อาชญากรรมออนไลน์] คำถามตัวอย่างที่ 1 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic8", + "text": "[อาชญากรรมออนไลน์] คำถามตัวอย่างที่ 2 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic8", + "text": "[อาชญากรรมออนไลน์] คำถามตัวอย่างที่ 3 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 2 + }, + { + "categoryId": "topic8", + "text": "[อาชญากรรมออนไลน์] คำถามตัวอย่างที่ 4 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic8", + "text": "[อาชญากรรมออนไลน์] คำถามตัวอย่างที่ 5 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic9", + "text": "[กระบวนการยุติธรรม] คำถามตัวอย่างที่ 1 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic9", + "text": "[กระบวนการยุติธรรม] คำถามตัวอย่างที่ 2 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic9", + "text": "[กระบวนการยุติธรรม] คำถามตัวอย่างที่ 3 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 2 + }, + { + "categoryId": "topic9", + "text": "[กระบวนการยุติธรรม] คำถามตัวอย่างที่ 4 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic9", + "text": "[กระบวนการยุติธรรม] คำถามตัวอย่างที่ 5 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic10", + "text": "[งานบริการกระทรวงยุติธรรม] คำถามตัวอย่างที่ 1 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic10", + "text": "[งานบริการกระทรวงยุติธรรม] คำถามตัวอย่างที่ 2 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 + }, + { + "categoryId": "topic10", + "text": "[งานบริการกระทรวงยุติธรรม] คำถามตัวอย่างที่ 3 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 2 + }, + { + "categoryId": "topic10", + "text": "[งานบริการกระทรวงยุติธรรม] คำถามตัวอย่างที่ 4 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 0 + }, + { + "categoryId": "topic10", + "text": "[งานบริการกระทรวงยุติธรรม] คำถามตัวอย่างที่ 5 (แก้ได้ใน Admin)", + "choices": [ + "ตัวเลือก ก", + "ตัวเลือก ข", + "ตัวเลือก ค" + ], + "correctIndex": 1 } ], "specialQuizSets": { @@ -1990,4 +2280,4 @@ } } } -} +} \ No newline at end of file diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index d7e3901..5317f05 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -3048,6 +3048,7 @@ let quizBattleMcqPool = []; let quizBattleAnsweredComps = new Set(); let quizBattleModalCompId = null; + let qbAutoLastComp = null; // โดมล่าสุดที่ trigger อัตโนมัติ (กันเด้งซ้ำขณะยืนค้าง) const SPACE_SHOOTER_SHIP_COLORS = ['#50fa7b', '#ff79c6', '#ff5555', '#f1fa8c', '#bd93f9', '#8be9fd']; document.getElementById('room-id').textContent = spaceId; @@ -12811,6 +12812,86 @@ hideQuizBattleMcqModal(); } + /** หา comp โดมที่ตัวละครเหยียบอยู่ — เช็คหลายจุด (กลาง/เท้า/หัว/ซ้าย-ขวา) ให้เด้งง่ายเมื่อถึงสถานี */ + function quizBattleDomeAtMe() { + if (!isQuizBattle()) return null; + const pts = [[me.x, me.y], [me.x, me.y + 0.45], [me.x, me.y - 0.25], [me.x + 0.3, me.y], [me.x - 0.3, me.y]]; + for (let i = 0; i < pts.length; i++) { + const c = getPrimaryQuizBattleDomeCompAt(pts[i][0], pts[i][1]); + if (c != null) return c; + } + return null; + } + + /** ZEP-style: เหยียบสถานี (โดม) ที่ยังไม่ตอบ → เปิดคำถามอัตโนมัติ (ครั้งเดียวต่อการเข้า · เดินออกแล้วกลับมาใหม่ถึงเด้งอีก) */ + function maybeAutoOpenQuizBattle() { + if (!isQuizBattle() || myId == null) return; + const compNow = quizBattleDomeAtMe(); + if (compNow == null) { qbAutoLastComp = null; return; } + const ov = document.getElementById('quiz-battle-mcq-overlay'); + const modalOpen = quizBattleModalCompId != null || (ov && !ov.classList.contains('is-hidden')); + if (modalOpen) return; + if (compNow === qbAutoLastComp) return; + qbAutoLastComp = compNow; + if (quizBattleAnsweredComps.has(compNow)) return; + const pool = quizBattleMcqPool; + if (!pool.length) return; + const q = pool[(compNow - 1) % pool.length]; + if (!q) return; + quizBattleModalCompId = compNow; + showQuizBattleMcqModal(q); + } + + function qbRoundRect(ctx, x, y, w, h, r) { + r = Math.min(r, w / 2, h / 2); + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); + } + /** หีบบนสถานีคำถาม — done=ปลดล็อก(เขียว+ติ๊ก) / ยังไม่ตอบ=ล็อก(น้ำตาล+กุญแจ) + เลขสถานี */ + function drawQuizBattleChest(ctx, cx, cy, size, done, num) { + const w = size * 0.84, h = size * 0.64; + const x0 = cx - w / 2, y0 = cy - h / 2; + ctx.save(); + ctx.shadowColor = done ? 'rgba(52,211,153,0.95)' : 'rgba(255,200,70,0.95)'; + ctx.shadowBlur = Math.max(4, size * 0.3); + ctx.fillStyle = done ? '#3f7d57' : '#8a5a2b'; + qbRoundRect(ctx, x0, y0 + h * 0.34, w, h * 0.66, size * 0.08); ctx.fill(); + ctx.fillStyle = done ? '#57a875' : '#a86f33'; + qbRoundRect(ctx, x0, y0, w, h * 0.44, size * 0.1); ctx.fill(); + ctx.shadowBlur = 0; + ctx.fillStyle = done ? '#d6f5e3' : '#f1c66a'; + ctx.fillRect(x0, y0 + h * 0.30, w, h * 0.10); + ctx.fillRect(cx - size * 0.045, y0, size * 0.09, h); + if (done) { + ctx.strokeStyle = '#eafff3'; ctx.lineWidth = Math.max(2, size * 0.08); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; + ctx.beginPath(); + ctx.moveTo(cx - size * 0.13, cy + size * 0.04); + ctx.lineTo(cx - size * 0.02, cy + size * 0.15); + ctx.lineTo(cx + size * 0.17, cy - size * 0.12); + ctx.stroke(); + } else { + ctx.strokeStyle = '#f1c66a'; ctx.lineWidth = Math.max(2, size * 0.05); + ctx.beginPath(); ctx.arc(cx, cy + h * 0.04, size * 0.085, Math.PI, 0); ctx.stroke(); + ctx.fillStyle = '#f1c66a'; + qbRoundRect(ctx, cx - size * 0.085, cy + h * 0.04, size * 0.17, size * 0.13, size * 0.03); ctx.fill(); + } + if (num) { + ctx.fillStyle = 'rgba(8,12,28,0.82)'; + ctx.beginPath(); ctx.arc(cx, y0 - size * 0.14, size * 0.17, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = '#fff'; + ctx.font = 'bold ' + Math.max(9, size * 0.23) + 'px Kanit, sans-serif'; + ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText(String(num), cx, y0 - size * 0.14); + ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic'; + } + ctx.restore(); + } + function tryOpenQuizBattleFromKey() { if (!isQuizBattle() || !mapData || myId == null) return; const compId = getPrimaryQuizBattleDomeCompAt(me.x, me.y); @@ -12835,10 +12916,13 @@ function pushSanitizedBattleMcqFromPayload(s) { if (!s || !Array.isArray(s.battleQuizMcq)) return 0; + /* กรองเฉพาะคลังคำถามของหัวข้อห้องนี้ (mapData.quizBattleCategoryId เช่น topic1..topic10) — ไม่ระบุ = ใช้ทั้งหมด */ + const roomCat = (mapData && mapData.quizBattleCategoryId) ? String(mapData.quizBattleCategoryId).trim() : ''; let n = 0; for (let i = 0; i < s.battleQuizMcq.length; i++) { const raw = s.battleQuizMcq[i]; if (!raw || !String(raw.text || '').trim()) continue; + if (roomCat && String(raw.categoryId || '').trim() !== roomCat) continue; const ch = Array.isArray(raw.choices) ? raw.choices : []; if (ch.length !== 3) continue; const a = String(ch[0] || '').trim(); @@ -19481,20 +19565,20 @@ } } if (isQuizBattle() && showGrid && mapData.quizBattlePathArea && mapData.quizBattlePathArea[y] && mapData.quizBattlePathArea[y][x] === 1) { - ctx.fillStyle = 'rgba(186, 130, 255, 0.4)'; + /* ไฮไลต์เส้นทางบาง ๆ (โชว์เฉพาะตอนเปิดกริด) ให้เห็นถนนจาก design ทะลุ */ + ctx.fillStyle = 'rgba(150, 195, 255, 0.12)'; ctx.fillRect(sx + 1, sy + 1, size - 2, size - 2); - ctx.strokeStyle = 'rgba(220, 170, 255, 0.55)'; - ctx.strokeRect(sx + 1, sy + 1, size - 2, size - 2); } - if (isQuizBattle() && showGrid && mapData.quizBattleDomeArea && mapData.quizBattleDomeArea[y] && mapData.quizBattleDomeArea[y][x] === 1) { + if (isQuizBattle() && mapData.quizBattleDomeArea && mapData.quizBattleDomeArea[y] && mapData.quizBattleDomeArea[y][x] === 1) { const cid = mapData.quizBattleDomeComp && mapData.quizBattleDomeComp[y] && mapData.quizBattleDomeComp[y][x]; - const done = cid > 0 && quizBattleAnsweredComps.has(cid); - ctx.fillStyle = done ? 'rgba(90, 90, 110, 0.4)' : 'rgba(100, 200, 255, 0.36)'; - ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); - ctx.strokeStyle = done ? 'rgba(140, 140, 160, 0.65)' : 'rgba(255, 100, 130, 0.82)'; - ctx.lineWidth = 2; - ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); - ctx.lineWidth = 1; + const comp = mapData.quizBattleDomeComp; + /* วาดหีบครั้งเดียวต่อโดม (ช่องบน-ซ้ายสุดของกลุ่ม comp) — กันวาดซ้อน */ + const upSame = cid > 0 && comp && comp[y - 1] && comp[y - 1][x] === cid; + const leftSame = cid > 0 && comp && comp[y] && comp[y][x - 1] === cid; + if (!upSame && !leftSame) { + const done = cid > 0 && quizBattleAnsweredComps.has(cid); + drawQuizBattleChest(ctx, sx + size / 2, sy + size / 2, size, done, cid || 0); + } } if (isStack()) { const sr = mapData.stackReleaseArea, sl = mapData.stackLandArea; @@ -20424,6 +20508,7 @@ enforceQuizBattleLaneOnMePlay(); const t = Date.now(); if (t - lastSend > 80) { lastSend = t; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } + maybeAutoOpenQuizBattle(); draw(); requestAnimationFrame(tick); return; @@ -20491,6 +20576,7 @@ me.isWalking = !!(accX !== 0 || accY !== 0) || playPath.length > 0 || movedThisTick; const now = Date.now(); if (now - lastSend > 80) { lastSend = now; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } + maybeAutoOpenQuizBattle(); draw(); requestAnimationFrame(tick); } diff --git a/www/html/Game/public/js/play.js.bak-120915 b/www/html/Game/public/js/play.js.bak-120915 new file mode 100644 index 0000000..d7e3901 --- /dev/null +++ b/www/html/Game/public/js/play.js.bak-120915 @@ -0,0 +1,20826 @@ +(function () { + const BASE = '/Game'; + function playDesignMainMenuHref() { + try { + if (typeof globalThis !== 'undefined' && typeof globalThis.designMainMenuHref === 'function') { + return globalThis.designMainMenuHref(); + } + } catch (e) { /* ignore */ } + return '/Main-Menu/'; + } + function playDesignMainLobbyHref() { + try { + if (typeof globalThis !== 'undefined' && typeof globalThis.designMainLobbyHref === 'function') { + return globalThis.designMainLobbyHref(); + } + } catch (e) { /* ignore */ } + return '/Main-Lobby/'; + } + /** รูปเลข 3–2–1 — เสิร์ฟที่ /Game/img/QUESTION/ (nginx alias /Game/ → public; อย่าใช้ /Game/public/img/) */ + const COUNTDOWN_321_IMG_BASE = BASE + '/img/QUESTION/'; + /** แผงคำถามบนแมป quiz_carry — ไฟล์อยู่ public/img/quiz-carry */ + const QUIZ_CARRY_MAP_FEEDBACK_IMG = { + correct: BASE + '/img/quiz-carry/icon-Correct.png', + incorrect: BASE + '/img/quiz-carry/icon-Incorrect.png', + scorePlus: BASE + '/img/quiz-carry/score+10.png', + }; + const QUIZ_MAP_CARRY_FEEDBACK_MS = 1400; + const params = new URLSearchParams(window.location.search); + const spaceId = params.get('space'); + function readStoredDisplayName() { + try { + return (localStorage.getItem('roomLobbyDisplayName') || localStorage.getItem('playerName') || '').trim(); + } catch (e) { return ''; } + } + let nick = (params.get('nick') || '').trim() || readStoredDisplayName() || 'ผู้เล่น'; + const previewMode = params.get('preview') === '1'; + const specialQuizForceDebug = params.get('sqForce') === '1'; + const forceDefaultCharacter = params.get('defaultChar') === '1'; + const editorEmbedReturn = params.get('editorEmbed') === '1'; + if (previewMode && editorEmbedReturn) { + try { document.documentElement.classList.add('play-preview-editor-embed'); } catch (e) { /* ignore */ } + } + const playMapIdFromQuery = (params.get('map') || '').trim(); + /** mapId จาก join / game-start — ใช้จับฉาก crown เมื่อ URL ไม่มี ?map= */ + let playSessionMapId = playMapIdFromQuery; + /** สรุปภารกิจแบบ mock (popup-result ฯลฯ) — ใช้เฉพาะฉากนี้จาก editor (id ใน URL เช่น editor.html?id=mnorwqx1) */ + const quizCarryMissionSummaryMapId = 'mnorwqx1'; + const quizCarryUseMissionSummaryOverlay = playMapIdFromQuery === quizCarryMissionSummaryMapId; + /** Gauntlet พรมแดง — ฉากนี้จาก editor (?map=mno9kb07): วาดตัวหันขวา + ใช้รูป lane/laser จาก game-timing */ + const GAUNTLET_FACE_RIGHT_MAP_ID = 'mno9kb07'; + /** Jump Survival ฉาก Jumper — UI สรุปภารกิจแบบ crown + รูปใน /img/Jumper/ (editor ?map=mnptfts2) */ + const JUMP_SURVIVE_MISSION_MAP_ID = 'mnptfts2'; + /** Space shooter ฉาก Violent Crime — flow เดียวกับ crown/howto (รูปใน /img/ViolentCrime/) */ + const SPACE_SHOOTER_MISSION_MAP_ID = 'mnpz6rkp'; + /** Quiz ฉากสอบสวน — flow เดียวกับ crown/howto แต่รูปใน /img/QUESTION/ */ + const QUIZ_QUESTION_MISSION_MAP_ID = 'mng8a80o'; + /** Stack ซ้อนตึก — ฉากภารกิจ (HOWTO → นับถอยหลัง → เล่น → สรุป) รูปใน `/Game/img/TowerBlock` */ + const STACK_TOWER_MISSION_MAP_ID = 'mnn93hpi'; + /** Stack Tower: บัฟเฟอร์วาดคงที่ 16:9 — 1920×1080 */ + const STACK_TOWER_FIXED_RENDER_W = 1920; + const STACK_TOWER_FIXED_RENDER_H = 1080; + /** คูณ zDraw ให้มุมมองโลกเท่าเดิมเมื่อบัฟเฟอร์กว้างกว่าฐานอ้างอิง 1280w (= 1.5 ที่ 1920w) */ + const STACK_TOWER_FIXED_RENDER_Z_MUL = STACK_TOWER_FIXED_RENDER_W / 1280; + /** เชือก Stack Tower: ปลายล่างตามเส้น (1 = ถึงจุดยึดบล็อก) */ + const STACK_TOWER_ROPE_DRAW_T1 = 1.0; + /** ปลายบนเชือก (จอ): ยืดเหนือขอบบนแคนวาสเป็นเศษส่วนของความสูง (เช่น 0.25 = 25% นอกจอด้านบน) */ + const STACK_TOWER_ROPE_TOP_ABOVE_CANVAS_FRAC = 0.25; + /** progress ≥ นี้ (ฉาก Tower mnn93hpi): รูป heavy + ความกว้าง heavy = เท่าเดียวกับปกติ × ค่าด้านล่าง + แอนิเมตซูม/เลื่อน */ + const STACK_TOWER_POST50_PROGRESS_THRESH = 60; + const STACK_TOWER_POST50_ZOOM_MUL = 1.1; + /** เลื่อน BG แนวตั้งเท่าเศษส่วนของความสูงแคนวาส (จอ) */ + const STACK_TOWER_POST50_MAP_SHIFT_SCREEN_FRAC = 0.1; + const STACK_TOWER_POST50_ANIM_MS = 680; + /** แมป mnn93hpi: สเกลความกว้าง/ความสูงชั้นของบล็อกในโลก (1 = ขนาดเดิม) */ + const STACK_TOWER_BLOCK_WORLD_SCALE = 1.5; + /** หลังเกณฑ์ progress (Tower): เลื่อนกล้องลงในมุมมองจอเท่าเศษส่วนของความสูงแคนวาส (เช่น 0.25 = 25% ของความสูงจอ) */ + const STACK_TOWER_POST50_VIEW_SHIFT_SCREEN_FRAC = 0.25; + /** Mega Virus — balloon_boss ฉากภารกิจ (flow เดียวกับ crown / mno9kb07) รูปใน `/Game/img/MegaVirus` */ + const BALLOON_BOSS_MISSION_MAP_ID = 'mnq1eml7'; + /** ครบชั้นนี้ขึ้นไป — ยกจุดแกว่ง/เชือกขึ้น (world px) ป้องกันชนกองสูง (เฉพาะ Tower mission) */ + const STACK_TOWER_SWING_LIFT_FROM_LAYER = 9; + /** Stack — เกณฑ์ใกล้ความจริง: ทับมากขึ้น, จำกัดการเยื้องจากกลางฐาน, ทับน้อยแล้วโคลงง่ายขึ้น */ + const STACK_OVERLAP_MISS_MIN_ON_LAND = 0.42; + const STACK_OVERLAP_MISS_FRAC_ON_LAND = 0.14; + const STACK_OVERLAP_MISS_MIN_ON_LAYER = 0.4; + const STACK_OVERLAP_MISS_FRAC_ON_LAYER = 0.3; + /** จุดกลางแท่งที่วาง vs กลางโซน land — เกิน (ความกว้างโซน × นี้) = พลาด (ชั้นที่ 2 ขึ้นไป) */ + const STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND = 0.38; + /** ไม่มี stackLandArea: ใช้กลางแมป — เยื้องได้ไม่เกิน fallW × นี้ (รวม) */ + const STACK_MAX_CENTER_DRIFT_NO_LAND_MULT = 1.0; + /** overlap/fallW ต่ำกว่านี้ → เข้า settling (แท่งไม่นิ่ง) */ + const STACK_STABLE_SUPPORT_RATIO_MIN = 0.62; + /** Stack ทั่วไป (ไม่ใช่ Tower post-50%): บล็อก heavy กว้างกว่าปกติ (~382×72 vs 314×103) */ + const STACK_BLOCK_HEAVY_WIDTH_MULT = 382 / 314; + /** Tower live + progress ≥เกณฑ์ + heavy: ความกว้างบล็อก = ปกติ × ค่านี้ (เช่น 0.6 = 60% ของช่องปกติ — รูปใหญ่วาดในกรอบเดียวกัน) */ + const STACK_TOWER_POST50_WIDTH_MULT = 0.6; + + function stackBlockWidthTilesForPlay(baseTiles, heavy) { + const b = Number(baseTiles); + const base = Number.isFinite(b) ? Math.max(0.85, Math.min(3.2, b)) : 2; + const towerHeavyNarrow = + !!heavy && + isStackTowerMissionUiMapPlay() && + stackTowerMissionPhase === 'live' && + stackMini && + Number.isFinite(Number(stackMini.progressPct)) && + Number(stackMini.progressPct) >= STACK_TOWER_POST50_PROGRESS_THRESH; + if (towerHeavyNarrow) { + const w = base * STACK_TOWER_POST50_WIDTH_MULT; + return Math.max(0.85, Math.min(3.2, Math.round(w * 200) / 200)); + } + if (!heavy) return base; + const w = base * STACK_BLOCK_HEAVY_WIDTH_MULT; + return Math.min(3.2, Math.round(w * 200) / 200); + } + /** Violent Crime mission: asteroid strikes ship → invuln → 3 strikes = eliminated */ + const SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS = 3; + const SPACE_SHOOTER_MISSION_HIT_INVULN_MS = 1400; + const SPACE_SHOOTER_MISSION_SHIP_HIT_RADIUS = 12; + /** อุกาบาต: HP สุ่ม 1–5 ต่อก้อน (ไม่วาดหัวใจบนก้อน) — ขนาดตาม HP เทียบสเกล 5 */ + const SPACE_SHOOTER_ASTEROID_MAX_HP = 5; + const SPACE_SHOOTER_ASTEROID_RADIUS_AT_FULL = 48; + const SPACE_SHOOTER_ASTEROID_RADIUS_AT_MIN_HP = 11; + + function spaceShooterAsteroidRadiusFromHpPlay(hp) { + const cap = SPACE_SHOOTER_ASTEROID_MAX_HP; + const h = Math.max(0, Math.min(cap, Math.floor(Number(hp)) || 0)); + const frac = h <= 0 ? 0 : h / cap; + return SPACE_SHOOTER_ASTEROID_RADIUS_AT_MIN_HP + + (SPACE_SHOOTER_ASTEROID_RADIUS_AT_FULL - SPACE_SHOOTER_ASTEROID_RADIUS_AT_MIN_HP) * frac; + } + + function spaceShooterShipFootprintCellsPlay(md) { + const m = md || mapData; + if (!m) return { cw: 1, ch: 1 }; + const { cw, ch } = getCharacterFootprintWH(m); + return { + cw: Math.max(1, Math.min(4, cw)), + ch: Math.max(1, Math.min(4, ch)), + }; + } + + /** ขนาดวาดยานบนจอ — สเกลตาม footprint แผนที่ (editor: กว้าง/สูงตัวละคร) เทียบ tileSize */ + function spaceShooterShipBodyScreenPxPlay(zDrawVal) { + const zD = Number(zDrawVal) > 0 ? zDrawVal : (typeof zoom === 'number' && zoom > 0 ? zoom : 1); + const ts = tileSize || 32; + const { cw, ch } = spaceShooterShipFootprintCellsPlay(mapData); + const kw = 14 / 32; + const kh = 22 / 32; + return { + bodyW: cw * ts * zD * kw, + bodyH: ch * ts * zD * kh, + }; + } + + /** รัศมีชนอุกาบาต (พิกัดโลก) — โตตาม footprint เพื่อให้สมกับยานที่วาดใหญ่ขึ้น */ + function spaceShooterShipHitRadiusWorldPxPlay() { + if (!mapData || mapData.gameType !== 'space_shooter') return SPACE_SHOOTER_MISSION_SHIP_HIT_RADIUS; + const ts = tileSize || 32; + const { cw, ch } = spaceShooterShipFootprintCellsPlay(mapData); + const base = SPACE_SHOOTER_MISSION_SHIP_HIT_RADIUS; + const geom = Math.sqrt(Math.max(1, cw * ch)); + const cap = Math.min(54, 0.42 * Math.min(cw * ts, ch * ts)); + return Math.max(9, Math.min(cap, base * geom)); + } + + /** ระยะห่างจากขอบโลกซ้าย/ขวาเมื่อบังคับยาน — กันยานใหญ่หลุดกรอบ */ + function spaceShooterShipEdgePadWorldPxPlay() { + if (!mapData || mapData.gameType !== 'space_shooter') return 18; + const ts = tileSize || 32; + const { cw } = spaceShooterShipFootprintCellsPlay(mapData); + return Math.max(18, cw * ts * 0.48); + } + + function spaceShooterResetMissionShipStateForAllPlay() { + function resetEnt(ent) { + if (!ent) return; + ent.spaceShooterHits = 0; + ent.spaceShooterEliminated = false; + ent.spaceShooterInvulnUntil = 0; + } + resetEnt(me); + others.forEach(function (o) { + resetEnt(o); + }); + } + + function spaceShooterMissionResolveShipAsteroidHitsPlay() { + if (!mapData || mapData.gameType !== 'space_shooter') return; + if (!isSpaceShooterMissionUiMapPlay() || spaceShooterMissionPhase !== 'live' || spaceShooterGameEnded) return; + const now = performance.now(); + const shipR = spaceShooterShipHitRadiusWorldPxPlay(); + + function tryHit(ent, cx, cy) { + if (!ent || ent.spaceShooterEliminated || cx == null || cy == null) return; + if (ent.spaceShooterInvulnUntil != null && now < ent.spaceShooterInvulnUntil) return; + for (let ai = spaceShooterAsteroids.length - 1; ai >= 0; ai--) { + const a = spaceShooterAsteroids[ai]; + if (!a) continue; + const dx = cx - a.x; + const dy = cy - a.y; + const rr = (a.r + shipR) * (a.r + shipR); + if (dx * dx + dy * dy > rr) continue; + ent.spaceShooterHits = Math.max(0, Number(ent.spaceShooterHits) || 0) + 1; + ent.spaceShooterInvulnUntil = now + SPACE_SHOOTER_MISSION_HIT_INVULN_MS; + spaceShooterSpawnAsteroidExplosion(a.x, a.y, a.r); + spaceShooterAsteroids.splice(ai, 1); + spaceShooterPopups.push({ x: cx, y: cy - 28, text: 'HIT', until: Date.now() + 650 }); + if (ent.spaceShooterHits >= SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS) { + ent.spaceShooterEliminated = true; + spaceShooterPopups.push({ x: cx, y: cy - 50, text: 'OUT', until: Date.now() + 1400 }); + } + break; + } + } + + if (myId != null) tryHit(me, me.spaceShooterCx, me.spaceShooterCy); + others.forEach(function (o) { + if (!o) return; + tryHit(o, o.spaceShooterCx, o.spaceShooterCy); + }); + if (spaceShooterMissionEveryParticipantEliminatedPlay()) { + endSpaceShooterMissionRound('all_dead'); + } + } + + /** Space Shooter ภารกิจ: ผู้เข้าร่วมทุกคนถูกกำจัดแล้ว (รวมเรา) */ + function spaceShooterMissionEveryParticipantEliminatedPlay() { + if (!isSpaceShooterMissionUiMapPlay() || spaceShooterMissionPhase !== 'live' || spaceShooterGameEnded) return false; + if (myId == null) return false; + if (!me.spaceShooterEliminated) return false; + let anyAlive = false; + others.forEach(function (o) { + if (!o) return; + if (!o.spaceShooterEliminated) anyAlive = true; + }); + return !anyAlive; + } + + /** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */ + const LEGACY_PLACEHOLDER_CHARACTER_ID = 'Chatest'; + let firstCharacterDefaultResolved = null; + /** รหัสตัวละครจาก GET /api/characters — ใช้สลับบอทพรีวิวให้ไม่ซ้ำรูปกับผู้เล่น */ + let playCharacterIdRoster = []; + const firstCharacterDefaultPromise = fetch(BASE + '/api/characters') + .then((r) => r.json()) + .then((list) => { + playCharacterIdRoster = []; + if (Array.isArray(list)) { + list.forEach((c) => { + if (c && c.id) playCharacterIdRoster.push(String(c.id)); + }); + } + if (Array.isArray(list) && list.length > 0 && list[0] && list[0].id) { + firstCharacterDefaultResolved = String(list[0].id); + } else { + firstCharacterDefaultResolved = ''; + } + }) + .catch(() => { + firstCharacterDefaultResolved = ''; + playCharacterIdRoster = []; + }) + .finally(() => { + try { + if (playBotsEnabled() && mapData) rebalancePreviewBots(); + } catch (e) { /* map ยังไม่พร้อม */ } + }); + const lobbyLevelParam = params.get('lobbyLevel'); + const lobbyCaseParam = params.get('case'); + if (lobbyLevelParam || lobbyCaseParam) { + try { + window.__detectiveLobbyMeta = { level: lobbyLevelParam, caseId: lobbyCaseParam }; + } catch (e) { /* ignore */ } + } + + const POST_CASE_LOBBY_MAP_ID = 'mn8nx46h'; + + function isDetectiveMinigamePlay() { + if (params.get('detectiveReturn') === '1') return true; + try { + return sessionStorage.getItem('detectiveMinigameReturn') === '1'; + } catch (e) { + return false; + } + } + + function buildRoomLobbyReturnHref() { + let h = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + h += '&map=' + encodeURIComponent(POST_CASE_LOBBY_MAP_ID); + const drParam = (params.get('displayRoom') || '').trim(); + if (drParam) { + h += '&displayRoom=' + encodeURIComponent(drParam); + } else { + try { + const dr = localStorage.getItem('lastCreatedSpaceName'); + if (dr) h += '&displayRoom=' + encodeURIComponent(dr); + } catch (e) { /* ignore */ } + } + try { + const meta = window.__detectiveLobbyMeta; + if (meta && meta.level != null && String(meta.level).trim()) { + h += '&lobbyLevel=' + encodeURIComponent(String(meta.level).trim()); + } + if (meta && meta.caseId != null && String(meta.caseId).trim()) { + h += '&caseId=' + encodeURIComponent(String(meta.caseId).trim()); + } + } catch (e2) { /* ignore */ } + try { + if (sessionStorage.getItem('detectiveMinigameReturn') === '1') { + h += '&detectiveReturn=1'; + sessionStorage.removeItem('detectiveMinigameReturn'); + } + } catch (e3) { /* ignore */ } + if (params.get('detectiveReturn') === '1' && h.indexOf('detectiveReturn') < 0) { + h += '&detectiveReturn=1'; + } + return h; + } + + function finishDetectiveMinigameAndReturnLobby() { + quizCarrySessionEnded = true; + cancelQuizCarryAnswerRoundTimeupAuto(); + quizCarryAnswerTimeupAwaitNext = false; + hideQuizCarryTimeupOnDeskLayer(); + try { sessionStorage.setItem('detectiveMinigameReturn', '1'); } catch (e) { /* ignore */ } + const go = function () { + window.location.href = buildRoomLobbyReturnHref(); + }; + if (socket && socket.connected) { + socket.emit('detective-minigame-finished', {}, function (res) { + try { + if (res && Array.isArray(res.myPlayerEvidence)) { + var hasAny = res.myPlayerEvidence.some(function (r) { return Array.isArray(r) && r.length; }); + if (hasAny) sessionStorage.setItem('justiceDetectiveMyEvidence', JSON.stringify(res.myPlayerEvidence)); + else sessionStorage.removeItem('justiceDetectiveMyEvidence'); + } + } catch (e) { /* ignore */ } + /* โชว์การ์ดหลักฐานที่เพิ่งได้ "ในฉากมินิเกม" ก่อนกลับ LobbyB */ + if (res && typeof res.suspectIndex === 'number' && res.suspectIndex >= 0) { + showInSceneEvidenceReveal(res, go); + } else { + go(); + } + }); + } else { + go(); + } + } + + /* ===== หน้าโชว์การ์ดหลักฐานในฉากมินิเกม (ก่อนกลับ LobbyB) ===== */ + var __evidenceConfigCache = null; + function ensureEvidenceConfigLoaded(cb) { + if (__evidenceConfigCache) { cb(__evidenceConfigCache); return; } + try { + fetch(BASE + '/api/evidence-cards?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.json(); }) + .then(function (j) { __evidenceConfigCache = (j && j.cases) ? j : { cases: {} }; cb(__evidenceConfigCache); }) + .catch(function () { cb({ cases: {} }); }); + } catch (e) { cb({ cases: {} }); } + } + + var EVIDENCE_RARITY_GLOW = { + common: 'rgba(168,180,200,.55)', + rare: 'rgba(94,170,255,.7)', + legendary: 'rgba(255,186,72,.85)', + }; + var EVIDENCE_RARITY_LABEL = { common: 'การ์ดธรรมดา', rare: 'การ์ด Rare', legendary: 'การ์ดชี้คนร้าย' }; + + function showInSceneEvidenceReveal(res, onDone) { + var done = false; + var finish = function () { if (done) return; done = true; onDone(); }; + var si = res.suspectIndex; + /* การ์ดที่เพิ่งได้รอบนี้ (object: rarity/imageUrl) — fallback ใช้ N ใบท้ายของ owned */ + var awarded = (res.awardedCards && Array.isArray(res.awardedCards)) + ? res.awardedCards.filter(function (c) { return c && c.imageUrl; }) + : []; + if (!awarded.length) { + var owned = (res.myPlayerEvidence && res.myPlayerEvidence[si]) || []; + var freeN = Math.max(0, Number(res.freeEvidenceCount) || 0); + var newN = Math.min(owned.length, 1 + freeN); + awarded = owned.slice(owned.length - newN).filter(function (c) { return c && c.imageUrl; }); + } + if (!awarded.length) { finish(); return; } + var newN = awarded.length; + var ov = document.createElement('div'); + ov.id = 'inscene-evidence-reveal'; + ov.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(5,8,18,.9);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:22px;backdrop-filter:blur(3px)'; + var title = document.createElement('div'); + title.style.cssText = 'font:900 30px/1.2 Kanit,system-ui,sans-serif;color:#ffe7a3;text-shadow:0 0 18px rgba(255,214,102,.5);text-align:center'; + title.textContent = newN > 1 ? ('คุณได้รับหลักฐาน ' + newN + ' ใบ!') : 'คุณได้รับหลักฐาน!'; + if (res.grade) { + var gradeEl = document.createElement('div'); + gradeEl.style.cssText = 'font:800 16px/1 Kanit,system-ui,sans-serif;color:#bfe3ff;opacity:.9'; + gradeEl.textContent = 'ผลงานทีม: เกรด ' + res.grade; + title.appendChild(document.createElement('br')); + title.appendChild(gradeEl); + } + var freeBonus = Math.max(0, Number(res.freeEvidenceCount) || 0); + if (freeBonus > 0) { + var bonusEl = document.createElement('div'); + bonusEl.style.cssText = 'font:800 15px/1.3 Kanit,system-ui,sans-serif;color:#9ece6a;margin-top:4px'; + bonusEl.textContent = '⭐ การ์ด Free Evidence: ทุกคนรับหลักฐานฟรี +' + freeBonus + ' ใบ'; + title.appendChild(document.createElement('br')); + title.appendChild(bonusEl); + } + var row = document.createElement('div'); + row.style.cssText = 'display:flex;gap:18px;flex-wrap:wrap;justify-content:center;align-items:center;max-width:92vw'; + awarded.forEach(function (c, i) { + var rar = String(c.rarity || 'common').toLowerCase(); + var glow = EVIDENCE_RARITY_GLOW[rar] || EVIDENCE_RARITY_GLOW.common; + var cardWrap = document.createElement('div'); + cardWrap.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:8px;opacity:0;transform:translateY(24px) scale(.92);transition:opacity .45s ease,transform .45s cubic-bezier(.2,.9,.3,1.3)'; + var img = document.createElement('img'); + img.src = c.imageUrl; + img.alt = EVIDENCE_RARITY_LABEL[rar] || 'หลักฐาน'; + img.style.cssText = 'display:block;width:min(34vw,260px);height:auto;border-radius:14px;box-shadow:0 0 36px ' + glow + ',0 18px 50px rgba(0,0,0,.6)'; + img.onerror = function () { this.onerror = null; this.style.width = '200px'; this.style.height = '300px'; this.style.background = '#1b2240'; this.removeAttribute('src'); }; + var tag = document.createElement('div'); + tag.style.cssText = 'font:800 14px/1 Kanit,system-ui,sans-serif;color:#fff;opacity:.92'; + tag.textContent = EVIDENCE_RARITY_LABEL[rar] || ''; + cardWrap.appendChild(img); + cardWrap.appendChild(tag); + row.appendChild(cardWrap); + setTimeout(function () { cardWrap.style.opacity = '1'; cardWrap.style.transform = 'translateY(0) scale(1)'; }, 120 + i * 160); + }); + var btn = document.createElement('button'); + btn.type = 'button'; + btn.textContent = 'คุณได้หลักฐาน · เก็บเข้าแฟ้ม'; + btn.style.cssText = 'cursor:pointer;border:none;border-radius:999px;padding:14px 40px;font:800 18px/1 Kanit,system-ui,sans-serif;color:#0a0e1a;background:linear-gradient(180deg,#ffd666,#f0b429);box-shadow:0 10px 30px rgba(240,180,41,.45)'; + btn.addEventListener('click', function () { + ov.style.transition = 'opacity .25s'; ov.style.opacity = '0'; + setTimeout(finish, 220); + }); + ov.appendChild(title); + ov.appendChild(row); + ov.appendChild(btn); + document.body.appendChild(ov); + /* กันค้าง: ถ้าไม่กดภายใน 12 วิ ให้ไปต่อเอง */ + setTimeout(function () { if (!done) { try { ov.remove(); } catch (e) {} finish(); } }, 12000); + } + + /** จบมินิเกมโหมดสืบสวน → LobbyB; คืน true ถ้า redirect แล้ว */ + function tryFinishDetectiveMinigameAndReturnLobby() { + if (!isDetectiveMinigamePlay()) return false; + finishDetectiveMinigameAndReturnLobby(); + return true; + } + + /** ทดสอบจากเอดิเตอร์: เติมบอทให้ครบจำนวน (รวมผู้เล่นจริง) — ?fillTotal=6 (ค่าเริ่ม 6) */ + const previewFillBots = previewMode && editorEmbedReturn; + let detectiveCaseFillBots = false; + let detectiveBotSlotCount = 0; + function playBotsEnabled() { + return previewFillBots || detectiveCaseFillBots; + } + function playBotTargetHeadcount() { + if (previewFillBots) return previewTargetHeadcount; + if (detectiveCaseFillBots) return countPlayHumans() + detectiveBotSlotCount; + return countPlayHumans(); + } + /** พรีวิวใน 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; + /** Stack preview: บอทไม่วาบบนแผนที่ — ใช้ไฮไลต์แถบแข่งแทน */ + let lastStackPreviewActorId = null; + let lastStackPreviewActorUntil = 0; + /** Stack preview: สลับตา Player 1 = มนุษย์, 2–6 = บอท (เฉพาะตาปัจจุบันเล่นได้) */ + const STACK_PREVIEW_TURN_COUNT = 6; + let stackPreviewTurnOrder = null; + let stackPreviewTurnIndex = 0; + let stackPreviewBotThinkUntil = 0; + + function rebuildStackPreviewTurnOrder() { + if (!playBotsEnabled() || !mapData || mapData.gameType !== 'stack') { + stackPreviewTurnOrder = null; + return; + } + const bots = [...others.keys()].filter(isPreviewBotId).sort(); + stackPreviewTurnOrder = [{ kind: 'human', seat: 1 }]; + for (let i = 0; i < STACK_PREVIEW_TURN_COUNT - 1; i++) { + stackPreviewTurnOrder.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null }); + } + stackPreviewTurnIndex = 0; + stackPreviewBotThinkUntil = 0; + markStackNextDropVisualDirty(); + } + + function advanceStackPreviewTurn() { + if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return; + stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % STACK_PREVIEW_TURN_COUNT; + stackPreviewBotThinkUntil = 0; + markStackNextDropVisualDirty(); + } + + function isStackPreviewHumanTurn() { + if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return true; + const cur = stackPreviewTurnOrder[stackPreviewTurnIndex]; + return !!(cur && cur.kind === 'human'); + } + + function getStackPreviewCurrentSeat() { + if (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return 1; + const cur = stackPreviewTurnOrder[stackPreviewTurnIndex]; + return cur && cur.seat ? cur.seat : stackPreviewTurnIndex + 1; + } + + function getStackDropActorSeat() { + if (playBotsEnabled() && stackPreviewTurnOrder && stackPreviewTurnOrder.length === STACK_PREVIEW_TURN_COUNT) { + return getStackPreviewCurrentSeat(); + } + return 1; + } + + function markStackNextDropVisualDirty() { + if (stackMini) stackMini.nextDropVisualDirty = true; + } + + function pickStackBlockHeavyVisual(seat) { + const slot = Math.max(0, Math.min(5, (Math.floor(Number(seat)) || 1) - 1)); + const hU = normalizeGauntletAssetUrlForPlay(playStackBlockHeavyUrls[slot] || ''); + /** Tower mnn93hpi: ตั้งแต่เกณฑ์ progress (ตาม stackTowerProgressBlocks) → ใช้รูป heavy จาก admin — นอก live ไม่สุ่ม heavy */ + if (isStackTowerMissionUiMapPlay()) { + if (!(stackTowerMissionPhase === 'live' && stackMini)) return false; + const p = Number(stackMini.progressPct); + if (!Number.isFinite(p) || p < STACK_TOWER_POST50_PROGRESS_THRESH) return false; + return !!hU; + } + const pct = Math.max(0, Math.min(100, Math.floor(Number(playStackHeavyBlockPercent) || 0))); + if (pct <= 0) return false; + if (!hU) return false; + if (Math.random() * 100 >= pct) return false; + return true; + } + + function ensureStackNextDropVisual() { + if (!stackMini || !stackMini.nextDropVisualDirty) return; + stackMini.pendingDropSeat = getStackDropActorSeat(); + stackMini.pendingDropHeavy = pickStackBlockHeavyVisual(stackMini.pendingDropSeat); + stackMini.widthTiles = stackBlockWidthTilesForPlay(stackMini.initialWidthTiles, !!stackMini.pendingDropHeavy); + stackMini.nextDropVisualDirty = false; + } + + function normalizePlayStackSixUrlsFromTiming(arr) { + const out = ['', '', '', '', '', '']; + if (!Array.isArray(arr)) return out; + for (let i = 0; i < 6; i++) { + out[i] = normalizeGauntletAssetUrlForPlay(typeof arr[i] === 'string' ? arr[i] : ''); + if (out[i]) ensureGauntletAssetImage(out[i]); + } + return out; + } + + function resolveStackBlockSpriteRec(seat, heavy) { + const slot = Math.max(0, Math.min(5, (Math.floor(Number(seat)) || 1) - 1)); + const tryHeavy = !!heavy; + const order = tryHeavy + ? [playStackBlockHeavyUrls[slot], playStackBlockNormalUrls[slot]] + : [playStackBlockNormalUrls[slot], playStackBlockHeavyUrls[slot]]; + for (let ti = 0; ti < order.length; ti++) { + const u = normalizeGauntletAssetUrlForPlay(order[ti] || ''); + if (!u) continue; + return ensureGauntletAssetImage(u); + } + return null; + } + + /** + * @returns {boolean} true = วาดสไปรต์แล้ว · false = วาดสี่เหลี่ยมสี fallback + * ใช้ scale แบบ cover + clip ให้ขอบภาพตรงกล่องฟิสิกส์ (drawW×drawH) — ไม่เว้นขอบซ้ายขวาแบบ contain + * เพื่อให้สายตาตรงกับ overlap / miss ใน computeStackDropResult + */ + function drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seat, heavy, hueFallback, opts) { + const o = opts || {}; + const rec = resolveStackBlockSpriteRec(seat, heavy); + const img = rec && rec.img; + if (rec && rec.ready && img && img.complete && img.naturalWidth > 0 && img.naturalHeight > 0) { + const iw = img.naturalWidth; + const ih = img.naturalHeight; + if (drawW > 0 && drawH > 0) { + const scale = Math.max(drawW / iw, drawH / ih); + const dw = iw * scale; + const dh = ih * scale; + const dx = sx + (drawW - dw) * 0.5; + const dy = sy + (drawH - dh) * 0.5; + const prevSmooth = ctx.imageSmoothingEnabled; + ctx.save(); + ctx.beginPath(); + ctx.rect(sx, sy, drawW, drawH); + ctx.clip(); + try { + ctx.imageSmoothingEnabled = false; + ctx.drawImage(img, 0, 0, iw, ih, dx, dy, dw, dh); + } finally { + ctx.imageSmoothingEnabled = prevSmooth; + } + ctx.restore(); + if (o.missOverlay) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.42)'; + ctx.fillRect(sx, sy, drawW, drawH); + } + return true; + } + } + const hue = ((hueFallback % 360) + 360) % 360; + ctx.fillStyle = o.missTint ? 'rgba(247, 118, 190, 0.88)' : ('hsla(' + hue + ', 68%, 56%, 0.9)'); + ctx.fillRect(sx, sy, drawW, drawH); + return false; + } + + if (!spaceId) { window.location.replace(playDesignMainMenuHref()); return; } + + const socket = io({ path: BASE + '/socket.io' }); + window.__jdGameSocket = socket; + const canvas = document.getElementById('game-canvas'); + const ctx = canvas.getContext('2d'); + let mapData = null, tileSize = 32, myId = null; + let stackMini = null; + let stackFall = null; + /** Tower / Stack — เชือกเหวี่ยงจาก `img/TowerBlock/sling.png` */ + let stackTowerSlingImg = null; + /** Tower mnn93hpi — +คะแนน (`score+10.png` / `score+20.png`) + เอฟเฟกต์ ×2 (`score-light.png`, `scorex2.png`) */ + let stackTowerScorePlus10Img = null; + let stackTowerScorePlus20Img = null; + let stackTowerScoreLightImg = null; + let stackTowerScoreX2Img = null; + /** พลาดแล้วโชว์ heart-1.png กลางเหนือกอง (mock Tower) — heart เต็มใน HUD ใช้ไฟล์เดียวกัน */ + let stackTowerHeartMinusImg = null; + /** HUD มุมขวา: `life-bar.png` + `life-full.png` / `life-hit.png` (mock รูป2) */ + let stackTowerLifeBarImg = null; + let stackTowerLifeHitImg = null; + let stackTowerLifeFullImg = null; + /** @type {{ until: number } | null} */ + let stackTowerHeartMinusFx = null; + const STACK_TOWER_HEART_MINUS_FX_MS = 1500; + const STACK_TOWER_PERFECT_GLOW_MS = 1500; + /** ป๊อปอัป +10 / +20 เหนือบล็อก */ + const STACK_TOWER_SCORE_POPUP_MS = 2000; + /** ระยะโชว์ score-light + scorex2 ข้างชั้นบน (คู่กับ +20 / perfect full) */ + const STACK_TOWER_PERFECT_X2_MS = 2200; + /** ทับบล็อกก่อนหน้า + ชั้นใหม่: support เต็มแทบไม่มีเพี้ยน */ + const STACK_TOWER_PERFECT_SUPPORT_MIN = 0.998; + let lastStackTickMs = performance.now(); + let lastStackTowerScrollBgTickMs = performance.now(); + let mapBackgroundImg = null; + /** space_shooter + แมป mnpz6rkp — พื้นหลังเลื่อนแนวตั้ง (ข้อมูลจาก editorBgScroll ในแมป) */ + const PLAY_SCROLL_BG_MAP_ID = 'mnpz6rkp'; + const PLAY_SCROLL_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnpz6rkp/intro.png'; + const PLAY_SCROLL_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnpz6rkp/loop.png'; + let playScrollBgIntroImg = null; + let playScrollBgLoopImg = null; + /** ขอบบนของ viewport ใน strip — เพิ่มค่า = เลื่อนลง strip (เข้า loop ใต้ intro) */ + let playScrollBgPx = 0; + let playScrollBgSpeedPxPerSec = 56; + let playScrollBgOff = false; + /** true = บน→ล่าง: พลิกมุมมองใน viewport (ยังเลื่อน S+ เข้า loop ใต้ intro) */ + let playScrollBgFlowDown = false; + + function playScrollBgMapEligible() { + return !!(mapData && mapData.gameType === 'space_shooter' && (currentPlayMapId() || '').trim() === PLAY_SCROLL_BG_MAP_ID); + } + + function playScrollBgDrawActive() { + if (!playScrollBgMapEligible() || playScrollBgOff) return false; + const a = playScrollBgIntroImg; + const b = playScrollBgLoopImg; + return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth); + } + + function reloadPlayScrollBgFromMap() { + playScrollBgIntroImg = null; + playScrollBgLoopImg = null; + playScrollBgPx = 0; + if (!playScrollBgMapEligible()) return; + const raw = mapData.editorBgScroll && typeof mapData.editorBgScroll === 'object' ? mapData.editorBgScroll : {}; + playScrollBgOff = raw.enabled === false; + playScrollBgSpeedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 56)); + const dirRaw = String(raw.scrollDirection || raw.direction || 'up').toLowerCase(); + playScrollBgFlowDown = (dirRaw === 'down' || dirRaw === 'top' || dirRaw === 'toptobottom'); + if (playScrollBgOff) return; + const introSrc = (typeof raw.introImage === 'string' && raw.introImage.length) ? raw.introImage : PLAY_SCROLL_BG_DEFAULT_INTRO; + const loopSrc = (typeof raw.loopImage === 'string' && raw.loopImage.length) ? raw.loopImage : PLAY_SCROLL_BG_DEFAULT_LOOP; + let pending = 2; + const bump = () => { + pending--; + if (pending <= 0) { + playScrollBgSyncInitialScrollToBottom(); + try { draw(); } catch (e) { /* ignore */ } + } + }; + const im1 = new Image(); + im1.onload = () => { playScrollBgIntroImg = im1; bump(); }; + im1.onerror = () => { bump(); }; + im1.src = introSrc; + const im2 = new Image(); + im2.onload = () => { playScrollBgLoopImg = im2; bump(); }; + im2.onerror = () => { bump(); }; + im2.src = loopSrc; + } + + function playScrollBgSyncInitialScrollToBottom() { + const intro = playScrollBgIntroImg; + if (!canvas || !intro || !intro.complete || !intro.naturalWidth) return; + const cwR = Math.max(1, Math.round(canvas.width)); + const chR = Math.max(1, Math.round(canvas.height)); + const drawHIntro = Math.round(intro.naturalHeight * (cwR / intro.naturalWidth)); + /* ล่าง→บน: S = HI−ch ให้ขอบล่าง intro ชิดล่างจอ · บน→ล่าง (พลิก dest): S=0 ถึงได้ขอบล่าง intro ชิดล่างจอ */ + if (playScrollBgFlowDown) { + playScrollBgPx = 0; + } else { + playScrollBgPx = Math.max(0, drawHIntro - chR); + } + } + + function drawPlayScrollBgFullCanvas(cw, ch) { + const intro = playScrollBgIntroImg; + const loop = playScrollBgLoopImg; + if (!intro || !intro.complete || !loop || !loop.complete) return; + const cwR = Math.max(1, Math.round(cw)); + const chR = Math.max(1, Math.round(ch)); + const scaleI = cwR / intro.naturalWidth; + const drawHIntro = Math.round(intro.naturalHeight * scaleI); + const scaleL = cwR / loop.naturalWidth; + const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * scaleL)); + const S = playScrollBgPx; + const vp0 = S; + const vp1 = S + chR; + const yLimit = vp1 + drawHLoop * 2; + const flowDown = playScrollBgFlowDown; + + function stripTopToDestY(stripTop, drawH) { + if (!flowDown) return stripTop - S; + return S + chR - (stripTop + drawH); + } + + ctx.save(); + ctx.imageSmoothingEnabled = false; + ctx.beginPath(); + ctx.rect(0, 0, cwR, chR); + ctx.clip(); + + if (vp0 < 0) { + let k = 1; + if (vp0 < -drawHLoop * 4) { + k = Math.max(1, Math.floor(-vp0 / drawHLoop) - 2); + } + for (; k < 50000; k++) { + const y0 = -k * drawHLoop; + if (y0 >= vp1 + drawHLoop) break; + if (y0 + drawHLoop <= vp0) continue; + const dy = Math.round(stripTopToDestY(y0, drawHLoop)); + ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop); + } + } + + if (drawHIntro > vp0 && vp1 > 0) { + const dy = Math.round(stripTopToDestY(0, drawHIntro)); + ctx.drawImage(intro, 0, 0, intro.naturalWidth, intro.naturalHeight, 0, dy, cwR, drawHIntro); + } + + let y = drawHIntro; + if (y < yLimit && vp1 > drawHIntro) { + if (vp0 > drawHIntro) { + const n = Math.floor((vp0 - drawHIntro) / drawHLoop); + y = drawHIntro + Math.max(0, n - 1) * drawHLoop; + } + while (y < yLimit) { + const dy = Math.round(stripTopToDestY(y, drawHLoop)); + ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop); + y += drawHLoop; + } + } + ctx.restore(); + } + + function tickPlayScrollBg(dtSec) { + if (!playScrollBgDrawActive()) return; + playScrollBgPx += (playScrollBgSpeedPxPerSec || 56) * dtSec; + } + + /** Last Light (mno9kb07) — พื้นหลังแนวนอน: start → loop 2→3→4 → finish ครั้งเดียว (ไม่วน) แล้วจบเกม + ซ่อนอุปสรรค (gauntletCrownRunwayBg ในแมป) */ + const GAUNTLET_CROWN_RUNWAY_BG_MAP_ID = 'mno9kb07'; + /** หลังหยุดเลื่อนที่จุด FINISH — รอก่อน latch + โฟลว์สรุป (มิลลิวิ) */ + const GAUNTLET_CROWN_RUNWAY_POST_STOP_MS = 2000; + const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_START = BASE + '/img/editor-bg-mno9kb07/start.png'; + const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP2 = BASE + '/img/editor-bg-mno9kb07/loop2.png'; + const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP3 = BASE + '/img/editor-bg-mno9kb07/loop3.png'; + const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP4 = BASE + '/img/editor-bg-mno9kb07/loop4.png'; + const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_FINISH = BASE + '/img/editor-bg-mno9kb07/finish.png'; + let gauntletCrownRunwayBgImgs = [null, null, null, null, null]; + let gauntletCrownRunwayBgOff = false; + let gauntletCrownRunwayBgSpeedPxPerSec = 48; + let gauntletCrownRunwayBgScrollPx = 0; + let gauntletCrownRunwayBgFinishLatched = false; + /** >0 = หยุดเลื่อนแล้ว (timestamp ms) รอ POST_STOP ก่อน latch */ + let gauntletCrownRunwayBgStripFreezeSinceMs = 0; + /** พรีวิว embed: แสดง GCM ครั้งเดียวเมื่อรันเวย์ครบแนว (ไม่รอเซิร์ฟเวอร์) */ + let gauntletCrownRunwayClientMissionShown = false; + /** พรมแดง mno9kb07 — จำกัดขอบขวาของตัว (ขอบขวา footprint ไม่เกินสัดส่วนนี้ของความกว้างแมปเป็นช่อง) */ + const GAUNTLET_CROWN_HEIST_MAX_X_WORLD_FRAC = 0.8; + const GAUNTLET_CROWN_SCORE_POP_URL = BASE + '/img/gauntlet-assets/score-.png'; + let gauntletCrownScorePenaltyImg = null; + function ensureGauntletCrownScorePenaltyImgPlay() { + if (gauntletCrownScorePenaltyImg) return gauntletCrownScorePenaltyImg; + const im = new Image(); + im.src = GAUNTLET_CROWN_SCORE_POP_URL; + gauntletCrownScorePenaltyImg = im; + return im; + } + let lastGauntletCrownRunwayBgTickMs = 0; + /** จัด offset เลื่อนรันเวย์ให้จุดเกิดตรงกับงานศิลป์ START (รูป 1) — รีเซ็ตตอน howto / โหลดแมปใหม่ */ + let gauntletCrownRunwaySpawnScrollSnapped = false; + + function gauntletCrownRunwayBgMapEligiblePlay() { + return !!(mapData && mapData.gameType === 'gauntlet' && (currentPlayMapId() || '').trim() === GAUNTLET_CROWN_RUNWAY_BG_MAP_ID); + } + + function gauntletCrownRunwayBgNatDims(im) { + if (!im) return null; + if (im.tagName === 'CANVAS' && im.width > 0 && im.height > 0) return { nw: im.width, nh: im.height }; + if (im.complete && im.naturalWidth > 0 && im.naturalHeight > 0) return { nw: im.naturalWidth, nh: im.naturalHeight }; + return null; + } + + function gauntletCrownRunwayBgImageSlotReadyPlay(im) { + return !!gauntletCrownRunwayBgNatDims(im); + } + + function gauntletCrownRunwayBgStripWidthsAtCh(chR) { + const ch0 = Math.max(1, Math.round(chR)); + const wOf = (im) => { + const d = gauntletCrownRunwayBgNatDims(im); + if (!d) return 0; + return Math.max(1, Math.round(d.nw * (ch0 / d.nh))); + }; + return { + w0: wOf(gauntletCrownRunwayBgImgs[0]), + w1: wOf(gauntletCrownRunwayBgImgs[1]), + w2: wOf(gauntletCrownRunwayBgImgs[2]), + w3: wOf(gauntletCrownRunwayBgImgs[3]), + w4: wOf(gauntletCrownRunwayBgImgs[4]), + }; + } + + function gauntletCrownRunwayBgImagesReadyPlay() { + for (let i = 0; i < 5; i++) { + if (!gauntletCrownRunwayBgImageSlotReadyPlay(gauntletCrownRunwayBgImgs[i])) return false; + } + return true; + } + + /** ถ้า URL บนเซิร์ฟยังไม่มีไฟล์ (404) ให้แผ่นเลื่อนได้ — พื้นผิวนุ่ม ไม่ใส่ข้อความใหญ่ (กันเหมือน debug) */ + function ensureGauntletCrownRunwayBgPlaceholdersPlay() { + if (!gauntletCrownRunwayBgMapEligiblePlay() || gauntletCrownRunwayBgOff) return; + const W = 960; + const H = 540; + const hues = [352, 210, 165, 225, 38]; + for (let i = 0; i < 5; i++) { + if (gauntletCrownRunwayBgImageSlotReadyPlay(gauntletCrownRunwayBgImgs[i])) continue; + const c = document.createElement('canvas'); + c.width = W; + c.height = H; + const g = c.getContext('2d'); + const h0 = hues[i]; + const grd = g.createLinearGradient(0, 0, W, H * 0.9); + grd.addColorStop(0, `hsl(${h0},26%,20%)`); + grd.addColorStop(0.45, `hsl(${h0},18%,14%)`); + grd.addColorStop(1, `hsl(${h0},12%,10%)`); + g.fillStyle = grd; + g.fillRect(0, 0, W, H); + g.strokeStyle = 'rgba(255,255,255,0.06)'; + g.lineWidth = 1; + for (let x = 0; x < W; x += 48) { + g.beginPath(); + g.moveTo(x, 0); + g.lineTo(x + H * 0.04, H); + g.stroke(); + } + const vg = g.createRadialGradient(W * 0.45, H * 0.35, 20, W * 0.5, H * 0.55, W * 0.85); + vg.addColorStop(0, 'rgba(0,0,0,0)'); + vg.addColorStop(1, 'rgba(0,0,0,0.38)'); + g.fillStyle = vg; + g.fillRect(0, 0, W, H); + g.fillStyle = 'rgba(255,255,255,0.1)'; + g.font = '10px system-ui,sans-serif'; + g.textAlign = 'right'; + g.fillText(String(i + 1), W - 10, H - 8); + gauntletCrownRunwayBgImgs[i] = c; + } + } + + function gauntletCrownRunwayBgDrawActivePlay() { + if (!gauntletCrownRunwayBgMapEligiblePlay() || gauntletCrownRunwayBgOff) return false; + return gauntletCrownRunwayBgImagesReadyPlay(); + } + + /** ตำแหน่งเส้น FINISH บนจอ (0=ซ้าย 1=ขวา) จากขอบซ้าย viewport — ใช้หยุดเลื่อน + fade สิ่งกีดขวาง */ + function gauntletCrownRunwayFinishLineScreenFracPlay(g) { + if (!g) return null; + const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; + let lineInArt = Number(raw.finishLineInFinishArtFrac); + if (!Number.isFinite(lineInArt)) { + /* mno9kb07: จบที่ "แถบ FINISH" สีเหลือง — หยุดทันทีหลังผ่านแถบ (ขอบขวาของแถบ FINISH) */ + lineInArt = gauntletCrownRunwayBgMapEligiblePlay() ? 0.34 : 0.12; + } + lineInArt = Math.max(0, Math.min(0.92, lineInArt)); + const finishLineWorldX = g.finishStart + g.w4Safe * lineInArt; + return (finishLineWorldX - g.scrollView) / Math.max(1e-6, g.cwWorld); + } + + function gauntletCrownRunwayFinishStopViewportFracPlay() { + const raw = mapData && mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; + const stopHi = gauntletCrownRunwayBgMapEligiblePlay() ? 0.85 : 0.75; + let vp = Number(raw.finishStopViewportFrac); + if (Number.isFinite(vp)) return Math.max(0.08, Math.min(stopHi, vp)); + let legacy = Number(raw.finishStopScreenFrac); + if (Number.isFinite(legacy)) { + legacy = Math.max(0.08, Math.min(0.96, legacy)); + return Math.max(0.12, Math.min(0.62, 0.14 + legacy * 0.42)); + } + /* mno9kb07: หยุดเลื่อนเร็วขึ้น (เส้น FINISH หยุดที่ ~0.55 จากซ้าย — ขอบขวาของแถบ FINISH ใกล้กลางจอ) */ + return gauntletCrownRunwayBgMapEligiblePlay() ? 0.55 : 0.34; + } + + function gauntletCrownRunwayObstacleFadeStartViewportFracPlay() { + const raw = mapData && mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; + let v = Number(raw.obstacleFadeStartViewportFrac); + /* mno9kb07: เริ่มจางอุปสรรคให้เร็วขึ้น (1.0 = ตอนเส้น FINISH ยังอยู่ขอบขวา) เพื่อให้อุปสรรคหายก่อนถึงเส้นชัย */ + if (!Number.isFinite(v)) v = gauntletCrownRunwayBgMapEligiblePlay() ? 1.05 : 0.9; + return Math.max(0.45, Math.min(1.15, v)); + } + + /** 1 = สิ่งกีดขวางเต็ม · 0 = หาย — ค่อยๆ จางเมื่อเส้น FINISH เข้าใกล้จุดหยุดบนจอ */ + function gauntletCrownRunwayObstacleDrawAlphaPlay() { + if (!gauntletCrownRunwayBgDrawActivePlay()) return 1; + if (gauntletCrownRunwayBgFinishLatched) return 0; + const g = gauntletCrownRunwayScrollGeometryPlay(); + if (!g) return 1; + const screenFrac = gauntletCrownRunwayFinishLineScreenFracPlay(g); + if (screenFrac == null) return 1; + const stopAt = gauntletCrownRunwayFinishStopViewportFracPlay(); + const fadeStart = gauntletCrownRunwayObstacleFadeStartViewportFracPlay(); + if (screenFrac >= fadeStart) return 1; + if (screenFrac <= stopAt) return 0; + return (screenFrac - stopAt) / Math.max(1e-6, fadeStart - stopAt); + } + + function gauntletCrownRunwayBgHideObstaclesPlay() { + return gauntletCrownRunwayObstacleDrawAlphaPlay() <= 0.001; + } + + /** Last Light: หยุดแอนิเมชันวิ่งเมื่อหยุดที่ FINISH หรือ latch แล้ว */ + function gauntletCrownRunwayAvatarRunAllowedPlay() { + if (gauntletCrownRunwayBgMapEligiblePlay() && gauntletCrownRunwayBgDrawActivePlay()) { + if (gauntletCrownRunwayBgFinishLatched) return false; + if (gauntletCrownRunwayBgStripFreezeSinceMs > 0) return false; + return true; + } + return true; + } + + function drawImageCoverCanvasPlay(ctx, img, cwR, chR) { + const d = gauntletCrownRunwayBgNatDims(img); + if (!d) return; + const iw = d.nw; + const ih = d.nh; + if (!iw || !ih) return; + const scale = Math.max(cwR / iw, chR / ih); + const dw = iw * scale; + const dh = ih * scale; + const dx = (cwR - dw) / 2; + const dy = (chR - dh) / 2; + ctx.drawImage(img, 0, 0, iw, ih, dx, dy, dw, dh); + } + + function drawRunwayHSlicePlay(ctx, img, worldX0, segW, scrollLeft, cwR, chR) { + const d0 = gauntletCrownRunwayBgNatDims(img); + if (!d0 || segW <= 0) return; + const natW = d0.nw; + const natH = d0.nh; + const v0 = scrollLeft; + const v1 = scrollLeft + cwR; + const o0 = Math.max(worldX0, v0); + const o1 = Math.min(worldX0 + segW, v1); + if (o1 <= o0) return; + const u0 = ((o0 - worldX0) / segW) * natW; + const u1 = ((o1 - worldX0) / segW) * natW; + const dx = o0 - scrollLeft; + const dw = o1 - o0; + /** ทาบแนวตั้งเล็กน้อยกันช่องว่างย่อย 1px (seam) ระหว่างแถบลูป */ + ctx.drawImage(img, u0, 0, u1 - u0, natH, dx - 0.25, 0, dw + 0.55, chR); + } + + /** วาดแถบรันเวย์ในระบบพิกัด X = ความกว้างมองเห็น (cwR) × สูงแถบ chR · scrollView = จุดเริ่ม viewport ในโลกรันเวย์ */ + function drawGauntletCrownRunwayStripCorePlay(ctx, cwR, chR, scrollView) { + const start = gauntletCrownRunwayBgImgs[0]; + const L2 = gauntletCrownRunwayBgImgs[1]; + const L3 = gauntletCrownRunwayBgImgs[2]; + const L4 = gauntletCrownRunwayBgImgs[3]; + const { w0, w1, w2, w3 } = gauntletCrownRunwayBgStripWidthsAtCh(chR); + const cycle = w1 + w2 + w3; + const S = scrollView; + ctx.imageSmoothingEnabled = true; + ctx.fillStyle = '#1a1b26'; + ctx.fillRect(0, 0, cwR, chR); + if (w0 > 0) drawRunwayHSlicePlay(ctx, start, 0, w0, S, cwR, chR); + if (cycle <= 0) return; + const ws = [w1, w2, w3]; + const ims = [L2, L3, L4]; + const uTotal = Math.max(0, S - w0); + let cycN = Math.floor(uTotal / cycle); + let uRem = uTotal - cycN * cycle; + let si = 0; + while (si < 3 && uRem >= ws[si]) { + uRem -= ws[si]; + si++; + } + if (si >= 3) { + cycN++; + si = 0; + uRem = 0; + } + let acc = 0; + for (let k = 0; k < si; k++) acc += ws[k]; + let curLeft = w0 + cycN * cycle + acc; + let guard = 0; + let idx = si; + while (curLeft < S + cwR && guard++ < 500) { + drawRunwayHSlicePlay(ctx, ims[idx], curLeft, ws[idx], S, cwR, chR); + curLeft += ws[idx]; + idx = (idx + 1) % 3; + } + } + + /** Last Light mno9kb07: แถบเดียว START → 2 → 3 → 4 → FINISH (ไม่วนลูป) */ + function drawGauntletCrownRunwayStripLinearOncePlay(ctx, cwR, chR, scrollView) { + const S = scrollView; + const { w0, w1, w2, w3, w4 } = gauntletCrownRunwayBgStripWidthsAtCh(chR); + ctx.imageSmoothingEnabled = true; + ctx.fillStyle = '#1a1b26'; + ctx.fillRect(0, 0, cwR, chR); + const segs = [ + { im: gauntletCrownRunwayBgImgs[0], w: w0, x0: 0 }, + { im: gauntletCrownRunwayBgImgs[1], w: w1, x0: w0 }, + { im: gauntletCrownRunwayBgImgs[2], w: w2, x0: w0 + w1 }, + { im: gauntletCrownRunwayBgImgs[3], w: w3, x0: w0 + w1 + w2 }, + { im: gauntletCrownRunwayBgImgs[4], w: w4, x0: w0 + w1 + w2 + w3 }, + ]; + for (let i = 0; i < segs.length; i++) { + const g = segs[i]; + if (g.w > 0 && g.im) drawRunwayHSlicePlay(ctx, g.im, g.x0, g.w, S, cwR, chR); + } + } + + /** + * @param {number} canvasW + * @param {number} canvasH + * @param {null|{ worldMinX:number, worldMaxX:number, camX:number, camY:number, zDraw:number, mapWpx:number, mapHpx:number }} worldAlign — ถ้ามี จะคลิป+สเกลให้ตรงกริดแมป (ตัว/สิ่งกีดขวางตรงพื้นที่ตั้ง) + */ + function drawGauntletCrownRunwayBgFullCanvasPlay(canvasW, canvasH, worldAlign) { + const finish = gauntletCrownRunwayBgImgs[4]; + const align = worldAlign && typeof worldAlign === 'object' + && Number.isFinite(worldAlign.worldMinX) + && Number.isFinite(worldAlign.worldMaxX) + && Number.isFinite(worldAlign.camX) + && Number.isFinite(worldAlign.camY) + && Number.isFinite(worldAlign.zDraw) + && Number.isFinite(worldAlign.mapWpx) + && Number.isFinite(worldAlign.mapHpx); + if (gauntletCrownRunwayBgFinishLatched && gauntletCrownRunwayBgImageSlotReadyPlay(finish)) { + if (align) { + const { camX, camY, zDraw, mapWpx, mapHpx } = worldAlign; + ctx.fillStyle = '#1a1b26'; + ctx.fillRect(0, 0, canvasW, canvasH); + ctx.save(); + /* เมทริกซ์เดียวกับ worldToScreen + คลิปโลก [0,map] — รูปจบครอบทั้งแผนที่ที่ world origin */ + ctx.setTransform(zDraw, 0, 0, zDraw, canvasW / 2 - camX * zDraw, canvasH / 2 - camY * zDraw); + ctx.beginPath(); + ctx.rect(0, 0, mapWpx, mapHpx); + ctx.clip(); + drawImageCoverCanvasPlay(ctx, finish, Math.max(1, Math.round(mapWpx)), Math.max(1, Math.round(mapHpx))); + ctx.restore(); + } else { + drawImageCoverCanvasPlay(ctx, finish, Math.max(1, Math.round(canvasW)), Math.max(1, Math.round(canvasH))); + } + return; + } + /* ใช้ความกว้างมองเห็นแบบ float ให้เท่ากับ canvas.width/zDraw — ห้าม Math.round ไม่งั้นซูมแล้วแถบ runway กว้างไม่ตรง worldToScreen (แผนหลุดจากตัว) */ + const cwR = align + ? Math.max(1e-6, worldAlign.worldMaxX - worldAlign.worldMinX) + : Math.max(1, Math.round(canvasW)); + const chR = align + ? Math.max(1, Math.round(worldAlign.mapHpx)) + : Math.max(1, Math.round(canvasH)); + const scrollView = align + ? (gauntletCrownRunwayBgScrollPx + worldAlign.worldMinX) + : gauntletCrownRunwayBgScrollPx; + if (align) { + const { camX, camY, zDraw, mapWpx, mapHpx, worldMinX } = worldAlign; + ctx.fillStyle = '#1a1b26'; + ctx.fillRect(0, 0, canvasW, canvasH); + ctx.save(); + ctx.setTransform(zDraw, 0, 0, zDraw, canvasW / 2 - camX * zDraw, canvasH / 2 - camY * zDraw); + ctx.beginPath(); + ctx.rect(0, 0, mapWpx, mapHpx); + ctx.clip(); + ctx.translate(worldMinX, 0); + if (gauntletCrownRunwayBgMapEligiblePlay()) { + drawGauntletCrownRunwayStripLinearOncePlay(ctx, cwR, chR, scrollView); + } else { + drawGauntletCrownRunwayStripCorePlay(ctx, cwR, chR, scrollView); + } + ctx.restore(); + return; + } + ctx.save(); + if (gauntletCrownRunwayBgMapEligiblePlay()) { + drawGauntletCrownRunwayStripLinearOncePlay(ctx, cwR, chR, scrollView); + } else { + drawGauntletCrownRunwayStripCorePlay(ctx, cwR, chR, scrollView); + } + ctx.restore(); + } + + /** + * Shared runway strip math (viewport in strip space). null if not drawable. + * Map tuning (mno9kb07): finishStopViewportFrac 0.08–0.75 = เส้น FINISH อยู่ที่สัดส่วนนี้จากซ้ายจอก่อนหยุด (ค่าเริ่ม 0.34) + * finishLineInFinishArtFrac = ตำแหน่งเส้นใน finish.png (ค่าเริ่ม 0.12) + * obstacleFadeStartViewportFrac = เริ่มจางสิ่งกีดขวางเมื่อเส้น FINISH อยู่ที่สัดส่วนนี้บนจอ (ค่าเริ่ม 0.9) + */ + function gauntletCrownRunwayScrollGeometryPlay() { + if (!mapData || !canvas || !gauntletCrownRunwayBgDrawActivePlay()) return null; + const zGaRaw = computePlayCameraZDrawPlay(); + const gCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zGaRaw); + if (!gCam) return null; + const halfW = canvas.width / (2 * zGaRaw); + const worldMinX = gCam.px - halfW; + const cwWorld = canvas.width / zGaRaw; + const h = mapData.height || 15; + const ts = mapData.tileSize || 32; + const mapHpx = Math.max(1, Math.round(h * ts)); + const { w0, w1, w2, w3, w4 } = gauntletCrownRunwayBgStripWidthsAtCh(mapHpx); + const w4Safe = Math.max(w4, 1); + const totalStrip = w0 + w1 + w2 + w3 + w4Safe; + if (!(totalStrip > 8)) return null; + const scrollView = gauntletCrownRunwayBgScrollPx + worldMinX; + const right = scrollView + cwWorld; + const finishStart = w0 + w1 + w2 + w3; + const geoMinScroll = finishStart + Math.max(0, cwWorld - w4Safe); + const wideVpPad = cwWorld > w4Safe * 1.02 ? w4Safe * 0.38 : 0; + const minScrollForFinishPass = geoMinScroll + wideVpPad; + return { + scrollView, right, cwWorld, w4Safe, totalStrip, finishStart, minScrollForFinishPass, + }; + } + + /** True when เส้น FINISH ถึงตำแหน่งบนจอที่กำหนด (ขวาเส้น FINISH ตามงานศิลป์ — ไม่เลื่อนเกินไป) */ + function gauntletCrownRunwayScrollStripStopZoneReachedPlay() { + const g = gauntletCrownRunwayScrollGeometryPlay(); + if (!g) return false; + if (g.scrollView < g.minScrollForFinishPass - 0.01) return false; + const screenFrac = gauntletCrownRunwayFinishLineScreenFracPlay(g); + if (screenFrac == null) return false; + return screenFrac <= gauntletCrownRunwayFinishStopViewportFracPlay() + 0.008; + } + + function resetGauntletCrownRunwaySpawnScrollSnap() { + gauntletCrownRunwaySpawnScrollSnapped = false; + } + + /** + * จัด gauntletCrownRunwayBgScrollPx ครั้งแรกให้คอลัมน์เกิด (ซ้ายสุด) ตรง ~runwaySpawnAlignFrac ของความกว้างแถบ START (w0) + * ในแมป: gauntletCrownRunwayBg.runwaySpawnAlignFrac 0.02–0.95 (ค่าเริ่ม 0.32) + * Anchor: คอลัมน์ซ้ายสุดระหว่างช่องเกิดในแมปกับตำแหน่งจริงของทุกคนในรอบ (รวม me + บอทพรีวิว) — ถ้า me ถูก resolve ไปคอลัมน์ซ้ายกว่า min ของช่องวาด แต่ runway ยึดแค่ช่องวาด จะเห็นตัวลอยซ้ายของพรมแดง + * Use min(editor spawn column, floor of every alive entity x) so runway START art stays tied to where characters actually stand. + */ + function trySnapGauntletCrownRunwayScrollToSpawnsPlay() { + if (gauntletCrownRunwaySpawnScrollSnapped) return; + if (!gauntletCrownRunwayBgMapEligiblePlay() || gauntletCrownRunwayBgOff || !mapData) return; + if (!gauntletCrownRunwayBgImagesReadyPlay()) return; + const h = mapData.height || 15; + const ts = mapData.tileSize || 32; + const mapHpx = Math.max(1, Math.round(h * ts)); + const { w0 } = gauntletCrownRunwayBgStripWidthsAtCh(mapHpx); + if (!(w0 > 0)) return; + const slots = collectGauntletSpawnSlotsPlay(mapData); + let minTX = Infinity; + if (slots.length) minTX = Math.min(...slots.map((s) => s.x)); + if (Number.isFinite(me.x)) minTX = Math.min(minTX, Math.floor(me.x)); + others.forEach((o, id) => { + if (!o || o.gauntletEliminated) return; + if (Number.isFinite(o.x)) minTX = Math.min(minTX, Math.floor(o.x)); + }); + if (!Number.isFinite(minTX) || minTX === Infinity) minTX = 1; + const anchorWorldX = (Math.max(0, minTX) + 0.5) * ts; + const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; + const frac = Math.max(0.02, Math.min(0.95, Number(raw.runwaySpawnAlignFrac) || 0.32)); + gauntletCrownRunwayBgScrollPx = frac * w0 - anchorWorldX; + gauntletCrownRunwaySpawnScrollSnapped = true; + } + + function reloadGauntletCrownRunwayBgFromMap() { + gauntletCrownRunwayBgImgs = [null, null, null, null, null]; + gauntletCrownRunwayBgScrollPx = 0; + gauntletCrownRunwayBgFinishLatched = false; + gauntletCrownRunwayBgStripFreezeSinceMs = 0; + gauntletCrownRunwayClientMissionShown = false; + lastGauntletCrownRunwayBgTickMs = 0; + resetGauntletCrownRunwaySpawnScrollSnap(); + if (!gauntletCrownRunwayBgMapEligiblePlay()) return; + const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; + gauntletCrownRunwayBgOff = raw.enabled === false; + gauntletCrownRunwayBgSpeedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 48)); + if (gauntletCrownRunwayBgOff) return; + const srcs = [ + (typeof raw.startImage === 'string' && raw.startImage.length) ? raw.startImage : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_START, + (typeof raw.loopImage2 === 'string' && raw.loopImage2.length) ? raw.loopImage2 : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP2, + (typeof raw.loopImage3 === 'string' && raw.loopImage3.length) ? raw.loopImage3 : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP3, + (typeof raw.loopImage4 === 'string' && raw.loopImage4.length) ? raw.loopImage4 : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP4, + (typeof raw.finishImage === 'string' && raw.finishImage.length) ? raw.finishImage : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_FINISH, + ]; + let pending = 5; + const bump = () => { + pending--; + if (pending <= 0) { + ensureGauntletCrownRunwayBgPlaceholdersPlay(); + try { draw(); } catch (e) { /* ignore */ } + } + }; + for (let i = 0; i < 5; i++) { + const im = new Image(); + const idx = i; + im.onload = () => { + gauntletCrownRunwayBgImgs[idx] = im; + bump(); + }; + im.onerror = () => { bump(); }; + im.src = srcs[i]; + } + } + + function tickGauntletCrownRunwayBgPlay(dtSec) { + if (!gauntletCrownRunwayBgDrawActivePlay()) return; + /** พรีรัน (howto / countdown) — ห้ามเลื่อนก่อน GO · Mega Virus ใช้ shell เดียวกัน */ + if (usesCrownLobbyShellPlay() && gauntletCrownPregamePhase !== 'live') return; + if (gauntletCrownRunwayBgFinishLatched) return; + + if (gauntletCrownRunwayBgStripFreezeSinceMs > 0) { + if (Date.now() - gauntletCrownRunwayBgStripFreezeSinceMs >= GAUNTLET_CROWN_RUNWAY_POST_STOP_MS) { + gauntletCrownRunwayBgFinishLatched = true; + gauntletCrownRunwayBgStripFreezeSinceMs = 0; + if (playBotsEnabled() && gauntletCrownRunwayBgMapEligiblePlay() && !gauntletCrownRunwayClientMissionShown) { + gauntletCrownRunwayClientMissionShown = true; + window.setTimeout(function () { + try { + gauntletCrownPregamePhase = null; + gauntletCrownHowtoVisible = false; + const hto = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto) hto.classList.add('is-hidden'); + const gcc = document.getElementById('gauntlet-crown-countdown'); + if (gcc) gcc.classList.add('is-hidden'); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + showGauntletCrownMissionOverlay(gauntletCrownHeistBuildLocalCrownMissionPlay()); + } catch (_e) { /* ignore */ } + try { + draw(); + } catch (_e2) { /* ignore */ } + }, 340); + } + } + return; + } + + if (gauntletCrownRunwayBgMapEligiblePlay() && gauntletCrownRunwayScrollStripStopZoneReachedPlay()) { + gauntletCrownRunwayBgStripFreezeSinceMs = Date.now(); + try { + syncGauntletCrownJumpButton(); + } catch (_sj) { /* ignore */ } + try { + draw(); + } catch (_d) { /* ignore */ } + return; + } + gauntletCrownRunwayBgScrollPx += (gauntletCrownRunwayBgSpeedPxPerSec || 48) * dtSec; + } + + /** stack + แมป mnn93hpi — พื้นหลัง intro+loop เลื่อนแนวตั้ง (stackTowerBgScroll ในแมป) */ + const STACK_TOWER_SCROLL_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnn93hpi/intro.png'; + const STACK_TOWER_SCROLL_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnn93hpi/loop.png'; + let stackTowerScrollBgIntroImg = null; + let stackTowerScrollBgLoopImg = null; + let stackTowerScrollBgPx = 0; + let stackTowerScrollBgSpeedPxPerSec = 40; + let stackTowerScrollBgOff = false; + let stackTowerScrollBgFlowDown = false; + /** Sraw = px+boost+post ครั้งแรกที่คำนวณ world offset — ใช้หา delta แบบคาบ loop (อย่าใช้ fold(Sraw) ลบตรงๆ จะดันโลกขึ้นหลายร้อย px) */ + let stackTowerScrollBgSrawWorldBaselinePlay = null; + /** ความสูงวาดของไทล์ loop (พิกเซลจอ) — ใช้ขั้นเลื่อนเมื่อ stepScrollPx = 0 */ + let stackTowerScrollBgLoopDrawHPlay = 0; + /** { from, to, t0, dur } — เลื่อนแถบ BG เป็นขั้นตามจำนวนชั้น */ + let stackTowerBgScrollStepAnimPlay = null; + let stackTowerBgScrollPendingStepDeltaPlay = 0; + + function stackTowerScrollBgMapEligible() { + return !!(mapData && mapData.gameType === 'stack' && (currentPlayMapId() || '').trim() === STACK_TOWER_MISSION_MAP_ID); + } + + function stackTowerScrollBgImagesReady() { + if (!stackTowerScrollBgMapEligible()) return false; + const a = stackTowerScrollBgIntroImg; + const b = stackTowerScrollBgLoopImg; + return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth); + } + + /** แถบ intro+loop เต็มจอ — เปิดเฉพาะเมื่อแมปตั้ง stackTowerBgScroll.enabled: true และรูปโหลดครบ (ไม่โหลด strip เมื่อปิด → ใช้รูปแผนที่ backgroundImage) */ + function stackTowerScrollBgDrawActive() { + return stackTowerScrollBgImagesReady() && !stackTowerScrollBgOff; + } + + function reloadStackTowerScrollBgFromMap() { + stackTowerScrollBgIntroImg = null; + stackTowerScrollBgLoopImg = null; + stackTowerScrollBgPx = 0; + stackTowerScrollBgSrawWorldBaselinePlay = null; + stackTowerScrollBgLoopDrawHPlay = 0; + stackTowerBgScrollStepAnimPlay = null; + stackTowerBgScrollPendingStepDeltaPlay = 0; + if (!stackTowerScrollBgMapEligible()) return; + const raw = mapData.stackTowerBgScroll && typeof mapData.stackTowerBgScroll === 'object' ? mapData.stackTowerBgScroll : {}; + stackTowerScrollBgOff = raw.enabled !== true; + { + const spNum = Number(raw.speedPxPerSec); + stackTowerScrollBgSpeedPxPerSec = Number.isFinite(spNum) + ? Math.max(0, Math.min(400, Math.floor(spNum))) + : 0; + } + /* คีย์เวิร์ด down/top = ตัวเลือก "บน→ล่าง" ในเอดิเตอร์ — ใช้สูตรเลื่อนแบบ stripTop−S (ฉากไหลขึ้น) ไม่ใช่ S+chR−… (ไหลลง) เพราะผู้เล่นคาดว่าหอสูง = มองขึ้น */ + const dirRaw = String(raw.scrollDirection || raw.direction || 'down').toLowerCase(); + const rawIsDownKeyword = (dirRaw === 'down' || dirRaw === 'top' || dirRaw === 'toptobottom'); + stackTowerScrollBgFlowDown = !rawIsDownKeyword; + if (stackTowerScrollBgOff) return; + const introSrc = (typeof raw.introImage === 'string' && raw.introImage.length) ? raw.introImage : STACK_TOWER_SCROLL_BG_DEFAULT_INTRO; + const loopSrc = (typeof raw.loopImage === 'string' && raw.loopImage.length) ? raw.loopImage : STACK_TOWER_SCROLL_BG_DEFAULT_LOOP; + let pending = 2; + const bump = () => { + pending--; + if (pending <= 0) { + const intro0 = stackTowerScrollBgIntroImg; + const loop0 = stackTowerScrollBgLoopImg; + if (loop0 && loop0.complete && intro0 && intro0.complete && canvas && canvas.width) { + const cwR0 = Math.max(1, Math.round(canvas.width)); + stackTowerScrollBgLoopDrawHPlay = Math.max(1, Math.round(loop0.naturalHeight * (cwR0 / loop0.naturalWidth))); + } + stackTowerScrollBgSyncInitialScrollToBottom(); + try { draw(); } catch (e) { /* ignore */ } + } + }; + const im1 = new Image(); + im1.onload = () => { stackTowerScrollBgIntroImg = im1; bump(); }; + im1.onerror = () => { bump(); }; + im1.src = introSrc; + const im2 = new Image(); + im2.onload = () => { stackTowerScrollBgLoopImg = im2; bump(); }; + im2.onerror = () => { bump(); }; + im2.src = loopSrc; + } + + function stackTowerScrollBgSyncInitialScrollToBottom() { + const intro = stackTowerScrollBgIntroImg; + if (!canvas || !intro || !intro.complete || !intro.naturalWidth) return; + const cwR = Math.max(1, Math.round(canvas.width)); + const chR = Math.max(1, Math.round(canvas.height)); + const drawHIntro = Math.round(intro.naturalHeight * (cwR / intro.naturalWidth)); + if (stackTowerScrollBgFlowDown) { + stackTowerScrollBgPx = 0; + } else { + /* ขอบล่าง intro ชิดขอบล่างแคนวาส — คู่กับ stripTop−S */ + stackTowerScrollBgPx = drawHIntro - chR; + } + } + + /** + * พับค่าเลื่อนแนวตั้ง S (หน่วยเดียวกับ drawStackTowerScrollBgFullCanvas) เข้าช่วงหนึ่งคาบของ loop + * เมื่อ vp0 ≥ drawHIntro — รูปบนจอซ้ำทุก drawHLoop; พับค่า S ก่อนส่งเข้า worldToScreen กัน px โตไม่จำกัดดันโลกหลุดจอ + */ + function foldStackTowerScrollStripViewSForPhase(S, drawHIntro, drawHLoop) { + const dH = Math.max(1, Math.round(Number(drawHLoop) || 1)); + const dI = Math.max(0, Math.round(Number(drawHIntro) || 0)); + const s = Number(S) || 0; + if (s < dI) return s; + return s - Math.floor((s - dI) / dH) * dH; + } + + /** + * offset แนวตั้งจอให้โลกซิงก์กับ draw ที่ใช้ S = fold(Sraw) — ใช้ความต่างเฟส fold(Sraw)−fold(Sraw0) + * เวลาเลื่อนครบคาบ loop ค่า fold วนกลับ → offset ไม่โตไม่จำกัด · ห้ามคืน fold(Sraw) ล้วนเมื่อ Sraw0 ยังอยู่ intro (baseline ไม่เข้า loop ทำให้สูตร f0+mod ไม่ทำงานและเฟสหลุด) + */ + function stackTowerScrollWorldTotalYFromStripPlay(Sraw, drawHIntro, drawHLoop, Sraw0) { + if (Sraw0 == null) return 0; + const dH = Math.max(1, Math.round(Number(drawHLoop) || 1)); + const dI = Math.max(0, Math.round(Number(drawHIntro) || 0)); + const s = Number(Sraw) || 0; + const s0 = Number(Sraw0) || 0; + const fs = foldStackTowerScrollStripViewSForPhase(s, dI, dH); + const fs0 = foldStackTowerScrollStripViewSForPhase(s0, dI, dH); + return fs - fs0; + } + + /** zDrawPan ส่งต่อเพื่อคูณ boost กับ z — ไม่ scale แคนวาสทั้งแผง (เคย scale รอบกลางทำให้จอบางส่วนว่างเมื่อ z ≠ zRef) */ + function drawStackTowerScrollBgFullCanvas(cw, ch, zDrawPan) { + const intro = stackTowerScrollBgIntroImg; + const loop = stackTowerScrollBgLoopImg; + if (!intro || !intro.complete || !loop || !loop.complete) return; + const zPan = Number(zDrawPan) > 0 ? zDrawPan : zoom; + const cwR = Math.max(1, Math.round(cw)); + const chR = Math.max(1, Math.round(ch)); + const scaleI = cwR / intro.naturalWidth; + const drawHIntro = Math.round(intro.naturalHeight * scaleI); + const scaleL = cwR / loop.naturalWidth; + const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * scaleL)); + const camFollowScreen = isStackTowerMissionUiMapPlay() ? getStackTowerBgScrollHeightBoostPx() * zPan : 0; + const post50Scroll = isStackTowerMissionUiMapPlay() ? getStackTowerPost50BgScrollExtraPxPlay(chR) : 0; + const Sraw = stackTowerScrollBgPx + camFollowScreen + post50Scroll; + const S = foldStackTowerScrollStripViewSForPhase(Sraw, drawHIntro, drawHLoop); + const vp0 = S; + const vp1 = S + chR; + const yLimit = vp1 + drawHLoop * 96; + const flowDown = stackTowerScrollBgFlowDown; + + function stripTopToDestY(stripTop, drawH) { + if (!flowDown) return stripTop - S; + return S + chR - (stripTop + drawH); + } + + ctx.save(); + ctx.imageSmoothingEnabled = false; + ctx.beginPath(); + ctx.rect(0, 0, cwR, chR); + ctx.clip(); + + if (vp0 < 0) { + let k = 1; + if (vp0 < -drawHLoop * 4) { + k = Math.max(1, Math.floor(-vp0 / drawHLoop) - 2); + } + for (; k < 50000; k++) { + const y0 = -k * drawHLoop; + if (y0 >= vp1 + drawHLoop) break; + if (y0 + drawHLoop <= vp0) continue; + const dy = Math.round(stripTopToDestY(y0, drawHLoop)); + ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop); + } + } + + if (drawHIntro > vp0 && vp1 > 0) { + const dy = Math.round(stripTopToDestY(0, drawHIntro)); + ctx.drawImage(intro, 0, 0, intro.naturalWidth, intro.naturalHeight, 0, dy, cwR, drawHIntro); + } + + let y = drawHIntro; + if (y < yLimit && vp1 > drawHIntro) { + if (vp0 > drawHIntro) { + const n = Math.floor((vp0 - drawHIntro) / drawHLoop); + /* ใช้ n ไม่ใช่ n−1 — เดิมทำให้แถบ loop เริ่มสูงกว่า vp0 หนึ่งความสูง จอล่างว่างดำเมื่อ S ใหญ่ */ + y = drawHIntro + n * drawHLoop; + } + while (y < yLimit) { + const dy = Math.round(stripTopToDestY(y, drawHLoop)); + ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop); + y += drawHLoop; + } + } + ctx.restore(); + } + + /** กล้องมองเหนือขอบบนแมป (worldMinY<0) + ใช้รูปแผนที่ — ยัด loop ท้องฟ้าเติมครึ่งบนจอ */ + function drawStackTowerLoopSkyFillAboveMap(ctx, worldMinY, zDraw, cw, ch) { + if (!isStackTowerMissionUiMapPlay() || worldMinY >= 0) return; + const loop = stackTowerScrollBgLoopImg; + if (!loop || !loop.complete || !loop.naturalWidth) return; + const cwR = Math.max(1, Math.round(cw)); + const chR = Math.max(1, Math.round(ch)); + const scaleL = cwR / loop.naturalWidth; + const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * scaleL)); + const skyScreenH = Math.min(chR, Math.ceil(-worldMinY * zDraw) + 4); + const stripPx = stackTowerScrollBgOff ? 0 : stackTowerScrollBgPx; + const rawScroll = stripPx + getStackTowerBgScrollHeightBoostPx() * zDraw + + getStackTowerPost50BgScrollExtraPxPlay(chR); + const scroll = ((rawScroll % drawHLoop) + drawHLoop) % drawHLoop; + let y = -scroll; + ctx.save(); + ctx.imageSmoothingEnabled = false; + while (y < skyScreenH + drawHLoop) { + if (y + drawHLoop > 0) { + ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, y, cwR, drawHLoop); + } + y += drawHLoop; + } + ctx.restore(); + } + + function tickStackTowerBgScrollStepAnimPlay() { + const q = stackTowerBgScrollStepAnimPlay; + if (!q) return; + const t = Math.min(1, (performance.now() - q.t0) / Math.max(1, q.dur)); + const e = 1 - Math.pow(1 - t, 3); + stackTowerScrollBgPx = q.from + (q.to - q.from) * e; + if (t >= 1) { + stackTowerScrollBgPx = q.to; + stackTowerBgScrollStepAnimPlay = null; + stackTowerScrollBgSrawWorldBaselinePlay = null; + if (stackTowerBgScrollPendingStepDeltaPlay !== 0) { + const d = stackTowerBgScrollPendingStepDeltaPlay; + stackTowerBgScrollPendingStepDeltaPlay = 0; + startStackTowerBgScrollStepAnimPlay(d); + } + } + } + + function getStackTowerBgScrollStepEveryLayersPlay() { + const raw = mapData && mapData.stackTowerBgScroll; + if (!raw || typeof raw !== 'object') return 0; + const n = Math.floor(Number(raw.stepEveryLayers)); + return Number.isFinite(n) && n > 0 ? Math.min(200, n) : 0; + } + + function getStackTowerBgScrollStepAnimMsPlay() { + const raw = mapData && mapData.stackTowerBgScroll; + if (!raw || typeof raw !== 'object') return 520; + const t = Math.floor(Number(raw.stepAnimMs)); + return Number.isFinite(t) ? Math.max(120, Math.min(4000, t)) : 520; + } + + function getStackTowerBgScrollStepScrollPxPlay() { + const raw = mapData && mapData.stackTowerBgScroll; + if (!raw || typeof raw !== 'object') return 0; + const d = Math.floor(Number(raw.stepScrollPx)); + return Number.isFinite(d) ? Math.max(0, Math.min(8000, d)) : 0; + } + + function startStackTowerBgScrollStepAnimPlay(deltaPx) { + if (!stackTowerScrollBgDrawActive() || !deltaPx) return; + if (stackTowerBgScrollStepAnimPlay) { + stackTowerBgScrollPendingStepDeltaPlay += deltaPx; + return; + } + const dur = getStackTowerBgScrollStepAnimMsPlay(); + stackTowerBgScrollStepAnimPlay = { + from: stackTowerScrollBgPx, + to: stackTowerScrollBgPx + deltaPx, + t0: performance.now(), + dur, + }; + } + + function maybeQueueStackTowerBgScrollStepPlay(layerCountAfter) { + if (!stackTowerScrollBgDrawActive()) return; + const every = getStackTowerBgScrollStepEveryLayersPlay(); + if (every <= 0 || layerCountAfter <= 0 || layerCountAfter % every !== 0) return; + let delta = getStackTowerBgScrollStepScrollPxPlay(); + if (!delta) delta = Math.max(8, stackTowerScrollBgLoopDrawHPlay || 200); + delta = Math.min(8000, Math.max(8, delta)); + startStackTowerBgScrollStepAnimPlay(delta); + } + + function getStackTowerStripReleaseGapWorldPxPlay() { + const raw = mapData && mapData.stackTowerBgScroll; + if (!isStackTowerMissionUiMapPlay() || !raw || typeof raw !== 'object') return null; + const g = Number(raw.releaseGapWorldPx); + if (!Number.isFinite(g) || g < 0) return null; + return Math.min(800, g); + } + + function updateStackTowerSwingYForStripGapPlay() { + if (!stackMini || !isStackTowerMissionUiMapPlay()) return; + const gap = getStackTowerStripReleaseGapWorldPxPlay(); + if (gap == null) return; + const m = stackMini; + const lh = m.layerWorldH || Math.max(14, tileSize * 0.3); + const nLay = m.layers ? m.layers.length : 0; + const topBlockTopY = m.floorWorldY - nLay * lh; + const swingLift = getStackTowerSwingLiftWorldPx(nLay, lh); + m.swingWorldY = topBlockTopY - gap + swingLift; + } + + function tickStackTowerScrollBg(dtSec) { + if (!stackTowerScrollBgMapEligible() || stackTowerScrollBgOff) return; + if (!stackTowerScrollBgImagesReady()) return; + tickStackTowerBgScrollStepAnimPlay(); + if (stackTowerBgScrollStepAnimPlay) return; + const spd = Math.max(0, Number(stackTowerScrollBgSpeedPxPerSec) || 0) * dtSec; + if (spd <= 0) return; + /* !flowDown (stripTop−S): S เพิ่ม → dy ลด → ฉากไหลขึ้น — ห้ามใช้ −= (จะลด S แล้วศิลป์ไหลลง) */ + stackTowerScrollBgPx += spd; + } + + /** + * ค่าลบจาก screen Y ใน worldToScreen — ซิงก์กับ drawStackTowerScrollBgFullCanvas (auto + boost + post-50) + * แถบ BG วาด 1:1 กับแคนวาส — ห้ามใช้ fold(Sraw) เป็นค่าลบตรงๆ (จะดันโลกขึ้นหลายร้อย px เหมือนครึ่งล่างจอว่าง) + */ + function getStackTowerWorldLayerScrollScreenOffsetYPlay(zPan) { + if (!isStackTowerMissionUiMapPlay() || !stackMini) return 0; + const zPn = Number(zPan) > 0 ? zPan : zoom; + const chPx = Math.max(1, Math.round(canvas && canvas.height ? canvas.height : 720)); + const boostScreen = getStackTowerBgScrollHeightBoostPx() * zPn; + const post50Screen = getStackTowerPost50BgScrollExtraPxPlay(chPx); + const intro = stackTowerScrollBgIntroImg; + const loop = stackTowerScrollBgLoopImg; + let totalScreen = boostScreen + post50Screen; + if (stackTowerScrollBgDrawActive() && intro && intro.complete && intro.naturalWidth + && loop && loop.complete && loop.naturalWidth && canvas && canvas.width) { + const cwR = Math.max(1, Math.round(canvas.width)); + const drawHIntro = Math.round(intro.naturalHeight * (cwR / intro.naturalWidth)); + const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * (cwR / loop.naturalWidth))); + const SrawDraw = stackTowerScrollBgPx + boostScreen + post50Screen; + if (stackTowerScrollBgSrawWorldBaselinePlay == null) stackTowerScrollBgSrawWorldBaselinePlay = SrawDraw; + const S0 = stackTowerScrollBgSrawWorldBaselinePlay; + let stripY; + if (stackTowerScrollBgFlowDown) { + const Sview = foldStackTowerScrollStripViewSForPhase(SrawDraw, drawHIntro, drawHLoop); + stripY = (boostScreen + post50Screen) * 2 - Sview; + } else { + stripY = stackTowerScrollWorldTotalYFromStripPlay(SrawDraw, drawHIntro, drawHLoop, S0); + } + totalScreen = stripY; + } else if (stackTowerScrollBgDrawActive()) { + const px = stackTowerScrollBgFlowDown ? -stackTowerScrollBgPx : stackTowerScrollBgPx; + totalScreen = px + boostScreen + post50Screen; + } + return totalScreen; + } + + /** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */ + let mapGridImageImgs = []; + let mapGridImageHeldImgs = []; + let me = { x: 1, y: 1, direction: 'down', nickname: nick, isWalking: false, playTint: null, gauntletScore: 0, gauntletEliminated: false, tx: null, ty: null, quizCarryHeld: null, gauntletCrownPenaltyFxUntil: 0 }; + const others = new Map(); + /** Stack preview HUD: เทอร์มินัลล็อก (cyber UI) */ + const STACK_PREVIEW_HUD_LOG_MAX = 8; + let stackPreviewHudLog = []; + function stackPreviewPushHudLog(line) { + if (!playBotsEnabled()) return; + stackPreviewHudLog.push(String(line || '').slice(0, 96)); + while (stackPreviewHudLog.length > STACK_PREVIEW_HUD_LOG_MAX) stackPreviewHudLog.shift(); + } + function stackPreviewLogStackDrop(hit, actor) { + if (!playBotsEnabled() || !hit) return; + const bid = actor && actor.botId; + let nm = 'NODE'; + if (actor && actor.human) nm = (me.nickname || 'YOU').toUpperCase().replace(/\s+/g, '_').slice(0, 12); + else if (bid && others.get(bid)) nm = String(others.get(bid).nickname || 'BOT').toUpperCase().replace(/\s+/g, '_').slice(0, 12); + if (hit.miss) stackPreviewPushHudLog(`>>> ${nm}: SECTOR_MISS`); + else if (hit.perfect) stackPreviewPushHudLog(`>>> ${nm}: PERFECT +${hit.pts} SYN`); + else stackPreviewPushHudLog(`>>> ${nm}: LOCK +${hit.pts}`); + } + const keys = {}; + const characterImages = {}; + /** data URL จาก canvas compose สี — กันยิง toDataURL ทุกเฟรมตอน sync HUD */ + const cyberHudScoreAvatarUrlCache = new Map(); + const CYBER_HUD_AV_URL_CACHE_CAP = 56; + function createDefaultAvatarImg() { + const c = document.createElement('canvas'); + c.width = 64; c.height = 64; + const ctx = c.getContext('2d'); + ctx.fillStyle = '#7aa2f7'; + ctx.beginPath(); + ctx.arc(32, 22, 14, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#9ece6a'; + ctx.beginPath(); + ctx.arc(32, 48, 18, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#1a1b26'; + ctx.beginPath(); + ctx.arc(28, 20, 3, 0, Math.PI * 2); + ctx.arc(36, 20, 3, 0, Math.PI * 2); + ctx.fill(); + const img = new Image(); + img.src = c.toDataURL('image/png'); + return img; + } + const defaultAvatarImg = createDefaultAvatarImg(); + const characterAnimations = {}; + const characterIdleAnimations = {}; + const CHARACTER_ANIM_FRAMES = 4; + const CHARACTER_ANIM_FRAME_MS = 200; + + /** จังหวะเดินลูปคงที่ 4 เฟรม — อย่า modulo ด้วยจำนวนเฟรมที่โหลดได้แล้ว (ไม่งั้นเฟรมกลางหาย/ลูปสั้นลง) */ + function walkAnimPhaseIndex(now, isWalking) { + const t = isWalking ? (typeof now === 'number' ? now : Date.now()) : 0; + return Math.floor(t / CHARACTER_ANIM_FRAME_MS) % CHARACTER_ANIM_FRAMES; + } + + /** เลือกเฟรมสูงสุดที่โหลดแล้วและ <= phase (เดิมถอย) */ + function pickLoadedWalkFrameIndex(anim, phase) { + if (!anim || !anim.frames || !anim.frames.length) return -1; + const maxK = Math.min(phase, anim.frames.length - 1, CHARACTER_ANIM_FRAMES - 1); + for (let k = maxK; k >= 0; k--) { + const f = anim.frames[k]; + if (f && f.complete && f.naturalWidth) return k; + } + return -1; + } + + /** ตัวละครอัปโหลดจากระบบมักเป็น char- + ตัวเลข (อาจมี suffix) — ไม่มี idle strip / idle layer ครบทุกทิศ */ + function isUploadedCharAssetId(id) { + if (!id) return false; + const s = String(id).trim(); + return /^char-\d/i.test(s); + } + + function useMultiFrameIdleSpriteSheets(id) { + if (!id) return true; + return !isUploadedCharAssetId(id); + } + + function ensureCharacterIdleAnim(id, dir) { + const d = dir || 'down'; + /* char-* ไม่มี *_idle_0.png — ใช้ strip เดิน dir_0..3 เป็น idle ต่อทิศ (มีลูป) แทนรูปเดียวที่มักไม่มี */ + if (!useMultiFrameIdleSpriteSheets(id)) { + const key = String(id) + '_' + d + '_idle'; + let anim = characterIdleAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterIdleAnimations[key] = anim; + const enc = encodeURIComponent(id); + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + enc + '_' + d + '_' + i + '.png'; + anim.frames.push(img); + } + const fb = new Image(); + fb.src = BASE + '/img/characters/' + enc + '_' + d + '_idle.png'; + anim.fallback = fb; + } + return anim; + } + const key = id + '_' + d + '_idle'; + let anim = characterIdleAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterIdleAnimations[key] = anim; + const enc = encodeURIComponent(id); + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + enc + '_' + d + '_idle_' + i + '.png'; + anim.frames.push(img); + } + const fb = new Image(); + fb.src = BASE + '/img/characters/' + enc + '_' + d + '_idle.png'; + anim.fallback = fb; + } + return anim; + } + + /** ลูป idle ขณะยืน — ใช้จังหวะเวลาเหมือนเดิน (CHARACTER_ANIM_FRAME_MS) */ + function idleAnimPhaseIndex(now) { + const t = typeof now === 'number' ? now : Date.now(); + return Math.floor(t / CHARACTER_ANIM_FRAME_MS) % CHARACTER_ANIM_FRAMES; + } + + function characterIdleSpritesVisible(id, dir) { + if (!id) return false; + const anim = ensureCharacterIdleAnim(id, dir || 'down'); + if (anim.fallback && anim.fallback.complete && anim.fallback.naturalWidth) return true; + for (let i = 0; i < anim.frames.length; i++) { + const f = anim.frames[i]; + if (f && f.complete && f.naturalWidth) return true; + } + return false; + } + + /** สุ่มสีตามเลเยอร์เดียวกับ character.html: bodyColor / hairColor / headColor (ลำดับซ้อนเหมือน composeLayeredFrame) */ + const PLAY_LAYER_ORDER = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face']; + const PLAY_LAYER_TINT_KEY = { bodyColor: 'body', headColor: 'head', hairColor: 'hair' }; + const PLAY_TINT_HEAD = ['#eaa78a', '#fbd5c4', '#fae9e1']; + const PLAY_TINT_HAIR = ['#d72520', '#ef8508', '#efe237', '#5bb443', '#2585cb', '#3f4ead', '#b53fd6', '#fec1fe']; + const PLAY_TINT_BODY = ['#d72520', '#ef8508', '#efe237', '#5bb443', '#2585cb', '#3f4ead', '#b53fd6', '#fec1fe']; + const playLayerMode = {}; + /** คิว probe ครบ up/down/left/right แล้ว — กันทิศที่ไม่มีไฟล์ layer ไปตก fallback แถบทั้งทั้งที่ทิศอื่นมี */ + const playLayerAllDirsQueued = {}; + /** จาก GET /api/characters — hasLayerFiles สแกนจากชื่อไฟล์จริง (แม่นกว่า probe จาก อย่างเดียว) */ + let playCharLayerApi = { status: 'idle', map: null }; + /** id → Set เลเยอร์ที่มีไฟล์จริง (จาก GET /api/characters) — กันยิง URL เลเยอร์ที่ไม่เคยอัปโหลด → 404 รกคอนโซล */ + const playCharDiskLayersById = Object.create(null); + + function ensurePlayCharLayerListFetch() { + if (playCharLayerApi.status !== 'idle') return; + playCharLayerApi.status = 'loading'; + fetch(BASE + '/api/characters') + .then((r) => r.json()) + .then((list) => { + const map = Object.create(null); + playCharacterIdRoster = []; + if (Array.isArray(list)) { + list.forEach((c) => { + if (!c || !c.id) return; + playCharacterIdRoster.push(String(c.id)); + map[c.id] = !!c.hasLayerFiles; + if (Array.isArray(c.layers) && c.layers.length) { + const st = new Set(); + c.layers.forEach((n) => { + if (typeof n === 'string' && n.length) st.add(n); + }); + if (st.size) playCharDiskLayersById[c.id] = st; + else delete playCharDiskLayersById[c.id]; + } else { + delete playCharDiskLayersById[c.id]; + } + }); + } + playCharLayerApi = { status: 'done', map }; + }) + .catch(() => { + playCharacterIdRoster = []; + playCharLayerApi = { status: 'done', map: Object.create(null) }; + }); + } + + /** @returns {boolean|null} true/false จาก API, null = ยังไม่โหลดหรือไม่มีรายการ id นี้ */ + function playCharLayerFromApi(characterId) { + if (playCharLayerApi.status !== 'done' || !characterId) return null; + if (Object.prototype.hasOwnProperty.call(playCharLayerApi.map, characterId)) { + return playCharLayerApi.map[characterId]; + } + return null; + } + + /** ถ้าเซิร์ฟเวอร์ส่งรายชื่อเลเยอร์ที่มีบนดิสก์ — ไม่โหลดเลเยอร์อื่น (ลด 404) */ + function playCharDiskLayersSet(characterId) { + if (!characterId || playCharLayerApi.status !== 'done') return null; + const s = playCharDiskLayersById[characterId]; + return s && s.size ? s : null; + } + + const playLayerImageCache = new Map(); + const playLayerCompositeCache = new Map(); + function pickRandomPlayTint() { + return { + head: PLAY_TINT_HEAD[Math.floor(Math.random() * PLAY_TINT_HEAD.length)], + hair: PLAY_TINT_HAIR[Math.floor(Math.random() * PLAY_TINT_HAIR.length)], + body: PLAY_TINT_BODY[Math.floor(Math.random() * PLAY_TINT_BODY.length)], + }; + } + + /** FNV-1a 32-bit — แยกดัชนี head/hair/body ได้กระจายกว่า hash*31 เดิม (กันบอท __pv_bot_N สีซ้ำ) */ + function hashStringFnv1a32(str) { + let h = 2166136261 >>> 0; + const s = String(str || ''); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619) >>> 0; + } + return h >>> 0; + } + + function playTintFromPeerId(id) { + const s = String(id || 'x'); + const h0 = hashStringFnv1a32(s); + const h1 = hashStringFnv1a32(s + '\n1hair'); + const h2 = hashStringFnv1a32(s + '\n2body'); + return { + head: PLAY_TINT_HEAD[h0 % PLAY_TINT_HEAD.length], + hair: PLAY_TINT_HAIR[h1 % PLAY_TINT_HAIR.length], + body: PLAY_TINT_BODY[h2 % PLAY_TINT_BODY.length], + }; + } + + const LOBBY_PLAYER_TINT_KEY = 'justiceLobbyPlayerTint'; + /** ธีมสี 8 ช่อง — ตรง room-lobby / customize */ + const PLAY_LOBBY_THEME_HEX = [ + '#d72520', '#ef8508', '#efe237', '#5bb443', + '#2585cb', '#3f4ead', '#b53fd6', '#fec1fe', + ]; + const PLAY_LOBBY_SKIN_HEX = ['#eaa78a', '#fbd5c4', '#fae9e1']; + let playLobbyBotThemes = []; + + function parsePlayLobbyThemeIndex(n) { + const v = Math.floor(Number(n)); + return (v >= 1 && v <= 8) ? v : null; + } + + function parsePlayLobbySkinIndex(n) { + const v = Math.floor(Number(n)); + return (v >= 1 && v <= 3) ? v : null; + } + + function playTintFromLobbyThemeIndices(themeIndex, skinIndex) { + const ti = parsePlayLobbyThemeIndex(themeIndex); + const si = parsePlayLobbySkinIndex(skinIndex); + const bodyHex = ti ? PLAY_LOBBY_THEME_HEX[ti - 1] : ''; + const headHex = si ? PLAY_LOBBY_SKIN_HEX[si - 1] : ''; + if (!bodyHex && !headHex) return null; + return playTintFromLobbyRgb(bodyHex || null, headHex || null); + } + + function playTintFromJoinPeer(peerRow) { + if (!peerRow) return null; + return playTintFromLobbyThemeIndices(peerRow.lobbyColorThemeIndex, peerRow.lobbySkinToneIndex); + } + + function playTintForPreviewBotId(peerId) { + if (!isPreviewBotId(peerId)) return null; + const idx = parseInt(String(peerId).slice(PREVIEW_BOT_PREFIX.length), 10); + if (!Number.isFinite(idx) || idx < 0) return null; + const slot = playLobbyBotThemes[idx]; + if (!slot) return null; + const tint = playTintFromLobbyThemeIndices(slot.themeIndex, slot.skinToneIndex); + if (!tint) return null; + try { + const map = JSON.parse(sessionStorage.getItem(lobbyBotTintsStorageKeyPlay()) || '{}'); + map['__lobby_bot_' + idx] = { + colorTheme: PLAY_LOBBY_THEME_HEX[parsePlayLobbyThemeIndex(slot.themeIndex) - 1] || '', + colorSkin: PLAY_LOBBY_SKIN_HEX[parsePlayLobbySkinIndex(slot.skinToneIndex) - 1] || '', + }; + sessionStorage.setItem(lobbyBotTintsStorageKeyPlay(), JSON.stringify(map)); + } catch (e) { /* ignore */ } + return tint; + } + + function lobbyBotTintsStorageKeyPlay() { + const sp = (typeof spaceId === 'string' && spaceId) ? spaceId : (new URLSearchParams(window.location.search).get('space') || 'default'); + return 'justiceLobbyBotTints:' + sp; + } + + function rgbCssToHexPlay(rgb) { + if (!rgb) return ''; + const s = String(rgb).trim(); + if (s.charAt(0) === '#') return s; + const m = s.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i); + if (!m) return ''; + return '#' + [m[1], m[2], m[3]].map((n) => { + const v = Math.max(0, Math.min(255, parseInt(n, 10))); + return v.toString(16).padStart(2, '0'); + }).join(''); + } + + function playTintFromLobbyRgb(themeRgb, skinRgb) { + const bodyHex = rgbCssToHexPlay(themeRgb); + const headHex = rgbCssToHexPlay(skinRgb); + if (!headHex && !bodyHex) return null; + return { + head: headHex || '#fbd5c4', + hair: bodyHex || '#fb4941', + body: bodyHex || '#fb4941', + }; + } + + function loadPlayTintFromLobbyPlayerStorage() { + try { + const raw = localStorage.getItem(LOBBY_PLAYER_TINT_KEY); + if (raw) { + const j = JSON.parse(raw); + if (j && typeof j === 'object') { + if (j.head && j.body) { + return { + head: String(j.head), + hair: String(j.hair || j.body), + body: String(j.body), + }; + } + const fromJson = playTintFromLobbyRgb(j.themeRgb, j.skinRgb); + if (fromJson) return fromJson; + } + } + } catch (e) { /* ignore */ } + try { + const theme = localStorage.getItem('lobbyThemeColor') || ''; + const skin = localStorage.getItem('lobbySkinTone') || ''; + return playTintFromLobbyRgb(theme, skin); + } catch (e2) { + return null; + } + } + + function loadPlayTintForBotPeerId(peerId) { + const sid = String(peerId || ''); + let botKey = sid; + if (sid.indexOf(PREVIEW_BOT_PREFIX) === 0) { + botKey = '__lobby_bot_' + sid.slice(PREVIEW_BOT_PREFIX.length); + } + try { + const raw = sessionStorage.getItem(lobbyBotTintsStorageKeyPlay()); + if (!raw) return null; + const map = JSON.parse(raw); + const saved = map && map[botKey]; + if (!saved) return null; + return playTintFromLobbyRgb(saved.colorTheme, saved.colorSkin); + } catch (e) { + return null; + } + } + + /** สีจากล็อบบี้ (ผู้เล่น + บอท) — fallback hash ตาม peer id */ + function resolvePlayTintForPeer(peerId, peerRow) { + const sid = String(peerId || ''); + const fromRow = peerRow ? playTintFromJoinPeer(peerRow) : null; + if (fromRow) return fromRow; + const previewTint = playTintForPreviewBotId(sid); + if (previewTint) return previewTint; + if (myId != null && sid === String(myId)) { + const meTint = loadPlayTintFromLobbyPlayerStorage(); + if (meTint) return meTint; + } + if ((sid === '__me' || sid === '__local_me') && me && me.playTint) return me.playTint; + const botTint = loadPlayTintForBotPeerId(sid); + if (botTint) return botTint; + return playTintFromPeerId(sid); + } + + function applyPlayTintFromLobbyIndices(peerId, themeIndex, skinIndex) { + const tint = playTintFromLobbyThemeIndices(themeIndex, skinIndex); + if (!tint) return; + const sid = peerId != null ? String(peerId) : ''; + if (myId != null && sid === String(myId)) { + me.playTint = tint; + return; + } + const o = others.get(peerId) || others.get(sid); + if (o) o.playTint = tint; + if (isPreviewBotId(sid)) playTintForPreviewBotId(sid); + } + + function applyPlayLobbyTintSync(data) { + if (!data || typeof data !== 'object') return; + if (Array.isArray(data.lobbyBotThemes)) playLobbyBotThemes = data.lobbyBotThemes; + if (Array.isArray(data.peers)) { + data.peers.forEach(function (p) { + if (!p || p.id == null) return; + applyPlayTintFromLobbyIndices(p.id, p.lobbyColorThemeIndex, p.lobbySkinToneIndex); + }); + } + if (playBotsEnabled()) rebalancePreviewBots(); + try { + preloadPlayOccupantsTints(); + preloadPlayLobbyIdleAvatarsForOccupants(); + } catch (e) { /* ignore */ } + } + + function getPlayTintForMissionAvatar(peerId) { + const sid = peerId != null ? String(peerId) : ''; + if (myId != null && sid === String(myId)) { + return me.playTint || resolvePlayTintForPeer(sid); + } + const o = others.get(peerId) || others.get(sid); + if (o && o.playTint) return o.playTint; + return resolvePlayTintForPeer(sid); + } + + /** สรุปภารกิจ — ผู้เล่นจริงต้องอยู่ใน top N เสมอ */ + function missionEnsureHumanInTopRanked(rows, maxN) { + const cap = Math.max(1, maxN || 5); + if (!Array.isArray(rows) || !rows.length) return []; + const humanId = myId != null ? String(myId) : null; + const humanRow = humanId + ? rows.find((r) => r && r.id != null && String(r.id) === humanId) + : rows.find((r) => r && r.isLocalHuman); + let top = rows.slice(0, cap); + if (!humanRow) return top; + const hid = humanRow.id != null ? String(humanRow.id) : ''; + if (top.some((r) => r && r.id != null && String(r.id) === hid)) return top; + if (top.length >= cap) top = top.slice(0, cap - 1); + top.push(humanRow); + return top; + } + + function hexToRgb01(hex) { + const h = (hex || '').replace('#', '').trim(); + if (h.length !== 6) return [1, 1, 1]; + return [ + parseInt(h.slice(0, 2), 16) / 255, + parseInt(h.slice(2, 4), 16) / 255, + parseInt(h.slice(4, 6), 16) / 255, + ]; + } + + function tintPlayLayerImageData(imageData, tintHex) { + const rgb = hexToRgb01(tintHex); + const tr = rgb[0], tg = rgb[1], tb = rgb[2]; + const d = imageData.data; + for (let i = 0; i < d.length; i += 4) { + if (d[i + 3] < 12) continue; + const r = d[i], g = d[i + 1], b = d[i + 2]; + const L = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + d[i] = Math.min(255, Math.round(tr * 255 * L)); + d[i + 1] = Math.min(255, Math.round(tg * 255 * L)); + d[i + 2] = Math.min(255, Math.round(tb * 255 * L)); + } + } + + function drawPlayTintedLayer(ctx, w, h, img, tintHex) { + if (!tintHex) { + ctx.drawImage(img, 0, 0, w, h); + return; + } + const c = document.createElement('canvas'); + c.width = w; + c.height = h; + const x = c.getContext('2d'); + x.drawImage(img, 0, 0, w, h); + try { + const idata = x.getImageData(0, 0, w, h); + tintPlayLayerImageData(idata, tintHex); + x.putImageData(idata, 0, 0); + } catch (e) { + x.clearRect(0, 0, w, h); + x.drawImage(img, 0, 0, w, h); + } + ctx.drawImage(c, 0, 0, w, h); + } + + function ensurePlayLayerImage(url) { + let img = playLayerImageCache.get(url); + if (!img) { + img = new Image(); + img.src = url; + playLayerImageCache.set(url, img); + } + return img; + } + + /** อุ่น cache เลเยอร์สีของตัวละคร (ทุกทิศ + เดิน 0-3 + idle) ก่อนเข้าเกม กันกระตุกตอนโหลดสีกลางเกม + * ยิงเฉพาะ layer ที่ "มีจริง" (อ่านจาก manifest/disk set) เพื่อกัน 404 รัว — รอ manifest พร้อมก่อน */ + const _playPreloadedChars = new Set(); + const _playPreloadedTintKeys = new Set(); + function preloadPlayCharacterLayers(charId, _tries) { + if (!charId || _playPreloadedChars.has(charId)) return; + try { ensurePlayCharLayerListFetch(); } catch (e) { /* ignore */ } + try { ensurePlayLayerProbesAllDirections(charId); } catch (e) { /* ignore */ } + const diskSet = playCharDiskLayersSet(charId); + if (!diskSet) { + // manifest ยังไม่พร้อม — ลองใหม่อีกครู่ (กันยิง layer ที่ไม่มี = 404) + const t = _tries || 0; + if (t < 8) setTimeout(function () { preloadPlayCharacterLayers(charId, t + 1); }, 450); + return; + } + _playPreloadedChars.add(charId); + const dirs = ['down', 'up', 'left', 'right']; + const warm = function (urls) { if (urls && urls.length) ensurePlayLayerImage(urls[0]); }; + dirs.forEach(function (dir) { + PLAY_LAYER_ORDER.forEach(function (ln) { + if (ln !== 'shadow' && !diskSet.has(ln)) return; // ข้าม layer ที่ไม่มีไฟล์ + if (ln === 'face' && dir === 'up' && isUploadedCharAssetId(charId)) return; // up ไม่มี face + for (let fi = 0; fi < CHARACTER_ANIM_FRAMES; fi++) { + warm(ln === 'shadow' ? shadowUrlCandidates(charId, dir, fi) : layerUrlCandidates(charId, dir, ln, fi)); + } + warm(ln === 'shadow' ? shadowUrlCandidatesIdle(charId, dir, 0) : layerUrlCandidatesIdle(charId, dir, ln, 0)); + }); + }); + } + + /** อุ่น canvas ย้อมสี (head/hair/body) ต่อตัวละคร+สี */ + function preloadPlayTintForAvatar(charId, tint) { + if (!charId || !tint || typeof tint.head !== 'string') return; + const key = [charId, tint.head, tint.hair, tint.body].join('|'); + if (_playPreloadedTintKeys.has(key)) return; + _playPreloadedTintKeys.add(key); + preloadPlayCharacterLayers(charId); + const dirs = ['down', 'up', 'left', 'right']; + const now = Date.now(); + dirs.forEach(function (dir) { + const rawWalk = getCharacterFrame(charId, dir, now, true); + const rawIdle = getCharacterFrame(charId, dir, now, false); + if (rawWalk) getPlayTintedAvatarSource(rawWalk, charId, dir, now, true, tint); + if (rawIdle) getPlayTintedAvatarSource(rawIdle, charId, dir, now + 400, false, tint); + }); + } + + function preloadPlayOccupantsTints() { + const meCid = me.characterId || getPlayCharacterId(); + if (meCid) { + preloadPlayTintForAvatar(meCid, me.playTint || resolvePlayTintForPeer(String((myId != null && myId !== '') ? myId : (nick || 'local')))); + } + others.forEach(function (o, id) { + if (!o || !o.characterId) return; + preloadPlayTintForAvatar(o.characterId, o.playTint || resolvePlayTintForPeer(id)); + }); + } + + /** ลำดับลอง URL: เฟรมปัจจุบันแบบ multi → เฟรม 0 → แบบ single (ไม่มี _0_) เพื่อกันชื่อไฟล์ไม่ตรงกับที่เซิร์ฟเวอร์เขียน */ + function layerUrlCandidates(id, dir, layerName, frameIndex) { + const enc = encodeURIComponent(id); + const base = BASE + '/img/characters/' + enc + '_' + dir; + const out = []; + function add(u) { + if (out.indexOf(u) === -1) out.push(u); + } + add(base + '_' + frameIndex + '_layer_' + layerName + '.png'); + if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png'); + add(base + '_layer_' + layerName + '.png'); + return out; + } + + /** เลเยอร์ idle: id__idle__layer_.png หรือ id__idle_layer_.png */ + function layerUrlCandidatesIdle(id, dir, layerName, frameIndex) { + const enc = encodeURIComponent(id); + const base = BASE + '/img/characters/' + enc + '_' + dir + '_idle'; + const out = []; + function add(u) { + if (out.indexOf(u) === -1) out.push(u); + } + // idle เฟรมเดียว — ชื่อจริงของ char-* คือ *_idle_layer_*.png (ไม่มี frame index) ลองตัวนี้ก่อนกัน 404 รัว + add(base + '_layer_' + layerName + '.png'); + add(base + '_' + frameIndex + '_layer_' + layerName + '.png'); + if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png'); + return out; + } + + function shadowUrlCandidates(id, dir, frameIndex) { + const c = layerUrlCandidates(id, dir, 'shadow', frameIndex); + const def = BASE + '/img/default-shadow-' + dir + '.png'; + if (c.indexOf(def) === -1) c.push(def); + return c; + } + + function shadowUrlCandidatesIdle(id, dir, frameIndex) { + const c = layerUrlCandidatesIdle(id, dir, 'shadow', frameIndex); + const def = BASE + '/img/default-shadow-' + dir + '.png'; + if (c.indexOf(def) === -1) c.push(def); + return c; + } + + /** ลองทีละ URL — อย่า set src พร้อมกันหลายรูป (เดิมทำให้ legacy `id_dir_layer_x` โดน 404 ทุกเลเยอร์ทุกทิศทั้งที่มีแค่ `id_dir_0_layer_x`) */ + function resolvePlayLayerImage(urls) { + for (let i = 0; i < urls.length; i++) { + const img = ensurePlayLayerImage(urls[i]); + if (!img.complete) return { status: 'pending' }; + if (img.naturalWidth > 0) return { status: 'ok', img }; + } + return { status: 'missing' }; + } + + function ensurePlayLayerProbesAllDirections(characterId) { + if (!characterId || playLayerAllDirsQueued[characterId]) return; + playLayerAllDirsQueued[characterId] = true; + ['up', 'down', 'left', 'right'].forEach((d) => ensurePlayLayerProbe(characterId, d)); + } + + function playCharAnyDirectionLayered(characterId) { + if (!characterId) return false; + return ['up', 'down', 'left', 'right'].some((d) => playLayerMode[characterId + '|' + d] === 'layered'); + } + + /** รอเฉพาะตอนยังไม่เจอ layered เลย — ถ้ามีทิศหนึ่ง layered แล้ว ไม่ต้องรอทิศอื่น */ + function playCharLayerDiscoveryPending(characterId) { + if (!characterId) return true; + const api = playCharLayerFromApi(characterId); + if (api === true || api === false) return false; + if (playCharAnyDirectionLayered(characterId)) return false; + return ['up', 'down', 'left', 'right'].some((d) => { + const m = playLayerMode[characterId + '|' + d]; + return m === undefined || m === 'pending'; + }); + } + + function ensurePlayLayerProbe(characterId, dir) { + const key = characterId + '|' + dir; + if (playLayerMode[key] !== undefined && playLayerMode[key] !== 'pending') return; + if (playLayerMode[key] === 'pending') return; + playLayerMode[key] = 'pending'; + const cands = layerUrlCandidates(characterId, dir, 'bodyColor', 0); + let ci = 0; + function tryNextProbe() { + if (playLayerMode[key] !== 'pending') return; + if (ci >= cands.length) { + playLayerMode[key] = 'none'; + return; + } + const url = cands[ci++]; + const img = ensurePlayLayerImage(url); + const fin = () => { + if (playLayerMode[key] !== 'pending') return; + if (img.naturalWidth > 0) { + playLayerMode[key] = 'layered'; + return; + } + tryNextProbe(); + }; + img.onload = fin; + img.onerror = fin; + if (img.complete) fin(); + } + tryNextProbe(); + } + + function getCharacterAnimFrameIndex(id, dir, now, isWalking) { + if (!id) return 0; + const key = id + '_' + dir; + const anim = characterAnimations[key]; + if (!anim) return 0; + const phase = walkAnimPhaseIndex(now, isWalking); + const fi = pickLoadedWalkFrameIndex(anim, phase); + return fi >= 0 ? fi : 0; + } + + function tryComposePlayLayersFromFiles(rawImg, id, dir, frameIndex, tint, opts) { + const useIdle = opts && opts.idle; + if (!rawImg || !rawImg.naturalWidth || !rawImg.naturalHeight) return null; + const w = rawImg.naturalWidth, h = rawImg.naturalHeight; + const c = document.createElement('canvas'); + c.width = w; + c.height = h; + const cctx = c.getContext('2d'); + let anyTintColorLayer = false; + let skippedShadowWhilePending = false; + const diskLayers = playCharDiskLayersSet(id); + /* ยืน (opts.idle): ลอง compose จาก *_idle_*_layer_* เหมือนหน้า character upload — รวม char-* ที่อัปโหลด idle เลเยอร์; ถ้าไม่มีไฟล์จะ missing → ไม่มีเลเยอร์ย้อม → getPlayTintedAvatarSource fallback ไป walk compose */ + const idleLayerStripes = !!useIdle; + const resDir = dir; + const resFi = frameIndex; + for (let li = 0; li < PLAY_LAYER_ORDER.length; li++) { + const layerName = PLAY_LAYER_ORDER[li]; + if (diskLayers && layerName !== 'shadow' && !diskLayers.has(layerName)) continue; + /* char-* หันหลัง (up): ไม่มี face ทิศนี้ — ข้ามกันตาหน้าซ้อนหลังหัว; ซ้าย/ขวา/หน้าใช้ face ตามทิศได้ */ + if (layerName === 'face' && isUploadedCharAssetId(id) && dir === 'up') { + continue; + } + let urls; + if (layerName === 'shadow') { + urls = idleLayerStripes + ? shadowUrlCandidatesIdle(id, dir, frameIndex) + : shadowUrlCandidates(id, resDir, resFi); + } else { + urls = idleLayerStripes + ? layerUrlCandidatesIdle(id, dir, layerName, frameIndex) + : layerUrlCandidates(id, resDir, layerName, resFi); + } + const r = resolvePlayLayerImage(urls); + /* เงาโหลดช้า/404 บ่อย — อย่าให้บล็อกทั้งคอมโพส (ไม่งั้นตกไป fallback แถบแนวตั้งแทนเลเยอร์จริง) */ + if (r.status === 'pending' && layerName === 'shadow') { + skippedShadowWhilePending = true; + continue; + } + if (r.status === 'pending') return null; + if (r.status === 'missing') continue; + const tintHex = PLAY_LAYER_TINT_KEY[layerName] ? tint[PLAY_LAYER_TINT_KEY[layerName]] : null; + if (PLAY_LAYER_TINT_KEY[layerName]) anyTintColorLayer = true; + drawPlayTintedLayer(cctx, w, h, r.img, tintHex); + } + /* ถ้าโหลดได้แต่ไม่มีเลเยอร์สีเลย จะเหลือแต่ stroke → ตัวขาว — ใช้ PNG รวมแทน */ + if (!anyTintColorLayer) return null; + return { canvas: c, skipCache: skippedShadowWhilePending }; + } + + function getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, isWalking, tint) { + if (!tint || !characterId || !rawImg) return rawImg; + ensurePlayCharLayerListFetch(); + ensurePlayLayerProbesAllDirections(characterId); + const dirn = dir || 'down'; + const frameIndexWalk = getCharacterAnimFrameIndex(characterId, dirn, timeMs, isWalking); + const idlePhase = idleAnimPhaseIndex(typeof timeMs === 'number' ? timeMs : Date.now()); + + if (playCharLayerDiscoveryPending(characterId)) return rawImg; + + const apiFlag = playCharLayerFromApi(characterId); + /* เหมือนเดิม: มีเลเยอร์แยกไฟล์เมื่อ API บอก hasLayerFiles หรือ probe เจอ — gen สีทำบน canvas จาก *_layer_* เท่านั้น */ + const charHasLayerFiles = apiFlag === true + || (apiFlag == null && playCharAnyDirectionLayered(characterId)); + + /* มี PNG idle รวม — ใช้เมื่อไม่มีระบบเลเยอร์สี (ถ้ามีเลเยอร์ต้อง compose ไม่ return raw ขาว) */ + if (!charHasLayerFiles && !isWalking && characterIdleSpritesVisible(characterId, dirn)) { + return rawImg; + } + const cacheKeyWalk = [characterId, dirn, frameIndexWalk, tint.head, tint.hair, tint.body, 'ct3'].join('|'); + + if (charHasLayerFiles) { + if (!isWalking) { + const composeFrame = isUploadedCharAssetId(characterId) ? 0 : idlePhase; + const cacheKeyIdle = [characterId, dirn, composeFrame, tint.head, tint.hair, tint.body, 'ct3', 'idleL'].join('|'); + const hitI = playLayerCompositeCache.get(cacheKeyIdle); + if (hitI) return hitI; + const packI = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, composeFrame, tint, { idle: true }); + if (packI && packI.canvas) { + if (!packI.skipCache) playLayerCompositeCache.set(cacheKeyIdle, packI.canvas); + return packI.canvas; + } + } + const hit = playLayerCompositeCache.get(cacheKeyWalk); + if (hit) return hit; + const pack = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, frameIndexWalk, tint, { idle: false }); + if (pack && pack.canvas) { + if (!pack.skipCache) playLayerCompositeCache.set(cacheKeyWalk, pack.canvas); + return pack.canvas; + } + return rawImg; + } + + /* + * ไม่มีไฟล์เลเยอร์ตาม API/probe — แสดง PNG รวม (ไม่ย้อมทั้งตัว: ไม่เหมือนระบบ gen สีแบบเลเยอร์เดิม) + */ + return rawImg; + } + + function getCharacterImg(id, direction) { + if (!id) return null; + const key = id + '_' + (direction || 'down'); + if (characterImages[key]) return characterImages[key]; + const img = new Image(); + img.src = BASE + '/img/characters/' + encodeURIComponent(id) + '_' + (direction || 'down') + '.png'; + characterImages[key] = img; + return img; + } + + function getCharacterFrame(id, direction, now, isWalking) { + if (!id) return null; + const dir = direction || 'down'; + const t = typeof now === 'number' ? now : Date.now(); + if (!isWalking) { + const idleAnim = ensureCharacterIdleAnim(id, dir); + let idlePhase = idleAnimPhaseIndex(t); + if (isUploadedCharAssetId(id)) idlePhase = 0; + const idleFi = pickLoadedWalkFrameIndex(idleAnim, idlePhase); + if (idleFi >= 0) return idleAnim.frames[idleFi]; + const idfb = idleAnim.fallback; + if (idfb && idfb.complete && idfb.naturalWidth) return idfb; + } + const key = id + '_' + dir; + let anim = characterAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterAnimations[key] = anim; + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + encodeURIComponent(id) + '_' + dir + '_' + i + '.png'; + anim.frames.push(img); + } + anim.fallback = getCharacterImg(id, dir); + } + const phase = walkAnimPhaseIndex(now, isWalking); + const fi = pickLoadedWalkFrameIndex(anim, phase); + if (fi >= 0) return anim.frames[fi]; + const fb = anim.fallback; + if (fb && fb.complete && fb.naturalWidth) return fb; + return null; + } + + function getAvatarImg(characterId, direction, now, isWalking) { + const img = characterId ? getCharacterFrame(characterId, direction, now, isWalking) : null; + if (img) return img; + return defaultAvatarImg; + } + + /** composite idle ลงหน้า — เดียวกับ #room-lobby-profile-avatar ในล็อบบี้ */ + const LOBBY_IDLE_DOWN_LS = 'jdCharLobbyIdleDown:'; + const PLAY_LOBBY_AVATAR_SS_PREFIX = 'justicePlayLobbyAvatar:'; + const playLobbyIdleAvatarCache = new Map(); + + function playLobbyIdleAvatarSrcForCharacter(characterId) { + const id = String(characterId || '').trim(); + if (!id) return ''; + try { + const ss = sessionStorage.getItem(PLAY_LOBBY_AVATAR_SS_PREFIX + id) || ''; + if (ss && ss.indexOf('data:image/') === 0) return ss; + const ls = localStorage.getItem(LOBBY_IDLE_DOWN_LS + id) || ''; + if (ls && ls.indexOf('data:image/') === 0) return ls; + } catch (e) { /* ignore */ } + return ''; + } + + function ensurePlayLobbyIdleAvatarImage(characterId) { + const id = String(characterId || '').trim(); + if (!id) return; + const hit = playLobbyIdleAvatarCache.get(id); + if (hit && hit.complete && hit.naturalWidth) return; + if (playLobbyIdleAvatarCache.get(id + '_loading')) return; + const src = playLobbyIdleAvatarSrcForCharacter(id); + if (!src) return; + const img = new Image(); + img.onload = function () { + playLobbyIdleAvatarCache.set(id, img); + playLobbyIdleAvatarCache.delete(id + '_loading'); + }; + img.onerror = function () { + playLobbyIdleAvatarCache.delete(id + '_loading'); + }; + img.src = src; + playLobbyIdleAvatarCache.set(id + '_loading', img); + } + + function getPlayLobbyIdleAvatarImageSync(characterId) { + const id = String(characterId || '').trim(); + if (!id) return null; + const hit = playLobbyIdleAvatarCache.get(id); + if (hit && hit.complete && hit.naturalWidth) return hit; + ensurePlayLobbyIdleAvatarImage(id); + return null; + } + + /** วาดตัวละคร — ทิศลง + useLobbyIdleComposite: composite เดียวกับล็อบบี้ (เฉพาะผู้เล่นจริง ตอนยืน); ตอนเดินใช้ strip เดิน down_0..3 */ + function resolvePlayDrawCharSource(characterId, direction, now, isWalking, playTint, useLobbyIdleComposite) { + const dir = direction || 'down'; + const walk = !!isWalking; + if (dir === 'down' && useLobbyIdleComposite && !walk) { + const lobbyImg = getPlayLobbyIdleAvatarImageSync(characterId); + if (lobbyImg) return lobbyImg; + } + const rawImg = getAvatarImg(characterId, dir, now, walk); + if (!playTint || !characterId) return rawImg; + return getPlayTintedAvatarSource(rawImg, characterId, dir, now, walk, playTint) || rawImg; + } + + function preloadPlayLobbyIdleAvatarsForOccupants() { + const meCid = me.characterId || getPlayCharacterId(); + if (meCid) ensurePlayLobbyIdleAvatarImage(meCid); + others.forEach(function (o) { + if (o && o.characterId) ensurePlayLobbyIdleAvatarImage(o.characterId); + }); + } + + function playHideTopChrome() { + try { + return document.documentElement.classList.contains('play-hide-top-chrome'); + } catch (e) { + return true; + } + } + + function getStoredCharacterId() { + try { + const v = localStorage.getItem('gameCharacterId'); + if (v) return v; + } catch (e) { /* ignore */ } + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; + } + function getPlayCharacterId() { + if (forceDefaultCharacter) { + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; + } + return getStoredCharacterId(); + } + + /** บอทพรีวิว: เลือกรหัสคนละตัวจาก roster (ไม่ซ้ำผู้เล่นถ้ามีตัวเลือก) */ + function pickPreviewBotCharacterId(botSlotIndex) { + const idx = Math.max(0, Math.floor(Number(botSlotIndex)) || 0); + const roster = playCharacterIdRoster; + const humanId = String(getPlayCharacterId() || ''); + if (!roster.length) return humanId || LEGACY_PLACEHOLDER_CHARACTER_ID; + const alts = roster.filter((id) => id !== humanId); + const pool = alts.length ? alts : roster.slice(); + return pool[idx % pool.length] || humanId || roster[0]; + } + const MOVE_SPEED = 0.15; + /** quiz_carry — ค่าเริ่มเมื่อไม่มี override จาก Admin (รหัสฉากตรงกันใน quiz-settings.json) */ + const QUIZ_CARRY_WALK_SPEED_MULT = 1.42; + /** ค่าที่ใช้จริงต่อเฟรม — อัปเดตจาก /api/quiz-settings + join snap */ + let quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT; + const PATH_ARRIVE_THRESH = 0.15; + + function clampQuizCarryWalkSpeedMultClient(n, def) { + const v = Number(n); + if (!Number.isFinite(v)) return def; + return Math.round(Math.max(0.5, Math.min(3, v)) * 100) / 100; + } + + function resolveQuizCarryWalkSpeedMultFromSettingsObj(s) { + const base = QUIZ_CARRY_WALK_SPEED_MULT; + if (!s || typeof s !== 'object') return base; + const forId = String(s.carryWalkSpeedMultForMapId ?? '').trim(); + const mult = Number(s.carryWalkSpeedMult); + const mapId = (currentPlayMapId() || '').trim(); + if (forId && mapId && forId === mapId && Number.isFinite(mult)) { + return clampQuizCarryWalkSpeedMultClient(mult, base); + } + return base; + } + + function applyQuizCarryWalkSpeedFromSettingsObj(s) { + if (!isQuizCarry()) { + quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT; + return; + } + quizCarryWalkSpeedMultActive = resolveQuizCarryWalkSpeedMultFromSettingsObj(s); + } + + function moveSpeedTilesThisFrameForWalk() { + return MOVE_SPEED * (isQuizCarry() ? quizCarryWalkSpeedMultActive : 1); + } + + function isMovementKey(code) { + return ['KeyW','KeyA','KeyS','KeyD','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].indexOf(code) !== -1; + } + function isChatFocused() { + return false; + } + let zoom = 1.4; + let froggerScore = 0; + let lastFroggerKey = 0; + let gauntletObstacles = []; + /** อินเทอร์โพเลตการวาด obstacle ระหว่างแพ็กเก็ต sync (~220ms) */ + let gauntletObsRenderPrev = []; + let gauntletObsRenderNext = []; + let gauntletObsBlendT0 = 0; + let meGauntletJumpTicks = 0; + /** ค่าที่ใช้วาดการยกตัว (เลอร์ปจาก meGauntletJumpTicks ให้โค้งกระโดดไม่กระตุก) */ + let meGauntletJumpVis = 0; + /** ซิงก์จากเซิร์ฟเวอร์ (gauntlet-sync / GET /api/game-timing) */ + let gauntletRuntimeTickMs = 220; + let gauntletRuntimeJumpTicks = 16; + /** Stack: รอบสวิงต่อวินาที — จาก GET /api/game-timing · ค่าน้อย = สวิงช้า */ + let playStackSwingHz = 0.55; + /** Stack: ความกว้างบล็อก (tile) — null = คำนวณจากโซนลงบนแผนที่ */ + let playStackBlockWidthTiles = null; + /** ภารกิจ Tower (mnn93hpi): จำกัดเวลารอบ (วินาที) — จาก game-timing.json */ + let playStackTowerMissionTimeSec = 90; + /** ภารกิจ Tower: ทีมพลาดได้กี่ครั้งก่อนเกมจบ — จาก game-timing.json */ + let playStackTeamMissesMax = 3; + /** ภารกิจ Tower: ชั้นสำเร็จกี่ชั้น (ฐาน) = Progress 100% — แต่ละชั้น +100/N %; คอมโบ ×2 ของชั้นนั้น */ + let playStackTowerProgressBlocks = 50; + /** รูปบล็อก Stack ต่อที่นั่ง 1–6 (game-timing) — ว่าง = วาดสีเดิม */ + let playStackBlockNormalUrls = ['', '', '', '', '', '']; + let playStackBlockHeavyUrls = ['', '', '', '', '', '']; + /** 0–100: โอกาสใช้สไปรต์ “ใหญ่” ต่อการปล่อย (ถ้ามี URL ใหญ่ช่องนั้น) */ + let playStackHeavyBlockPercent = 35; + /** กระโดดให้รอด: ความสูงกระโดด = ทวีคูณ × ความสูงตัว (ch×tile) — จาก GET /api/game-timing */ + let playJumpSurviveJumpHeightMult = 1.5; + /** จำกัดเวลารอบภารกิจกระโดดขึ้นแท่น (วินาที) — จาก GET /api/game-timing; 0 = ใช้ค่าเริ่ม 60 ในเกม */ + let playJumpSurviveMissionTimeSec = 0; + /** รูปแพลตฟอร์ม jump_survive ช่อง 1–3 จาก game-timing — url ว่าง = วาด cyan; w/h 0 = ใช้ขนาดไทล์ */ + let playJumpSurvivePlatformTiles = [ + { url: '', w: 0, h: 0 }, + { url: '', w: 0, h: 0 }, + { url: '', w: 0, h: 0 }, + ]; + /** ยิงยานอวกาศ (space_shooter): เวลารอบจาก game-timing; 0 = ใช้ค่าเริ่ม 90 ในเกมถ้าแมปไม่ทับ */ + let playSpaceShooterMissionTimeSec = 0; + /** รูปยานช่อง 1–6 จาก game-timing (spawn slot); ว่าง = วาดยานเวกเตอร์ */ + let playSpaceShooterShipImageUrls = ['', '', '', '', '', '']; + /** อุกาบาต: [0]=ตก, [1..]=แตก — จาก game-timing */ + let playSpaceShooterAsteroidSpriteUrls = []; + let playSpaceShooterAsteroidExplodeFrameMs = 70; + /** ระยะห่างเกิดอุกาบาต (ms) จาก game-timing — แมป spaceShooterAsteroidIntervalMs ≥200 ทับ */ + let playSpaceShooterAsteroidIntervalMs = 1040; + /** ทับยานเมื่อชนอุกาบาต ครั้งที่ 1–3 (PNG โปร่ง) — ภารกิจ Violent Crime */ + let playSpaceShooterShipDamageOverlayUrls = ['', '', '']; + /** balloon_boss / Mega Virus — จาก game-timing */ + let playBalloonBossMissionTimeSec = 0; + let playBalloonBossBossImageUrl = ''; + let playBalloonBossPlayerBalloonImageUrls = ['', '', '', '', '', '']; + let playBalloonBossPlayerBalloonFallbackUrl = ''; + /** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync (พรมแดง Gauntlet เท่านั้น) */ + let gauntletRuntimeTimeLimitSec = 0; + /** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */ + let gauntletEndsAtMs = null; + /** Crown (mno9kb07): เซิร์ฟยังไม่ปล่อยรัน — หยุด tick / เวลา */ + let gauntletCrownRunHeldRemote = false; + /** null | 'howto' | 'countdown' | 'live' — พรีรันก่อน GO */ + let gauntletCrownPregamePhase = null; + let gauntletCrownLobbyReadyMap = {}; + let gauntletCrownCountdownTimer = null; + let lastGauntletJumpKey = 0; + /** รูป lane: หลาย URL สุ่มตาม id คงที่ · laser: แยกบน/ล่าง/เส้น + สี/ความหนา */ + let gauntletLaneImageUrls = []; + let gauntletLaserTopUrl = ''; + let gauntletLaserBottomUrl = ''; + let gauntletLaserLineUrl = ''; + let gauntletLaserFillColor = 'rgba(140,230,255,0.42)'; + let gauntletLaserStrokeColor = 'rgba(255,220,255,0.9)'; + let gauntletLaserLineWidthPx = 2; + const gauntletAssetImageCache = new Map(); + + function encodeSpacesInUrlPath(t) { + const s = String(t || ''); + const q = s.indexOf('?'); + const pathPart = q === -1 ? s : s.slice(0, q); + const query = q === -1 ? '' : s.slice(q); + return pathPart.replace(/ /g, '%20') + query; + } + + function decodeAssetUrlPercentRuns(t) { + let s = String(t || ''); + const q = s.indexOf('?'); + let base = q === -1 ? s : s.slice(0, q); + const query = q === -1 ? '' : s.slice(q); + for (let i = 0; i < 2; i += 1) { + try { + const d = decodeURIComponent(base); + if (d === base) break; + base = d; + } catch (e) { + break; + } + } + return base + query; + } + + function normalizeGameAssetUrlForWebPlay(t) { + let s = String(t || ''); + s = s.replace(/^\/Game\/public\/img\//i, '/Game/img/'); + s = s.replace(/^Game\/public\/img\//i, 'Game/img/'); + return s; + } + + /** แปลง URL จาก Admin/game-timing ให้โหลดได้ (nginx เสิร์ฟจาก /Game/...) */ + function normalizeGauntletAssetUrlForPlay(u) { + if (typeof u !== 'string') return ''; + let raw = normalizeGameAssetUrlForWebPlay(decodeAssetUrlPercentRuns(u.trim())); + const bare0 = raw.split('?')[0].replace(/\/+$/, ''); + if (/^\/Game\/img\/MegaVirus\/Artboard$/i.test(bare0) || /^Game\/img\/MegaVirus\/Artboard$/i.test(bare0)) { + raw = '/Game/img/MegaVirus/Artboard%209.png'; + } + if (!raw) return ''; + if (/^https?:\/\//i.test(raw)) { + const z = raw.replace(/\/+$/, ''); + return z ? encodeSpacesInUrlPath(z) : ''; + } + const qIdx = raw.indexOf('?'); + const base = (qIdx >= 0 ? raw.slice(0, qIdx) : raw).trim().replace(/\/+$/, ''); + const qs = qIdx >= 0 ? raw.slice(qIdx) : ''; + if (!base) return ''; + if (/\.{4,}/.test(base)) return ''; + if (base.startsWith('/')) return encodeSpacesInUrlPath(base + qs); + if (/^Game\//i.test(base)) return encodeSpacesInUrlPath('/' + base.replace(/^\/+/, '') + qs); + return encodeSpacesInUrlPath(base + qs); + } + + function ensureGauntletAssetImage(url) { + const u = normalizeGauntletAssetUrlForPlay(typeof url === 'string' ? url : ''); + if (!u) return null; + let rec = gauntletAssetImageCache.get(u); + if (rec) return rec; + rec = { img: new Image(), ready: false }; + if (/^https?:\/\//i.test(u)) { + try { rec.img.crossOrigin = 'anonymous'; } catch (e) { /* ignore */ } + } + rec.img.onload = function () { rec.ready = true; }; + rec.img.onerror = function () { rec.ready = false; }; + rec.img.src = u; + /** รูปจากแคชบางครั้ง complete ก่อน onload — ต้องเซ็ต ready ทันทีไม่งั้นวาดลูกโป่งไม่ขึ้น */ + if (rec.img.complete && rec.img.naturalWidth > 0) rec.ready = true; + else if (rec.img.complete && rec.img.naturalWidth <= 0) rec.ready = false; + gauntletAssetImageCache.set(u, rec); + return rec; + } + + /** ลูกโป่ง Mega Virus: ถ้ามี URL ต่อที่นั่งแต่โหลดไม่สำเร็จ (404) ให้ใช้ fallback แทน · Per-seat URL wins only when image loads */ + function resolveBalloonBossBalloonSpriteRec(perSeatRaw, fallbackRaw) { + const per = normalizeGauntletAssetUrlForPlay(String(perSeatRaw || '').trim()); + const fb = normalizeGauntletAssetUrlForPlay(String(fallbackRaw || '').trim()); + if (per) { + const rPer = ensureGauntletAssetImage(per); + const img = rPer && rPer.img; + if (img && img.complete && img.naturalWidth > 0) return rPer; + if (img && img.complete && img.naturalWidth <= 0 && fb) return ensureGauntletAssetImage(fb); + return rPer; + } + if (fb) return ensureGauntletAssetImage(fb); + return null; + } + + function pickGauntletLaneImageRec(obsId) { + if (!gauntletLaneImageUrls.length) return null; + const n = gauntletLaneImageUrls.length; + const idx = Math.abs(Number(obsId) | 0) % n; + const u = gauntletLaneImageUrls[idx]; + return u ? ensureGauntletAssetImage(u) : null; + } + + function clampClientLaserColor(s, def) { + const t = String(s || '').trim().slice(0, 100); + if (!t || /[<>"'`]/.test(t)) return def; + return t; + } + + let playQuizPhaseLocal = null; + let playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + let playQuizText = ''; + let playQuizPhaseEndsAt = 0; + let playQuizTimerInterval = null; + /** เกมถูก/ผิด + ภารกิจ mng8a80o: เลขข้อจากเซิร์ฟเวอร์ (1-based) / จำนวนข้อในรอบ — ใช้บรรทัดเล็กเหนือคำถามบนแผนที่ */ + let playQuizQuestionIndex = 0; + let playQuizQuestionTotal = 0; + /** Preview-only: real question pool + phased timer (matches server quiz-settings / map quizQuestions). */ + let previewQuizPool = []; + /** ดัชนีข้อในรอบพรีวิว (0..len-1) — หลังสับแล้วตัดตาม quizRoundQuestionCount เหมือนเซิร์ฟเวอร์ */ + let previewQuizQIndex = 0; + let previewQuizTiming = { readMs: 10000, answerMs: 5000, betweenMs: 3500 }; + let previewQuizStep = 'read'; + let previewQuizCurrent = null; + let playLiveQuizScores = {}; + /** เกมถูก/ผิด (multiplayer): ใครเคยตอบผิดในเซสชันนี้ — ใช้โชว์แสตมป์ในสรุปภารกิจ mng8a80o */ + let playQuizEverWrong = {}; + /** ผีตามตัวเมื่อตอบผิด — ใช้เฉพาะแมป mng8a80o (เป็นสัญลักษณ์/บทลงโทษเชิงสนุก) */ + const QUIZ_WRONG_GHOST_URL = BASE + '/img/QUESTION/ghost/ghost-front.png'; + let quizWrongGhostImg = null; + function getQuizWrongGhostImg() { + if (quizWrongGhostImg) return quizWrongGhostImg; + const img = new Image(); + img.decoding = 'async'; + img.src = QUIZ_WRONG_GHOST_URL; + quizWrongGhostImg = img; + return img; + } + /** ตรวจว่า peer/me ตอบผิดในแมป mng8a80o แล้ว → คุมผีแทน avatar */ + function isQuizWrongGhostActiveForEnt(entId) { + if (!isQuizQuestionMissionUiMapPlay()) return false; + const sid = String(entId == null ? '' : entId); + const isMeEnt = (myId != null && sid === String(myId)); + return isMeEnt + ? !!(playQuizPlayerLocal && (playQuizPlayerLocal.cannotTrue || playQuizPlayerLocal.cannotFalse)) + : !!playQuizEverWrong[sid]; + } + + /** วาด "ผี" แทน avatar ทั้งตัวในแมป mng8a80o เมื่อตอบผิด — คุมผีเลย + * args ทุกตัวเป็น "screen pixels" แล้ว (คำนวณจาก worldToScreen ที่ caller) + * - sxCenter = พิกัด X กึ่งกลาง avatar บนจอ (px) + * - syBottom = พิกัด Y เท้า avatar บนจอ (px) + * - heightPx = ความสูง avatar บนจอ (px) ใช้คุมขนาดผีให้พอดี + */ + function drawQuizWrongGhostOverlayPlay(sxCenter, syBottom, heightPx, entId, nameLabel) { + const img = getQuizWrongGhostImg(); + if (!img || !img.complete || !img.naturalWidth) return; + /* ผีสูงเท่า avatar ตามจริง — คงสัดส่วนภาพ */ + const hPx = Math.max(24, Number(heightPx) || 0); + const ratio = img.naturalWidth / Math.max(1, img.naturalHeight); + const wPx = hPx * ratio; + const sid = String(entId == null ? '' : entId); + const tBob = Math.sin(performance.now() / 360 + (sid ? sid.charCodeAt(0) || 0 : 0)) * (hPx * 0.04); + /* วาดให้ "เท้าผี" ตรงกับเท้า avatar */ + const drawX = sxCenter - wPx / 2; + const drawY = syBottom - hPx + tBob; + ctx.save(); + ctx.globalAlpha = 0.98; + ctx.drawImage(img, drawX, drawY, wPx, hPx); + ctx.restore(); + /* ชื่อใต้ผี (ถ้ามี — เพื่อระบุว่าเป็นใคร) */ + if (nameLabel) { + ctx.save(); + ctx.font = '600 12px system-ui, "Segoe UI", "Kanit", sans-serif'; + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(255,255,255,0.95)'; + ctx.strokeStyle = 'rgba(0,0,0,0.85)'; + ctx.lineWidth = 3; + const ny = syBottom + 14; + ctx.strokeText(nameLabel, sxCenter, ny); + ctx.fillText(nameLabel, sxCenter, ny); + ctx.restore(); + } + } + /** ช่วยแปลง tile coords → screen pos สำหรับ ghost (ใช้เฉพาะที่ caller ใน render scope) + * รับ helpers ที่เป็น scope-local (worldToScreen, zDraw) เพราะถูกเรียกในรอบ draw เท่านั้น */ + function _quizGhostScreenAnchorPlay(axTile, ayTile, w2s, zDrawIn) { + const md = mapData; + const { cw, ch } = getCharacterFootprintWH(md); + const cxWorld = (axTile + cw * 0.5) * tileSize; + const cyBottomWorld = (ayTile + ch) * tileSize; + const [sx, sy] = w2s(cxWorld, cyBottomWorld); + /* ความสูง = สูง avatar บนจอ (สอดคล้องกับ drawAvatar: boxH = tileSize * zDraw * ch * 1.35) */ + const sizeH = tileSize * zDrawIn * Math.max(ch, 1) * 1.35; + return { sx, sy, sizeH }; + } + /** เกมถูก/ผิด — ตรงกับ server QUIZ_TF_POINTS_PER_CORRECT */ + const QUIZ_TF_POINTS_PER_CORRECT = 10; + const QUIZ_TF_SCORE_PLUS_URL = BASE + '/img/QUESTION/score+.png'; + const QUIZ_TF_SCORE_POPUP_MS = 1700; + /** ขนาดป๊อปอัป score+ เทียบกับ tile — เดิม ~1.35×tile; +100% = 2× → 2.7×tile */ + const QUIZ_TF_SCORE_POPUP_TILE_MULT = 2.7; + const QUIZ_TF_SCORE_POPUP_MIN_BASE_PX = 112; + let quizTfScorePopups = []; + let quizTfScorePlusImg = null; + /** คะแนนต่อคำตอบที่ถูกเมื่อส่งป้ายที่ฮับ/โซนส่ง — quiz_carry เท่านั้น */ + const QUIZ_CARRY_POINTS_PER_CORRECT = 10; + /** ครบเซสชัน (แมปสรุปภารกิจ): โชว์ result-complete / gameover กี่ ms ก่อนแผงสรุปผล */ + const QUIZ_CARRY_SESSION_END_SPLASH_MS = 3000; + /** quiz_carry: หยิบตัวเลือกมาวางโซนกลาง — เล่นพร้อมกัน (ไม่สลับตา) */ + let quizCarryPool = []; + let quizCarryCurrent = null; + /** editor embed + preview: นับ 3–2–1 ก่อนโชว์คำถามและให้เดิน/หยิบได้ */ + let quizCarryEmbedPendingQuestion = null; + let quizCarryEmbedCountdownStartAt = 0; + let quizCarryEmbedCountdownEndAt = 0; + /** editor embed: นับ 3–2–1 รอบสองหลังโชว์คำถาม ก่อนเปิดป้ายตัวเลือกบนแมป */ + let quizCarryEmbedPreOptionCountdownStartAt = 0; + let quizCarryEmbedPreOptionCountdownEndAt = 0; + /** quiz_carry: epoch ให้หยิบตัวเลือกได้ · epoch ปิดรอบตอบ (ตั้งที่ Admin แท็บหยิบมาวาง) */ + let quizCarryOptionRevealAt = 0; + let quizCarryAnswerCloseAt = 0; + /** หมดเวลาตอบรอบนี้แล้ว — โชว์ TIME'S UP ~3s แล้วเรียก quizCarryAfterRoundResolved */ + let quizCarryAnswerTimeupAwaitNext = false; + let quizCarryAnswerRoundTimeupAutoT = null; + let quizCarryAnswerRoundTimeupAdvanceAt = 0; + let quizCarryCarryTimingMs = { carryReadMs: 3000, carryAnswerMs: 5000 }; + /** พรีวิว (รวม editor embed): จำนวนข้อที่เล่นก่อนโอเวอร์เลย์จบ — 0 = ไม่จบอัตโนมัติ */ + let quizCarrySessionLength = 0; + let quizCarryRoundsCompleted = 0; + let quizCarrySessionEnded = false; + /** หลัง timeup บนโต๊ะ (embed): รอ 5s แล้วโชว์ result-complete / result-gameover */ + let quizCarryResultEndTimer = null; + /** หลังโชว์ result-end (embed preview): รอ 5s แล้วกลับ room-lobby — ใช้ร่วม quiz_carry + crown */ + let embedPreviewLobbyReturnTimer = null; + /** ครบเซสชัน (mission summary แมป): รอ splash แล้วเปิด #quiz-carry-mission-overlay */ + let quizCarrySessionCompleteResultToSummaryT = null; + /** embed พรีวิว: รอ Ready / START ของโฮสต์ก่อนนับ 3–2–1 */ + let quizCarryPregameActive = false; + let playHostId = null; + /** ซิงก์จากเซิร์ฟเวอร์: socketId → กด Ready แล้ว */ + let quizCarryLobbyReadyMap = {}; + /** จาก /api/quiz-settings carryMapPanelTheme — ใช้กับ #quiz-map-question-panel เฉพาะ quiz_carry */ + let quizCarryMapPanelTheme = null; + /** จาก /api/quiz-settings quizMapPanelTheme — แผงคำถามบนแผนที่ เกมถูก/ผิด (ไม่ใช่ quiz_carry) */ + let quizMapPanelTheme = null; + /** จาก /api/quiz-settings carryEmbedCountdownTheme — สี/ขนาด overlay นับ 3-2-1 (embed) */ + let quizCarryEmbedCountdownTheme = null; + /** จาก /api/quiz-settings carryChoicePlaqueThemes — ป้าย canvas ต่อช่องตัวเลือก 0..15 (null = ใช้ค่าเริ่มต้นทุกช่อง) */ + let quizCarryChoicePlaqueThemes = null; + /** จาก carryChoicePlaqueMapScale — ขยายป้ายบนแมป + ฟอนต์ (0.85–2.5) */ + let quizCarryPlaqueMapScale = 1.25; + const quizCarryChoiceImageCache = new Map(); + /** สแนปจาก join-space (Node) — ใช้เมื่อ fetch HTTP quiz-settings ไม่ได้หรือไม่มีธีม */ + let quizCarryJoinSettingsSnap = null; + + /** quiz_carry — โซนตัวเลือกบนแมป / ธีมป้ายต่อช่องสูงสุด 16 */ + const QUIZ_CARRY_MAX_OPTION_SLOTS = 16; + + /** jump_survive — กล้องตามตัวในกรอบ + แพลตฟอร์มเลื่อนลง (scroll เพิ่ม world Y ของแพลตฟอร์ม) ไม่ลากทั้งฉาก */ + let jumpSurviveCamCenterX = 0; + let jumpSurviveCamCenterY = 0; + let jumpSurviveEliminated = false; + /** จับเวลาโหมดกระโดดให้รอด (วินาทีบน HUD) */ + let jumpSurviveSessionStartMs = 0; + let jumpSurvivePlatformScrollPx = 0; + let jumpSurviveVy = 0; + let jumpSurviveLastTickMs = 0; + let jumpSurviveJumpQueued = false; + let jumpSurviveOnGround = false; + let jumpSurviveLastEmitT = 0; + /** mnptfts2: howto → countdown → live → ended (คะแนน 100 ผู้รอดสุดท้าย) */ + let jumpSurviveMissionPhase = null; + let jumpSurviveGameEnded = false; + let jumperMissionCountdownTimer = null; + + /** space_shooter — ยิงหิน (จุดเกิดจากกริด P1–P6) */ + let spaceShooterBullets = []; + let spaceShooterAsteroids = []; + /** { x, y, r, fi, acc } — fi = index เฟรมแตก (0 = urls[1]) */ + let spaceShooterAsteroidExplosions = []; + let spaceShooterPopups = []; + let spaceShooterLastTickMs = 0; + let spaceShooterSpawnAccMs = 0; + let spaceShooterFireCd = 0; + let spaceShooterSessionStartMs = 0; + let spaceShooterLastMoveEmit = 0; + let spaceShooterGameEnded = false; + let spaceShooterMissionPhase = null; + let spaceShooterMissionCountdownTimer = null; + + /** mng8a80o: howto → countdown → live → ended (สรุปแบบ crown mission) */ + let quizQuestionMissionPhase = null; + let quizQuestionMissionCountdownTimer = null; + let quizQuestionMissionDeferredPhase = null; + /** mng8a80o live: virtual joystick (+x right, +y down) normalized ~[-1,1] */ + let quizQuestionMissionJoyVecX = 0; + let quizQuestionMissionJoyVecY = 0; + let quizQuestionMissionJoyPointerId = null; + /** Stack Tower (mnn93hpi) — flow เดียวกับ crown / quiz mission */ + let stackTowerMissionPhase = null; + let stackTowerMissionCountdownTimer = null; + let stackTowerMissionDeferredPhase = null; + let stackTowerSessionStartAt = 0; + let stackTowerMissionEndedOnce = false; + /** progress ข้ามเกณฑ์ Tower: เริ่มแอนิเมตซูม+เลื่อนแมป — null ยังไม่ข้าม, ตัวเลข = performance.now() ตอนเริ่ม */ + let stackTowerPost50AnimStartMs = null; + /** แฟลชรูปผล Tower (timeup / gameover / complete) ก่อน GCM 5 วิ */ + let stackTowerResultFlashTimer = null; + const STACK_TOWER_RESULT_FLASH_MS = 5000; + + /** รูปอุกาบาตขณะตก: 0 = เต็ม HP (Meteo-1); 1 = โดนยิงแล้วแต่ยังมี HP (Meteo-2 เมื่อมี URL ≥3) */ + function spaceShooterAsteroidLiveSpriteIndexPlay(a) { + const urls = playSpaceShooterAsteroidSpriteUrls; + if (!urls || !urls.length) return 0; + const maxHp = Math.max(1, Math.floor(Number(a && a.maxHp)) || SPACE_SHOOTER_ASTEROID_MAX_HP); + const hp = Math.max(0, Math.floor(Number(a && a.hp)) || maxHp); + if (hp >= maxHp) return 0; + if (urls.length >= 3) return 1; + return 0; + } + + /** เริ่มแอนิเมชันแตกที่เฟรมถัดจาก «โดนแล้ว» เพื่อไม่ซ้ำ Meteo-2 — แสดง Meteo-3 แล้วจบตาม maxFi */ + function spaceShooterAsteroidExplosionStartFiPlay() { + const urls = playSpaceShooterAsteroidSpriteUrls; + if (!urls || urls.length < 2) return 0; + return urls.length >= 3 ? 1 : 0; + } + + function spaceShooterSpawnAsteroidExplosion(worldX, worldY, r) { + const urls = playSpaceShooterAsteroidSpriteUrls; + if (!urls || urls.length < 2) return; + spaceShooterAsteroidExplosions.push({ + x: worldX, + y: worldY, + r: Math.max(6, Number(r) || 14), + fi: spaceShooterAsteroidExplosionStartFiPlay(), + acc: 0, + }); + } + + function spaceShooterTickAsteroidExplosions(dt) { + const urls = playSpaceShooterAsteroidSpriteUrls; + if (!urls || urls.length < 2) { + spaceShooterAsteroidExplosions.length = 0; + return; + } + const fms = Math.max(30, Math.min(500, Number(playSpaceShooterAsteroidExplodeFrameMs) || 70)); + const maxFi = urls.length - 2; + for (let i = spaceShooterAsteroidExplosions.length - 1; i >= 0; i--) { + const ex = spaceShooterAsteroidExplosions[i]; + ex.acc += dt * 1000; + while (ex.acc >= fms) { + ex.acc -= fms; + ex.fi++; + if (ex.fi > maxFi) { + spaceShooterAsteroidExplosions.splice(i, 1); + break; + } + } + } + } + + /** balloon_boss — ลูกโป้งยิงบอส (Mega Virus) */ + let balloonBossPendingShots = []; + let balloonBossPlayerBullets = []; + let balloonBossBossBullets = []; + let balloonBossSessionStartMs = 0; + let balloonBossLastTickMs = 0; + let balloonBossLastMoveEmit = 0; + let balloonBossPlayerFireCd = 0; + let balloonBossGameEnded = false; + let balloonBossHitFx = []; + /** Mega Virus — ป๊อปดาเมจต่อบอส (ข้อความ +2) เมื่อกระสุนโดนบอส */ + let balloonBossScorePopups = []; + let balloonBossBossFireAcc = 0; + /** quiz_battle — โดม MCQ กด E (ข้อจาก battleQuizMcq) */ + let quizBattleMcqPool = []; + let quizBattleAnsweredComps = new Set(); + let quizBattleModalCompId = null; + const SPACE_SHOOTER_SHIP_COLORS = ['#50fa7b', '#ff79c6', '#ff5555', '#f1fa8c', '#bd93f9', '#8be9fd']; + + document.getElementById('room-id').textContent = spaceId; + + function hidePlayQuizHud() { + const sb = document.getElementById('play-quiz-scoreboard'); + if (sb) sb.classList.add('is-hidden'); + const fb = document.getElementById('play-quiz-feedback'); + if (fb) { fb.classList.add('is-hidden'); fb.textContent = ''; } + } + + function renderPlayQuizScoreboard(scores) { + const wrap = document.getElementById('play-quiz-scoreboard'); + const ul = document.getElementById('play-quiz-scoreboard-list'); + if (!wrap || !ul || !mapData || (!isQuiz() && !isQuizCarry() && !isQuizBattle())) return; + if (!scores || typeof scores !== 'object') return; + wrap.classList.remove('is-hidden'); + ul.textContent = ''; + const merged = { ...scores }; + others.forEach((_, id) => { if (merged[id] == null) merged[id] = 0; }); + if (myId != null && merged[myId] == null) merged[myId] = 0; + const rows = []; + if (myId != null) { + rows.push({ id: myId, nick: me.nickname || 'คุณ', sc: merged[myId] != null ? merged[myId] : 0 }); + } + others.forEach((o, id) => { + rows.push({ id, nick: (o && o.nickname) ? String(o.nickname) : id, sc: merged[id] != null ? merged[id] : 0 }); + }); + rows.sort((a, b) => b.sc - a.sc || a.nick.localeCompare(b.nick, 'th')); + rows.forEach((row) => { + const li = document.createElement('li'); + if (row.id === myId) li.className = 'play-quiz-scoreboard-me'; + const spN = document.createElement('span'); + spN.className = 'play-quiz-scoreboard-name'; + spN.textContent = row.nick; + const spV = document.createElement('span'); + spV.className = 'play-quiz-scoreboard-val'; + spV.textContent = String(row.sc); + li.appendChild(spN); + li.appendChild(spV); + ul.appendChild(li); + }); + if (isQuizCarry() && useCyberPlayHud()) { + wrap.classList.add('is-hidden'); + } + /* แมป mng8a80o: คะแนนอยู่ที่ cyber SCORE ซ้ายแล้ว — ซ่อนกล่อง "คะแนน" มุมขวาไม่ให้ซ้ำกับ mock */ + if (isQuizQuestionMissionUiMapPlay()) { + wrap.classList.add('is-hidden'); + } + } + + function initPlayLiveQuizScoresZeros() { + if ((!isQuiz() && !isQuizCarry() && !isQuizBattle()) || myId == null) return; + playLiveQuizScores = {}; + playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + if (isQuiz()) playQuizEverWrong = {}; + renderPlayQuizScoreboard(playLiveQuizScores); + } + + function showPlayQuizFeedback(r) { + const el = document.getElementById('play-quiz-feedback'); + if (!el || !r || !r.results || myId == null) return; + let mine = null; + let botRight = 0, botWrong = 0; + for (let i = 0; i < r.results.length; i++) { + const row = r.results[i]; + if (row.id === myId) mine = row; + else if (isPreviewBotId(row.id)) { + if (row.right) botRight++; + else botWrong++; + } + } + if (!mine) return; + el.classList.remove('is-hidden'); + const botExtra = playBotsEnabled() && (botRight + botWrong > 0) + ? ' · บอท ถูก ' + botRight + ' / ผิด ' + botWrong + ' (Bots: ' + botRight + ' right / ' + botWrong + ' wrong)' + : ''; + if (mine.right) { + el.className = 'play-quiz-feedback play-quiz-feedback-ok'; + el.textContent = 'คุณตอบถูก · +' + QUIZ_TF_POINTS_PER_CORRECT + ' · คะแนนรวม ' + (typeof mine.score === 'number' ? mine.score : 0) + ' แต้ม' + botExtra; + } else { + el.className = 'play-quiz-feedback play-quiz-feedback-bad'; + el.textContent = (mine.choice == null + ? 'คุณไม่ได้ยืนในโซนตอบ — นับเป็นผิด' + : 'คุณตอบผิด — กลับจุดเกิด และเข้าโซนตอบไม่ได้อีก') + botExtra; + } + if (typeof window.__playQuizFeedbackT === 'number') clearTimeout(window.__playQuizFeedbackT); + window.__playQuizFeedbackT = setTimeout(() => { el.classList.add('is-hidden'); }, 4200); + } + + function ensureQuizTfScorePlusImgPlay() { + if (quizTfScorePlusImg && quizTfScorePlusImg.src) return; + quizTfScorePlusImg = new Image(); + quizTfScorePlusImg.src = QUIZ_TF_SCORE_PLUS_URL; + quizTfScorePlusImg.onerror = function () { + this.onerror = null; + }; + } + + /** โชว์ score+.png กลางโซนจริง/เท็จที่เป็นคำตอบข้อนี้ (world px) */ + function spawnQuizTrueFalseScorePopupPlay(correctTrue) { + if (!isQuiz() || !mapData) return; + const grid = correctTrue ? mapData.quizTrueArea : mapData.quizFalseArea; + const b = getTileBoundsForOnesGrid(grid); + if (!b) return; + const ts = tileSize || 32; + const wx = ((b.minX + b.maxX + 1) / 2) * ts; + const wy = ((b.minY + b.maxY + 1) / 2) * ts; + const now = Date.now(); + ensureQuizTfScorePlusImgPlay(); + quizTfScorePopups.push({ wx, wy, startMs: now, untilMs: now + QUIZ_TF_SCORE_POPUP_MS }); + } + + function drawQuizTfScorePopupsLayer(ctx, worldToScreen, zDraw, timeMs) { + if (!quizTfScorePopups.length) return; + const img = quizTfScorePlusImg; + if (!img || !img.complete || !img.naturalWidth) return; + const now = typeof timeMs === 'number' ? timeMs : Date.now(); + quizTfScorePopups = quizTfScorePopups.filter((p) => p.untilMs > now); + const baseW = Math.max(QUIZ_TF_SCORE_POPUP_MIN_BASE_PX, (tileSize || 32) * zDraw * QUIZ_TF_SCORE_POPUP_TILE_MULT); + for (let i = 0; i < quizTfScorePopups.length; i++) { + const p = quizTfScorePopups[i]; + const span = Math.max(1, p.untilMs - p.startMs); + const t = Math.max(0, Math.min(1, (now - p.startMs) / span)); + const [sx, sy] = worldToScreen(p.wx, p.wy); + const rise = t * 56 * zDraw; + const fade = t < 0.72 ? 1 : Math.max(0, 1 - (t - 0.72) / 0.28); + const pulse = 1 + Math.sin(t * Math.PI) * 0.1; + const scale = (baseW / img.naturalWidth) * pulse; + const w = img.naturalWidth * scale; + const h = img.naturalHeight * scale; + ctx.save(); + ctx.globalAlpha = fade; + ctx.drawImage(img, sx - w / 2, sy - h / 2 - rise, w, h); + ctx.restore(); + } + } + + function getTileBoundsForOnesGrid(grid) { + if (!grid || !grid.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (let yy = 0; yy < grid.length; yy++) { + const row = grid[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (Number(row[xx]) === 1) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + } + } + } + if (minX === Infinity) return null; + return { minX, minY, maxX, maxY }; + } + + function getQuizQuestionAreaTileBounds(md) { + if (!md || md.gameType !== 'quiz') return null; + return getTileBoundsForOnesGrid(md.quizQuestionArea); + } + + /** quiz_carry: โซนทองพิเศษสำหรับข้อความคำถาม (ถ้าไม่วาด ใช้โซนกลางม่วงแทนใน syncPlayQuizMapPanel) */ + function getQuizCarryQuestionAreaTileBounds(md) { + if (!md || md.gameType !== 'quiz_carry') return null; + return getTileBoundsForOnesGrid(md.quizQuestionArea); + } + + function clearQuizMapPanelThemeInline(panel, textEl) { + if (!panel || !textEl) return; + panel.classList.remove('quiz-map-question-panel--has-kicker'); + panel.classList.remove('quiz-map-question-panel--question-mission-live'); + const qKick = document.getElementById('quiz-map-question-kicker'); + if (qKick) { + qKick.textContent = ''; + qKick.classList.add('is-hidden'); + qKick.setAttribute('aria-hidden', 'true'); + } + panel.style.removeProperty('background-image'); + panel.style.removeProperty('background-size'); + panel.style.removeProperty('background-position'); + delete panel.dataset.qmPlaqueTry; + panel.style.removeProperty('--qmap-bg'); + panel.style.removeProperty('--qmap-border'); + panel.style.removeProperty('--qmap-border-w'); + panel.style.removeProperty('--qmap-shadow'); + panel.style.removeProperty('background'); + panel.style.removeProperty('border'); + panel.style.removeProperty('border-color'); + panel.style.removeProperty('border-width'); + panel.style.removeProperty('border-style'); + panel.style.removeProperty('box-shadow'); + textEl.style.removeProperty('--qmap-text'); + textEl.style.removeProperty('--qmap-text-shadow'); + textEl.style.removeProperty('color'); + textEl.style.removeProperty('text-shadow'); + textEl.style.removeProperty('font-size'); + textEl.style.removeProperty('line-height'); + textEl.style.removeProperty('width'); + textEl.style.removeProperty('box-sizing'); + textEl.style.removeProperty('max-height'); + textEl.style.removeProperty('overflow'); + } + + function clearQuizMapQuestionCarryFeedback() { + if (typeof window.__quizMapCarryFeedbackT === 'number') { + clearTimeout(window.__quizMapCarryFeedbackT); + window.__quizMapCarryFeedbackT = null; + } + const icon = document.getElementById('quiz-map-q-feedback-icon'); + const scoreWrap = document.getElementById('quiz-map-q-feedback-score-wrap'); + const scoreEl = document.getElementById('quiz-map-q-feedback-score'); + if (icon) { + icon.classList.add('is-hidden'); + icon.removeAttribute('src'); + } + if (scoreEl) scoreEl.classList.remove('quiz-map-q-feedback-score--pop'); + if (scoreWrap) { + scoreWrap.classList.add('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'true'); + } + } + + /** ไอคอนถูก/ผิดที่ขอบบนแผง + score+10 ตรงกลางใต้ข้อความ (เฉพาะถูก) — ตาม mock quiz_carry */ + function showQuizMapQuestionCarryFeedback(isCorrect) { + if (!isQuizCarry()) return; + const panel = document.getElementById('quiz-map-question-panel'); + const icon = document.getElementById('quiz-map-q-feedback-icon'); + const scoreWrap = document.getElementById('quiz-map-q-feedback-score-wrap'); + const scoreEl = document.getElementById('quiz-map-q-feedback-score'); + if (!panel || !icon || panel.classList.contains('is-hidden')) return; + clearQuizMapQuestionCarryFeedback(); + icon.src = isCorrect ? QUIZ_CARRY_MAP_FEEDBACK_IMG.correct : QUIZ_CARRY_MAP_FEEDBACK_IMG.incorrect; + icon.classList.remove('is-hidden'); + if (isCorrect && scoreWrap && scoreEl) { + scoreEl.src = QUIZ_CARRY_MAP_FEEDBACK_IMG.scorePlus; + scoreWrap.classList.remove('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'false'); + scoreEl.classList.remove('quiz-map-q-feedback-score--pop'); + void scoreEl.offsetWidth; + scoreEl.classList.add('quiz-map-q-feedback-score--pop'); + } else if (scoreWrap) { + scoreWrap.classList.add('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'true'); + } + window.__quizMapCarryFeedbackT = setTimeout(() => { + window.__quizMapCarryFeedbackT = null; + clearQuizMapQuestionCarryFeedback(); + }, QUIZ_MAP_CARRY_FEEDBACK_MS); + } + + /** ค่าเริ่มต้นแผงคำถามบนแผนที่ (ตรงกับ server defaultCarryMapPanelTheme) — ใช้เมื่อยังไม่โหลดจาก API */ + function defaultCarryMapPanelThemePlay() { + return { + panelBg: 'rgba(12, 14, 28, 0.88)', + panelBorder: 'rgba(255, 214, 102, 0.7)', + borderWidthPx: 2, + textColor: '#f1f5f9', + questionFontMinPx: 10, + questionFontMaxPx: 24, + }; + } + + /** ตรงกับ drawQuizCarryChoiceLabels — carryChoicePlaqueMapScale */ + function quizCarryPlaqueMapScaleClampedPlay() { + const sc = Number(quizCarryPlaqueMapScale); + if (!Number.isFinite(sc)) return 1.25; + return Math.max(0.85, Math.min(2.5, sc)); + } + + /** + * ขนาดตัวอักษรแผงคำถาม DOM ให้เท่าป้ายคำตอบบนพื้น: Math.max(10, Math.min(24, tileSize*zoom*0.24*ps)) + * แล้ว clamp ด้วย questionFontMinPx / questionFontMaxPx จากธีม (ถ้าต้องการใหญ่กว่าป้ายได้โดยยกเพดาน max) + */ + function quizMapQuestionFontPxLikeCarryPlaques(th, zoomVal) { + const ts = tileSize * zoomVal; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + const base = Math.max(10, Math.min(24, ts * 0.24 * ps)); + const fb = defaultCarryMapPanelThemePlay(); + let mn = Number(th && th.questionFontMinPx); + let mx = Number(th && th.questionFontMaxPx); + if (!Number.isFinite(mn)) mn = fb.questionFontMinPx; + if (!Number.isFinite(mx)) mx = fb.questionFontMaxPx; + mn = Math.round(Math.max(8, Math.min(40, mn))); + mx = Math.round(Math.max(10, Math.min(72, mx))); + if (mx < mn) { + const s = mn; + mn = mx; + mx = s; + } + return Math.max(mn, Math.min(mx, base)); + } + + /** + * ลด px ทีละ 1 จนเนื้อหาไม่ล้นกล่อง (ไม่ใช้ scrollbar) + * zoom out กล่องเตี้ยมาก: อนุญาตต่ำกว่า questionFontMinPx ของธีมได้ถึง hardMin (≥6) ไม่ให้บรรทัดถูกตัด + * เกมถูก/ผิด: แผงมีคลาส quiz-map-question-panel--true-false จัดข้อความกึ่งกลางแนวตั้งในโซน + */ + function fitQuizMapQuestionFontToPanel(panel, textEl, startPx, minPx) { + const cs = window.getComputedStyle(panel); + const padT = parseFloat(cs.paddingTop) || 0; + const padB = parseFloat(cs.paddingBottom) || 0; + const padL = parseFloat(cs.paddingLeft) || 0; + const padR = parseFloat(cs.paddingRight) || 0; + let kickerReserve = 0; + if (panel.classList.contains('quiz-map-question-panel--question-mission-live') + && panel.classList.contains('quiz-map-question-panel--has-kicker')) { + kickerReserve = 30; + } + const availH = Math.max(12, panel.clientHeight - padT - padB - kickerReserve); + const availW = Math.max(24, panel.clientWidth - padL - padR); + const themeMin = Math.max(8, Math.floor(Number(minPx) || 8)); + const hardMin = Math.min(themeMin, Math.max(6, Math.floor(availH / 7))); + let px = Math.round(Math.min(80, Math.max(hardMin, Number(startPx) || hardMin))); + textEl.style.setProperty('line-height', '1.28', 'important'); + textEl.style.setProperty('width', '100%', 'important'); + textEl.style.setProperty('box-sizing', 'border-box', 'important'); + textEl.style.removeProperty('max-height'); + textEl.style.setProperty('overflow', 'hidden', 'important'); + for (let i = 0; i < 96 && px > hardMin; i++) { + textEl.style.setProperty('font-size', String(px) + 'px', 'important'); + const sh = textEl.scrollHeight; + const sw = textEl.scrollWidth; + if (sh <= availH + 2 && sw <= availW + 2) break; + px -= 1; + } + textEl.style.setProperty('font-size', String(px) + 'px', 'important'); + textEl.style.setProperty('max-height', availH + 'px', 'important'); + } + + /** ธีมจาก quiz-settings / join API — ถ้า JSON ไม่มีฟอนต์ให้ใช้ค่าเริ่มต้น */ + function parseCarryMapPanelThemeObject(raw) { + let t = raw; + if (typeof t === 'string') { + try { + t = JSON.parse(t); + } catch (e) { + t = null; + } + } + if (!t || typeof t !== 'object') return null; + const bw = Number(t.borderWidthPx); + const borderWidthPx = Number.isFinite(bw) ? Math.max(0, Math.min(12, Math.round(bw))) : 2; + const fb = defaultCarryMapPanelThemePlay(); + let qMin = Number(t.questionFontMinPx); + let qMax = Number(t.questionFontMaxPx); + if (!Number.isFinite(qMin)) qMin = fb.questionFontMinPx; + if (!Number.isFinite(qMax)) qMax = fb.questionFontMaxPx; + qMin = Math.round(Math.max(10, Math.min(40, qMin))); + qMax = Math.round(Math.max(14, Math.min(56, qMax))); + if (qMax < qMin) { + const s = qMin; + qMin = qMax; + qMax = s; + } + return { + panelBg: String(t.panelBg || '').trim().slice(0, 120), + panelBorder: String(t.panelBorder || '').trim().slice(0, 120), + borderWidthPx, + textColor: String(t.textColor || '').trim().slice(0, 120), + questionFontMinPx: qMin, + questionFontMaxPx: qMax, + }; + } + + /** + * ชั้นทับจาก carryMapPanelTheme ในไฟล์แมป — ฟอนต์ใส่เฉพาะเมื่อ JSON มี key จริง + * (กันธีมแมปมีแค่สีแล้วไปทับขนาดฟอนต์จาก Admin เป็นค่าเริ่ม 16/24) + */ + function parseCarryMapPanelThemeMapOverlay(raw) { + let t = raw; + if (typeof t === 'string') { + try { + t = JSON.parse(t); + } catch (e) { + t = null; + } + } + if (!t || typeof t !== 'object') return null; + const bw = Number(t.borderWidthPx); + const borderWidthPx = Number.isFinite(bw) ? Math.max(0, Math.min(12, Math.round(bw))) : 2; + const out = { + panelBg: String(t.panelBg || '').trim().slice(0, 120), + panelBorder: String(t.panelBorder || '').trim().slice(0, 120), + borderWidthPx, + textColor: String(t.textColor || '').trim().slice(0, 120), + }; + const hasMin = Object.prototype.hasOwnProperty.call(t, 'questionFontMinPx'); + const hasMax = Object.prototype.hasOwnProperty.call(t, 'questionFontMaxPx'); + if (hasMin || hasMax) { + const fb = defaultCarryMapPanelThemePlay(); + let qMin = hasMin ? Number(t.questionFontMinPx) : fb.questionFontMinPx; + let qMax = hasMax ? Number(t.questionFontMaxPx) : fb.questionFontMaxPx; + if (!Number.isFinite(qMin)) qMin = fb.questionFontMinPx; + if (!Number.isFinite(qMax)) qMax = fb.questionFontMaxPx; + qMin = Math.round(Math.max(10, Math.min(40, qMin))); + qMax = Math.round(Math.max(14, Math.min(56, qMax))); + if (qMax < qMin) { + const s = qMin; + qMin = qMax; + qMax = s; + } + out.questionFontMinPx = qMin; + out.questionFontMaxPx = qMax; + } + return out; + } + + function setQuizCarryMapPanelThemeFromApi(s) { + quizCarryMapPanelTheme = null; + if (!s || typeof s !== 'object') return; + const parsed = parseCarryMapPanelThemeObject(s.carryMapPanelTheme); + if (!parsed) return; + quizCarryMapPanelTheme = parsed; + } + + function setQuizMapPanelThemeFromApi(s) { + quizMapPanelTheme = null; + if (!s || typeof s !== 'object') return; + const parsed = parseCarryMapPanelThemeObject(s.quizMapPanelTheme); + if (!parsed) return; + quizMapPanelTheme = parsed; + } + + function defaultCarryChoicePlaqueThemePlay() { + return { + borderMode: 'neon', + fixedBorder: 'rgba(122, 200, 255, 0.9)', + fillBg: 'rgba(12, 10, 20, 0.88)', + textColor: 'rgba(248, 249, 255, 1)', + borderWidthPx: 2.5, + plaqueImageUrl: '', + }; + } + + function parseCarryChoicePlaqueThemeObject(raw) { + const d = defaultCarryChoicePlaqueThemePlay(); + if (!raw || typeof raw !== 'object') return d; + const safeColor = (x, fb) => { + const t = String(x == null ? '' : x).trim().slice(0, 120); + if (!t || /url\s*\(|expression|@import|\/\*|javascript|<|>|\\0/i.test(t)) return fb; + if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(t)) return t; + if (/^rgba?\(\s*[^)]+\)$/i.test(t)) return t.replace(/\s+/g, ' ').trim(); + return fb; + }; + const mode = String(raw.borderMode || '').toLowerCase() === 'fixed' ? 'fixed' : 'neon'; + let bw = Number(raw.borderWidthPx); + if (!Number.isFinite(bw)) bw = d.borderWidthPx; + bw = Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10; + return { + borderMode: mode, + fixedBorder: safeColor(raw.fixedBorder, d.fixedBorder), + fillBg: safeColor(raw.fillBg, d.fillBg), + textColor: safeColor(raw.textColor, d.textColor), + borderWidthPx: bw, + plaqueImageUrl: sanitizeQuizCarryImageUrlClient(raw.plaqueImageUrl), + }; + } + + function buildCarryChoicePlaqueThemesArrayFromApi(s) { + if (!s || typeof s !== 'object') return null; + if (Array.isArray(s.carryChoicePlaqueThemes) && s.carryChoicePlaqueThemes.length) { + const out = []; + for (let i = 0; i < QUIZ_CARRY_MAX_OPTION_SLOTS; i++) { + out.push(parseCarryChoicePlaqueThemeObject(s.carryChoicePlaqueThemes[i])); + } + return out; + } + if (s.carryChoicePlaqueTheme && typeof s.carryChoicePlaqueTheme === 'object') { + const one = parseCarryChoicePlaqueThemeObject(s.carryChoicePlaqueTheme); + return Array.from({ length: QUIZ_CARRY_MAX_OPTION_SLOTS }, () => ({ ...one })); + } + return null; + } + + function getEffectiveCarryChoicePlaqueThemeForChoice(choiceIndex) { + const i = Math.max(0, Math.min(QUIZ_CARRY_MAX_OPTION_SLOTS - 1, choiceIndex | 0)); + if (quizCarryChoicePlaqueThemes && quizCarryChoicePlaqueThemes[i]) { + return quizCarryChoicePlaqueThemes[i]; + } + return defaultCarryChoicePlaqueThemePlay(); + } + + function setQuizCarryChoicePlaqueThemeFromApi(s) { + quizCarryChoicePlaqueThemes = buildCarryChoicePlaqueThemesArrayFromApi(s); + } + + function sanitizeQuizCarryImageUrlClient(u) { + const s = String(u == null ? '' : u).trim().slice(0, 512); + if (!s || /[\s<>'"`]/.test(s)) return ''; + if (s.startsWith('/')) { + if (!/^\/[\w\-./?#=&%+]+$/i.test(s)) return ''; + return s; + } + const low = s.toLowerCase(); + if (!low.startsWith('https://') && !low.startsWith('http://')) return ''; + try { + const parsed = new URL(s); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return ''; + return s; + } catch (e) { + return ''; + } + } + + /** crossOrigin=anonymous กับ same-origin บางเซิร์ฟ/nginx ทำให้โหลดรูปล้ม — ใช้เฉพาะข้ามโดเมน */ + function quizCarryApplyImageCrossOrigin(img, rawUrl) { + if (!img) return; + try { + img.removeAttribute('crossorigin'); + const u = sanitizeQuizCarryImageUrlClient(rawUrl); + if (!u || u.startsWith('data:')) return; + let imgOrigin = ''; + if (u.startsWith('http://') || u.startsWith('https://')) { + imgOrigin = new URL(u).origin; + } else if (u.startsWith('/')) { + imgOrigin = window.location.origin; + } + if (imgOrigin && imgOrigin !== window.location.origin) { + img.crossOrigin = 'anonymous'; + } + } catch (e) { /* ignore */ } + } + + function preloadQuizCarryChoiceImages(q) { + if (!q) return; + if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) { + for (let i = 0; i < q.choiceImageUrls.length; i++) { + const raw = sanitizeQuizCarryImageUrlClient(q.choiceImageUrls[i]); + if (!raw) continue; + if (quizCarryChoiceImageCache.has(raw)) continue; + const im = new Image(); + quizCarryApplyImageCrossOrigin(im, raw); + im.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(raw, im); + try { + im.src = raw; + } catch (e) { /* ignore */ } + } + } + if (Array.isArray(q.choices)) { + for (let j = 0; j < q.choices.length; j++) { + const raw2 = getQuizCarryPlaqueImageUrlForIndex(q, j); + if (!raw2 || quizCarryChoiceImageCache.has(raw2)) continue; + const im2 = new Image(); + quizCarryApplyImageCrossOrigin(im2, raw2); + im2.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(raw2, im2); + try { + im2.src = raw2; + } catch (e) { /* ignore */ } + } + } + } + + function getQuizCarryChoiceImageCached(url) { + const u = sanitizeQuizCarryImageUrlClient(url); + if (!u) return null; + const im = quizCarryChoiceImageCache.get(u); + if (im && im.complete && im.naturalWidth > 0) return im; + return null; + } + + function getQuizCarryChoiceImageUrlForIndex(q, idx) { + if (!q || !Array.isArray(q.choiceImageUrls)) return ''; + const u = q.choiceImageUrls[idx]; + return sanitizeQuizCarryImageUrlClient(u); + } + + /** รูปป้าย: คำถามต่อช่องก่อน แล้วค่อยรูปจากธีมช่อง (carryChoicePlaqueThemes) */ + function getQuizCarryPlaqueImageUrlForIndex(q, idx) { + const perQ = getQuizCarryChoiceImageUrlForIndex(q, idx); + if (perQ) return perQ; + const th = getEffectiveCarryChoicePlaqueThemeForChoice(idx); + const u = th && th.plaqueImageUrl ? sanitizeQuizCarryImageUrlClient(th.plaqueImageUrl) : ''; + return u || ''; + } + + function defaultCarryEmbedCountdownThemePlay() { + return { + overlayBackdrop: 'rgba(6, 8, 14, 0.52)', + innerBg: 'rgba(0, 0, 0, 0.78)', + innerBorder: 'rgba(94, 234, 212, 0.82)', + innerBorderWpx: 1, + innerRadiusPx: 14, + digitColor: '#ffe066', + mapDigitCqmin: 78, + mapDigitCqh: 82, + mapDigitMaxPx: 220, + screenDigitVw: 32, + screenDigitMaxPx: 200, + }; + } + + function parseCarryEmbedCountdownThemeObject(raw) { + const d = defaultCarryEmbedCountdownThemePlay(); + if (!raw || typeof raw !== 'object') return d; + const clampN = (v, lo, hi, def) => { + const n = Number(v); + if (!Number.isFinite(n)) return def; + return Math.max(lo, Math.min(hi, Math.round(n))); + }; + const clipStr = (x, maxLen) => { + const t = String(x == null ? '' : x).trim().slice(0, maxLen); + return t || null; + }; + const safeColor = (x, fb) => { + const t = clipStr(x, 120); + if (!t || /url\s*\(|expression|@import|\/\*|javascript|<|>|\\0/i.test(t)) return fb; + if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(t)) return t; + if (/^rgba?\(\s*[^)]+\)$/i.test(t)) return t.replace(/\s+/g, ' ').trim(); + return fb; + }; + return { + overlayBackdrop: safeColor(raw.overlayBackdrop, d.overlayBackdrop), + innerBg: safeColor(raw.innerBg, d.innerBg), + innerBorder: safeColor(raw.innerBorder, d.innerBorder), + innerBorderWpx: clampN(raw.innerBorderWpx, 0, 12, d.innerBorderWpx), + innerRadiusPx: clampN(raw.innerRadiusPx, 0, 32, d.innerRadiusPx), + digitColor: safeColor(raw.digitColor, d.digitColor), + mapDigitCqmin: clampN(raw.mapDigitCqmin, 35, 100, d.mapDigitCqmin), + mapDigitCqh: clampN(raw.mapDigitCqh, 35, 100, d.mapDigitCqh), + mapDigitMaxPx: clampN(raw.mapDigitMaxPx, 48, 400, d.mapDigitMaxPx), + screenDigitVw: clampN(raw.screenDigitVw, 6, 44, d.screenDigitVw), + screenDigitMaxPx: clampN(raw.screenDigitMaxPx, 48, 220, d.screenDigitMaxPx), + }; + } + + function setQuizCarryEmbedCountdownThemeFromApi(s) { + quizCarryEmbedCountdownTheme = parseCarryEmbedCountdownThemeObject(s && s.carryEmbedCountdownTheme); + applyQuizCarryEmbedCountdownThemeToDom(); + } + + const CARRY_ECD_VAR_KEYS = [ + '--carry-ecd-overlay', + '--carry-ecd-inner-bg', + '--carry-ecd-inner-border', + '--carry-ecd-inner-border-w', + '--carry-ecd-inner-radius', + '--carry-ecd-digit-color', + '--carry-ecd-map-digit-fs', + '--carry-ecd-screen-vw', + '--carry-ecd-screen-max', + ]; + + function clearQuizCarryEmbedCountdownThemeDomVars() { + const root = document.getElementById('quiz-carry-embed-countdown'); + if (!root) return; + CARRY_ECD_VAR_KEYS.forEach((k) => { try { root.style.removeProperty(k); } catch (e) { /* ignore */ } }); + } + + /** ใส่ CSS variables บน #quiz-carry-embed-countdown — อ่านจาก quizCarryEmbedCountdownTheme */ + function applyQuizCarryEmbedCountdownThemeToDom() { + const root = document.getElementById('quiz-carry-embed-countdown'); + const numEl = document.getElementById('quiz-carry-embed-countdown-num'); + if (!root) return; + clearQuizCarryEmbedCountdownThemeDomVars(); + const th = quizCarryEmbedCountdownTheme || defaultCarryEmbedCountdownThemePlay(); + root.style.setProperty('--carry-ecd-overlay', th.overlayBackdrop); + root.style.setProperty('--carry-ecd-inner-bg', th.innerBg); + root.style.setProperty('--carry-ecd-inner-border', th.innerBorder); + root.style.setProperty('--carry-ecd-inner-border-w', `${Math.max(0, Math.min(12, Math.round(Number(th.innerBorderWpx) || 1)))}px`); + root.style.setProperty('--carry-ecd-inner-radius', `${Math.max(0, Math.min(32, Math.round(Number(th.innerRadiusPx) || 12)))}px`); + root.style.setProperty('--carry-ecd-digit-color', th.digitColor); + const cqmin = Math.max(35, Math.min(100, Math.round(Number(th.mapDigitCqmin) || 78))); + const cqh = Math.max(35, Math.min(100, Math.round(Number(th.mapDigitCqh) || 82))); + const mpx = Math.max(48, Math.min(400, Math.round(Number(th.mapDigitMaxPx) || 200))); + root.style.setProperty('--carry-ecd-map-digit-fs', `clamp(14px, min(${cqmin}cqmin, ${cqh}cqh), ${mpx}px)`); + const vw = Math.max(6, Math.min(44, Math.round(Number(th.screenDigitVw) || 28))); + const smx = Math.max(48, Math.min(220, Math.round(Number(th.screenDigitMaxPx) || 132))); + root.style.setProperty('--carry-ecd-screen-vw', `${vw}vw`); + root.style.setProperty('--carry-ecd-screen-max', `${smx}px`); + if (numEl && String(numEl.tagName || '').toUpperCase() !== 'IMG') { + numEl.style.textShadow = '0 0 0.45em currentColor, 0 4px 14px rgba(0,0,0,0.55)'; + } else if (numEl) { + try { numEl.style.removeProperty('text-shadow'); } catch (e) { /* ignore */ } + } + } + + function setCountdown321QuestionAssetGraphic(numEl, n) { + if (!numEl) return; + const d = Math.max(1, Math.min(3, n)); + if (String(numEl.tagName || '').toUpperCase() === 'IMG') { + numEl.src = COUNTDOWN_321_IMG_BASE + d + '.png'; + numEl.alt = String(d); + } else { + numEl.textContent = String(d); + } + } + + /** default ← quiz-settings/API ← แมป (สีจากแมปทับได้; ฟอนต์จากแมปเฉพาะเมื่อแมประบุ key) */ + function getEffectiveCarryMapPanelThemeForApply() { + const d = defaultCarryMapPanelThemePlay(); + const api = quizCarryMapPanelTheme != null && typeof quizCarryMapPanelTheme === 'object' ? quizCarryMapPanelTheme : null; + let merged = api ? { ...d, ...api } : { ...d }; + if (mapData && mapData.carryMapPanelTheme != null) { + const mp = parseCarryMapPanelThemeMapOverlay(mapData.carryMapPanelTheme); + if (mp) merged = { ...merged, ...mp }; + } + return merged; + } + + /** เกมถูก/ผิด: default ← quizMapPanelTheme (API) ← quizMapPanelTheme บนแมป (ถ้ามี) */ + function getEffectiveQuizMapPanelThemeForApply() { + const d = defaultCarryMapPanelThemePlay(); + const api = quizMapPanelTheme != null && typeof quizMapPanelTheme === 'object' ? quizMapPanelTheme : null; + let merged = api ? { ...d, ...api } : { ...d }; + if (mapData && mapData.quizMapPanelTheme != null) { + const mp = parseCarryMapPanelThemeMapOverlay(mapData.quizMapPanelTheme); + if (mp) merged = { ...merged, ...mp }; + } + return merged; + } + + function paintQuizMapPanelTheme(panel, textEl, th) { + if (!panel || !textEl || !th) return; + clearQuizMapPanelThemeInline(panel, textEl); + const bgStr = th.panelBg != null ? String(th.panelBg).trim() : ''; + const brStr = th.panelBorder != null ? String(th.panelBorder).trim() : ''; + const txStr = th.textColor != null ? String(th.textColor).trim() : ''; + const wRaw = Number(th.borderWidthPx); + const w = Number.isFinite(wRaw) ? Math.max(0, Math.min(12, Math.round(wRaw))) : 0; + const bgUse = bgStr || 'transparent'; + const brUse = brStr || 'transparent'; + const txUse = txStr || '#f1f5f9'; + panel.style.setProperty('--qmap-bg', bgUse); + panel.style.setProperty('--qmap-border', brUse); + panel.style.setProperty('--qmap-border-w', String(w) + 'px'); + panel.style.setProperty('--qmap-shadow', 'none'); + textEl.style.setProperty('--qmap-text', txUse); + textEl.style.setProperty('--qmap-text-shadow', 'none'); + /* โหลด play.html แบบแคชเก่า (ไม่มี var) ยังต้องทับได้ — ใช้ !important คู่กับตัวแปร */ + panel.style.setProperty('background', bgUse, 'important'); + panel.style.setProperty('border', String(w) + 'px solid ' + brUse, 'important'); + panel.style.setProperty('box-shadow', 'none', 'important'); + textEl.style.setProperty('color', txUse, 'important'); + textEl.style.setProperty('text-shadow', 'none', 'important'); + } + + function applyQuizCarryMapPanelThemeIfNeeded(panel, textEl) { + if (!panel || !textEl) return; + if (!isQuizCarry()) { + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + paintQuizMapPanelTheme(panel, textEl, getEffectiveCarryMapPanelThemeForApply()); + } + + function applyQuizTrueFalseMapPanelThemeIfNeeded(panel, textEl) { + if (!panel || !textEl) return; + if (!isQuiz() || isQuizCarry()) return; + paintQuizMapPanelTheme(panel, textEl, getEffectiveQuizMapPanelThemeForApply()); + } + + function syncPlayQuizMapPanel() { + const panel = document.getElementById('quiz-map-question-panel'); + const textEl = document.getElementById('quiz-map-question-text'); + if (!panel || !textEl || !mapData) return; + /* เอดิเตอร์ embed: ซ่อนแผงทองเฉพาะ quiz ทั่วไป — แมปภารกิจ mng8a80o ต้องโชว์คำถามในโซนที่วาด (quizQuestionArea) */ + if (previewMode && editorEmbedReturn && !isQuizCarry() && !(isQuiz() && isQuizQuestionMissionUiMapPlay())) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + if (isQuizBattle()) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + if (!isQuiz() && !isQuizCarry()) { + clearQuizMapQuestionCarryFeedback(); + return; + } + const bounds = isQuiz() + ? getQuizQuestionAreaTileBounds(mapData) + : (getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData)); + const text = (playQuizText || '').trim(); + if (!bounds || !text) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + const qmCenter = isQuizQuestionMissionHudActivePlay() ? getQuizQuestionMissionMapCenterWorldPxPlay() : null; + const carryCam = !qmCenter && isQuizCarry() ? getQuizCarryMapCameraWorldCenterPxPlay() : null; + const camX = qmCenter ? qmCenter.cx : (carryCam ? carryCam.cx : me.x * tileSize); + const camY = qmCenter ? qmCenter.cy : (carryCam ? carryCam.cy : me.y * tileSize); + 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'; + panel.style.width = Math.round(Math.max(48, wPx)) + 'px'; + panel.style.height = Math.round(Math.max(40, hPx)) + 'px'; + panel.classList.remove('is-hidden'); + panel.setAttribute('aria-hidden', 'false'); + if (isQuiz() && !isQuizCarry()) { + panel.classList.add('quiz-map-question-panel--true-false'); + } else { + panel.classList.remove('quiz-map-question-panel--true-false'); + } + if (isQuizCarry()) { + applyQuizCarryMapPanelThemeIfNeeded(panel, textEl); + } else if (isQuiz()) { + applyQuizTrueFalseMapPanelThemeIfNeeded(panel, textEl); + } else { + clearQuizMapPanelThemeInline(panel, textEl); + } + const thFont = isQuizCarry() + ? getEffectiveCarryMapPanelThemeForApply() + : getEffectiveQuizMapPanelThemeForApply(); + 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') { + panel.classList.add('quiz-map-question-panel--question-mission-live'); + } else { + panel.classList.remove('quiz-map-question-panel--question-mission-live'); + if (panel.dataset.qmPlaqueTry) { + panel.style.removeProperty('background-image'); + panel.style.removeProperty('background-size'); + panel.style.removeProperty('background-position'); + delete panel.dataset.qmPlaqueTry; + } + } + const kickerEl = document.getElementById('quiz-map-question-kicker'); + const missionLiveQ = isQuiz() && isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'live'; + const inReadOrAnswer = playQuizPhaseLocal === 'read' || playQuizPhaseLocal === 'answer'; + if (kickerEl) { + let showK = false; + let line = ''; + if (missionLiveQ && inReadOrAnswer) { + const qi = Math.max(0, Number(playQuizQuestionIndex) || 0); + const qt = Math.max(0, Number(playQuizQuestionTotal) || 0); + if (qi > 0 && qt > 0) { + line = '- Quiz ' + qi + ' / ' + qt + ' -'; + showK = true; + } else if (qi > 0) { + line = '- Quiz ' + qi + ' -'; + showK = true; + } + } + if (showK && line) { + kickerEl.textContent = line; + kickerEl.classList.remove('is-hidden'); + kickerEl.setAttribute('aria-hidden', 'false'); + panel.classList.add('quiz-map-question-panel--has-kicker'); + } else { + kickerEl.textContent = ''; + kickerEl.classList.add('is-hidden'); + kickerEl.setAttribute('aria-hidden', 'true'); + panel.classList.remove('quiz-map-question-panel--has-kicker'); + } + } + fitQuizMapQuestionFontToPanel(panel, textEl, startFontPx, mnFont); + } + + /** โซนเดียวกับแผงคำถามบนแผนที่ — ยึด timeup ให้ตรงกล่อง #quiz-map-question-panel (ห้ามคำนวณซ้ำแล้วคลาด) */ + function syncQuizCarryTimeupDeskLayerPosition() { + const layer = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!layer || layer.classList.contains('is-hidden')) return; + const anchor = layer.querySelector('.qc-timeup-desk-anchor') + || layer.querySelector('.qc-timeup-desk-inner') + || (() => { + const img = document.getElementById('quiz-carry-timeup-txt-img'); + return img && img.parentElement && img.parentElement !== layer ? img.parentElement : null; + })(); + if (!anchor || !canvas) return; + const clearToFullStack = () => { + anchor.style.left = '0'; + anchor.style.top = '0'; + anchor.style.width = '100%'; + anchor.style.height = '100%'; + }; + const stack = document.getElementById('play-canvas-stack'); + const panel = document.getElementById('quiz-map-question-panel'); + if (stack && panel && !panel.classList.contains('is-hidden')) { + try { + const sr = stack.getBoundingClientRect(); + const pr = panel.getBoundingClientRect(); + const rw = pr.width; + const rh = pr.height; + if (rw >= 8 && rh >= 8 && sr.width > 0 && sr.height > 0) { + anchor.style.left = Math.round(pr.left - sr.left) + 'px'; + anchor.style.top = Math.round(pr.top - sr.top) + 'px'; + anchor.style.width = Math.round(rw) + 'px'; + anchor.style.height = Math.round(rh) + 'px'; + return; + } + } catch (e) { /* fallback ด้านล่าง */ } + } + const pl = panel && panel.style && String(panel.style.left || '').trim(); + const pt = panel && panel.style && String(panel.style.top || '').trim(); + const pw = panel && panel.style && String(panel.style.width || '').trim(); + const ph = panel && panel.style && String(panel.style.height || '').trim(); + if (pl && pt && pw && ph) { + anchor.style.left = pl; + anchor.style.top = pt; + anchor.style.width = pw; + anchor.style.height = ph; + return; + } + if (!mapData || mapData.gameType !== 'quiz_carry') { + clearToFullStack(); + return; + } + const bounds = getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData); + if (!bounds) { + clearToFullStack(); + return; + } + const cc = getQuizCarryMapCameraWorldCenterPxPlay(); + const camX = cc ? cc.cx : me.x * tileSize; + const camY = cc ? cc.cy : me.y * tileSize; + 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'; + anchor.style.height = Math.round(Math.max(40, hPx)) + 'px'; + } + + function carryEmbedCountdownAnchorResolved() { + if (!mapData || !isQuizCarry()) return 'screen'; + const v = mapData.carryEmbedCountdownAnchor; + if (v === 'grid' || v === 'map') return v; + return 'screen'; + } + + function resetQuizCarryEmbedCountdownLayoutDom() { + const ov = document.getElementById('quiz-carry-embed-countdown'); + const inner = document.getElementById('quiz-carry-embed-countdown-inner'); + if (ov) ov.classList.remove('quiz-carry-embed-countdown--map-anchor'); + if (inner) { + inner.classList.remove('quiz-carry-embed-countdown-inner--map-rect'); + inner.style.position = ''; + inner.style.left = ''; + inner.style.top = ''; + inner.style.width = ''; + inner.style.height = ''; + inner.style.transform = ''; + inner.style.maxWidth = ''; + inner.style.boxSizing = ''; + inner.style.display = ''; + inner.style.margin = ''; + } + } + + /** พรีวิว embed: วางกล่อง 3–2–1 กลางจอ / ชิดโซนทองหรือโซนกลาง / ชิดกริด carryEmbedCountdownArea — ตาม carryEmbedCountdownAnchor */ + function syncQuizCarryEmbedCountdownLayout() { + if (!quizCarryEmbedMissionFlowActive() || !mapData) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + const ov = document.getElementById('quiz-carry-embed-countdown'); + const inner = document.getElementById('quiz-carry-embed-countdown-inner'); + const canvasEl = document.getElementById('game-canvas'); + if (!ov || !inner || !canvasEl) return; + if (ov.classList.contains('is-hidden')) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + const anchor = carryEmbedCountdownAnchorResolved(); + if (anchor === 'screen') { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + let bounds = null; + if (anchor === 'grid') { + bounds = getTileBoundsForOnesGrid(mapData.carryEmbedCountdownArea); + if (!bounds) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + } else { + bounds = getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData); + if (!bounds) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + } + const cc = getQuizCarryMapCameraWorldCenterPxPlay(); + const camX = cc ? cc.cx : me.x * tileSize; + const camY = cc ? cc.cy : me.y * tileSize; + 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; + ov.classList.add('quiz-carry-embed-countdown--map-anchor'); + inner.classList.add('quiz-carry-embed-countdown-inner--map-rect'); + inner.style.position = 'fixed'; + inner.style.left = Math.round(sl) + 'px'; + inner.style.top = Math.round(st) + 'px'; + inner.style.width = Math.round(Math.max(100, wPx)) + 'px'; + inner.style.height = Math.round(Math.max(72, hPx)) + 'px'; + inner.style.transform = 'none'; + inner.style.boxSizing = 'border-box'; + inner.style.display = 'flex'; + inner.style.flexDirection = 'column'; + inner.style.alignItems = 'center'; + inner.style.justifyContent = 'center'; + inner.style.margin = '0'; + inner.style.maxWidth = 'none'; + } + + /** quiz_carry + embed: คำถามไปที่แผงทองบนแผนที่ / ใน overlay นับถอยหลัง — ไม่ใช้แถบบน */ + function syncQuizCarryEmbedQuestionStrip() { + const wrap = document.getElementById('quiz-carry-embed-q-strip'); + const p = document.getElementById('quiz-carry-embed-q-strip-text'); + if (!wrap || !p) return; + wrap.classList.remove('quiz-carry-embed-q-strip--countdown'); + wrap.classList.add('is-hidden'); + wrap.setAttribute('aria-hidden', 'true'); + } + + function updatePlayQuizTimerDisplay() { + const el = document.getElementById('quiz-game-timer'); + if (!el) return; + if (!playQuizPhaseEndsAt) { + el.textContent = ''; + return; + } + const s = Math.max(0, Math.ceil((playQuizPhaseEndsAt - Date.now()) / 1000)); + el.textContent = s + ' วินาที'; + } + + function clampPreviewMs(n, def, minV, maxV) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(minV, Math.min(maxV, Math.floor(v))); + } + + function clampCarrySessionLen(n, def) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(0, Math.min(500, Math.floor(v))); + } + + function buildQuizPoolFromMap(md) { + if (!md || !Array.isArray(md.quizQuestions)) return []; + return md.quizQuestions + .filter((q) => q && String(q.text || '').trim()) + .map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue })); + } + + function shufflePreviewQuizQuestionsPlay(arr) { + const a = (arr || []).slice(); + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; + } + + /** ตรงกับ server clampQuizRoundQuestionCount — 1–50 ค่าเริ่ม 10 */ + function clampPreviewQuizRoundQuestionCount(settings) { + const v = Number(settings && settings.quizRoundQuestionCount); + if (!Number.isFinite(v)) return 10; + return Math.max(1, Math.min(50, Math.floor(v))); + } + + function finishPreviewQuizSessionPlay() { + previewQuizStep = 'done'; + playQuizPhaseLocal = null; + previewQuizCurrent = null; + playQuizPhaseEndsAt = 0; + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const qov = document.getElementById('quiz-game-overlay'); + if (qov) qov.classList.add('is-hidden'); + const mapPanel = document.getElementById('quiz-map-question-panel'); + if (mapPanel) { + mapPanel.classList.add('is-hidden'); + mapPanel.setAttribute('aria-hidden', 'true'); + } + if (isQuizQuestionMissionUiMapPlay()) { + quizQuestionMissionPhase = 'ended'; + applyQuizQuestionMissionPanelImages(); + const mission = quizQuestionMissionBuildPayload(); + showGauntletCrownMissionOverlay(mission); + return; + } + playQuizText = 'ครบทุกข้อในรอบทดสอบแล้ว'; + if (qov) qov.classList.remove('is-hidden'); + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] จบรอบ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'จำนวนข้อต่อรอบใช้ค่าเดียวกับ Admin (quizRoundQuestionCount)'; + } + + function clearPreviewBotAnswerPaths() { + if (!playBotsEnabled()) return; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botPath = []; + o.botAnswerWander = false; + }); + } + + function resetPreviewBotsQuizState() { + if (!playBotsEnabled()) return; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.quizCannotTrue = false; + o.quizCannotFalse = false; + o.botPath = []; + o.botAnswerWander = false; + }); + } + + function applyPreviewReadPhase(q, poolLen) { + clearPreviewBotAnswerPaths(); + previewQuizStep = 'read'; + playQuizPhaseLocal = 'read'; + previewQuizCurrent = q; + playQuizText = q.text; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.readMs; + playQuizQuestionIndex = previewQuizQIndex + 1; + playQuizQuestionTotal = Math.max(0, Number(poolLen) || 0); + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) { + phaseEl.textContent = '[ทดสอบ] อ่านคำถาม · ข้อ ' + (previewQuizQIndex + 1) + ' / ' + poolLen; + } + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'โซนทองบนแผนที่ = ข้อความคำถาม · ฟ้า = จริง · ชมพู = เท็จ'; + } + + function applyPreviewAnswerPhase() { + previewQuizStep = 'answer'; + playQuizPhaseLocal = 'answer'; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.answerMs; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] เดินไปโซน ถูก / ผิด'; + const leg = document.getElementById('quiz-play-legend'); + if (leg && previewQuizCurrent) { + leg.textContent = previewQuizCurrent.answerTrue + ? 'เฉลย (ทดสอบ): คำตอบที่ถูกคือ «จริง» — โซนสีฟ้า' + : 'เฉลย (ทดสอบ): คำตอบที่ถูกคือ «เท็จ» — โซนสีชมพู'; + } + previewBotsPrepareAnswerRound(); + } + + function applyPreviewBetweenPhase() { + clearPreviewBotAnswerPaths(); + previewQuizStep = 'between'; + playQuizPhaseLocal = null; + playQuizText = 'ข้อถัดไปกำลังจะมา…'; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.betweenMs; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] พักระหว่างข้อ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'รอสักครู่แล้วไปข้อถัดไป (สับแล้วตามจำนวนข้อต่อรอบ)'; + } + + function quizCellOnPlay(grid, x, y) { + return !!(grid && grid[y] && grid[y][x] === 1); + } + + /** ทั้ง footprint ทับโซน จริง/เท็จ — หลังตอบผิดควรถูกดีดออกแล้ว ไม่วาดทับ (กันจุดเล็กค้างกลาง SAFE/SCAM) */ + function playQuizPeerFootprintOverlapsAnswerZone(ax, ay) { + if (!mapData || !isQuiz()) return false; + for (const k of quizTilesFootprintPlay(ax, ay)) { + const parts = k.split(','); + const fx = +parts[0]; + const fy = +parts[1]; + if (quizCellOnPlay(mapData.quizTrueArea, fx, fy) || quizCellOnPlay(mapData.quizFalseArea, fx, fy)) return true; + } + return false; + } + + /** ไม่วาดตัวที่โดนล็อก/เคยตอบผิดแล้วแต่ยังทับโซน (กันจุดเล็กค้าง + desync มัลติเพลย์) */ + function playQuizAvatarHideWhileOverlappingAnswerZone(ent, entId, ax, ay) { + if (!playQuizPeerFootprintOverlapsAnswerZone(ax, ay)) return false; + if (entId === myId || ent === me) { + return !!(playQuizPlayerLocal && playQuizPlayerLocal.cannotTrue && playQuizPlayerLocal.cannotFalse); + } + const locked = !!(ent && ent.quizCannotTrue && ent.quizCannotFalse); + const everWrong = !!playQuizEverWrong[String(entId)]; + return locked || everWrong; + } + + function spawnTileWalkablePlay(md, x, y) { + const w = md.width || 20; + const h = md.height || 15; + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + if (row && row[x] === 1) return false; + return true; + } + + function spawnFootprintFitsPlay(md, anchorX, anchorY) { + if (!spawnTileWalkablePlay(md, anchorX, anchorY)) return false; + const cellsW = Math.max(1, Math.min(4, Math.floor(Number(md.characterCellsW) || Number(md.characterCells) || 1))); + const cellsH = Math.max(1, Math.min(4, Math.floor(Number(md.characterCellsH) || Number(md.characterCells) || 1))); + const w = md.width || 20; + const h = md.height || 15; + const maxX = Math.min(w, anchorX + cellsW); + const maxY = Math.min(h, anchorY + cellsH); + for (let ty = anchorY; ty < maxY; ty++) { + for (let tx = anchorX; tx < maxX; tx++) { + if (!spawnTileWalkablePlay(md, tx, ty)) return false; + } + } + return true; + } + + /** เหมือน server pickRandomSpawnFromMap — สุ่มด้วย crypto.getRandomValues */ + function pickRandomSpawnFromMapPlay(md) { + const fallback = md.spawn || { x: 1, y: 1 }; + const fx = Number.isFinite(Number(fallback.x)) ? Number(fallback.x) : 1; + const fy = Number.isFinite(Number(fallback.y)) ? Number(fallback.y) : 1; + const grid = md.spawnArea; + if (!grid || !Array.isArray(grid)) return { x: fx, y: fy }; + const w = md.width || 20; + const h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && spawnFootprintFitsPlay(md, x, y)) pool.push({ x, y }); + } + } + if (!pool.length) return { x: fx, y: fy }; + const u = new Uint32Array(1); + (typeof crypto !== 'undefined' && crypto.getRandomValues) + ? crypto.getRandomValues(u) + : (u[0] = (Math.floor(Math.random() * 0xffffffff) >>> 0)); + const pick = pool[u[0] % pool.length]; + return { x: pick.x, y: pick.y }; + } + + function parseLobbyPlayerSpawnsFromMapPlay(md) { + const w = md.width || 20; + const h = md.height || 15; + const out = [null, null, null, null, null, null]; + const raw = md && md.lobbyPlayerSpawns; + if (!Array.isArray(raw)) return out; + for (let i = 0; i < 6 && i < raw.length; i++) { + const cell = raw[i]; + if (!cell || typeof cell !== 'object') continue; + const x = Math.floor(Number(cell.x)); + const y = Math.floor(Number(cell.y)); + if (!Number.isFinite(x) || !Number.isFinite(y)) continue; + if (x < 0 || x >= w || y < 0 || y >= h) continue; + out[i] = { x, y }; + } + return out; + } + + /** jump_survive: ช่อง P1–P6 ที่วาดแบบยาน (shooterSpawnSlots) เติมใน slots6 เมื่อ lobbyPlayerSpawns ว่าง */ + function augmentLobbySlotsFromShooterPaintJumpSurvivePlay(md, slots6) { + if (!md || md.gameType !== 'jump_survive' || !Array.isArray(slots6)) return; + const g = md.shooterSpawnSlots; + if (!g) return; + const w = md.width || 20, h = md.height || 15; + for (let slot = 1; slot <= 6; slot++) { + const idx = slot - 1; + if (slots6[idx]) continue; + let found = false; + for (let yy = 0; yy < h && !found; yy++) { + const row = g[yy]; + if (!row) continue; + for (let xx = 0; xx < w; xx++) { + if (row[xx] === slot) { + slots6[idx] = { x: xx, y: yy }; + found = true; + break; + } + } + } + } + } + + /** จุดเกิด Quiz Battle — ต้องอยู่บนเส้นทาง (กล่องสไปรต์) ไม่ใช่แค่ snap จาก spawnArea นอกเลน */ + function collectQuizBattleValidSpawnCentersPlay(md) { + const out = []; + if (!md || md.gameType !== 'quiz_battle' || !quizBattlePathModeActive(md)) return out; + const w = md.width || 20, h = md.height || 15; + const g = md.quizBattlePathArea; + const seen = new Set(); + const prevMap = mapData; + mapData = md; + try { + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) { + const key = nx.toFixed(3) + ',' + ny.toFixed(3); + if (seen.has(key)) continue; + if (!quizBattleSnapTargetValidPlay(nx, ny)) continue; + seen.add(key); + out.push({ tx, ty, x: nx, y: ny }); + } + } + } + } finally { + mapData = prevMap; + } + out.sort((a, b) => a.ty - b.ty || a.tx - b.tx || a.x - b.x); + return out; + } + + /** จุดโลกใกล้ pref — ลองกลางช่องก่อน แล้วหาใน spawnArea (ฟ้า) ที่วางตัวได้ */ + function quizBattleNearestValidSpawnWorldPlay(md, prefTx, prefTy) { + const ptx = Math.floor(Number(prefTx)) || 0; + const pty = Math.floor(Number(prefTy)) || 0; + const prevMap = mapData; + mapData = md; + try { + for (const [nx, ny] of [[ptx + 0.5, pty + 0.5], [ptx + 0.01, pty + 0.01]]) { + if (quizBattleSpawnWallClearPlay(md, nx, ny)) return { x: nx, y: ny }; + } + const pool = collectQuizBattleSpawnPoolPlay(md); + if (pool.length) { + let best = pool[0], bestD = Infinity; + for (const c of pool) { + const d = Math.abs(c.tx - ptx) + Math.abs(c.ty - pty); + if (d < bestD) { bestD = d; best = c; } + } + return { x: best.x, y: best.y }; + } + const valid = collectQuizBattleValidSpawnCentersPlay(md); + if (!valid.length) return { x: ptx + 0.5, y: pty + 0.5 }; + let best = valid[0], bestD = Infinity; + for (const c of valid) { + const d = Math.abs(c.tx - ptx) + Math.abs(c.ty - pty); + if (d < bestD) { bestD = d; best = c; } + } + return { x: best.x, y: best.y }; + } finally { + mapData = prevMap; + } + } + + /** ช่อง spawnArea (ฟ้า) ที่วางตัวได้ — สุ่มในพื้นที่ฟ้า ไม่บังคับอยู่บนเลนม่วง */ + function collectQuizBattleSpawnPoolPlay(md) { + const out = []; + if (!md) return out; + const prevMap = mapData; + mapData = md; + try { + const w = md.width || 20, h = md.height || 15; + const grid = md.spawnArea; + const seen = new Set(); + const cells = []; + if (grid && Array.isArray(grid)) { + for (let ty = 0; ty < h; ty++) { + const row = grid[ty]; + if (!row) continue; + for (let tx = 0; tx < w; tx++) { + if (Number(row[tx]) === 1) cells.push({ tx, ty }); + } + } + } + if (!cells.length && md.spawn) { + cells.push({ + tx: Math.max(0, Math.min(w - 1, Math.floor(Number(md.spawn.x)) || 1)), + ty: Math.max(0, Math.min(h - 1, Math.floor(Number(md.spawn.y)) || 1)), + }); + } + for (const { tx, ty } of cells) { + for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) { + const key = nx.toFixed(3) + ',' + ny.toFixed(3); + if (seen.has(key)) continue; + if (quizBattlePathModeActive(md)) { + if (!quizBattleSpawnWallClearPlay(md, nx, ny)) continue; + } else if (!spawnFootprintFitsPlay(md, tx, ty)) { + continue; + } + seen.add(key); + out.push({ tx, ty, x: nx, y: ny }); + } + } + if (!out.length && quizBattlePathModeActive(md)) { + return collectQuizBattleValidSpawnCentersPlay(md); + } + } finally { + mapData = prevMap; + } + return out; + } + + function pickQuizBattleRandomSpawnWorldPlay(md) { + const pool = collectQuizBattleSpawnPoolPlay(md); + const fb = md.spawn || { x: 1, y: 1 }; + if (!pool.length) { + return quizBattleNearestValidSpawnWorldPlay(md, fb.x, fb.y); + } + const u = new Uint32Array(1); + if (typeof crypto !== 'undefined' && crypto.getRandomValues) crypto.getRandomValues(u); + else u[0] = (Math.floor(Math.random() * 0xffffffff) >>> 0); + const pick = pool[u[0] % pool.length]; + return { x: pick.x, y: pick.y }; + } + + function quizBattleSpawnWorldFromJoinOrderPlay(md, joinOrderIndex) { + if (!md) return { x: 1.5, y: 1.5 }; + const ord = joinOrderIndex | 0; + const mode = md.lobbySpawnMode; + if (mode === 'slots6') { + const slots = parseLobbyPlayerSpawnsFromMapPlay(md); + const j = Math.min(Math.max(0, ord), 5); + const slot = slots[j]; + if (slot) return quizBattleNearestValidSpawnWorldPlay(md, slot.x, slot.y); + return pickQuizBattleRandomSpawnWorldPlay(md); + } + if (mode === 'fixed' && md.spawn) { + const sx = Number(md.spawn.x) || 1, sy = Number(md.spawn.y) || 1; + return quizBattleNearestValidSpawnWorldPlay(md, sx, sy); + } + /* random (ค่าเริ่มต้นใน editor) — สุ่มในพื้นที่ฟ้า ไม่เรียงตาม join order */ + return pickQuizBattleRandomSpawnWorldPlay(md); + } + + function pickQuizBattleSpawnFromMapPlay(md, joinOrderIndex) { + const world = quizBattleSpawnWorldFromJoinOrderPlay(md, joinOrderIndex); + return { x: Math.floor(world.x), y: Math.floor(world.y) }; + } + + /** สอดคล้องกับ server pickSpawnForJoin — ใช้พรีวิวบอท / ทดสอบ */ + function pickSpawnForJoinPlay(md, joinOrderIndex) { + if (!md) return { x: 1, y: 1 }; + if (md.gameType === 'quiz_battle' && quizBattlePathModeActive(md)) { + const qb = pickQuizBattleSpawnFromMapPlay(md, joinOrderIndex); + if (qb) return qb; + } + const mode = md.lobbySpawnMode; + const ord = joinOrderIndex | 0; + if (mode === 'slots6' && ord >= 6) return pickRandomSpawnFromMapPlay(md); + const j = Math.min(Math.max(0, ord), 5); + if (mode === 'fixed' && md.spawn) { + const fx = Number.isFinite(Number(md.spawn.x)) ? Math.floor(Number(md.spawn.x)) : 1; + const fy = Number.isFinite(Number(md.spawn.y)) ? Math.floor(Number(md.spawn.y)) : 1; + const w = md.width || 20; + const h = md.height || 15; + const x = Math.max(0, Math.min(w - 1, fx)); + const y = Math.max(0, Math.min(h - 1, fy)); + if (spawnFootprintFitsPlay(md, x, y)) return { x, y }; + return pickRandomSpawnFromMapPlay(md); + } + if (mode === 'slots6') { + const slots = parseLobbyPlayerSpawnsFromMapPlay(md); + augmentLobbySlotsFromShooterPaintJumpSurvivePlay(md, slots); + const pick = slots[j]; + if (pick && spawnFootprintFitsPlay(md, pick.x, pick.y)) return { x: pick.x, y: pick.y }; + return pickRandomSpawnFromMapPlay(md); + } + return pickRandomSpawnFromMapPlay(md); + } + + const GAUNTLET_PREVIEW_MAX = 6; + function gauntletSpawnYsPlay(md, playerCount) { + const h = md.height || 15; + const lo = 1; + const hi = Math.max(lo, h - 2); + const n = Math.min(GAUNTLET_PREVIEW_MAX, Math.max(1, playerCount)); + if (hi <= lo) return Array.from({ length: n }, () => lo); + const ys = []; + for (let i = 0; i < n; i++) { + ys.push(Math.round(lo + (i * (hi - lo)) / Math.max(1, n - 1))); + } + return ys; + } + function collectGauntletSpawnSlotsFromSpawnAreaPlay(md) { + const grid = md.spawnArea; + if (!grid || !Array.isArray(grid)) return []; + const w = md.width || 20; + const h = md.height || 15; + const slots = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && spawnTileWalkablePlay(md, x, y)) slots.push({ x, y }); + } + } + slots.sort((a, b) => a.y - b.y || a.x - b.x); + return slots; + } + function collectGauntletSpawnSlotsPlay(md) { + const explicit = md.gauntletPlayerSpawns; + if (Array.isArray(explicit) && explicit.length > 0) { + const w = md.width || 20; + const h = md.height || 15; + const slots = []; + for (const raw of explicit) { + if (slots.length >= GAUNTLET_PREVIEW_MAX) break; + const x = Math.floor(Number(raw && raw.x)); + const y = Math.floor(Number(raw && raw.y)); + if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x >= w || y < 0 || y >= h) continue; + if (!spawnTileWalkablePlay(md, x, y)) continue; + slots.push({ x, y }); + } + if (slots.length > 0) return slots; + } + return collectGauntletSpawnSlotsFromSpawnAreaPlay(md); + } + function gauntletResolveSpawnXForRowPlay(md, spawnColX, y, slotXFallback) { + const w = md.width || 20; + if (spawnTileWalkablePlay(md, spawnColX, y)) return spawnColX; + if (slotXFallback != null && spawnTileWalkablePlay(md, slotXFallback, y)) return slotXFallback; + for (let d = 0; d < w; d++) { + if (spawnColX + d < w && spawnTileWalkablePlay(md, spawnColX + d, y)) return spawnColX + d; + if (spawnColX - d >= 0 && spawnTileWalkablePlay(md, spawnColX - d, y)) return spawnColX - d; + } + return Math.max(0, Math.min(w - 1, spawnColX)); + } + function gauntletSpawnPositionsPlay(md, playerCount) { + const n = Math.min(GAUNTLET_PREVIEW_MAX, Math.max(1, playerCount)); + const slots = collectGauntletSpawnSlotsPlay(md); + const fallbackYs = gauntletSpawnYsPlay(md, n); + const spawnColX = slots.length ? Math.min(...slots.map((s) => s.x)) : 1; + const out = []; + for (let i = 0; i < n; i++) { + const y = i < slots.length + ? slots[i].y + : (fallbackYs[i] != null ? fallbackYs[i] : fallbackYs[fallbackYs.length - 1]); + const slotX = i < slots.length ? slots[i].x : null; + const x = gauntletResolveSpawnXForRowPlay(md, spawnColX, y, slotX); + out.push({ x, y }); + } + return out; + } + /** + * โหมดทดสอบพรมแดง: จัดตำแหน่งเกิดให้ตรง server (spawnArea หรือกระจาย y) + * @param {boolean} [onlyBots=false] ถ้า true = จัดแค่บอท (หลัง game-start; ไม่ทับ me/คนจริงจาก peersSnap) + */ + function applyGauntletPreviewSpawnLayout(onlyBots) { + if (!mapData || mapData.gameType !== 'gauntlet') return; + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + const humanCount = 1 + realIds.length; + const total = Math.min(GAUNTLET_PREVIEW_MAX, humanCount + botIds.length); + const pos = gauntletSpawnPositionsPlay(mapData, total); + if (onlyBots) { + let idx = humanCount; + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + return; + } + let idx = 0; + if (idx < pos.length) { + me.x = pos[idx].x; + me.y = pos[idx].y; + me.tx = me.x; + me.ty = me.y; + idx++; + } + realIds.forEach((rid) => { + const o = others.get(rid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + } + + function isPreviewBotId(id) { + return typeof id === 'string' && id.indexOf(PREVIEW_BOT_PREFIX) === 0; + } + + function countPlayHumans() { + let n = 1; + others.forEach((_, id) => { if (!isPreviewBotId(id)) n++; }); + return n; + } + + function rebalancePreviewBots() { + if (!playBotsEnabled() || !mapData) return; + const human = countPlayHumans(); + const wantBots = Math.max(0, playBotTargetHeadcount() - human); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o) return; + o.playTint = playTintForPreviewBotId(bid) || resolvePlayTintForPeer(bid); + if (o.botTier == null) { + const tierRoll = Math.random(); + const stackPrev = mapData && mapData.gameType === 'stack'; + o.botTier = stackPrev + ? (tierRoll < 0.4 ? 'sharp' : tierRoll < 0.78 ? 'avg' : 'weak') + : (tierRoll < 0.28 ? 'sharp' : tierRoll < 0.62 ? 'avg' : 'weak'); + } + if (o.quizCannotTrue == null) o.quizCannotTrue = false; + if (o.quizCannotFalse == null) o.quizCannotFalse = false; + if (!Array.isArray(o.botPath)) o.botPath = []; + if (o.botAnswerWander == null) o.botAnswerWander = false; + if (o.gauntletJumpTicks == null) o.gauntletJumpTicks = 0; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks; + if (o.gauntletScore == null) { + o.gauntletScore = (mapData && mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) ? 100 : 0; + } + if (o.gauntletEliminated == null) o.gauntletEliminated = false; + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)]; + o.botWanderDx = wd[0]; + o.botWanderDy = wd[1]; + } + if (typeof o.botWanderNextTurn !== 'number') { + o.botWanderNextTurn = Date.now() + 400 + Math.floor(Math.random() * 900); + } + }); + let botIds = [...others.keys()].filter(isPreviewBotId); + while (botIds.length > wantBots) { + const drop = botIds.pop(); + if (drop) others.delete(drop); + } + botIds = [...others.keys()].filter(isPreviewBotId); + while (botIds.length < wantBots) { + /* หา slot ว่างเล็กสุด (0,1,2,…) — IDX ของ ID ต้องตรงกับ playLobbyBotThemes[idx] เพื่อให้สีตรง lobby */ + let nextSlot = 0; + while (others.has(PREVIEW_BOT_PREFIX + nextSlot)) nextSlot++; + previewBotSeq = Math.max(previewBotSeq, nextSlot + 1); + const id = PREVIEW_BOT_PREFIX + nextSlot; + const joinIdx = countPlayHumans() + botIds.length; + let x; + let y; + if (mapData.gameType === 'jump_survive') { + const pos = jumpSurviveSpawnWorldFromJoinOrderPlay(mapData, joinIdx); + x = pos.x; + y = pos.y; + } else if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) { + const pos = quizBattleSpawnWorldFromJoinOrderPlay(mapData, joinIdx); + x = pos.x; + y = pos.y; + } else { + const sp = pickSpawnForJoinPlay(mapData, joinIdx); + const jx = (Math.random() - 0.5) * 0.4; + const jy = (Math.random() - 0.5) * 0.4; + x = sp.x + 0.5 + jx; + y = sp.y + 0.5 + jy; + } + const tierRoll = Math.random(); + const stackPrev = mapData && mapData.gameType === 'stack'; + const botTier = stackPrev + ? (tierRoll < 0.4 ? 'sharp' : tierRoll < 0.78 ? 'avg' : 'weak') + : (tierRoll < 0.28 ? 'sharp' : tierRoll < 0.62 ? 'avg' : 'weak'); + const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)]; + const crownStart = mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay(); + others.set(id, { + x, y, tx: x, ty: y, + direction: ['down', 'up', 'left', 'right'][Math.floor(Math.random() * 4)], + nickname: 'บอท', + characterId: pickPreviewBotCharacterId(botIds.length), + spawnJoinOrder: joinIdx, + playTint: playTintForPreviewBotId(id) || resolvePlayTintForPeer(id), + botTier, + gauntletJumpTicks: 0, + gauntletJumpVis: 0, + gauntletScore: crownStart ? 100 : 0, + gauntletEliminated: false, + quizCannotTrue: false, + quizCannotFalse: false, + quizCarryHeld: null, + botPath: [], + botAnswerWander: false, + botWanderDx: wd[0], + botWanderDy: wd[1], + botWanderNextTurn: Date.now() + 400 + Math.floor(Math.random() * 1000), + jumpSurviveEliminated: false, + spaceShooterScore: 0, + balloonBossScore: 0, + balloonBossBossDmg: 0, + balloonBossBalloons: mapData && mapData.gameType === 'balloon_boss' ? balloonBossBalloonsStartPlay() : 5, + balloonBossEliminated: false, + botQuizCarryPathfindAfter: performance.now() + previewBotSeq * 75 + Math.floor(Math.random() * 160), + }); + botIds.push(id); + } + let k = 0; + [...others.keys()].filter(isPreviewBotId).sort().forEach((bid) => { + const o = others.get(bid); + if (!o) return; + const tag = o.botTier === 'sharp' ? '(ฉลาด)' : o.botTier === 'weak' ? '(พลาดบ่อย)' : '(กลาง)'; + o.nickname = 'บอท ' + (++k) + ' ' + tag; + }); + [...others.keys()].filter(isPreviewBotId).sort().forEach((bid, botIdx) => { + const o = others.get(bid); + if (o) o.characterId = pickPreviewBotCharacterId(botIdx); + }); + if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) { + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o) return; + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + o.tx = s.x; + o.ty = s.y; + }); + } + if (mapData.gameType === 'quiz_carry') { + const t0 = performance.now(); + [...others.keys()].filter(isPreviewBotId).forEach((bid, idx) => { + const o = others.get(bid); + if (!o) return; + o.botQuizCarryPathfindAfter = t0 + idx * 90 + Math.floor(Math.random() * 140); + }); + } + if (mapData.gameType === 'gauntlet') { + applyGauntletPreviewSpawnLayout(false); + emitGauntletPreviewRowsToServer(); + } + if (mapData.gameType === 'stack') { + applyStackPreviewSpawnLayout(); + rebuildStackPreviewTurnOrder(); + } + if (mapData.gameType === 'jump_survive') { + applyJumpSurvivePreviewSpawnLayout(false); + } + if (mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + } + if (quizCarryPregameActive && mapData && mapData.gameType === 'quiz_carry') updateQuizCarryPregameHud(); + try { + preloadPlayOccupantsTints(); + preloadPlayLobbyIdleAvatarsForOccupants(); + } catch (e) { /* ignore */ } + } + + function pickRandomWalkableCellInAnswerGrid(grid, o) { + if (!grid || !mapData) return null; + const w = mapData.width || 20, h = mapData.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] !== 1) continue; + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, NaN, NaN, o)) pool.push({ x, y }); + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + /** ช่วงตอบ: บอทสุ่มเดินไปโซนถูก/ผิด/เดินมั่ว ตามระดับ (ฉลาด/กลาง/พลาดบ่อย) */ + function previewBotsPrepareAnswerRound() { + if (!playBotsEnabled() || !mapData || !isQuiz() || !previewQuizCurrent) return; + const correctTrue = !!previewQuizCurrent.answerTrue; + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botPath = []; + o.botAnswerWander = false; + const tier = o.botTier || 'avg'; + let pCorrect = 0.52; + if (tier === 'sharp') pCorrect = 0.84; + else if (tier === 'weak') pCorrect = 0.22; + if (Math.random() < 0.14) { + o.botAnswerWander = true; + return; + } + const wantsCorrect = Math.random() < pCorrect; + const targetTrue = wantsCorrect ? correctTrue : !correctTrue; + const zoneGrid = targetTrue ? qt : qf; + const dest = pickRandomWalkableCellInAnswerGrid(zoneGrid, o); + if (!dest) { + o.botAnswerWander = true; + return; + } + const path = pathfindPlayForBot(o.x, o.y, dest.x + 0.5, dest.y + 0.5, o); + if (!path || path.length <= 1) { + o.botAnswerWander = true; + return; + } + o.botPath = path.slice(1); + }); + } + + /** คัดลอกตรรกะจาก server — ดีดออกนอกโซนจริง/เท็จ (โหมดทดสอบ preview บน play.html) */ + function findNearestOutsideQuizAnswerZonesPlay(md, sx, sy) { + const w = md.width || 20; + const h = md.height || 15; + const tGrid = md.quizTrueArea || []; + const fGrid = md.quizFalseArea || []; + const inAnswer = (x, y) => quizCellOnPlay(tGrid, x, y) || quizCellOnPlay(fGrid, x, y); + const walkable = (x, y) => { + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + return row && row[x] !== 1; + }; + const fx = Math.floor(Number(sx)); + const fy = Math.floor(Number(sy)); + if (!walkable(fx, fy)) { + const sp = md.spawn || { x: 1, y: 1 }; + return { x: (typeof sp.x === 'number' ? sp.x : 1) + 0.5, y: (typeof sp.y === 'number' ? sp.y : 1) + 0.5 }; + } + if (!inAnswer(fx, fy)) return { x: sx, y: sy }; + const q = [[fx, fy]]; + const seen = new Set([`${fx},${fy}`]); + const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; + while (q.length) { + const [cx, cy] = q.shift(); + for (let di = 0; di < dirs.length; di++) { + const nx = cx + dirs[di][0]; + const ny = cy + dirs[di][1]; + const k = `${nx},${ny}`; + if (seen.has(k)) continue; + if (!walkable(nx, ny)) continue; + seen.add(k); + if (!inAnswer(nx, ny)) { + return { x: nx + 0.5, y: ny + 0.5 }; + } + q.push([nx, ny]); + } + } + const sp = md.spawn || { x: 1, y: 1 }; + return { x: (typeof sp.x === 'number' ? sp.x : 1) + 0.5, y: (typeof sp.y === 'number' ? sp.y : 1) + 0.5 }; + } + + function quizResolveChoiceFromStanding(ox, oy, correctTrue) { + const tx = Math.floor(ox); + const ty = Math.floor(oy); + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + const inT = quizCellOnPlay(qt, tx, ty); + const inF = quizCellOnPlay(qf, tx, ty); + let choice = null; + if (inT && !inF) choice = true; + else if (inF && !inT) choice = false; + else if (inT && inF) choice = true; + const right = choice !== null && choice === correctTrue; + return { choice, right }; + } + + /** ลำดับจุดเกิดเดียวกับ pickSpawnForJoinPlay — ห้ามสุ่มทั้ง spawnArea หลังตอบผิด (จะหลุดจาก P1–P6) */ + function quizPreviewJoinOrderForRespawn(actorId) { + if (actorId === myId) { + const jm = Number(me && me.spawnJoinOrder); + return Number.isFinite(jm) ? Math.max(0, Math.floor(jm)) : 0; + } + const o = others.get(actorId); + if (o && typeof o.spawnJoinOrder === 'number' && Number.isFinite(o.spawnJoinOrder)) { + return Math.max(0, Math.floor(o.spawnJoinOrder)); + } + if (!isPreviewBotId(actorId)) { + const othersReal = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const idx = othersReal.indexOf(actorId); + return idx >= 0 ? 1 + idx : 1; + } + const bots = [...others.keys()].filter(isPreviewBotId).sort(); + const bi = bots.indexOf(actorId); + if (bi < 0) return 0; + return countPlayHumans() + bi; + } + + /** เฉลยรอบทดสอบบนเครื่อง — ให้พฤติกรรมใกล้เคียง server (ผิด = ล็อกโซน + ดีดออก) */ + function resolvePreviewQuizRound() { + if (!previewMode || !mapData || !isQuiz() || !previewQuizCurrent || myId == null) return; + const correctTrue = !!previewQuizCurrent.answerTrue; + const results = []; + const mine = quizResolveChoiceFromStanding(me.x, me.y, correctTrue); + let right = mine.right; + let choice = mine.choice; + if (mine.right) { + playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + QUIZ_TF_POINTS_PER_CORRECT; + } else { + playQuizPlayerLocal.cannotTrue = true; + playQuizPlayerLocal.cannotFalse = true; + const ordMe = quizPreviewJoinOrderForRespawn(myId); + const sp = pickSpawnForJoinPlay(mapData, ordMe); + const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5); + me.x = pos.x; + me.y = pos.y; + me.tx = me.x; + me.ty = me.y; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + results.push({ + id: myId, + right, + choice, + score: playLiveQuizScores[myId] != null ? playLiveQuizScores[myId] : 0, + }); + if (playBotsEnabled()) { + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + const br = quizResolveChoiceFromStanding(o.x, o.y, correctTrue); + o.botPath = []; + o.botAnswerWander = false; + if (br.right) { + playLiveQuizScores[id] = (playLiveQuizScores[id] || 0) + QUIZ_TF_POINTS_PER_CORRECT; + } else { + o.quizCannotTrue = true; + o.quizCannotFalse = true; + const ordB = quizPreviewJoinOrderForRespawn(id); + const sp = pickSpawnForJoinPlay(mapData, ordB); + const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5); + o.x = pos.x + (Math.random() - 0.5) * 0.35; + o.y = pos.y + (Math.random() - 0.5) * 0.35; + o.tx = o.x; + o.ty = o.y; + } + results.push({ + id, + right: br.right, + choice: br.choice, + score: playLiveQuizScores[id] != null ? playLiveQuizScores[id] : 0, + }); + }); + } + const r = { results, scores: { ...playLiveQuizScores } }; + results.forEach(function (row) { + if (!row || row.id == null) return; + if (!row.right) playQuizEverWrong[String(row.id)] = true; + }); + renderPlayQuizScoreboard(playLiveQuizScores); + if (results.some(function (row) { return row && row.right; })) { + spawnQuizTrueFalseScorePopupPlay(correctTrue); + } + showPlayQuizFeedback(r); + } + + function advancePreviewQuizIfDue() { + if (isQuizQuestionMissionUiMapPlay() && isQuizQuestionMissionPregameBlockingPlay()) return; + if (previewQuizStep === 'done') return; + if (!previewMode || !isQuiz() || !playQuizPhaseEndsAt || Date.now() < playQuizPhaseEndsAt) return; + const pool = previewQuizPool; + if (!pool || !pool.length) return; + if (previewQuizStep === 'read') { + applyPreviewAnswerPhase(); + return; + } + if (previewQuizStep === 'answer') { + resolvePreviewQuizRound(); + applyPreviewBetweenPhase(); + return; + } + if (previewQuizStep === 'between') { + previewQuizQIndex += 1; + if (previewQuizQIndex >= pool.length) { + finishPreviewQuizSessionPlay(); + return; + } + const q = pool[previewQuizQIndex]; + if (q) applyPreviewReadPhase(q, pool.length); + } + } + + async function loadPreviewQuizAndStart() { + resetPreviewBotsQuizState(); + playQuizEverWrong = {}; + let settings = null; + try { + const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' }); + if (r.ok) settings = await r.json(); + } catch (e) { /* use map fallback */ } + let pool = []; + if (settings && Array.isArray(settings.questions) && settings.questions.length) { + pool = settings.questions + .filter((q) => q && String(q.text || '').trim()) + .map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue })); + } + if (!pool.length && mapData) pool = buildQuizPoolFromMap(mapData); + const cap = clampPreviewQuizRoundQuestionCount(settings); + const shuffled = shufflePreviewQuizQuestionsPlay(pool); + previewQuizPool = shuffled.slice(0, Math.min(cap, shuffled.length)); + previewQuizQIndex = 0; + const dRead = 10000; + const dAns = 5000; + const dBet = 3500; + previewQuizTiming = { + readMs: clampPreviewMs(settings && settings.readMs, dRead, 1000, 300000), + answerMs: clampPreviewMs(settings && settings.answerMs, dAns, 1000, 300000), + betweenMs: clampPreviewMs(settings && settings.betweenMs, dBet, 0, 300000), + }; + previewQuizCurrent = null; + if (!pool.length) { + playQuizPhaseLocal = 'read'; + previewQuizStep = 'read'; + playQuizText = 'ยังไม่มีคำถามในชุด — ตั้งคำถามใน Admin → คำถามเกม หรือในแมป (quizQuestions)'; + playQuizPhaseEndsAt = 0; + playQuizQuestionIndex = 0; + playQuizQuestionTotal = 0; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] ไม่มีคำถามในระบบ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'โซนสีบนแผนที่ยังใช้ดูเลย์เอาต์ได้ตามปกติ'; + initPlayLiveQuizScoresZeros(); + startPlayQuizTimer(); + return; + } + const session = previewQuizPool; + const q = session[0]; + if (q) applyPreviewReadPhase(q, session.length); + initPlayLiveQuizScoresZeros(); + startPlayQuizTimer(); + } + + function startPlayQuizTimer() { + if (playQuizTimerInterval) clearInterval(playQuizTimerInterval); + playQuizTimerInterval = setInterval(() => { + updatePlayQuizTimerDisplay(); + advancePreviewQuizIfDue(); + }, 200); + updatePlayQuizTimerDisplay(); + } + + function teardownPlayQuizUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) ov.classList.add('is-hidden'); + const panel = document.getElementById('quiz-map-question-panel'); + const mapQText = document.getElementById('quiz-map-question-text'); + if (panel) { + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, mapQText); + } + playQuizPhaseLocal = null; + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + playQuizText = ''; + playQuizPhaseEndsAt = 0; + playQuizQuestionIndex = 0; + playQuizQuestionTotal = 0; + previewQuizPool = []; + previewQuizQIndex = 0; + previewQuizCurrent = null; + previewQuizStep = 'read'; + playLiveQuizScores = {}; + playQuizEverWrong = {}; + quizTfScorePopups = []; + playPath = []; + hidePlayQuizHud(); + quizCarryJoinSettingsSnap = null; + quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT; + quizCarryMapPanelTheme = null; + quizMapPanelTheme = null; + quizCarryChoicePlaqueThemes = null; + resetQuizCarryPlayState(); + resetQuizBattlePlayState(); + quizQuestionMissionPhase = null; + quizQuestionMissionDeferredPhase = null; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + const gcmTeardown = document.getElementById('gauntlet-crown-mission-overlay'); + if (gcmTeardown) gcmTeardown.classList.add('is-hidden'); + cancelEmbedPreviewLobbyReturnTimer(); + const grabBtnTeardown = document.getElementById('quiz-carry-grab-btn'); + if (grabBtnTeardown) { + grabBtnTeardown.classList.add('is-hidden'); + grabBtnTeardown.classList.remove('quiz-carry-grab-btn--active'); + grabBtnTeardown.classList.remove('quiz-carry-grab-btn--place'); + grabBtnTeardown.style.pointerEvents = ''; + const im = grabBtnTeardown.querySelector('img'); + if (im) im.src = '/Game/img/quiz-carry/btn-grab.png'; + } + const gchJumpTeardown = document.getElementById('gauntlet-crown-jump-btn'); + if (gchJumpTeardown) { + gchJumpTeardown.classList.add('is-hidden'); + gchJumpTeardown.setAttribute('aria-hidden', 'true'); + gchJumpTeardown.style.pointerEvents = ''; + } + const stackDropTeardown = document.getElementById('stack-tower-drop-btn'); + if (stackDropTeardown) { + stackDropTeardown.classList.add('is-hidden'); + stackDropTeardown.setAttribute('aria-hidden', 'true'); + stackDropTeardown.style.pointerEvents = ''; + stackDropTeardown.classList.remove('stack-tower-drop-btn--active'); + } + hideStackTowerResultFlashDomOnlyPlay(); + } + + function setupPlayQuizUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) { + const hideBottomQuizBar = (previewMode && editorEmbedReturn && !isQuizCarry()) + || (isQuiz() && isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'live'); + if (hideBottomQuizBar) ov.classList.add('is-hidden'); + else ov.classList.remove('is-hidden'); + } + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + const leg = document.getElementById('quiz-play-legend'); + if (previewMode) { + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] กำลังโหลดคำถาม…'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'ดึงชุดคำถามจาก /api/quiz-settings (หรือจากแมป)…'; + if (leg) leg.textContent = ''; + if (isQuizQuestionMissionUiMapPlay()) { + if (phaseEl) phaseEl.textContent = '[ทดสอบ] รอกด READY เพื่อเริ่มภารกิจ'; + if (qEl) qEl.textContent = 'โหมดภารกิจ — หน้าปก HOWTO จาก /img/QUESTION/'; + if (leg) leg.textContent = ''; + } else { + loadPreviewQuizAndStart(); + } + } else { + playQuizPhaseLocal = null; + playQuizText = 'รอคำถามจากโฮสต์…'; + playQuizPhaseEndsAt = 0; + playQuizQuestionIndex = 0; + playQuizQuestionTotal = 0; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'แมพตอบคำถาม'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'เข้าร่วมห้องแล้ว — เมื่อโฮสต์เริ่มเกม ข้อความและเวลาจะอัปเดตที่นี่ (เล่นจริงแนะนำผ่าน room-lobby)'; + if (leg) leg.textContent = 'โซนสีบนแผนที่คือจุดตอบเหมือนใน Lobby'; + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + } + if (isQuiz() && !isQuizCarry()) { + (async () => { + try { + let s = {}; + const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' }); + if (r.ok) { + const raw = await r.text(); + const trimmed = (raw || '').trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + try { + s = JSON.parse(trimmed); + } catch (e) { /* ignore */ } + } + } + s = await mergeQuizCarrySettingsFromDisk(s && typeof s === 'object' ? s : {}); + setQuizMapPanelThemeFromApi(s); + try { + syncPlayQuizMapPanel(); + } catch (e2) { /* ignore */ } + } catch (e) { /* ignore */ } + })(); + } + } + + /** อัปเดตแถบคำถาม quiz จากเซิร์ฟเวอร์ — แยกไว้ให้ภารกิจ mng8a80o เรียกหลังจบ countdown */ + function applyQuizPhaseFromServer(p) { + if (!p || !mapData || !isQuiz()) return; + if (p.text) playQuizText = p.text; + playQuizPhaseLocal = p.phase; + playQuizPhaseEndsAt = p.endsAt || 0; + playQuizQuestionIndex = Math.max(0, Number(p.questionIndex) || 0); + playQuizQuestionTotal = Math.max(0, Number(p.questionTotal) || 0); + const phaseEl = document.getElementById('quiz-game-phase-label'); + const qEl = document.getElementById('quiz-game-question'); + if (phaseEl) { + phaseEl.textContent = p.phase === 'read' + ? ('อ่านคำถาม ข้อ ' + (p.questionIndex || '') + '/' + (p.questionTotal || '')) + : ('เดินไปโซน ถูก / ผิด — ข้อ ' + (p.questionIndex || '') + '/' + (p.questionTotal || '')); + } + if (qEl) qEl.textContent = playQuizText || ''; + const ov = document.getElementById('quiz-game-overlay'); + if (isQuizQuestionMissionHudActivePlay()) { + if (ov) ov.classList.add('is-hidden'); + } else if (ov) { + ov.classList.remove('is-hidden'); + } + if (playQuizTimerInterval) clearInterval(playQuizTimerInterval); + playQuizTimerInterval = setInterval(updatePlayQuizTimerDisplay, 200); + updatePlayQuizTimerDisplay(); + if (!previewMode && isQuiz() && !Object.keys(playLiveQuizScores).length) initPlayLiveQuizScoresZeros(); + if (!previewMode && isQuiz() && p.phase === 'read' && Number(p.questionIndex) === 1) playQuizEverWrong = {}; + try { + syncPlayQuizMapPanel(); + } catch (eQm) { /* ignore */ } + } + + function isFrogger() { return mapData && mapData.gameType === 'frogger'; } + function isGauntlet() { return mapData && mapData.gameType === 'gauntlet'; } + function isGauntletFaceRightMapMno9kb07() { + if (!isGauntlet()) return false; + /** ใช้ currentPlayMapId (session → mapData.id → ?map=) ให้สอดคล้อง runway / timing — กัน HUD crown หลุดค้างเป็นแผงเก่าแนวตั้งทับ TIME */ + return (currentPlayMapId() || '').trim() === GAUNTLET_FACE_RIGHT_MAP_ID; + } + /** Crown heist rules + UI (เดียวกับหันขวา — ฉาก mno9kb07) */ + function isGauntletCrownHeistMapPlay() { + return isGauntletFaceRightMapMno9kb07(); + } + function isMegaVirusMissionShellMapPlay() { + return !!(mapData && mapData.gameType === 'balloon_boss' && currentPlayMapId() === BALLOON_BOSS_MISSION_MAP_ID); + } + /** Crown gauntlet หรือ Mega Virus — lobby HOWTO / Ready / นับถอยหลัง / held run */ + function usesCrownLobbyShellPlay() { + return isGauntletCrownHeistMapPlay() || isMegaVirusMissionShellMapPlay(); + } + /** Last Light (mno9kb07) embed — ค่าเริ่มต้นเมื่อโหลดแมป (ปรับซูมได้ด้วยล้อ / [ ]) */ + const PLAY_EMBED_LAST_LIGHT_DEFAULT_ZOOM_MUL = 1.49; + /** Stack Tower (mnn93hpi) ใน embed พรีวิว: ซูมคงที่ ×1 — ล้อ/ปุ่มไม่ปรับได้ */ + const PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL = 1; + function isStackTowerEmbedZoomLockedPlay() { + return !!(previewMode && editorEmbedReturn && mapData && isStackTowerMissionUiMapPlay()); + } + function applyPlayEmbedZoomForCurrentMapPlay() { + if (isStackTowerEmbedZoomLockedPlay()) { + playEmbedUserZoomMul = PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL; + } else if (previewMode && editorEmbedReturn && mapData && isGauntletCrownHeistMapPlay()) { + playEmbedUserZoomMul = PLAY_EMBED_LAST_LIGHT_DEFAULT_ZOOM_MUL; + } + } + + /** smoothstep 0..1 */ + function stackTowerPost50SmoothstepPlay(t) { + const x = Math.max(0, Math.min(1, t)); + return x * x * (3 - 2 * x); + } + + /** Tower live + progress ≥เกณฑ์: 0→1 ระหว่าง STACK_TOWER_POST50_ANIM_MS — ใช้เฉพาะเมื่อเปิดกล้องตามหอ (ไม่งั้นฉากธรรมดาไม่เลื่อน/ซูมหลังเกณฑ์) */ + function getStackTowerPost50AnimEasePlay() { + if (!getStackTowerCameraFollowPlayConfig().enabled) { + stackTowerPost50AnimStartMs = null; + return 0; + } + if (!isStackTowerMissionUiMapPlay() || stackTowerMissionPhase !== 'live' || !stackMini) return 0; + const p = Number(stackMini.progressPct) || 0; + if (p < STACK_TOWER_POST50_PROGRESS_THRESH) { + stackTowerPost50AnimStartMs = null; + return 0; + } + if (stackTowerPost50AnimStartMs == null) stackTowerPost50AnimStartMs = performance.now(); + const u = Math.min(1, Math.max(0, (performance.now() - stackTowerPost50AnimStartMs) / STACK_TOWER_POST50_ANIM_MS)); + return stackTowerPost50SmoothstepPlay(u); + } + + function getStackTowerPost50ZMulPlay() { + const e = getStackTowerPost50AnimEasePlay(); + if (e <= 0) return 1; + return 1 + (STACK_TOWER_POST50_ZOOM_MUL - 1) * e; + } + + function getStackTowerPost50BgScrollExtraPxPlay(ch) { + const e = getStackTowerPost50AnimEasePlay(); + if (e <= 0) return 0; + const chR = Math.max(1, Number(ch) || 720); + return STACK_TOWER_POST50_MAP_SHIFT_SCREEN_FRAC * chR * e; + } + + /** + * Same zDraw chain as draw() (zoom, special maps, Last Light embed lock, preview embed mul). + * Runway end checks must not rely on lastPlayZDrawForInput alone — it can lag map/canvas or default 1.4 vs real zoom. + */ + function computePlayCameraZDrawPlay() { + if (!canvas || !mapData) return zoom; + const w = mapData.width || 20; + const h = mapData.height || 15; + let zDraw = zoom; + if (isSpaceShooter() || isBalloonBoss()) { + const mwPx = w * tileSize; + const mhPx = h * tileSize; + zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96; + } else if (isQuizQuestionMissionHudActivePlay()) { + const qmc = getQuizQuestionMissionMapCenterWorldPxPlay(); + if (qmc) { + zDraw = Math.min(canvas.width / qmc.mwPx, canvas.height / qmc.mhPx) * 0.96; + } + } else if (isQuizCarry()) { + const mwPx = w * tileSize; + const mhPx = h * tileSize; + zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96; + } + if (isStackTowerEmbedZoomLockedPlay()) { + playEmbedUserZoomMul = PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL; + } + if (previewMode && editorEmbedReturn && mapData && !isQuizQuestionMissionHudActivePlay()) { + zDraw *= playEmbedUserZoomMul; + } + if (isStackTowerMissionUiMapPlay()) { + zDraw *= STACK_TOWER_FIXED_RENDER_Z_MUL; + zDraw *= getStackTowerPost50ZMulPlay(); + } + return zDraw; + } + let gauntletCrownHowtoVisible = false; + function isGauntletCrownPregameBlockingPlay() { + if (!usesCrownLobbyShellPlay()) return false; + if (gauntletCrownRunHeldRemote) return true; + if (gauntletCrownPregamePhase != null && gauntletCrownPregamePhase !== 'live') return true; + return false; + } + + /** + * Last Light (crown heist): center camera on the whole party bbox instead of only `me` + * — reduces empty void on the side when you fall behind and keeps bots + you in frame together when possible. + */ + function getGauntletCrownHeistGroupCameraCenterPxPlay(tileSizeRef, canvasW, canvasH, zDrawVal) { + if (!isGauntletCrownHeistMapPlay() || !mapData) return null; + const md = mapData; + const { cw, ch } = getCharacterFootprintWH(md); + const ww = md.width || 20; + const hh = md.height || 15; + const mapWpx = ww * tileSizeRef; + const mapHpx = hh * tileSizeRef; + const halfW = canvasW / (2 * zDrawVal); + const halfH = canvasH / (2 * zDrawVal); + const ents = []; + if (!me.gauntletEliminated && Number.isFinite(me.x) && Number.isFinite(me.y)) { + ents.push({ cx: (me.x + cw * 0.5) * tileSizeRef, cy: (me.y + ch * 0.5) * tileSizeRef }); + } + others.forEach((o, id) => { + if (!o || o.gauntletEliminated) return; + if (!playBotsEnabled() && isPreviewBotId(id)) return; + if (!Number.isFinite(o.x) || !Number.isFinite(o.y)) return; + ents.push({ cx: (o.x + cw * 0.5) * tileSizeRef, cy: (o.y + ch * 0.5) * tileSizeRef }); + }); + if (!ents.length) return null; + let minCx = Infinity; + let maxCx = -Infinity; + let sumCy = 0; + for (let i = 0; i < ents.length; i++) { + const e = ents[i]; + minCx = Math.min(minCx, e.cx); + maxCx = Math.max(maxCx, e.cx); + sumCy += e.cy; + } + let px = (minCx + maxCx) * 0.5; + let py = sumCy / ents.length; + const minCamX = halfW; + const maxCamX = Math.max(minCamX, mapWpx - halfW); + const minCamY = halfH; + const maxCamY = Math.max(minCamY, mapHpx - halfH); + const viewW = halfW * 2; + const viewH = halfH * 2; + /* + * ถ้ามุมมองกว้าง/สูงกว่าแมป: อย่าใช้ clamp แบบ minCamX (= กล้องชิดซ้ายโลก) — จะดูเหมือนแมปติดมุมซ้ายบน (เด่นใน embed / ซูมออก) + * ให้จัดกลางแมปแทน — ภารกิจคำถาม mng8a80o ช่วง live ใช้กล้องกลางแมปเหมือนกัน (ดู isQuizQuestionMissionHudActivePlay + draw) + */ + if (viewW >= mapWpx) px = mapWpx * 0.5; + else px = Math.max(minCamX, Math.min(maxCamX, px)); + if (viewH >= mapHpx) py = mapHpx * 0.5; + else py = Math.max(minCamY, Math.min(maxCamY, py)); + return { px, py }; + } + + /** Preview bots only — full tail-off loss needs server support for real humans */ + const GAUNTLET_CROWN_TAIL_OFFSCREEN_MARGIN_PX = 40; + function maybeEliminateGauntletPreviewBotsTailOffCameraPlay(camX, camY, zDrawVal) { + if (!playBotsEnabled() || !isGauntletCrownHeistMapPlay() || !mapData) return; + if (gauntletCrownPregamePhase !== 'live') return; + const halfW = canvas.width / (2 * zDrawVal); + const worldMinX = camX - halfW; + const gate = worldMinX - GAUNTLET_CROWN_TAIL_OFFSCREEN_MARGIN_PX; + const { cw } = getCharacterFootprintWH(mapData); + const ts = mapData.tileSize || 32; + others.forEach((o, id) => { + if (!isPreviewBotId(id) || !o || o.gauntletEliminated) return; + const rightWorld = (Number(o.x) + cw) * ts; + if (rightWorld < gate) o.gauntletEliminated = true; + }); + } + + function isStack() { return mapData && mapData.gameType === 'stack'; } + function isJumpSurvive() { return mapData && mapData.gameType === 'jump_survive'; } + + function currentPlayMapId() { + const q = (playSessionMapId || '').trim(); + if (q) return q; + if (mapData && mapData.id != null && String(mapData.id).trim() !== '') return String(mapData.id).trim(); + return (playMapIdFromQuery || '').trim(); + } + + function isQuizQuestionMissionUiMapPlay() { + return isQuiz() && currentPlayMapId() === QUIZ_QUESTION_MISSION_MAP_ID; + } + + function isQuizQuestionMissionPregameBlockingPlay() { + if (!isQuizQuestionMissionUiMapPlay()) return false; + return quizQuestionMissionPhase != null && quizQuestionMissionPhase !== 'live' && quizQuestionMissionPhase !== 'ended'; + } + + function isStackTowerMissionUiMapPlay() { + return isStack() && currentPlayMapId() === STACK_TOWER_MISSION_MAP_ID; + } + + function getStackTowerBlockWorldScalePlay() { + return isStackTowerMissionUiMapPlay() ? STACK_TOWER_BLOCK_WORLD_SCALE : 1; + } + + function isStackTowerMissionPregameBlockingPlay() { + if (!isStackTowerMissionUiMapPlay()) return false; + return stackTowerMissionPhase != null && stackTowerMissionPhase !== 'live' && stackTowerMissionPhase !== 'ended'; + } + + function isStackTowerMissionHudActivePlay() { + return isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'live'; + } + + /** mnptfts2 Jumper — ใช้ cyber HUD แบบภารกิจคำถาม (TIME plaque + SCORE แถว QM + กรอบโปรไฟล์) */ + function isJumpSurviveMissionHudActivePlay() { + return isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'live'; + } + + /** Space Shooter mnpz6rkp — ช่วงเล่นจริง: HUD เดียวกับ Stack mission (คลาส question-mission + TIME plaque) */ + function isSpaceShooterMissionHudActivePlay() { + return isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'live'; + } + + function stackTowerAssetUrl(file) { + return BASE + '/img/TowerBlock/' + String(file || '').replace(/^\/+/, ''); + } + + function ensureStackTowerSlingImagePlay() { + if (!stackTowerSlingImg) { + stackTowerSlingImg = new Image(); + stackTowerSlingImg.decoding = 'async'; + stackTowerSlingImg.src = stackTowerAssetUrl('sling.png'); + } + const im = stackTowerSlingImg; + return im && im.complete && im.naturalWidth > 0 ? im : null; + } + + function ensureStackTowerScorePopupImagesPlay() { + if (!stackTowerScorePlus10Img) { + stackTowerScorePlus10Img = new Image(); + stackTowerScorePlus10Img.decoding = 'async'; + stackTowerScorePlus10Img.src = stackTowerAssetUrl('score+10.png'); + } + if (!stackTowerScorePlus20Img) { + stackTowerScorePlus20Img = new Image(); + stackTowerScorePlus20Img.decoding = 'async'; + stackTowerScorePlus20Img.src = stackTowerAssetUrl('score+20.png'); + } + return { p10: stackTowerScorePlus10Img, p20: stackTowerScorePlus20Img }; + } + + function ensureStackTowerPerfectFxImagesPlay() { + if (!stackTowerScoreLightImg) { + stackTowerScoreLightImg = new Image(); + stackTowerScoreLightImg.decoding = 'async'; + stackTowerScoreLightImg.src = stackTowerAssetUrl('score-light.png'); + } + if (!stackTowerScoreX2Img) { + stackTowerScoreX2Img = new Image(); + stackTowerScoreX2Img.decoding = 'async'; + stackTowerScoreX2Img.src = stackTowerAssetUrl('scorex2.png'); + } + return { light: stackTowerScoreLightImg, x2: stackTowerScoreX2Img }; + } + + /** score-light หลัง scorex2 — วาดเป็นคู่ข้างชั้นบน (mock รูป2) */ + function drawStackTowerPerfectX2ClusterPlay(ctx, lightImg, x2Img, sx, sy, drawW, drawH, nowT, fxUntil) { + if (!x2Img || !x2Img.complete || !x2Img.naturalWidth || drawW <= 0 || drawH <= 0) return; + const iw = x2Img.naturalWidth; + const ih = x2Img.naturalHeight; + const targetH = Math.max(drawH * 0.92, Math.min(drawH * 1.35, 72)); + const scale = targetH / ih; + const dw = iw * scale; + const dh = ih * scale; + const cx = sx + drawW + drawW * 0.06; + const cy = sy + drawH * 0.5; + const life = fxUntil != null ? Math.max(0, Math.min(1, (fxUntil - nowT) / STACK_TOWER_PERFECT_X2_MS)) : 1; + const pulse = 0.4 + 0.48 * Math.sin((1 - life) * Math.PI); + if (lightImg && lightImg.complete && lightImg.naturalWidth) { + const lp = 1.06; + const lw = dw * lp; + const lh = dh * lp; + const lnx = lightImg.naturalWidth; + const lny = lightImg.naturalHeight; + ctx.save(); + ctx.globalAlpha = Math.min(0.9, pulse + 0.12); + ctx.globalCompositeOperation = 'screen'; + try { + ctx.drawImage(lightImg, 0, 0, lnx, lny, cx - lw * 0.5, cy - lh * 0.5, lw, lh); + } catch (e) { /* ignore */ } + ctx.restore(); + } + ctx.save(); + ctx.globalAlpha = 0.98; + ctx.globalCompositeOperation = 'source-over'; + try { + ctx.drawImage(x2Img, 0, 0, iw, ih, cx - dw * 0.48, cy - dh * 0.5, dw, dh); + } catch (e) { /* ignore */ } + ctx.restore(); + } + + function drawStackTowerScorePopupsPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY) { + if (!m.scorePopups || !m.scorePopups.length) return; + const now = performance.now(); + const dur = STACK_TOWER_SCORE_POPUP_MS; + const imgs = ensureStackTowerScorePopupImagesPlay(); + m.scorePopups = m.scorePopups.filter((p) => p.until > now); + const hidPop = getStackTowerVisualHiddenLayerCountPlay(); + for (let pi = 0; pi < m.scorePopups.length; pi++) { + const p = m.scorePopups[pi]; + const li = Math.floor(Number(p.layerIndex)) || 0; + if (li < 0 || li >= m.layers.length) continue; + if (li < hidPop) continue; + const L = m.layers[li]; + const lw = (L.w != null ? L.w : m.initialWidthTiles) * tileSize * getStackTowerBlockWorldScalePlay(); + const cxW = L.cx * tileSize; + const yb = floorY - (li + 1) * layerWorldH; + const xl = cxW - lw / 2; + const [sx, sy] = worldToScreen(xl, yb); + const drawW = lw * zoom; + const drawH = layerWorldH * zoom; + const t0 = p.until - dur; + const t = Math.max(0, Math.min(1, (now - t0) / dur)); + const alpha = 0.2 + 0.78 * Math.sin(Math.PI * t); + const rise = Math.sin((t * Math.PI) / 2) * 20 * zoom; + const im = p.kind === '20' ? imgs.p20 : imgs.p10; + const targetW = Math.min(drawW * 0.92, Math.max(52, 64 * zoom)); + let iw = targetW; + let ih = targetW * 0.38; + if (im && im.complete && im.naturalWidth > 0 && im.naturalHeight > 0) { + ih = (targetW * im.naturalHeight) / im.naturalWidth; + } + const cx = sx + drawW / 2; + const cy = sy - Math.max(8, 6 * zoom) - rise - ih * 0.5; + ctx.save(); + ctx.globalAlpha = Math.min(0.98, Math.max(0.08, alpha)); + if (im && im.complete && im.naturalWidth > 0) { + try { + ctx.drawImage(im, 0, 0, im.naturalWidth, im.naturalHeight, cx - iw / 2, cy - ih / 2, iw, ih); + } catch (e) { /* ignore */ } + } else { + ctx.font = 'bold ' + Math.round(Math.max(12, 15 * zoom)) + 'px ui-monospace, monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = p.kind === '20' ? '#7af8ff' : '#b4f9cd'; + ctx.fillText(p.kind === '20' ? '+20' : '+10', cx, cy); + } + ctx.restore(); + } + } + + function ensureStackTowerHeartMinusImgPlay() { + if (!stackTowerHeartMinusImg) { + stackTowerHeartMinusImg = new Image(); + stackTowerHeartMinusImg.decoding = 'async'; + stackTowerHeartMinusImg.src = stackTowerAssetUrl('heart-1.png'); + } + const im = stackTowerHeartMinusImg; + return im && im.complete && im.naturalWidth > 0 ? im : null; + } + + function ensureStackTowerLifeHudImagesPlay() { + if (!stackTowerLifeBarImg) { + stackTowerLifeBarImg = new Image(); + stackTowerLifeBarImg.decoding = 'async'; + stackTowerLifeBarImg.src = stackTowerAssetUrl('life-bar.png'); + } + if (!stackTowerLifeHitImg) { + stackTowerLifeHitImg = new Image(); + stackTowerLifeHitImg.decoding = 'async'; + stackTowerLifeHitImg.src = stackTowerAssetUrl('life-hit.png'); + } + if (!stackTowerLifeFullImg) { + stackTowerLifeFullImg = new Image(); + stackTowerLifeFullImg.decoding = 'async'; + stackTowerLifeFullImg.src = stackTowerAssetUrl('life-full.png'); + } + } + + /** + * สัดส่วนสูงของ life-bar.png ที่เป็นข้อความ "SYSTEM INTEGRITY" (ที่เหลือ = แถวหัวใจแยกวาดเอง ไม่ทับขอบ `[` `]` ใน PNG) + */ + const STACK_TOWER_LIFE_BAR_TITLE_FRAC = 0.38; + + /** + * วาดหัวข้อจาก life-bar.png (ครอปบน) + แถวหัวใจ life-full / life-hit กึ่งกลาง — คืนความสูงที่ใช้จริง + */ + function drawStackTowerLifeIntegrityBarPlay(ctx, destX, destY, maxW, maxH, lifeSlots, livesRem) { + ensureStackTowerLifeHudImagesPlay(); + const bar = stackTowerLifeBarImg; + const fullIm = (stackTowerLifeFullImg && stackTowerLifeFullImg.complete && stackTowerLifeFullImg.naturalWidth > 0) + ? stackTowerLifeFullImg + : ensureStackTowerHeartMinusImgPlay(); + const hitIm = stackTowerLifeHitImg && stackTowerLifeHitImg.complete && stackTowerLifeHitImg.naturalWidth > 0 + ? stackTowerLifeHitImg + : null; + const slots = Math.max(1, Math.min(20, Math.floor(lifeSlots) || 1)); + const lives = Math.max(0, Math.min(slots, Math.floor(livesRem) || 0)); + + if (!bar || !bar.complete || !bar.naturalWidth || maxW <= 4 || maxH <= 4) { + ctx.save(); + ctx.fillStyle = '#bb9af7'; + ctx.font = '600 9px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + let heartStr = ''; + for (let i = 0; i < slots; i++) heartStr += i < lives ? '♥ ' : '♡ '; + ctx.fillText('SYSTEM INTEGRITY ' + heartStr.trim(), destX, destY + maxH * 0.5); + ctx.restore(); + return Math.min(maxH, 28); + } + + const bw0 = bar.naturalWidth; + const bh0 = bar.naturalHeight; + const titleSrcH = Math.max(2, Math.floor(bh0 * STACK_TOWER_LIFE_BAR_TITLE_FRAC)); + const scale = Math.min(maxW / bw0, 1.45); + const bw = bw0 * scale; + const titleDh = titleSrcH * scale; + const gap = 3; + const bx = destX; + const by0 = destY + Math.max(0, (maxH - titleDh - gap - Math.min(40, maxH * 0.45)) * 0.35); + const heartsTop = by0 + titleDh + gap; + const roomHearts = Math.max(10, destY + maxH - heartsTop - 2); + const heartsH = Math.max(18, Math.min(roomHearts, 40, Math.floor(maxH * 0.5))); + + ctx.save(); + try { + ctx.drawImage(bar, 0, 0, bw0, titleSrcH, bx, by0, bw, titleDh); + } catch (e) { /* ignore */ } + + const cy = heartsTop + heartsH * 0.5; + const bracketW = Math.min(14, Math.max(9, bw * 0.07)); + ctx.fillStyle = 'rgba(94, 239, 255, 0.88)'; + ctx.font = '600 ' + Math.round(Math.min(17, heartsH * 0.55)) + 'px ui-monospace, "Cascadia Mono", Consolas, monospace'; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'left'; + ctx.shadowColor = 'rgba(0, 234, 255, 0.45)'; + ctx.shadowBlur = 4; + ctx.fillText('[', bx + 1, cy); + ctx.textAlign = 'right'; + ctx.fillText(']', bx + bw - 1, cy); + ctx.shadowBlur = 0; + + const ix = bx + bracketW + 4; + const iwInner = Math.max(24, bw - (bracketW + 4) * 2); + const slotW = iwInner / slots; + const iconMax = Math.min(heartsH * 0.78, slotW * 0.72, 34); + + for (let i = 0; i < slots; i++) { + const cx = ix + slotW * (i + 0.5); + const useFull = i < lives; + const im = useFull ? fullIm : hitIm; + if (im && im.complete && im.naturalWidth > 0 && im.naturalHeight > 0) { + const nw = im.naturalWidth; + const nh = im.naturalHeight; + let ih = iconMax; + let iw = (ih * nw) / nh; + const capW = slotW * 0.82; + const capH = heartsH * 0.88; + if (iw > capW) { + iw = capW; + ih = (iw * nh) / nw; + } + if (ih > capH) { + ih = capH; + iw = (ih * nw) / nh; + } + if (iw < 6 && nh > 0) { + iw = 6; + ih = (iw * nh) / nw; + if (ih > capH) { + ih = capH; + iw = (ih * nw) / nh; + } + } + try { + ctx.drawImage(im, 0, 0, nw, nh, cx - iw / 2, cy - ih / 2, iw, ih); + } catch (e2) { /* ignore */ } + } else { + ctx.fillStyle = useFull ? '#9ece6a' : '#565f89'; + ctx.font = 'bold ' + Math.round(Math.max(10, iconMax * 0.72)) + 'px sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(useFull ? '♥' : '♡', cx, cy); + } + } + ctx.restore(); + const usedH = heartsTop + heartsH - by0 + 2; + return Math.min(maxH, Math.max(titleDh, usedH)); + } + + function triggerStackTowerHeartMinusFxPlay() { + if (!isStackTowerMissionHudActivePlay()) return; + stackTowerHeartMinusFx = { until: performance.now() + STACK_TOWER_HEART_MINUS_FX_MS }; + } + + function drawStackTowerHeartMinusFxPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY, nLay) { + if (!stackTowerHeartMinusFx) return; + const nt = performance.now(); + if (nt >= stackTowerHeartMinusFx.until) { + stackTowerHeartMinusFx = null; + return; + } + const dur = STACK_TOWER_HEART_MINUS_FX_MS; + const t0 = stackTowerHeartMinusFx.until - dur; + const u = Math.max(0, Math.min(1, (nt - t0) / dur)); + const alpha = 1 - u * u; + const worldCx = m.topCenterX * tileSize; + const stackTopY = floorY - nLay * layerWorldH; + const worldCy = stackTopY - tileSize * 1.55 - u * tileSize * 0.5; + const [sx, sy] = worldToScreen(worldCx, worldCy); + const im = ensureStackTowerHeartMinusImgPlay(); + const maxW = Math.max(72, Math.min(240, 6.8 * tileSize * zoom)); + let dw = maxW; + let dh = maxW * 0.52; + if (im && im.naturalWidth > 0 && im.naturalHeight > 0) { + dh = (maxW * im.naturalHeight) / im.naturalWidth; + } + ctx.save(); + ctx.globalAlpha = Math.max(0, Math.min(0.98, alpha)); + if (im) { + ctx.drawImage(im, sx - dw / 2, sy - dh / 2, dw, dh); + } else { + ctx.fillStyle = '#f7768e'; + ctx.font = 'bold ' + Math.round(Math.max(14, 18 * zoom)) + 'px ui-monospace, monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('♥ −1', sx, sy); + } + ctx.restore(); + } + + /** Glow สี่เหลี่ยมสีเหลือง — ขอบเขตเท่ากล่องบล็อก (drawW×drawH) ไม่ล้ำออกนอกสไปรต์ */ + function drawStackTowerPerfectYellowGlowBehindBlockPlay(ctx, sx, sy, drawW, drawH, nowT, untilT) { + if (drawW <= 0 || drawH <= 0) return; + const life = Math.max(0, Math.min(1, (untilT - nowT) / STACK_TOWER_PERFECT_GLOW_MS)); + const pulse = 0.38 + 0.48 * Math.sin((1 - life) * Math.PI); + ctx.save(); + ctx.beginPath(); + ctx.rect(sx, sy, drawW, drawH); + ctx.clip(); + ctx.globalCompositeOperation = 'source-over'; + ctx.shadowColor = 'rgba(255, 210, 40, 0.9)'; + ctx.shadowBlur = Math.max(3, Math.min(10, 4 + pulse * 5)); + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.globalAlpha = Math.min(0.85, 0.4 + pulse * 0.34); + ctx.fillStyle = 'rgba(255, 228, 72, 0.52)'; + ctx.fillRect(sx, sy, drawW, drawH); + ctx.shadowBlur = 0; + ctx.shadowColor = 'transparent'; + ctx.globalAlpha = Math.min(0.72, 0.26 + pulse * 0.32); + ctx.strokeStyle = 'rgba(255, 250, 200, 0.78)'; + const sw = Math.max(1.2, Math.min(2.8, drawW * 0.022)); + ctx.lineWidth = sw; + ctx.strokeRect(sx + sw * 0.5, sy + sw * 0.5, drawW - sw, drawH - sw); + ctx.restore(); + } + + /** + * เชือกจากจุดหนีบบน (จอ) ลงจุดยึดบล็อก — รูป sling แนวตั้ง (แกน Y = สาย) ยืดตามความยาว + */ + function drawStackSlingSegmentPlay(ctx, sx0, sy0, sx1, sy1, zoom) { + const dx = sx1 - sx0; + const dy = sy1 - sy0; + const len = Math.hypot(dx, dy); + if (len < 1.5) return; + const sling = ensureStackTowerSlingImagePlay(); + if (sling) { + const nw = sling.naturalWidth; + const nh = sling.naturalHeight; + const ropeW = Math.max(3, Math.min(40, nw * Math.min(1.1, zoom * 0.11) + 2)); + const ang = Math.atan2(dy, dx); + ctx.save(); + ctx.translate(sx0, sy0); + ctx.rotate(ang - Math.PI / 2); + ctx.globalAlpha = 0.96; + try { + ctx.drawImage(sling, -ropeW / 2, 0, ropeW, len); + } catch (e) { /* ignore */ } + ctx.globalAlpha = 1; + ctx.restore(); + } else { + ctx.strokeStyle = 'rgba(90, 88, 86, 0.82)'; + ctx.lineWidth = Math.max(2.2, zoom * 1.4); + ctx.beginPath(); + ctx.moveTo(sx0, sy0); + ctx.lineTo(sx1, sy1); + ctx.stroke(); + ctx.lineWidth = 1; + } + } + + const GCHOWTO_STACK_TOWER_CLASS = 'gauntlet-crown-howto-overlay--stack-tower'; + + function setGauntletCrownHowtoStackTowerArtMaskPlay(on) { + const hov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!hov) return; + hov.classList.toggle(GCHOWTO_STACK_TOWER_CLASS, !!on); + } + + function hideStackTowerHowtoRulesDom() { + /* กล่องกติกา Stack Tower ถูกถอนออกจาก DOM — เก็บชื่อฟังก์ชันเพื่อไม่ให้จุดเรียกพัง */ + } + + function teardownStackTowerMissionUiPlay() { + stackTowerMissionPhase = null; + stackTowerMissionDeferredPhase = null; + stackTowerSessionStartAt = 0; + stackTowerMissionEndedOnce = false; + stackTowerPost50AnimStartMs = null; + hideStackTowerHowtoRulesDom(); + setGauntletCrownHowtoStackTowerArtMaskPlay(false); + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + hideStackTowerResultFlashDomOnlyPlay(); + } + + function hideStackTowerResultFlashDomOnlyPlay() { + if (stackTowerResultFlashTimer) { + clearTimeout(stackTowerResultFlashTimer); + stackTowerResultFlashTimer = null; + } + const ov = document.getElementById('stack-tower-result-flash'); + const img = document.getElementById('stack-tower-result-flash-img'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + if (img) { + img.onerror = null; + img.removeAttribute('src'); + } + } + + /** โชว์รูปผล Tower จาก TowerBlock 5 วิ แล้วเปิด GCM สรุปเดิม */ + function beginStackTowerResultFlashThenGcm(mission) { + if (!mission || mission.uiSkin !== 'stack_tower') { + showGauntletCrownMissionOverlay(mission); + return; + } + hideStackTowerResultFlashDomOnlyPlay(); + const ov = document.getElementById('stack-tower-result-flash'); + const imgEl = document.getElementById('stack-tower-result-flash-img'); + if (!ov || !imgEl) { + showGauntletCrownMissionOverlay(mission); + return; + } + const outc = String(mission.stackTowerOutcome || 'time_up'); + let file = 'result-timeup.png'; + if (outc === 'decrypt_complete') file = 'result-complete.png'; + else if (outc === 'misses_exhausted') file = 'result-gameover.png'; + const url = stackTowerAssetUrl(file); + const finishToGcm = function () { + hideStackTowerResultFlashDomOnlyPlay(); + showGauntletCrownMissionOverlay(mission); + }; + imgEl.onerror = function () { + imgEl.onerror = null; + finishToGcm(); + }; + imgEl.src = url; + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + stackTowerResultFlashTimer = setTimeout(function () { + stackTowerResultFlashTimer = null; + finishToGcm(); + }, STACK_TOWER_RESULT_FLASH_MS); + } + + /** Jumper mnptfts2: เกรด F = ภาพ gameover · อื่น = complete (ก่อนแผงสรุปผล) */ + function jumpSurviveMissionOutcomeImageFile(mission) { + const g = mission && String(mission.grade || '').toUpperCase(); + if (g === 'F') return 'result-gameover.png'; + return 'result-complete.png'; + } + + /** + * Jumper ภารกิจ: ใช้ #stack-tower-result-flash เหมือน Tower — time_up = timeup แล้ว outcome; + * all_dead = ข้าม timeup แล้ว outcome อย่างเดียว แล้วค่อย GCM + */ + function beginJumpSurviveMissionResultFlashSequenceThenGcm(mission, endKind) { + if (!mission || mission.uiSkin !== 'jumper') { + showGauntletCrownMissionOverlay(mission); + return; + } + hideStackTowerResultFlashDomOnlyPlay(); + const ov = document.getElementById('stack-tower-result-flash'); + const imgEl = document.getElementById('stack-tower-result-flash-img'); + if (!ov || !imgEl) { + showGauntletCrownMissionOverlay(mission); + return; + } + const outcomeFile = jumpSurviveMissionOutcomeImageFile(mission); + const finishToGcm = function () { + hideStackTowerResultFlashDomOnlyPlay(); + showGauntletCrownMissionOverlay(mission); + }; + const showOneFlash = function (file, onDone) { + imgEl.onerror = function () { + imgEl.onerror = null; + onDone(); + }; + imgEl.src = jumperAssetUrl(file); + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + stackTowerResultFlashTimer = setTimeout(function () { + stackTowerResultFlashTimer = null; + onDone(); + }, STACK_TOWER_RESULT_FLASH_MS); + }; + if (endKind === 'time_up') { + showOneFlash('result-timeup.png', function () { + hideStackTowerResultFlashDomOnlyPlay(); + showOneFlash(outcomeFile, finishToGcm); + }); + } else { + showOneFlash(outcomeFile, finishToGcm); + } + } + + /** + * Space Shooter mnpz6rkp: หมดเวลา = แฟลช result-timeup แล้ว result-gameover แล้ว GCM; + * ทุกคนตาย = แฟลช result-gameover อย่างเดียวแล้ว GCM + */ + function beginSpaceShooterMissionResultFlashSequenceThenGcm(mission, endKind) { + if (!mission || mission.uiSkin !== 'violent_crime') { + showGauntletCrownMissionOverlay(mission); + return; + } + hideStackTowerResultFlashDomOnlyPlay(); + const ov = document.getElementById('stack-tower-result-flash'); + const imgEl = document.getElementById('stack-tower-result-flash-img'); + if (!ov || !imgEl) { + showGauntletCrownMissionOverlay(mission); + return; + } + const finishToGcm = function () { + hideStackTowerResultFlashDomOnlyPlay(); + showGauntletCrownMissionOverlay(mission); + }; + const showOneFlash = function (file, onDone) { + imgEl.onerror = function () { + imgEl.onerror = null; + onDone(); + }; + imgEl.src = violentCrimeAssetUrl(file); + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + stackTowerResultFlashTimer = setTimeout(function () { + stackTowerResultFlashTimer = null; + onDone(); + }, STACK_TOWER_RESULT_FLASH_MS); + }; + if (endKind === 'time_up') { + showOneFlash('result-timeup.png', function () { + hideStackTowerResultFlashDomOnlyPlay(); + showOneFlash('result-gameover.png', finishToGcm); + }); + } else { + showOneFlash('result-gameover.png', finishToGcm); + } + } + + /** + * Mega Virus (balloon_boss shell): แฟลชรูปเดียวแล้วค่อย GCM + * — แพ้ (ทุกคนตาย / หมดเวลา) = result-gameover.png (TowerBlock เหมือนภารกิจอื่น — รูป2 flow) + * — ชนะ = end-victory-2.png ใน MegaVirus + */ + function beginBalloonBossMegaVirusResultFlashSequenceThenGcm(mission, endReason) { + if (!mission || mission.uiSkin !== 'mega_virus') { + showGauntletCrownMissionOverlay(mission); + return; + } + hideStackTowerResultFlashDomOnlyPlay(); + const ov = document.getElementById('stack-tower-result-flash'); + const imgEl = document.getElementById('stack-tower-result-flash-img'); + if (!ov || !imgEl) { + showGauntletCrownMissionOverlay(mission); + return; + } + const lose = endReason === 'all_dead' || endReason === 'time'; + const flashSrc = lose ? stackTowerAssetUrl('result-gameover.png') : megaVirusAssetUrl('end-victory-2.png'); + const finishToGcm = function () { + hideStackTowerResultFlashDomOnlyPlay(); + showGauntletCrownMissionOverlay(mission); + }; + imgEl.onerror = function () { + imgEl.onerror = null; + finishToGcm(); + }; + imgEl.src = flashSrc; + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + stackTowerResultFlashTimer = setTimeout(function () { + stackTowerResultFlashTimer = null; + finishToGcm(); + }, STACK_TOWER_RESULT_FLASH_MS); + } + + function stackTowerMissionTimeLimitSecPlay() { + const t = Number(playStackTowerMissionTimeSec); + if (Number.isFinite(t) && t > 0) return Math.max(10, Math.min(7200, Math.floor(t))); + return 90; + } + + function stackTowerProgressBlocksPlay() { + const n = Number(playStackTowerProgressBlocks); + if (Number.isFinite(n) && n >= 1) return Math.max(1, Math.min(500, Math.floor(n))); + return 50; + } + + function stackTowerDecryptPctPlay() { + if (!stackMini) return 0; + if (isStackTowerMissionUiMapPlay()) { + const p = Number(stackMini.progressPct); + const x = Math.min(100, Math.max(0, Number.isFinite(p) ? p : 0)); + if (x >= 99.999) return 100; + return Math.floor(x); + } + const teamScore = stackMini.score || 0; + return Math.min(100, Math.floor((stackMini.layers.length || 0) * 11 + Math.min(40, teamScore * 3))); + } + + function stackTowerMissionElapsedSecPlay() { + if (stackTowerSessionStartAt <= 0) return 0; + return (performance.now() - stackTowerSessionStartAt) / 1000; + } + + function applyStackTowerMissionPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = stackTowerAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = stackTowerAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + ensureStackTowerSlingImagePlay(); + } + + function showStackTowerMissionHowtoOverlay() { + if (!isStackTowerMissionUiMapPlay()) return; + hideConflictingOverlaysForGauntletCrown(); + applyStackTowerMissionPanelImages(); + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + stackTowerMissionPhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + setGauntletCrownHowtoStackTowerArtMaskPlay(true); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) st.classList.remove('gch-status--jumper'); + const btn = document.getElementById('btn-gch-ready'); + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + const soleParticipant = totPlayers === 1; + if (soleParticipant) { + if (st) { + st.textContent = ''; + st.classList.add('is-hidden'); + } + if (btn) { + const canGo = isMePlayHost() || humans.length === 1; + btn.classList.remove('is-start-phase'); + btn.classList.toggle('is-read-only', !canGo); + btn.disabled = !canGo; + btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host'; + btn.setAttribute('aria-pressed', 'false'); + } + } else { + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateStackTowerMissionHowtoHud(); + } + } + + /** Stack Tower mnn93hpi — Ready Status / ปุ่ม แบบ quiz mission (mng8a80o) */ + function updateStackTowerMissionHowtoHud() { + if (!isStackTowerMissionUiMapPlay() || stackTowerMissionPhase !== 'howto') return; + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!st || !btn) return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + /** Jump Survival mnptfts2 — Ready Status / ปุ่ม เดียวกับ Stack Tower */ + function updateJumpSurviveMissionHowtoHud() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'howto') return; + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!st || !btn) return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + /** Space Shooter mnpz6rkp — Ready Status / ปุ่ม เดียวกับ Jumper */ + function updateSpaceShooterMissionHowtoHud() { + if (!isSpaceShooterMissionUiMapPlay() || spaceShooterMissionPhase !== 'howto') return; + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!st || !btn) return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + function beginStackTowerMissionCountdownThenRun() { + if (!isStackTowerMissionUiMapPlay()) return; + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + stackTowerMissionPhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) { + howto.classList.add('is-hidden'); + howto.classList.remove(GCHOWTO_STACK_TOWER_CLASS); + } + gauntletCrownHowtoVisible = false; + hideStackTowerHowtoRulesDom(); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.classList.add('is-hidden'); + st.classList.remove('gch-status--jumper'); + } + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = function () { + if (cd) cd.classList.add('is-hidden'); + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + stackTowerMissionPhase = 'live'; + stackTowerMissionEndedOnce = false; + stackTowerSessionStartAt = performance.now(); + lastStackTickMs = performance.now(); + resetStackMinigameState(); + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + setCountdown321QuestionAssetGraphic(numEl, n); + const step = function () { + n--; + if (n > 0) { + setCountdown321QuestionAssetGraphic(numEl, n); + stackTowerMissionCountdownTimer = setTimeout(step, 1000); + } else { + stackTowerMissionCountdownTimer = null; + runFinish(); + } + }; + stackTowerMissionCountdownTimer = setTimeout(step, 1000); + } + + function stackTowerMissionBuildPayload(endOpts) { + const outcome = endOpts && endOpts.outcome ? String(endOpts.outcome) : 'decrypt_complete'; + const rows = []; + const teamPts = Math.max(0, Number(stackMini && stackMini.score) || 0); + function pushEnt(id, nickname, characterId, score) { + rows.push({ + id: id, + nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id), + characterId: characterId ?? null, + baseScore: Math.max(0, Number(score) || 0), + eliminated: false, + hadQuizWrong: false, + rankBonus: 0, + finalScore: Math.max(0, Number(score) || 0), + }); + } + if (playBotsEnabled() && stackPreviewTurnOrder && stackPreviewTurnOrder.length === STACK_PREVIEW_TURN_COUNT) { + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), me.stackPreviewHumanPts || 0); + } + stackPreviewTurnOrder.forEach(function (entry) { + if (!entry || entry.kind !== 'bot' || !entry.botId) return; + const o = others.get(entry.botId); + pushEnt(entry.botId, o ? o.nickname : entry.botId, o ? o.characterId : null, o ? (o.stackBotScore || 0) : 0); + }); + } else { + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), teamPts); + } + others.forEach(function (o, id) { + if (!o) return; + if (playBotsEnabled() && isPreviewBotId(id)) return; + pushEnt(id, o.nickname, o.characterId, teamPts); + }); + } + const seen = new Set(); + const uniq = []; + rows.forEach(function (r) { + const sid = String(r.id); + if (seen.has(sid)) return; + seen.add(sid); + uniq.push(r); + }); + uniq.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = uniq.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: false, + hadQuizWrong: false, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: row.baseScore, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (totalSum <= 0 && uniq.length) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'stack_tower', + survivorCount: uniq.length, + participantCount: uniq.length, + stackTowerOutcome: outcome, + }; + } + + function stackTowerMissionMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'stack_tower' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: false, + hadQuizWrong: false, + rankBonus: 0, + finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0), + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: Math.max(0, Number(o.stackBotScore) || 0), + eliminated: false, + hadQuizWrong: false, + rankBonus: 0, + finalScore: Math.max(0, Number(o.stackBotScore) || 0), + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: false, + hadQuizWrong: false, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n2 = ranked.length || 1; + const averageScore = Math.floor(totalSum / n2); + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (totalSum <= 0 && baseRows.length) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'stack_tower', + survivorCount: baseRows.length, + participantCount: baseRows.length, + stackTowerOutcome: mission.stackTowerOutcome, + }; + } + + function stackTowerMissionMaybeEndPlay() { + if (!isStackTowerMissionUiMapPlay()) return; + if (stackTowerMissionPhase !== 'live') return; + if (stackTowerMissionEndedOnce) return; + const pct = stackTowerDecryptPctPlay(); + const elapsed = stackTowerMissionElapsedSecPlay(); + const timeLim = stackTowerMissionTimeLimitSecPlay(); + const missesOut = stackMini && stackMini.teamMissesLeft != null && stackMini.teamMissesLeft <= 0; + const timeOut = elapsed >= timeLim; + if (pct < 100 && !timeOut && !missesOut) return; + stackTowerMissionEndedOnce = true; + stackTowerMissionPhase = 'ended'; + applyStackTowerMissionPanelImages(); + let outcome = 'time_up'; + if (pct >= 100) outcome = 'decrypt_complete'; + else if (missesOut) outcome = 'misses_exhausted'; + beginStackTowerResultFlashThenGcm(stackTowerMissionBuildPayload({ outcome: outcome })); + } + + function isJumpSurviveMissionUiMapPlay() { + return isJumpSurvive() && currentPlayMapId() === JUMP_SURVIVE_MISSION_MAP_ID; + } + + function isJumpSurviveMissionPregameBlockingPlay() { + if (!isJumpSurviveMissionUiMapPlay()) return false; + return jumpSurviveMissionPhase != null && jumpSurviveMissionPhase !== 'live' && jumpSurviveMissionPhase !== 'ended'; + } + + function jumperAssetUrl(file) { + return BASE + '/img/Jumper/' + file; + } + + function hideConflictingOverlaysForJumpSurviveMission() { + hideConflictingOverlaysForGauntletCrown(); + } + + function jumpSurviveTimeLimitSecForMission() { + const m = Number(mapData && mapData.jumpSurviveTimeSec); + if (Number.isFinite(m) && m > 0 && m < 7200) return Math.floor(m); + const j = Number(playJumpSurviveMissionTimeSec); + if (Number.isFinite(j) && j > 0 && j < 7200) return Math.floor(j); + return 60; + } + + function jumpSurviveRemainingSecMission() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveSessionStartMs <= 0) return null; + const lim = jumpSurviveTimeLimitSecForMission(); + if (!(lim > 0)) return null; + const elapsed = (performance.now() - jumpSurviveSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function applyJumpSurviveJumperPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = jumperAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = jumperAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + } + + function showJumpSurviveMissionHowtoOverlay() { + if (!isJumpSurviveMissionUiMapPlay()) return; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + jumpSurviveMissionPhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) st.classList.remove('gch-status--jumper'); + const btn = document.getElementById('btn-gch-ready'); + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + const soleParticipant = totPlayers === 1; + if (soleParticipant) { + if (st) { + st.textContent = ''; + st.classList.add('is-hidden'); + } + if (btn) { + const canGo = humans.length === 1 || isMePlayHost(); + btn.classList.remove('is-start-phase'); + btn.classList.toggle('is-read-only', !canGo); + btn.disabled = !canGo; + btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host'; + btn.setAttribute('aria-pressed', 'false'); + } + } else { + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateJumpSurviveMissionHowtoHud(); + } + } + + function beginJumpSurviveMissionCountdownThenRun() { + if (!isJumpSurviveMissionUiMapPlay()) return; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + jumpSurviveMissionPhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.classList.add('is-hidden'); + st.classList.remove('gch-status--jumper'); + } + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = function () { + if (cd) cd.classList.add('is-hidden'); + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + jumpSurviveMissionPhase = 'live'; + jumpSurviveGameEnded = false; + jumpSurviveEliminated = false; + applyJumpSurvivePreviewSpawnLayout(false); + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + setCountdown321QuestionAssetGraphic(numEl, n); + const step = function () { + n--; + if (n > 0) { + setCountdown321QuestionAssetGraphic(numEl, n); + jumperMissionCountdownTimer = setTimeout(step, 1000); + } else { + jumperMissionCountdownTimer = null; + runFinish(); + } + }; + jumperMissionCountdownTimer = setTimeout(step, 1000); + } + + /** Jumper ภารกิจ mnptfts2: เกรดจากจำนวนผู้รอด — 6=A … 3=D; น้อยกว่า 2 = F; พอดี 2 คน = E */ + function jumpSurviveGradeFromSurvivorCount(survivors) { + const s = Math.max(0, Math.floor(Number(survivors) || 0)); + if (s >= 6) return 'A'; + if (s === 5) return 'B'; + if (s === 4) return 'C'; + if (s === 3) return 'D'; + if (s === 2) return 'E'; + return 'F'; + } + + function jumpSurviveRollRewardCardForGrade(grade) { + if (grade === 'F') return null; + const r = Math.random(); + if (grade === 'A' || grade === 'B') { + if (r < 0.35) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.7) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'C' || grade === 'D') { + if (r < 0.25) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.45) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'E') { + if (r < 0.12) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (r < 0.25) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.45) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + + function jumpSurviveMissionTotalParticipants() { + let n = myId != null ? 1 : 0; + if (others && typeof others.size === 'number') n += others.size; + return n; + } + + function jumpSurviveMissionCountAlive() { + let n = 0; + if (myId != null && !jumpSurviveEliminated) n++; + if (others && typeof others.forEach === 'function') { + others.forEach(function (o) { + if (o && !o.jumpSurviveEliminated) n++; + }); + } + return n; + } + + /** จบทันทีเมื่อไม่มีผู้รอด — เหลือคนสุดท้ายยังเล่นต่อจนหมดเวลา (ไม่จบเร็วแบบ “ชนะขาดลอย”) */ + function jumpSurviveMissionMaybeEarlyFinish() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) return; + const alive = jumpSurviveMissionCountAlive(); + if (alive === 0) { + endJumpSurviveMissionRound('all_dead'); + } + } + + function jumpSurviveBuildMissionPayload() { + const rows = []; + const pushEnt = function (id, nickname, characterId, eliminated) { + const alive = !eliminated; + const baseScore = alive ? 100 : 0; + rows.push({ + id: id, + nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id), + characterId: characterId ?? null, + baseScore: baseScore, + eliminated: !!eliminated, + rankBonus: 0, + finalScore: baseScore, + }); + }; + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), jumpSurviveEliminated); + } + others.forEach(function (o, id) { + if (!o) return; + pushEnt(id, o.nickname, o.characterId, !!o.jumpSurviveEliminated); + }); + rows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + let survivorCount = 0; + for (let si = 0; si < rows.length; si++) { + if (rows[si] && !rows[si].eliminated) survivorCount++; + } + const participantCount = rows.length; + const grade = jumpSurviveGradeFromSurvivorCount(survivorCount); + const top = missionEnsureHumanInTopRanked(rows, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: row.baseScore, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const rewardCard = jumpSurviveRollRewardCardForGrade(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'jumper', + survivorCount: survivorCount, + participantCount: participantCount, + }; + } + + function jumpSurviveMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'jumper' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + rankBonus: 0, + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: o.jumpSurviveEliminated ? 0 : 100, + eliminated: !!o.jumpSurviveEliminated, + rankBonus: 0, + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + let survivorCount = 0; + for (let si = 0; si < baseRows.length; si++) { + if (baseRows[si] && !baseRows[si].eliminated) survivorCount++; + } + const participantCount = baseRows.length; + const grade = jumpSurviveGradeFromSurvivorCount(survivorCount); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: jumpSurviveRollRewardCardForGrade(grade), + totalParts: totalParts, + uiSkin: 'jumper', + survivorCount: survivorCount, + participantCount: participantCount, + }; + } + + function endJumpSurviveMissionRound(endKind) { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveGameEnded) return; + jumpSurviveGameEnded = true; + jumpSurviveMissionPhase = 'ended'; + applyJumpSurviveJumperPanelImages(); + const mission = jumpSurviveBuildMissionPayload(); + const kind = endKind === 'all_dead' ? 'all_dead' : 'time_up'; + beginJumpSurviveMissionResultFlashSequenceThenGcm(mission, kind); + } + function isSpaceShooter() { return mapData && mapData.gameType === 'space_shooter'; } + + function isSpaceShooterMissionUiMapPlay() { + return isSpaceShooter() && currentPlayMapId() === SPACE_SHOOTER_MISSION_MAP_ID; + } + + function isSpaceShooterMissionPregameBlockingPlay() { + if (!isSpaceShooterMissionUiMapPlay()) return false; + return spaceShooterMissionPhase != null && spaceShooterMissionPhase !== 'live' && spaceShooterMissionPhase !== 'ended'; + } + + function violentCrimeAssetUrl(file) { + return BASE + '/img/ViolentCrime/' + String(file || '').replace(/^\//, ''); + } + + function megaVirusAssetUrl(file) { + return BASE + '/img/MegaVirus/' + String(file || '').replace(/^\//, ''); + } + + function applyMegaVirusMissionPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = megaVirusAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = megaVirusAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + } + + function applySpaceShooterMissionPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = violentCrimeAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = violentCrimeAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + } + + function showSpaceShooterMissionHowtoOverlay() { + if (!isSpaceShooterMissionUiMapPlay()) return; + hideConflictingOverlaysForGauntletCrown(); + applySpaceShooterMissionPanelImages(); + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + spaceShooterMissionPhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) st.classList.remove('gch-status--jumper'); + const btn = document.getElementById('btn-gch-ready'); + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + const soleParticipant = totPlayers === 1; + if (soleParticipant) { + if (st) { + st.textContent = ''; + st.classList.add('is-hidden'); + } + if (btn) { + const canGo = humans.length === 1 || isMePlayHost(); + btn.classList.remove('is-start-phase'); + btn.classList.toggle('is-read-only', !canGo); + btn.disabled = !canGo; + btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host'; + btn.setAttribute('aria-pressed', 'false'); + } + } else { + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateSpaceShooterMissionHowtoHud(); + } + spaceShooterResetMissionShipStateForAllPlay(); + } + + function beginSpaceShooterMissionCountdownThenRun() { + if (!isSpaceShooterMissionUiMapPlay()) return; + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + spaceShooterMissionPhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.classList.add('is-hidden'); + st.classList.remove('gch-status--jumper'); + } + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = function () { + if (cd) cd.classList.add('is-hidden'); + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + spaceShooterMissionPhase = 'live'; + spaceShooterGameEnded = false; + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterAsteroidExplosions = []; + spaceShooterPopups = []; + spaceShooterLastTickMs = performance.now(); + spaceShooterSpawnAccMs = 0; + spaceShooterFireCd = 0; + spaceShooterSessionStartMs = performance.now(); + spaceShooterLastMoveEmit = 0; + if (myId != null) me.spaceShooterScore = 0; + others.forEach(function (o) { + if (o) o.spaceShooterScore = 0; + }); + spaceShooterResetMissionShipStateForAllPlay(); + if (mapData) applySpaceShooterSpawnLayoutPlay(); + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + setCountdown321QuestionAssetGraphic(numEl, n); + const step = function () { + n--; + if (n > 0) { + setCountdown321QuestionAssetGraphic(numEl, n); + spaceShooterMissionCountdownTimer = setTimeout(step, 1000); + } else { + spaceShooterMissionCountdownTimer = null; + runFinish(); + } + }; + spaceShooterMissionCountdownTimer = setTimeout(step, 1000); + } + + function spaceShooterBuildMissionPayload() { + const rows = []; + function pushEnt(id, nickname, characterId, score, eliminated) { + rows.push({ + id: id, + nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id), + characterId: characterId ?? null, + baseScore: Math.max(0, Number(score) || 0), + eliminated: !!eliminated, + rankBonus: 0, + finalScore: Math.max(0, Number(score) || 0), + }); + } + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), me.spaceShooterScore, !!me.spaceShooterEliminated); + } + others.forEach(function (o, id) { + if (!o) return; + pushEnt(id, o.nickname, o.characterId, o.spaceShooterScore, !!o.spaceShooterEliminated); + }); + rows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = rows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: row.baseScore, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const survivorCount = rows.filter(function (r) { return !r.eliminated; }).length; + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (survivorCount <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'violent_crime', + survivorCount: survivorCount, + participantCount: rows.length, + }; + } + + function spaceShooterMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'violent_crime' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + rankBonus: 0, + finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0), + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: Math.max(0, Number(o.spaceShooterScore) || 0), + eliminated: !!o.spaceShooterEliminated, + rankBonus: 0, + finalScore: Math.max(0, Number(o.spaceShooterScore) || 0), + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n2 = ranked.length || 1; + const averageScore = Math.floor(totalSum / n2); + const survivorCount = baseRows.filter(function (r) { return !r.eliminated; }).length; + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (survivorCount <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'violent_crime', + survivorCount: survivorCount, + participantCount: baseRows.length, + }; + } + + function balloonBossMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'mega_virus' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + rankBonus: 0, + finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0), + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: Math.max(0, Number(o.balloonBossScore) || 0), + eliminated: !!o.balloonBossEliminated, + rankBonus: 0, + finalScore: Math.max(0, Number(o.balloonBossScore) || 0), + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n2 = ranked.length || 1; + const averageScore = Math.floor(totalSum / n2); + const maxHp = balloonBossMaxHpPlay(); + let bossDmgSum = Math.max(0, me.balloonBossBossDmg | 0); + others.forEach(function (o) { + if (o) bossDmgSum += Math.max(0, o.balloonBossBossDmg | 0); + }); + const progress = Math.min(1, bossDmgSum / Math.max(1, maxHp)); + const score100 = Math.min(100, Math.floor(progress * 100)); + let grade = 'C'; + const endR = mission && mission.balloonBossEndReason; + if (endR === 'victory' || progress >= 1) grade = 'A'; + else if (endR === 'all_dead') grade = 'F'; + else if (score100 >= 50) grade = 'B'; + else if (totalSum <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'mega_virus', + survivorCount: baseRows.filter(function (r) { return !r.eliminated; }).length, + participantCount: baseRows.length, + balloonBossEndReason: mission.balloonBossEndReason, + }; + } + + function balloonBossBuildMissionPayloadPlay(reason) { + const rows = []; + if (myId != null) { + rows.push({ + id: myId, + nickname: ((me.nickname || nick || 'คุณ').trim() || 'คุณ'), + characterId: me.characterId ?? null, + baseScore: Math.max(0, me.balloonBossScore | 0), + eliminated: !!me.balloonBossEliminated, + }); + } + others.forEach((o, id) => { + rows.push({ + id: id, + nickname: (o && o.nickname) ? String(o.nickname).trim() : id, + characterId: o && o.characterId != null ? o.characterId : null, + baseScore: Math.max(0, o.balloonBossScore | 0), + eliminated: !!(o && o.balloonBossEliminated), + }); + }); + rows.sort((a, b) => b.baseScore - a.baseScore + || String(a.nickname).localeCompare(String(b.nickname), 'th') + || String(a.id).localeCompare(String(b.id))); + const top = rows.slice(0, 5); + const ranked = top.map((row, idx) => { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map((r) => r.baseScore); + const totalSum = totalParts.reduce((s, n) => s + n, 0); + const n2 = ranked.length || 1; + const averageScore = Math.floor(totalSum / n2); + const maxHp = balloonBossMaxHpPlay(); + let bossDmgSum = Math.max(0, me.balloonBossBossDmg | 0); + others.forEach((o) => { + if (o) bossDmgSum += Math.max(0, o.balloonBossBossDmg | 0); + }); + const progress = Math.min(1, bossDmgSum / Math.max(1, maxHp)); + const score100 = Math.min(100, Math.floor(progress * 100)); + let grade = 'C'; + if (reason === 'victory' || progress >= 1) grade = 'A'; + else if (reason === 'all_dead') grade = 'F'; + else if (score100 >= 50) grade = 'B'; + else if (totalSum <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'mega_virus', + survivorCount: rows.filter((r) => !r.eliminated).length, + participantCount: rows.length, + balloonBossEndReason: reason, + }; + } + + function endSpaceShooterMissionRound(endKind) { + if (!isSpaceShooterMissionUiMapPlay() || spaceShooterGameEnded) return; + const kind = endKind === 'all_dead' ? 'all_dead' : 'time_up'; + spaceShooterGameEnded = true; + spaceShooterMissionPhase = 'ended'; + applySpaceShooterMissionPanelImages(); + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterAsteroidExplosions = []; + spaceShooterSpawnAccMs = 0; + spaceShooterPopups = []; + const mission = spaceShooterBuildMissionPayload(); + mission.spaceShooterEndKind = kind; + beginSpaceShooterMissionResultFlashSequenceThenGcm(mission, kind); + } + + function questionMissionAssetUrl(file) { + return BASE + '/img/QUESTION/' + String(file || '').replace(/^\//, ''); + } + + /** HUD กลางจอ (เวลา / แผ่นคำถาม) — ชื่อไฟล์คู่กับ public/img/QUESTION บนดิสก์ → URL /Game/img/QUESTION */ + function questionMissionHudAssetUrl(file) { + return questionMissionAssetUrl(file); + } + + function isQuizQuestionMissionHudActivePlay() { + return isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'live'; + } + + /** ภารกิจคำถาม mng8a80o ช่วง live: กล้องกลางแมป (world px) — ไม่ตาม me */ + function getQuizQuestionMissionMapCenterWorldPxPlay() { + if (!mapData || !isQuiz()) return null; + const w = mapData.width || 20; + const h = mapData.height || 15; + const ts = tileSize; + const mwPx = w * ts; + const mhPx = h * ts; + return { cx: mwPx * 0.5, cy: mhPx * 0.5, mwPx, mhPx }; + } + + function applyQuizQuestionMissionPanelImages() { + /* ติดคลาส scope ให้ overlay howto (popup HOW TO PLAY) ใช้สไตล์ mockup เฉพาะ mng8a80o */ + const howtoOv = document.getElementById('gauntlet-crown-howto-overlay'); + if (howtoOv) { + const tfHowto = isQuizQuestionMissionUiMapPlay(); + howtoOv.classList.toggle('gch-quiz-tf-mock', tfHowto); + if (tfHowto) { + ensureQuizTfScaleResizeListener(); + updateGchTfMockScale(howtoOv); + } else { + howtoOv.style.removeProperty('--gch-tf-scale'); + } + } + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = questionMissionAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = questionMissionAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + } + + function showQuizQuestionMissionHowtoOverlay() { + if (!isQuizQuestionMissionUiMapPlay()) return; + hideConflictingOverlaysForGauntletCrown(); + applyQuizQuestionMissionPanelImages(); + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + quizQuestionMissionPhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) st.classList.remove('gch-status--jumper'); + const btn = document.getElementById('btn-gch-ready'); + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + /** ผู้เล่นจริงในห้องคนเดียว และไม่มีบอท/peer อื่นในนับรวม — เท่านั้นที่ข้าม lobby */ + const soleParticipant = totPlayers === 1; + if (soleParticipant) { + if (st) { + st.textContent = ''; + st.classList.add('is-hidden'); + } + if (btn) { + const canGo = isMePlayHost() || humans.length === 1; + btn.classList.remove('is-start-phase'); + btn.classList.toggle('is-read-only', !canGo); + btn.disabled = !canGo; + btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host'; + btn.setAttribute('aria-pressed', 'false'); + } + } else { + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateQuizQuestionMissionHowtoHud(); + } + } + + function beginQuizQuestionMissionCountdownThenRun() { + if (!isQuizQuestionMissionUiMapPlay()) return; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + quizQuestionMissionPhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.classList.add('is-hidden'); + st.classList.remove('gch-status--jumper'); + } + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = function () { + if (cd) cd.classList.add('is-hidden'); + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + quizQuestionMissionPhase = 'live'; + playEmbedUserZoomMul = 1; + const qov = document.getElementById('quiz-game-overlay'); + if (qov) { + if (isQuizQuestionMissionUiMapPlay()) qov.classList.add('is-hidden'); + else if (previewMode && editorEmbedReturn) qov.classList.add('is-hidden'); + else qov.classList.remove('is-hidden'); + } + const dp = quizQuestionMissionDeferredPhase; + quizQuestionMissionDeferredPhase = null; + if (dp) applyQuizPhaseFromServer(dp); + if (previewMode && isQuiz()) loadPreviewQuizAndStart(); + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + setCountdown321QuestionAssetGraphic(numEl, n); + const step = function () { + n--; + if (n > 0) { + setCountdown321QuestionAssetGraphic(numEl, n); + quizQuestionMissionCountdownTimer = setTimeout(step, 1000); + } else { + quizQuestionMissionCountdownTimer = null; + runFinish(); + } + }; + quizQuestionMissionCountdownTimer = setTimeout(step, 1000); + } + + function quizQuestionMissionBuildPayload() { + const rows = []; + function pushEnt(id, nickname, characterId, score) { + const sid = String(id); + rows.push({ + id: id, + nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id), + characterId: characterId ?? null, + baseScore: Math.max(0, Number(score) || 0), + eliminated: false, + hadQuizWrong: !!playQuizEverWrong[sid], + rankBonus: 0, + finalScore: Math.max(0, Number(score) || 0), + }); + } + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), playLiveQuizScores[myId]); + } + others.forEach(function (o, id) { + if (!o) return; + pushEnt(id, o.nickname, o.characterId, playLiveQuizScores[id]); + }); + rows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = rows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: false, + hadQuizWrong: !!row.hadQuizWrong, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: row.baseScore, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (totalSum <= 0 && rows.length) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'question_mission', + survivorCount: rows.length, + participantCount: rows.length, + }; + } + + function quizQuestionMissionMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'question_mission' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: false, + hadQuizWrong: !!r.hadQuizWrong, + rankBonus: 0, + finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0), + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: Math.max(0, Number(playLiveQuizScores[id]) || 0), + eliminated: false, + hadQuizWrong: !!playQuizEverWrong[sid], + rankBonus: 0, + finalScore: Math.max(0, Number(playLiveQuizScores[id]) || 0), + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: false, + hadQuizWrong: !!row.hadQuizWrong, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n2 = ranked.length || 1; + const averageScore = Math.floor(totalSum / n2); + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (totalSum <= 0 && baseRows.length) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'question_mission', + survivorCount: baseRows.length, + participantCount: baseRows.length, + }; + } + + function isBalloonBoss() { return mapData && mapData.gameType === 'balloon_boss'; } + function isQuizCarry() { return mapData && mapData.gameType === 'quiz_carry'; } + + /** พรีวิว editor + ภารกิจสืบสวน mnorwqx1 — flow เต็ม: HOWTO/READY, นับ 3-2-1, ครบ carrySessionLength, สรุปภารกิจ */ + function quizCarryEmbedMissionFlowActive() { + if (!isQuizCarry()) return false; + if (previewMode && editorEmbedReturn) return true; + return !!(isDetectiveMinigamePlay() && quizCarryUseMissionSummaryOverlay); + } + /** quiz_carry: จุดกลางแมป (world px) — กล้องไม่ตามตัวละคร; ใช้ร่วม draw + overlay DOM */ + 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 }; + } + function isQuizBattle() { return mapData && mapData.gameType === 'quiz_battle'; } + + function quizCarryNeonColorForChoice(choiceIndex) { + const i = Math.max(0, choiceIndex | 0); + const h = (i * 47 + 100) % 360; + return 'hsl(' + h + ', 72%, 62%)'; + } + + function quizCarryMinimapOptionFillCss(ov) { + const th = getEffectiveCarryChoicePlaqueThemeForChoice((Number(ov) | 0) - 1); + if (th.borderMode === 'fixed' && th.fixedBorder) { + const m = /^rgba?\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i.exec(String(th.fixedBorder).trim()); + if (m) { + const a0 = m[4] != null && m[4] !== '' ? Number(m[4]) : 1; + const aa = (Number.isFinite(a0) ? Math.max(0, Math.min(1, a0)) : 1) * 0.32; + const t = Math.round(aa * 1000) / 1000; + return 'rgba(' + m[1] + ',' + m[2] + ',' + m[3] + ',' + t + ')'; + } + } + const i = Math.max(0, ov - 1); + const h = (i * 47 + 100) % 360; + return 'hsla(' + h + ', 58%, 52%, 0.32)'; + } + + function normalizeQuizCarryQuestionFromAny(q) { + if (!q) return null; + let text = String(q.text || q.question || q.prompt || q.title || '').trim(); + if (Array.isArray(q.choices) && q.choices.length >= 2) { + const choices = q.choices.map((c) => String(c)); + let ci = Number(q.correctIndex); + if (!Number.isFinite(ci) || ci < 0 || ci >= choices.length) ci = 0; + if (!text) text = 'เลือกคำตอบที่ถูกต้อง — หยิบตัวเลือกไปวางโซนส่งคำตอบ'; + let chSlot = Number(q.countdownHighlightSlot); + if (!Number.isFinite(chSlot) || chSlot < 1 || chSlot > 16) chSlot = null; + else chSlot = Math.floor(chSlot); + let choiceImageUrls = null; + if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) { + const urls = choices.map((_, idx) => sanitizeQuizCarryImageUrlClient(q.choiceImageUrls[idx])); + if (urls.some((u) => u)) choiceImageUrls = urls; + } + return { text, choices, correctIndex: ci, countdownHighlightSlot: chSlot, choiceImageUrls }; + } + if (!text) return null; + const t = !!q.answerTrue; + return { text, choices: ['จริง', 'เท็จ'], correctIndex: t ? 0 : 1, countdownHighlightSlot: null }; + } + + function buildQuizCarryPoolFromMap(md) { + if (!md || !Array.isArray(md.quizQuestions)) return []; + const out = []; + for (let i = 0; i < md.quizQuestions.length; i++) { + const n = normalizeQuizCarryQuestionFromAny(md.quizQuestions[i]); + if (n) out.push(n); + } + return out; + } + + function getQuizCarryHubTileBounds(md) { + if (!md || md.gameType !== 'quiz_carry') return null; + const grid = md.quizCarryHubArea; + if (!grid || !grid.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (let yy = 0; yy < grid.length; yy++) { + const row = grid[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (row[xx] === 1) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + } + } + } + if (minX === Infinity) return null; + return { minX, minY, maxX, maxY }; + } + + /** จุดกลางโซนตัวเลือกหยิบมาวาง (ค่า grid 1..N = ลำดับ choices) เพื่อวาดข้อความบนแผนที่ */ + function getQuizCarryOptionClusterBounds(md, slot1toN) { + const g = md && md.quizCarryOptionArea; + if (!g || !g.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let n = 0, sumX = 0, sumY = 0; + for (let yy = 0; yy < g.length; yy++) { + const row = g[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (Number(row[xx]) === slot1toN) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + sumX += xx + 0.5; + sumY += yy + 0.5; + n++; + } + } + } + if (!n || minX === Infinity) return null; + return { + minX, minY, maxX, maxY, + cx: sumX / n, + cy: sumY / n, + }; + } + + function canvasWordWrapLines(ctx, text, maxWidth) { + const raw = String(text || '').trim(); + if (!raw) return []; + const words = raw.split(/\s+/); + const lines = []; + let cur = words[0]; + for (let i = 1; i < words.length; i++) { + const w = words[i]; + const test = cur + ' ' + w; + if (ctx.measureText(test).width <= maxWidth) cur = test; + else { + lines.push(cur); + cur = w; + } + } + lines.push(cur); + return lines; + } + + function quizCarryOptionHeldByAnyone(optionIdx) { + if (optionIdx == null || optionIdx < 0) return false; + if (me.quizCarryHeld === optionIdx) return true; + for (const o of others.values()) { + if (o && o.quizCarryHeld === optionIdx) return true; + } + return false; + } + + /** สไปรต์ทับช่องโซนกลาง (ม่วง) อย่างน้อย 1 ช่อง */ + function spriteOverlapsQuizCarryHubArea(md, sp) { + if (!md || md.gameType !== 'quiz_carry' || !sp) return false; + const hub = md.quizCarryHubArea; + if (!hub || !hub.length) return false; + const mw = md.width || 20, mh = md.height || 15; + const x0 = sp.x | 0, y0 = sp.y | 0, ww = sp.w | 0, hh = sp.h | 0; + for (let yy = y0; yy < y0 + hh; yy++) { + if (yy < 0 || yy >= mh) continue; + const row = hub[yy]; + if (!row) continue; + for (let xx = x0; xx < x0 + ww; xx++) { + if (xx < 0 || xx >= mw) continue; + if (row[xx] === 1) return true; + } + } + return false; + } + + /** ตัวเลือกหนึ่งข้อ — มีคนถือป้ายอยู่ได้แค่คนเดียว (หยิบซ้ำไม่ได้) */ + function pickQuizCarryTargetOptionIndexAvoidingTaken(preferredIdx) { + const n = quizCarryCurrent && quizCarryCurrent.choices ? quizCarryCurrent.choices.length : 0; + if (n < 1) return null; + let p = preferredIdx | 0; + if (p < 0 || p >= n) p = 0; + if (!quizCarryOptionHeldByAnyone(p)) return p; + const avail = []; + for (let i = 0; i < n; i++) { + if (!quizCarryOptionHeldByAnyone(i)) avail.push(i); + } + if (!avail.length) return null; + return avail[Math.floor(Math.random() * avail.length)]; + } + + /** + * วาดป้ายมุมมน + ขอบเรือง + ข้อความ (รูปแบบเดียวกันทั้งแมปและติดตัว) — สีจาก carryChoicePlaqueThemes[choiceIndex] · รูปจาก plaqueExtra.imageUrl + * @param tether ถ้ามี วาดเส้นขาวจาก (x0,y0) ถึง (x1,y1) ก่อนป้าย + * @param plaqueExtra optional { imageUrl } + */ + function drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, tether, plaqueExtra) { + const th = getEffectiveCarryChoicePlaqueThemeForChoice(choiceIndex); + const neon = th.borderMode === 'fixed' ? th.fixedBorder : quizCarryNeonColorForChoice(choiceIndex); + const fillCol = th.fillBg || 'rgba(12, 10, 20, 0.88)'; + const textCol = th.textColor || '#f8f9ff'; + const baseLw = Math.max(0.5, Math.min(8, Number(th.borderWidthPx))); + const imgUrl = plaqueExtra && plaqueExtra.imageUrl ? String(plaqueExtra.imageUrl) : ''; + const elImg = plaqueExtra && plaqueExtra.imageElement; + const imgFromEl = (elImg && elImg.complete && elImg.naturalWidth > 0) ? elImg : null; + const img = !imgFromEl && imgUrl ? getQuizCarryChoiceImageCached(imgUrl) : null; + const drawPlaqueImg = imgFromEl || ((img && img.complete && img.naturalWidth > 0) ? img : null); + const rr = Math.max(6, Math.min(12, Math.min(w, h) * 0.2)); + const simplePreviewPlaque = previewMode && editorEmbedReturn; + function pathBoard() { + ctx.beginPath(); + if (typeof ctx.roundRect === 'function') ctx.roundRect(signX, signY, w, h, rr); + else ctx.rect(signX, signY, w, h); + } + if (!simplePreviewPlaque) { + for (let g = 4; g >= 1; g--) { + ctx.strokeStyle = neon; + ctx.globalAlpha = 0.07 + g * 0.06; + ctx.lineWidth = baseLw + g * 3.2; + pathBoard(); + ctx.stroke(); + } + } + ctx.globalAlpha = 1; + ctx.fillStyle = fillCol; + pathBoard(); + ctx.fill(); + if (drawPlaqueImg) { + ctx.save(); + pathBoard(); + ctx.clip(); + const padImg = Math.max(2, padY * 0.65); + const ix = signX + padImg; + const iy = signY + padImg; + const iw = Math.max(0, w - padImg * 2); + const ih = Math.max(0, h - padImg * 2); + if (iw > 0 && ih > 0) { + const nw = drawPlaqueImg.naturalWidth; + const nh = drawPlaqueImg.naturalHeight; + const ir = nw / nh; + let dw; + let dh; + if (iw / ih > ir) { + dh = ih; + dw = dh * ir; + } else { + dw = iw; + dh = dw / ir; + } + const dx = ix + (iw - dw) / 2; + const dy = iy + (ih - dh) / 2; + ctx.drawImage(drawPlaqueImg, 0, 0, nw, nh, dx, dy, dw, dh); + } + ctx.restore(); + } + ctx.strokeStyle = neon; + if (simplePreviewPlaque) { + ctx.lineWidth = Math.max(1, baseLw * 0.85); + ctx.shadowBlur = 0; + pathBoard(); + ctx.stroke(); + } else { + ctx.lineWidth = baseLw; + ctx.shadowColor = neon; + ctx.shadowBlur = 14; + pathBoard(); + ctx.stroke(); + ctx.shadowBlur = 8; + pathBoard(); + ctx.stroke(); + ctx.shadowBlur = 0; + } + const attachX = signX + w / 2; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = textCol; + const lh = lineH || 12; + if (!simplePreviewPlaque && lines && lines.length) { + ctx.shadowColor = 'rgba(0,0,0,0.55)'; + ctx.shadowBlur = 3; + } + if (lines && lines.length) { + const n = lines.length; + const textBlockH = n * lh; + const y0 = signY + Math.max(padY, (h - textBlockH) / 2); + const firstLineCenterY = y0 + lh / 2; + for (let li = 0; li < n; li++) { + ctx.fillText(lines[li], attachX, firstLineCenterY + li * lh); + } + } + ctx.shadowBlur = 0; + if (tether && Number.isFinite(tether.x0) && Number.isFinite(tether.y0)) { + ctx.strokeStyle = 'rgba(255,255,255,0.95)'; + ctx.lineWidth = 1.5; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.shadowBlur = 0; + ctx.beginPath(); + ctx.moveTo(tether.x0, tether.y0); + ctx.lineTo(tether.x1, tether.y1); + ctx.stroke(); + } + } + + /** + * กล่องป้ายบนจอ (signX, signY, w, h) ให้ตรงกับ drawQuizCarryChoiceLabels สำหรับช่อง idx 0-based + */ + function computeQuizCarryChoicePlaqueScreenRect(ctx, worldToScreen, zoom, idx0Based) { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return null; + const q = quizCarryCurrent; + const choices = q.choices; + if (!choices || !choices.length || idx0Based < 0 || idx0Based >= choices.length) return null; + const i = idx0Based; + const b = getQuizCarryOptionClusterBounds(mapData, i + 1); + if (!b) return null; + const ts = tileSize * zoom; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + const choiceText = String(choices[i] || '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(q, i); + const gridIdleEl = findQuizCarryGridIdleImgForChoiceIndex(i); + const plaqueExtra = imgUrl ? { imageUrl: imgUrl } + : (gridIdleEl && gridIdleEl.complete && gridIdleEl.naturalWidth ? { imageElement: gridIdleEl } : {}); + if (!choiceText && !plaqueExtra.imageUrl && !plaqueExtra.imageElement) return null; + const tileSpanX = (b.maxX - b.minX + 1); + const tileSpanY = (b.maxY - b.minY + 1); + const minPlaqueW = Math.ceil(Math.max(72, tileSpanX * ts - 16) * ps); + const minPlaqueH = Math.ceil(Math.max(52, tileSpanY * ts - 12) * ps); + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + ctx.save(); + let signX; + let signY; + let w; + let h; + if (!choiceText && (plaqueExtra.imageUrl || plaqueExtra.imageElement)) { + const minW = minPlaqueW; + const minH = minPlaqueH; + signX = sx - minW / 2; + signY = sy - minH / 2; + w = minW; + h = minH; + ctx.restore(); + return { signX, signY, w, h }; + } + const line = choiceText; + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + let maxW = Math.max(56, tileSpanX * ts - 14); + const lineH = fontPx * 1.2; + let textLines = canvasWordWrapLines(ctx, line, maxW); + if (!textLines.length) textLines = [line]; + if (textLines.length * lineH > tileSpanY * ts - 8 && textLines.length > 1) { + const fp2 = Math.max(9, fontPx * 0.85); + ctx.font = '600 ' + fp2 + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + const lh2 = fp2 * 1.2; + maxW = Math.max(48, maxW - 4); + textLines = canvasWordWrapLines(ctx, line, maxW); + if (textLines.length * lh2 > tileSpanY * ts - 8) { + textLines = textLines.slice(0, Math.max(1, Math.floor((tileSpanY * ts - 8) / lh2))); + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + const maxLineW = Math.max(...textLines.map((t) => ctx.measureText(t).width), 40); + const padX = 10; + const padY = 8; + w = Math.ceil(maxLineW + padX * 2); + h = Math.ceil(textLines.length * lh2 + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + signX = sx - w / 2; + signY = sy - h / 2; + ctx.restore(); + return { signX, signY, w, h }; + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + let maxLineW = 0; + for (let ti = 0; ti < textLines.length; ti++) { + const lw = ctx.measureText(textLines[ti]).width; + if (lw > maxLineW) maxLineW = lw; + } + const padX = 10; + const padY = 8; + w = Math.ceil(Math.max(maxLineW, 52) + padX * 2); + h = Math.ceil(textLines.length * lineH + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + signX = sx - w / 2; + signY = sy - h / 2; + ctx.restore(); + return { signX, signY, w, h }; + } + + /** + * ขอบเรืองรอบช่องตัวเลือก (carryEmbedCountdownHighlight + carryEmbedCountdownHighlightColor) + * ขนาดกรอบเท่าป้ายบนแมป (เดียวกับ drawQuizCarryChoiceLabels) ไม่ใช่ทั้ง cluster กริด + */ + function drawQuizCarryEmbedCountdownHighlight(ctx, worldToScreen, zoom, timeMs) { + if (!isQuizCarry() || !mapData) return; + if (mapData.carryEmbedCountdownHighlight === false) return; + const optIdx = getQuizCarryLocalGrabbableOptionIndex(); + if (optIdx == null) return; + const pr = computeQuizCarryChoicePlaqueScreenRect(ctx, worldToScreen, zoom, optIdx); + if (!pr || pr.w < 4 || pr.h < 4) return; + const rx = pr.signX; + const ry = pr.signY; + const rw = pr.w; + const rh = pr.h; + const rr = Math.max(6, Math.min(16, Math.min(rw, rh) * 0.14)); + const colRaw = mapData.carryEmbedCountdownHighlightColor; + const strokeBase = typeof colRaw === 'string' && /^#[0-9a-fA-F]{6}$/.test(colRaw.trim()) ? colRaw.trim() : '#ffb44a'; + const pulse = 0.55 + 0.45 * Math.sin((timeMs || 0) / 180); + ctx.save(); + function pathRr() { + ctx.beginPath(); + if (typeof ctx.roundRect === 'function') ctx.roundRect(rx, ry, rw, rh, rr); + else ctx.rect(rx, ry, rw, rh); + } + pathRr(); + ctx.fillStyle = strokeBase; + ctx.globalAlpha = 0.12 * pulse; + ctx.fill(); + for (let g = 6; g >= 1; g--) { + ctx.strokeStyle = strokeBase; + ctx.globalAlpha = (0.07 + g * 0.055) * pulse; + ctx.lineWidth = 2.5 + g * 4; + pathRr(); + ctx.stroke(); + } + ctx.globalAlpha = 0.98; + ctx.strokeStyle = strokeBase; + ctx.lineWidth = 4; + ctx.shadowColor = strokeBase; + ctx.shadowBlur = 22 * pulse; + pathRr(); + ctx.stroke(); + ctx.shadowBlur = 12 * pulse; + pathRr(); + ctx.stroke(); + ctx.shadowBlur = 0; + ctx.globalAlpha = 1; + ctx.restore(); + } + + function drawQuizCarryChoiceLabels(ctx, worldToScreen, zoom) { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return; + if (!quizCarryOptionsPickableNow()) return; + const choices = quizCarryCurrent.choices; + if (!choices || !choices.length) return; + const maxSlots = choices.length; + const ts = tileSize * zoom; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + ctx.save(); + for (let i = 0; i < Math.min(choices.length, maxSlots); i++) { + if (quizCarryOptionHeldByAnyone(i)) continue; + const b = getQuizCarryOptionClusterBounds(mapData, i + 1); + if (!b) continue; + const choiceText = String(choices[i] || '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(quizCarryCurrent, i); + const gridIdleEl = findQuizCarryGridIdleImgForChoiceIndex(i); + const plaqueExtra = imgUrl ? { imageUrl: imgUrl } + : (gridIdleEl && gridIdleEl.complete && gridIdleEl.naturalWidth ? { imageElement: gridIdleEl } : {}); + if (!choiceText && !plaqueExtra.imageUrl && !plaqueExtra.imageElement) continue; + const tileSpanX = (b.maxX - b.minX + 1); + const tileSpanY = (b.maxY - b.minY + 1); + const minPlaqueW = Math.ceil(Math.max(72, tileSpanX * ts - 16) * ps); + const minPlaqueH = Math.ceil(Math.max(52, tileSpanY * ts - 12) * ps); + if (!choiceText && (plaqueExtra.imageUrl || plaqueExtra.imageElement)) { + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + const lineH = fontPx * 1.2; + const padY = 8; + const minW = minPlaqueW; + const minH = minPlaqueH; + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + drawQuizCarryNeonPlaque(ctx, sx - minW / 2, sy - minH / 2, minW, minH, [], lineH, padY, i, null, plaqueExtra); + continue; + } + const line = choiceText; + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + let maxW = Math.max(56, tileSpanX * ts - 14); + const lineH = fontPx * 1.2; + let textLines = canvasWordWrapLines(ctx, line, maxW); + if (!textLines.length) textLines = [line]; + if (textLines.length * lineH > tileSpanY * ts - 8 && textLines.length > 1) { + const fp2 = Math.max(9, fontPx * 0.85); + ctx.font = '600 ' + fp2 + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + const lh2 = fp2 * 1.2; + maxW = Math.max(48, maxW - 4); + textLines = canvasWordWrapLines(ctx, line, maxW); + if (textLines.length * lh2 > tileSpanY * ts - 8) { + textLines = textLines.slice(0, Math.max(1, Math.floor((tileSpanY * ts - 8) / lh2))); + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + const maxLineW = Math.max(...textLines.map((t) => ctx.measureText(t).width), 40); + const padX = 10; + const padY = 8; + let w = Math.ceil(maxLineW + padX * 2); + let h = Math.ceil(textLines.length * lh2 + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const signX = sx - w / 2; + const signY = sy - h / 2; + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, textLines, lh2, padY, i, null, plaqueExtra); + continue; + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + let maxLineW = 0; + for (let ti = 0; ti < textLines.length; ti++) { + const lw = ctx.measureText(textLines[ti]).width; + if (lw > maxLineW) maxLineW = lw; + } + const padX = 10; + const padY = 8; + let w = Math.ceil(Math.max(maxLineW, 52) + padX * 2); + let h = Math.ceil(textLines.length * lineH + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const signX = sx - w / 2; + const signY = sy - h / 2; + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, textLines, lineH, padY, i, null, plaqueExtra); + } + ctx.restore(); + } + + function quizCarryHubCellPlay(md, tx, ty) { + const g = md && md.quizCarryHubArea; + return !!(g && g[ty] && g[ty][tx] === 1); + } + + function quizCarryInteractiveCellPlay(md, tx, ty) { + const g = md && md.interactive; + return !!(g && g[ty] && g[ty][tx] === 1); + } + + function quizCarryMapHasAnswerInteractive(md) { + if (!md || !md.interactive || !md.interactive.length) return false; + const h = md.height || 15, w = md.width || 20; + for (let y = 0; y < h; y++) { + const row = md.interactive[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1) return true; + } + } + return false; + } + + /** @returns {number|null} index ใน choices จากช่องหมายเลข 1..N บนแมป */ + function quizCarryOptionIndexAtPlay(md, tx, ty) { + const g = md && md.quizCarryOptionArea; + if (!g || !g[ty]) return null; + const v = g[ty][tx]; + if (v < 1 || v > QUIZ_CARRY_MAX_OPTION_SLOTS) return null; + const idx = v - 1; + const n = quizCarryCurrent && quizCarryCurrent.choices ? quizCarryCurrent.choices.length : 0; + if (n < 1 || idx >= n) return null; + return idx; + } + + /** ร่างตัวละครทับช่องโซนกลาง (ม่วง) — ใช้บล็อกการเดิน */ + function quizCarryFootprintOverlapsHub(px, py) { + if (!mapData || !isQuizCarry()) return false; + const md = mapData; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryHubCellPlay(md, tx, ty)) return true; + } + return false; + } + + /** ยืนข้างนอกแต่ชิดโซนกลาง — ใช้ให้กด F ส่งคำตอบได้โดยไม่ต้องย่ำบนม่วง */ + function quizCarryFootprintAdjacentToHub(md, px, py) { + if (!md) return false; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryHubCellPlay(md, tx, ty)) continue; + for (let di = 0; di < dirs.length; di++) { + const nx = tx + dirs[di][0], ny = ty + dirs[di][1]; + if (quizCarryHubCellPlay(md, nx, ny)) return true; + } + } + return false; + } + + function quizCarryCanSubmitAtHub(md, px, py) { + if (!md) return false; + return quizCarryFootprintOverlapsHub(px, py) || quizCarryFootprintAdjacentToHub(md, px, py); + } + + function quizCarryFootprintOverlapsInteractive(md, px, py) { + if (!md) return false; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryInteractiveCellPlay(md, tx, ty)) return true; + } + return false; + } + + function quizCarryFootprintAdjacentToInteractive(md, px, py) { + if (!md) return false; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryInteractiveCellPlay(md, tx, ty)) continue; + for (let di = 0; di < dirs.length; di++) { + const nx = tx + dirs[di][0], ny = ty + dirs[di][1]; + if (quizCarryInteractiveCellPlay(md, nx, ny)) return true; + } + } + return false; + } + + function quizCarryCanSubmitAtInteractiveZone(md, px, py) { + if (!md) return false; + return quizCarryFootprintOverlapsInteractive(md, px, py) || quizCarryFootprintAdjacentToInteractive(md, px, py); + } + + /** ส่งคำตอบ: ถ้ามีโซน interactive (เขียว) ในแมป ใช้โซนนั้น — ไม่มี fallback ชิดโซนกลาง */ + function quizCarryCanSubmitAnswerAt(md, px, py) { + if (!md) return false; + if (quizCarryMapHasAnswerInteractive(md)) { + return quizCarryCanSubmitAtInteractiveZone(md, px, py); + } + return quizCarryCanSubmitAtHub(md, px, py); + } + + /** ข้อความใน HUD / โซนคำถาม — ไม่รวมตัวเลือก (แสดงบนแผนที่ตามจุดสี) */ + function formatQuizCarryQuestionHud(q) { + if (!q) return ''; + return String(q.text || q.question || q.prompt || q.title || '').trim(); + } + + function quizCarryRestoreEmbedPhaseLabel() { + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (!phaseEl || !mapData) return; + phaseEl.textContent = quizCarryMapHasAnswerInteractive(mapData) + ? 'กด F หรือปุ่ม GRAB หยิบบนโซนตัวเลือก · ส่งที่โซน interactive (เขียว) — เล่นพร้อมกันได้' + : 'กด F หรือปุ่ม GRAB หยิบบนโซนตัวเลือก · ส่งเมื่อชิดโซนกลาง (ม่วงเป็นกำแพง) — เล่นพร้อมกันได้'; + } + + function isQuizCarryEmbedCountdownBlockingMovement() { + if (!quizCarryEmbedMissionFlowActive()) return false; + const now = Date.now(); + return (quizCarryEmbedCountdownEndAt > now) || (quizCarryEmbedPreOptionCountdownEndAt > now); + } + + function isQuizCarryEmbedLobbyBlockingMovement() { + return !!(quizCarryEmbedMissionFlowActive() && quizCarryPregameActive); + } + + function quizCarryPregameHumanIds() { + const ids = []; + if (myId != null && !isPreviewBotId(myId)) ids.push(String(myId)); + others.forEach((_, id) => { + if (!isPreviewBotId(id)) ids.push(String(id)); + }); + return ids; + } + + function quizCarryPregameBotCount() { + let n = 0; + others.forEach((_, id) => { + if (isPreviewBotId(id)) n++; + }); + return n; + } + + function quizCarryPregameReadyNumerator() { + let n = quizCarryPregameBotCount(); + quizCarryPregameHumanIds().forEach((id) => { + if (quizCarryLobbyReadyMap[id]) n++; + }); + return n; + } + + function quizCarryPregameTotalPlayers() { + return quizCarryPregameHumanIds().length + quizCarryPregameBotCount(); + } + + function isMePlayHost() { + if (myId == null) return false; + if (playHostId == null) return true; + return String(playHostId) === String(myId); + } + + function hideQuizCarryPregameOverlay() { + const ov = document.getElementById('quiz-carry-pregame-overlay'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + } + + function showQuizCarryPregameOverlay() { + const ov = document.getElementById('quiz-carry-pregame-overlay'); + if (ov) { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + updateQuizCarryPregameHud(); + } + + function quizCarrySyncGuestReadyIfNeeded() { + if (!quizCarryPregameActive || myId == null || isMePlayHost()) return; + const sid = String(myId); + if (quizCarryLobbyReadyMap[sid]) return; + quizCarryLobbyReadyMap[sid] = true; + if (socket && socket.connected) socket.emit('quiz-carry-lobby-ready', { ready: true }); + } + + function updateQuizCarryPregameHud() { + if (!quizCarryPregameActive) return; + const st = document.getElementById('quiz-carry-pregame-status'); + const primary = document.getElementById('quiz-carry-pregame-primary'); + const primaryImg = document.getElementById('quiz-carry-pregame-primary-img'); + const num = quizCarryPregameReadyNumerator(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + if (st) { + st.textContent = 'Ready Status : ' + num + '/' + tot; + } + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]); + const meReady = !!(myId && quizCarryLobbyReadyMap[String(myId)]); + if (primaryImg) { + primaryImg.src = humansReady ? '/Game/img/quiz-carry/btn-start.png' : '/Game/img/quiz-carry/btn-ready.png'; + primaryImg.alt = humansReady ? 'START' : 'READY'; + } + if (primary) { + primary.classList.toggle('is-start-phase', humansReady); + primary.classList.toggle('is-read-only', !isMePlayHost()); + primary.classList.toggle('is-pressed', isMePlayHost() && meReady && !humansReady); + primary.disabled = !isMePlayHost(); + primary.setAttribute('aria-pressed', humansReady ? 'false' : (meReady ? 'true' : 'false')); + primary.title = isMePlayHost() + ? (humansReady ? 'START' : (meReady ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + } + + function beginQuizCarryEmbedPregame() { + if (!quizCarryEmbedMissionFlowActive()) return; + quizCarryPregameActive = true; + quizCarryLobbyReadyMap = {}; + quizCarryPregameHumanIds().forEach((id) => { quizCarryLobbyReadyMap[id] = false; }); + showQuizCarryPregameOverlay(); + if (socket && socket.connected) socket.emit('quiz-carry-lobby-sync-request'); + quizCarrySyncGuestReadyIfNeeded(); + updateQuizCarryPregameHud(); + } + + function endQuizCarryEmbedPregameAndStart() { + if (!quizCarryPregameActive) return; + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryPickNextQuestion(); + const nowEmb = Date.now(); + if (!quizCarryEmbedMissionFlowActive()) { + quizCarryRestoreEmbedPhaseLabel(); + } else if (!(quizCarryEmbedCountdownEndAt > nowEmb) && !(quizCarryEmbedPreOptionCountdownEndAt > nowEmb)) { + quizCarryRestoreEmbedPhaseLabel(); + } + } + + function quizCarryOptionsPickableNow() { + if (!isQuizCarry() || !quizCarryCurrent) return true; + /* embed: ระหว่าง 3–2–1 รอบสอง (คำถามขึ้นแล้ว) — ยังไม่วาด/ไม่ให้หยิบป้ายตัวเลือก */ + if (quizCarryEmbedMissionFlowActive() && quizCarryEmbedPreOptionCountdownEndAt > Date.now()) return false; + if (!quizCarryOptionRevealAt || quizCarryOptionRevealAt <= 0) return true; + return Date.now() >= quizCarryOptionRevealAt; + } + + function quizCarryApplyPhaseTimersForCurrentQuestion() { + if (!quizCarryCurrent) { + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + return; + } + const readMs = Math.max(0, Math.floor(Number(quizCarryCarryTimingMs.carryReadMs)) || 0); + const ansMs = Math.max(1000, Math.floor(Number(quizCarryCarryTimingMs.carryAnswerMs)) || 5000); + const t0 = Date.now(); + if (readMs <= 0) { + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = t0 + ansMs; + } else { + quizCarryOptionRevealAt = t0 + readMs; + quizCarryAnswerCloseAt = quizCarryOptionRevealAt + ansMs; + } + } + + /** embed: หลัง 3–2–1 ก่อนหยิบ — เปิดป้ายตัวเลือกทันที + จับเวลาตอบ carryAnswerMs */ + function quizCarryApplyEmbedAnswerWindowAfterPreOption321() { + if (!quizCarryCurrent) return; + const ansMs = Math.max(1000, Math.floor(Number(quizCarryCarryTimingMs.carryAnswerMs)) || 5000); + const t0 = Date.now(); + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = t0 + ansMs; + } + + function updateQuizCarryCarryPhaseHud() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!isQuizCarry() || !quizCarryCurrent) return; + if (isDetectiveMinigamePlay() && !quizCarryEmbedMissionFlowActive()) return; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (!phaseEl) return; + if (quizCarryAnswerTimeupAwaitNext && quizCarryAnswerRoundTimeupAdvanceAt > 0) { + const sec = Math.max(0, Math.ceil((quizCarryAnswerRoundTimeupAdvanceAt - Date.now()) / 1000)); + phaseEl.textContent = 'หมดเวลาตอบ — ไปข้อถัดไปใน ' + sec + ' วิ…'; + return; + } + const now = Date.now(); + if (quizCarryEmbedMissionFlowActive() && quizCarryEmbedPreOptionCountdownEndAt > 0 && now < quizCarryEmbedPreOptionCountdownEndAt) { + const sec = Math.max(0, Math.ceil((quizCarryEmbedPreOptionCountdownEndAt - now) / 1000)); + phaseEl.textContent = 'เตรียมหยิบคำตอบ — นับ 3·2·1 (' + sec + ' วิ)'; + return; + } + if (quizCarryOptionRevealAt > 0 && now < quizCarryOptionRevealAt) { + const remain = Math.max(0, Math.ceil((quizCarryOptionRevealAt - now) / 1000)); + phaseEl.textContent = 'อ่านคำถาม — ตัวเลือกขึ้นใน ' + remain + ' วิ'; + return; + } + if (quizCarryAnswerCloseAt > now) { + const remain = Math.max(0, Math.ceil((quizCarryAnswerCloseAt - now) / 1000)); + phaseEl.textContent = 'หยิบ/ส่งคำตอบ — เหลือ ' + remain + ' วิ'; + return; + } + quizCarryRestoreEmbedPhaseLabel(); + } + + function tickQuizCarryRoundTimers() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return; + if (quizCarryEmbedCountdownEndAt > Date.now()) return; + if (quizCarryEmbedPreOptionCountdownEndAt > Date.now()) return; + if (quizCarryEmbedPendingQuestion) return; + if (quizCarryAnswerTimeupAwaitNext) return; + if (!quizCarryAnswerCloseAt || quizCarryAnswerCloseAt <= 0) return; + if (Date.now() < quizCarryAnswerCloseAt) return; + quizCarryAnswerCloseAt = 0; + quizCarryOptionRevealAt = 0; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + renderPlayQuizScoreboard(playLiveQuizScores); + if (isDetectiveMinigamePlay()) { + quizCarryAfterRoundResolved(); + return; + } + quizCarryAnswerTimeupAwaitNext = true; + showQuizCarryAnswerRoundTimeupOverlay(); + updateQuizCarryCarryPhaseHud(); + } + + function resetQuizCarryEmbedCountdownOverlayInners() { + const cq = document.getElementById('quiz-carry-embed-countdown-q'); + const ck = document.getElementById('quiz-carry-embed-countdown-kicker'); + if (cq) { + cq.textContent = ''; + cq.style.display = 'none'; + } + if (ck) { + ck.textContent = 'คำถาม · Question'; + ck.style.display = 'none'; + ck.classList.remove('quiz-carry-embed-countdown-kicker--mission'); + } + } + + /** นับ 3–2–1: ซ่อนคำถามและหัวข้อ — เหลือแค่รูปเลขจาก /Game/img/QUESTION */ + function showQuizCarryEmbedCountdownDigitsOnlyChrome() { + const cq = document.getElementById('quiz-carry-embed-countdown-q'); + const ck = document.getElementById('quiz-carry-embed-countdown-kicker'); + if (cq) { + cq.textContent = ''; + cq.style.display = 'none'; + } + if (ck) { + ck.textContent = ''; + ck.style.display = 'none'; + ck.classList.remove('quiz-carry-embed-countdown-kicker--mission'); + } + } + + function hideQuizCarryEmbedCountdownOverlay() { + resetQuizCarryEmbedCountdownLayoutDom(); + const ov = document.getElementById('quiz-carry-embed-countdown'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + resetQuizCarryEmbedCountdownOverlayInners(); + } + + function tickQuizCarryEmbedCountdown() { + if (!quizCarryEmbedMissionFlowActive()) return; + const now = Date.now(); + const ov = document.getElementById('quiz-carry-embed-countdown'); + const numEl = document.getElementById('quiz-carry-embed-countdown-num'); + + if (quizCarryCurrent && quizCarryEmbedPreOptionCountdownEndAt > 0) { + if (!numEl) return; + if (now >= quizCarryEmbedPreOptionCountdownEndAt) { + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryEmbedPreOptionCountdownStartAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + quizCarryApplyEmbedAnswerWindowAfterPreOption321(); + quizCarryRestoreEmbedPhaseLabel(); + syncQuizCarryEmbedQuestionStrip(); + updateQuizCarryCarryPhaseHud(); + return; + } + const elapsedPre = now - quizCarryEmbedPreOptionCountdownStartAt; + const nPre = 3 - Math.min(2, Math.floor(elapsedPre / 1000)); + setCountdown321QuestionAssetGraphic(numEl, Math.max(1, Math.min(3, nPre))); + if (ov) { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + syncQuizCarryEmbedCountdownLayout(); + showQuizCarryEmbedCountdownDigitsOnlyChrome(); + syncQuizCarryEmbedQuestionStrip(); + return; + } + + if (!quizCarryEmbedPendingQuestion || quizCarryEmbedCountdownEndAt <= 0) return; + if (now >= quizCarryEmbedCountdownEndAt) { + const pq = quizCarryEmbedPendingQuestion; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + if (!pq) return; + quizCarryCurrent = pq; + preloadQuizCarryChoiceImages(pq); + playQuizText = formatQuizCarryQuestionHud(pq); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + quizCarryRestoreEmbedPhaseLabel(); + others.forEach((o) => { + if (!o) return; + o.botQuizCarryPathfindAfter = 0; + }); + syncQuizCarryEmbedQuestionStrip(); + quizCarryEmbedPreOptionCountdownStartAt = Date.now(); + quizCarryEmbedPreOptionCountdownEndAt = quizCarryEmbedPreOptionCountdownStartAt + 3000; + updateQuizCarryCarryPhaseHud(); + return; + } + const elapsed = now - quizCarryEmbedCountdownStartAt; + const n = 3 - Math.min(2, Math.floor(elapsed / 1000)); + setCountdown321QuestionAssetGraphic(numEl, Math.max(1, Math.min(3, n))); + if (ov) { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + syncQuizCarryEmbedCountdownLayout(); + showQuizCarryEmbedCountdownDigitsOnlyChrome(); + syncQuizCarryEmbedQuestionStrip(); + } + + function quizCarryPickNextQuestion() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!quizCarryPool.length) { + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + playQuizText = 'ไม่มีคำถาม — ใส่ quizQuestions ในแมป (choices + correctIndex) หรือตั้งใน Admin'; + return; + } + let q; + let guard = 0; + do { + q = quizCarryPool[Math.floor(Math.random() * quizCarryPool.length)]; + guard++; + } while (guard < 12 && quizCarryPool.length > 1 && quizCarryCurrent && q && q.text === quizCarryCurrent.text); + if (quizCarryEmbedMissionFlowActive()) { + quizCarryEmbedPendingQuestion = q; + preloadQuizCarryChoiceImages(q); + quizCarryEmbedCountdownStartAt = Date.now(); + quizCarryEmbedCountdownEndAt = quizCarryEmbedCountdownStartAt + 3000; + quizCarryEmbedPreOptionCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryCurrent = null; + playQuizText = ''; + clearQuizMapQuestionCarryFeedback(); + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + o.botPathStuckTicks = 0; + /* ต้องใช้ performance.now() ช่วงกับ stepQuizCarryPreviewBots — ห้ามใช้ Date.now() epoch ไม่งั้นบอทค้าง pathfind ตลอด */ + o.botQuizCarryPathfindAfter = 0; + }); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = '…'; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'รอเริ่มคำถาม — นับถอยหลัง 3 · 2 · 1'; + tickQuizCarryEmbedCountdown(); + syncQuizCarryEmbedQuestionStrip(); + return; + } + quizCarryCurrent = q; + preloadQuizCarryChoiceImages(q); + playQuizText = formatQuizCarryQuestionHud(q); + clearQuizMapQuestionCarryFeedback(); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + }); + quizCarryApplyPhaseTimersForCurrentQuestion(); + } + + /** หลังจบข้อหนึ่งข้อ (ถูกหรือหมดเวลา): พรีวิวนับรอบจนครบ carrySessionLength แล้วจบเซสชัน */ + function quizCarryAfterRoundResolved() { + if (!isQuizCarry()) return; + if (isDetectiveMinigamePlay() && !quizCarryEmbedMissionFlowActive()) { + finishDetectiveMinigameAndReturnLobby(); + return; + } + if (!quizCarryEmbedMissionFlowActive() && !previewMode) { + quizCarryPickNextQuestion(); + return; + } + quizCarryRoundsCompleted++; + const lim = quizCarrySessionLength; + if (lim > 0 && quizCarryRoundsCompleted >= lim) { + showQuizCarrySessionCompleteOverlay(); + return; + } + quizCarryPickNextQuestion(); + } + + function playQuizCarryAvatarUrlForMission(characterId) { + if (!characterId) return ''; + return BASE + '/img/characters/' + encodeURIComponent(String(characterId)) + '_down.png'; + } + + /** อวาตาร์สรุปภารกิจ / DOM — ใช้สี gen เดียวกับในเกม (เลเยอร์ + tint) */ + function applyQuizCarryRankAvatarTint(imgEl, characterId, peerId) { + if (!imgEl || !characterId) return; + let attempts = 0; + const fallbackUrl = playQuizCarryAvatarUrlForMission(characterId); + const tick = () => { + attempts++; + const nowT = Date.now(); + const rawImg = getCharacterFrame(characterId, 'down', nowT, false) || getCharacterImg(characterId, 'down'); + if (!rawImg) { + if (fallbackUrl) imgEl.src = fallbackUrl; + return; + } + if ((!rawImg.complete || !rawImg.naturalWidth) && attempts < 28) { + if (fallbackUrl) imgEl.src = fallbackUrl + '?p=' + String(attempts); + setTimeout(tick, 90); + return; + } + if (!rawImg.naturalWidth) { + if (fallbackUrl) imgEl.src = fallbackUrl; + return; + } + const tint = getPlayTintForMissionAvatar(peerId != null ? peerId : ((myId != null && myId !== '') ? myId : 'local')); + const isMePeer = (peerId != null && myId != null && String(peerId) === String(myId)); + const out = resolvePlayDrawCharSource(characterId, 'down', nowT, false, tint, isMePeer); + if (out && out.tagName === 'CANVAS' && out.width > 0) { + try { + imgEl.src = out.toDataURL('image/png'); + return; + } catch (e) { /* ต่อไปลอง walk หรือ URL */ } + } + if (out && out.src && out !== rawImg) { + imgEl.src = out.src; + return; + } + if (attempts < 28 && playCharLayerDiscoveryPending(characterId)) { + setTimeout(tick, 120); + return; + } + imgEl.src = rawImg.src || fallbackUrl; + }; + tick(); + } + + /** เกรดจากค่าเฉลี่ยคะแนนทีม (0–100 หลังปัด) — A 80–100, B 60–79, C 0–59 */ + function quizCarryGradeFromTeamAverage(avgRounded) { + if (avgRounded >= 80) return 'A'; + if (avgRounded >= 60) return 'B'; + return 'C'; + } + + function quizCarryRollCardRewardForGrade(grade) { + const u = Math.random() * 100; + if (grade === 'A') { + if (u < 30) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' }; + if (u < 80) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'B') { + if (u < 20) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' }; + if (u < 50) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'C') { + if (u < 20) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + return null; + } + + function quizCarryBuildMissionRankList() { + const ranks = []; + if (myId != null) { + ranks.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + score: Math.max(0, Number(playLiveQuizScores[myId]) || 0), + characterId: me.characterId || getPlayCharacterId(), + }); + } + others.forEach((o, id) => { + if (!o) return; + ranks.push({ + id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id), + score: Math.max(0, Number(playLiveQuizScores[id]) || 0), + characterId: o.characterId || null, + }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + return ranks.slice(0, 5); + } + + function cancelEmbedPreviewLobbyReturnTimer() { + if (embedPreviewLobbyReturnTimer != null) { + clearTimeout(embedPreviewLobbyReturnTimer); + embedPreviewLobbyReturnTimer = null; + } + } + + function cancelQuizCarryResultEndAfterTimeup() { + if (quizCarryResultEndTimer != null) { + clearTimeout(quizCarryResultEndTimer); + quizCarryResultEndTimer = null; + } + cancelEmbedPreviewLobbyReturnTimer(); + } + + function cancelQuizCarrySessionCompleteResultToSummary() { + if (quizCarrySessionCompleteResultToSummaryT != null) { + clearTimeout(quizCarrySessionCompleteResultToSummaryT); + quizCarrySessionCompleteResultToSummaryT = null; + } + } + + function cancelQuizCarryAnswerRoundTimeupAuto() { + if (quizCarryAnswerRoundTimeupAutoT != null) { + clearTimeout(quizCarryAnswerRoundTimeupAutoT); + quizCarryAnswerRoundTimeupAutoT = null; + } + quizCarryAnswerRoundTimeupAdvanceAt = 0; + } + + function quizCarrySetAnswerRoundTimeupVisualActive(on) { + try { + if (on) document.documentElement.classList.add('quiz-carry-answer-timeup-active'); + else document.documentElement.classList.remove('quiz-carry-answer-timeup-active'); + } catch (e) { /* ignore */ } + } + + /** หลังภาพ result-complete / gameover (editor embed + preview): รอ 5s แล้วไปล็อบบี้ห้องพรีวิว */ + function scheduleEmbedPreviewReturnToLobbyAfterResultEnd() { + cancelEmbedPreviewLobbyReturnTimer(); + if (!(previewMode && editorEmbedReturn)) return; + embedPreviewLobbyReturnTimer = setTimeout(function () { + embedPreviewLobbyReturnTimer = null; + window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + }, 5000); + } + + /** มีผู้เล่นอย่างน้อยหนึ่งคนได้คะแนน > 0 (= ส่งคำตอบถูกที่ฮับอย่างน้อย 1 ครั้ง; คะแนนต่อข้อคงที่ QUIZ_CARRY_POINTS_PER_CORRECT) */ + function quizCarryAnyPlayerHasCorrectAnswer() { + if (!playLiveQuizScores || typeof playLiveQuizScores !== 'object') return false; + const keys = Object.keys(playLiveQuizScores); + for (let i = 0; i < keys.length; i++) { + if (Math.max(0, Number(playLiveQuizScores[keys[i]]) || 0) > 0) return true; + } + return false; + } + + function hideQuizCarryResultEndLayer() { + cancelEmbedPreviewLobbyReturnTimer(); + const el = document.getElementById('quiz-carry-result-end-layer'); + if (!el) return; + el.classList.add('is-hidden'); + el.setAttribute('aria-hidden', 'true'); + } + + function showQuizCarryResultEndLayer(useComplete) { + const el = document.getElementById('quiz-carry-result-end-layer'); + const img = document.getElementById('quiz-carry-result-end-img'); + if (!el || !img) return; + const fname = useComplete ? 'result-complete.png' : 'result-gameover.png'; + if (isSpaceShooterMissionUiMapPlay()) { + img.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now()); + }; + img.src = violentCrimeAssetUrl(fname) + '?v=' + String(Date.now()); + } else { + img.onerror = null; + img.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now()); + } + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + } + + /** Embed editor: หลัง timeup บนโต๊ะ — รอ 5 วิ แล้วโชว์ result-complete / result-gameover (ใช้ร่วม quiz_carry + crown) */ + function scheduleEmbedDeskResultAfterDelay(okResolver) { + cancelQuizCarryAnswerRoundTimeupAuto(); + cancelQuizCarryResultEndAfterTimeup(); + hideQuizCarryResultEndLayer(); + const resolve = typeof okResolver === 'function' ? okResolver : function () { return !!okResolver; }; + quizCarryResultEndTimer = setTimeout(function () { + quizCarryResultEndTimer = null; + let anyOk = false; + try { + anyOk = !!resolve(); + } catch (e) { + anyOk = false; + } + hideQuizCarryTimeupOnDeskLayer(); + showQuizCarryResultEndLayer(anyOk); + scheduleEmbedPreviewReturnToLobbyAfterResultEnd(); + }, 5000); + } + + function hideQuizCarryTimeupOnDeskLayer() { + cancelQuizCarryAnswerRoundTimeupAuto(); + cancelQuizCarryResultEndAfterTimeup(); + quizCarrySetAnswerRoundTimeupVisualActive(false); + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) { + quizCarryAnswerTimeupAwaitNext = false; + return; + } + el.classList.remove('qc-timeup-desk--interactive'); + quizCarryAnswerTimeupAwaitNext = false; + const anchor = el.querySelector('.qc-timeup-desk-anchor') || el.querySelector('.qc-timeup-desk-inner'); + if (anchor) { + anchor.style.left = ''; + anchor.style.top = ''; + anchor.style.width = ''; + anchor.style.height = ''; + } + el.classList.add('is-hidden'); + el.setAttribute('aria-hidden', 'true'); + } + + /** หมดเวลาตอบ (carryAnswerMs) — โชว์ TIME'S UP บนโซนคำถาม ~3s แล้วไปข้อถัดไป (ไม่ใช่ flow 5s → result ของ embed จบภารกิจ) */ + function showQuizCarryAnswerRoundTimeupOverlay() { + if (!isQuizCarry()) return; + cancelQuizCarryAnswerRoundTimeupAuto(); + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) return; + const img = document.getElementById('quiz-carry-timeup-txt-img'); + if (img) { + img.onerror = null; + img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + } + quizCarrySetAnswerRoundTimeupVisualActive(true); + quizCarryAnswerRoundTimeupAdvanceAt = Date.now() + 3000; + el.classList.add('qc-timeup-desk--interactive'); + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + try { + syncQuizCarryTimeupDeskLayerPosition(); + } catch (e) { /* ignore */ } + quizCarryAnswerRoundTimeupAutoT = setTimeout(function () { + quizCarryAnswerRoundTimeupAutoT = null; + dismissQuizCarryAnswerRoundTimeupAndContinue(); + }, 3000); + } + + function dismissQuizCarryAnswerRoundTimeupAndContinue() { + if (!quizCarryAnswerTimeupAwaitNext) return; + cancelQuizCarryAnswerRoundTimeupAuto(); + quizCarryAnswerTimeupAwaitNext = false; + hideQuizCarryTimeupOnDeskLayer(); + quizCarryAfterRoundResolved(); + } + + /** หลังกดรับหลักฐาน (embed preview) — แสดง timeup กลางแคนวาส (Jumper mnptfts2 = img/Jumper/result-timeup.png) แยกจากป๊อปสรุปคะแนน + * @param {function():boolean} [embedResultOkFn] — ถ้าไม่ส่ง = ใช้คะแนน quiz_carry (คนตอบถูก); ส่งเมื่อ crown เพื่อเลือก complete vs gameover */ + function showQuizCarryTimeupOnDeskLayer(embedResultOkFn) { + cancelQuizCarryAnswerRoundTimeupAuto(); + quizCarrySetAnswerRoundTimeupVisualActive(false); + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) return; + // Last Light (Gauntlet Crown): skip TIME'S UP desk graphic; keep embed 5s → result flow. + if (isGauntletCrownHeistMapPlay()) { + if (previewMode && editorEmbedReturn) { + scheduleEmbedDeskResultAfterDelay( + typeof embedResultOkFn === 'function' ? embedResultOkFn : quizCarryAnyPlayerHasCorrectAnswer + ); + } + return; + } + const img = document.getElementById('quiz-carry-timeup-txt-img'); + if (img) { + if (isJumpSurviveMissionUiMapPlay()) { + img.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + }; + img.src = jumperAssetUrl('result-timeup.png') + '?v=' + String(Date.now()); + } else if (isSpaceShooterMissionUiMapPlay()) { + img.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + }; + img.src = violentCrimeAssetUrl('result-timeup.png') + '?v=' + String(Date.now()); + } else { + img.onerror = null; + img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + } + } + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + try { + syncQuizCarryTimeupDeskLayerPosition(); + } catch (e) { /* ignore */ } + if (previewMode && editorEmbedReturn) { + scheduleEmbedDeskResultAfterDelay(embedResultOkFn || quizCarryAnyPlayerHasCorrectAnswer); + } + } + + function showQuizCarryMissionSummaryOverlay(opts) { + opts = opts || {}; + const forceF = !!opts.forceGradeF; + const overlay = document.getElementById('quiz-carry-mission-overlay'); + if (!overlay) return; + hideQuizCarryTimeupOnDeskLayer(); + const ranks = quizCarryBuildMissionRankList(); + const parts = ranks.map((r) => Math.max(0, Number(r.score) || 0)); + const total = parts.reduce((a, b) => a + b, 0); + const n = ranks.length; + const avgForGrade = n ? Math.min(100, Math.max(0, Math.round(total / n))) : 0; + const grade = forceF ? 'F' : quizCarryGradeFromTeamAverage(avgForGrade); + const reward = grade === 'F' ? null : quizCarryRollCardRewardForGrade(grade); + + const rowEl = document.getElementById('qc-mission-rank-row'); + const totalEl = document.getElementById('qc-mission-total-line'); + const gradeEl = document.getElementById('qc-mission-grade'); + const cardTextEl = document.getElementById('qc-mission-card-text'); + const checkEl = document.getElementById('qc-mission-check'); + const successTxt = document.getElementById('qc-mission-success-txt'); + const rankTagLabels = ['[1st]', '[2nd]', '[3rd]', '[4th]', '[5th]']; + if (rowEl) { + rowEl.innerHTML = ''; + ranks.forEach((r, idx) => { + const rank = idx + 1; + const cell = document.createElement('div'); + cell.className = 'qc-mission-rank-cell'; + const tag = document.createElement('div'); + tag.className = 'qc-mission-rank-tag'; + tag.textContent = rankTagLabels[rank - 1] || ('[' + rank + ']'); + const frame = document.createElement('div'); + frame.className = 'qc-mission-rank-frame'; + const img = document.createElement('img'); + img.className = 'qc-mission-rank-avatar'; + img.alt = ''; + applyQuizCarryRankAvatarTint(img, r.characterId, r.id); + img.onerror = function () { + this.onerror = null; + this.src = 'data:image/svg+xml,' + encodeURIComponent('?'); + }; + frame.appendChild(img); + const nick = document.createElement('div'); + nick.className = 'qc-mission-rank-nick'; + nick.textContent = r.nickname; + nick.title = r.nickname; + const sc = document.createElement('div'); + sc.className = 'qc-mission-rank-score'; + sc.textContent = String(r.score); + cell.appendChild(tag); + cell.appendChild(frame); + cell.appendChild(nick); + cell.appendChild(sc); + rowEl.appendChild(cell); + }); + } + if (totalEl) { + totalEl.textContent = ''; + if (!n) { + totalEl.textContent = '—'; + } else { + const lab = document.createElement('span'); + lab.textContent = 'คะแนนรวม '; + const nums = document.createElement('span'); + nums.className = 'qc-mission-total-nums'; + nums.textContent = '(' + parts.join('+') + ') = ' + total; + const tail = document.createElement('span'); + tail.textContent = ' · เฉลี่ยทีม ' + avgForGrade + '/100'; + totalEl.appendChild(lab); + totalEl.appendChild(nums); + totalEl.appendChild(tail); + } + } + if (gradeEl) { + gradeEl.textContent = grade; + gradeEl.className = 'qc-mission-grade qc-mission-grade--' + String(grade).toLowerCase(); + } + const okMission = grade !== 'F'; + if (checkEl) checkEl.style.display = okMission ? '' : 'none'; + if (successTxt) { + successTxt.style.display = okMission ? '' : 'none'; + if (okMission) successTxt.textContent = 'ภารกิจสำเร็จ'; + } + if (cardTextEl) { + cardTextEl.textContent = ''; + if (grade === 'F') { + const pl = document.createElement('div'); + pl.className = 'qc-mission-reward-plaque qc-mission-reward-plaque--muted'; + const t = document.createElement('div'); + t.className = 'qc-mission-reward-plaque-title'; + t.textContent = 'ไม่ได้รับการ์ด'; + const s = document.createElement('div'); + s.className = 'qc-mission-reward-plaque-sub'; + s.textContent = 'เกรด F'; + pl.appendChild(t); + pl.appendChild(s); + cardTextEl.appendChild(pl); + } else if (reward) { + const pl = document.createElement('div'); + pl.className = 'qc-mission-reward-plaque'; + const t = document.createElement('div'); + t.className = 'qc-mission-reward-plaque-title'; + t.textContent = reward.th; + const s = document.createElement('div'); + s.className = 'qc-mission-reward-plaque-sub'; + s.textContent = reward.en; + pl.appendChild(t); + pl.appendChild(s); + cardTextEl.appendChild(pl); + } + /* การ์ดพิเศษจากชุดคำถาม (ได้มาจากการตอบคำถามพิเศษในฉาก) — แสดงเสมอถ้าได้รับ */ + if (specialQuizAwardedCard) renderSpecialCardInBonus(cardTextEl, specialQuizAwardedCard); + } + overlay.classList.remove('is-hidden'); + const btn = document.getElementById('btn-quiz-carry-mission-done'); + if (btn) { + btn.onclick = function () { + cancelQuizCarrySessionCompleteResultToSummary(); + overlay.classList.add('is-hidden'); + if (isDetectiveMinigamePlay()) { + finishDetectiveMinigameAndReturnLobby(); + } else { + window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + } + }; + } + } + + function showQuizCarrySessionCompleteOverlay() { + if (quizCarrySessionEnded) return; + quizCarrySessionEnded = true; + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + playQuizText = 'ครบชุดคำถามแล้ว'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'ครบ ' + String(quizCarryRoundsCompleted) + ' ข้อ'; + const missionOv = document.getElementById('quiz-carry-mission-overlay'); + if (missionOv && quizCarryUseMissionSummaryOverlay) { + cancelQuizCarrySessionCompleteResultToSummary(); + hideQuizCarryResultEndLayer(); + const summaryOpts = { forceGradeF: !!(playQuizPlayerLocal && playQuizPlayerLocal.eliminated) }; + const splashRanks = quizCarryBuildMissionRankList(); + const splashParts = splashRanks.map((r) => Math.max(0, Number(r.score) || 0)); + const splashTotal = splashParts.reduce((a, b) => a + b, 0); + const splashN = splashRanks.length; + const splashAvg = splashN ? Math.min(100, Math.max(0, Math.round(splashTotal / splashN))) : 0; + const splashGrade = summaryOpts.forceGradeF ? 'F' : quizCarryGradeFromTeamAverage(splashAvg); + const useCompleteSplash = splashGrade !== 'F'; + showQuizCarryResultEndLayer(useCompleteSplash); + quizCarrySessionCompleteResultToSummaryT = setTimeout(function () { + quizCarrySessionCompleteResultToSummaryT = null; + hideQuizCarryResultEndLayer(); + showQuizCarryMissionSummaryOverlay(summaryOpts); + }, QUIZ_CARRY_SESSION_END_SPLASH_MS); + renderPlayQuizScoreboard(playLiveQuizScores); + return; + } + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) titleEl.textContent = 'เกมจบ · Game over'; + msgEl.textContent = 'ครบจำนวนคำถามที่ตั้งไว้ (' + String(quizCarryRoundsCompleted) + ' ข้อ) · Quiz carry session finished'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + score: Math.max(0, Number(playLiveQuizScores[myId]) || 0), + }); + } + others.forEach((o, id) => { + if (!o) return; + ranks.push({ + id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id), + score: Math.max(0, Number(playLiveQuizScores[id]) || 0), + }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + if (tryFinishDetectiveMinigameAndReturnLobby()) return; + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + renderPlayQuizScoreboard(playLiveQuizScores); + } + + /** Toast กลางบน (หยิบ / ถูก / ผิด / คำใบ้) — ปิดการแสดงตามคำขอ UX */ + function showQuizCarryToast(msg, ok) { + if (typeof window.__quizCarryToastT === 'number') clearTimeout(window.__quizCarryToastT); + window.__quizCarryToastT = null; + const el = document.getElementById('play-quiz-feedback'); + if (!el) return; + el.classList.add('is-hidden'); + el.className = 'play-quiz-feedback'; + el.textContent = ''; + } + + /** + * หยิบ/ส่งคำตอบ — ต้องกด F (fromKey) สำหรับผู้เล่น · บอท preview เรียกแบบ silent เมื่อหยุดเดินแล้ว + * @returns {boolean} ทำ action สำเร็จหรือไม่ + */ + function tryQuizCarryInteractionForPlayer(actorId, ox, oy, opts) { + opts = opts || {}; + if (quizCarryPregameActive) return false; + if (quizCarrySessionEnded) return false; + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return false; + const md = mapData; + const footprint = quizTilesFootprintPlay(ox, oy); + const ent = actorId === myId ? me : others.get(actorId); + if (!ent) return false; + let pickupIdx = null; + for (const k of footprint) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + const opt = quizCarryOptionIndexAtPlay(md, tx, ty); + if (opt != null) pickupIdx = pickupIdx === null ? opt : pickupIdx; + } + const canSubmitAnswer = quizCarryCanSubmitAnswerAt(md, ox, oy); + const correct = quizCarryCurrent.correctIndex; + let held = ent.quizCarryHeld; + if (held != null && canSubmitAnswer) { + if (!quizCarryOptionsPickableNow()) return false; + ent.quizCarryHeld = null; + const ok = held === correct; + if (ok) { + playLiveQuizScores[actorId] = (playLiveQuizScores[actorId] || 0) + QUIZ_CARRY_POINTS_PER_CORRECT; + const lim = previewMode && isQuizCarry() ? quizCarrySessionLength : 0; + const willEnd = lim > 0 && (quizCarryRoundsCompleted + 1) >= lim; + const showMapFb = !opts.silent && actorId === myId; + if (showMapFb) showQuizMapQuestionCarryFeedback(true); + if (!opts.silent) { + const pts = QUIZ_CARRY_POINTS_PER_CORRECT; + showQuizCarryToast(willEnd ? ('ถูกต้อง +' + pts + ' แต้ม — ครบชุดคำถาม') : ('ถูกต้อง +' + pts + ' แต้ม — สุ่มคำถามใหม่'), true); + } + me.quizCarryHeld = null; + others.forEach((o) => { if (o) o.quizCarryHeld = null; }); + const afterOk = () => { quizCarryAfterRoundResolved(); }; + if (showMapFb) { + window.setTimeout(afterOk, 560); + } else { + afterOk(); + } + } else { + if (!opts.silent && actorId === myId) { + showQuizMapQuestionCarryFeedback(false); + } + if (!opts.silent) { + showQuizCarryToast('ผิด — คืนป้ายที่โซนตัวเลือกแล้ว กด F หยิบใหม่ได้', false); + } + } + renderPlayQuizScoreboard(playLiveQuizScores); + return true; + } + if (held == null && pickupIdx != null) { + if (!quizCarryOptionsPickableNow()) return false; + if (quizCarryOptionHeldByAnyone(pickupIdx)) { + if (opts.fromKey && actorId === myId && !opts.silent) { + showQuizCarryToast('ตัวเลือกนี้มีคนถืออยู่แล้ว — เลือกข้ออื่น', false); + } + return false; + } + ent.quizCarryHeld = pickupIdx; + const label = (quizCarryCurrent.choices && quizCarryCurrent.choices[pickupIdx]) || String(pickupIdx); + if (!opts.silent) { + showQuizCarryToast('หยิบ: ' + label, true); + } + return true; + } + if (opts.fromKey && actorId === myId && !opts.silent) { + const t = Date.now(); + if (t - (me.quizCarryHintT || 0) > 2200) { + me.quizCarryHintT = t; + if (held != null && !canSubmitAnswer) { + showQuizCarryToast(quizCarryMapHasAnswerInteractive(md) + ? 'ถือป้ายอยู่ — ยืนที่โซน interactive (เขียว) หรือชิดขอบแล้วกด F เพื่อส่ง' + : 'ถือป้ายอยู่ — ยืนชิดโซนกลาง (ม่วง) แล้วกด F เพื่อส่ง', false); + } else { + showQuizCarryToast(quizCarryMapHasAnswerInteractive(md) + ? 'ยืนโซนตัวเลือกแล้วกด F หยิบ · ยืนโซนเขียว (interactive) แล้วกด F ส่งเมื่อถือป้าย' + : 'ยืนโซนตัวเลือกแล้วกด F หยิบ · ยืนชิดโซนกลางแล้วกด F ส่งเมื่อถือป้าย', false); + } + } + } + return false; + } + + function quizCarryGrabCoreGatesOk() { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return false; + if (quizCarryPregameActive || quizCarrySessionEnded) return false; + if (myId == null) return false; + if (isQuizCarryEmbedCountdownBlockingMovement()) return false; + if (!quizCarryOptionsPickableNow()) return false; + return true; + } + + /** ดัชนีตัวเลือก 0-based ที่ผู้เล่นท้องถิ่นยืนอยู่และหยิบได้ — ไม่มีคืน null */ + function getQuizCarryLocalGrabbableOptionIndex() { + if (!quizCarryGrabCoreGatesOk()) return null; + if (me.quizCarryHeld != null) return null; + const md = mapData; + const footprint = quizTilesFootprintPlay(me.x, me.y); + let pickupIdx = null; + for (const k of footprint) { + const p = k.split(','); + const tx = +p[0]; + const ty = +p[1]; + const opt = quizCarryOptionIndexAtPlay(md, tx, ty); + if (opt != null) pickupIdx = pickupIdx === null ? opt : pickupIdx; + } + if (pickupIdx == null || quizCarryOptionHeldByAnyone(pickupIdx)) return null; + return pickupIdx; + } + + /** ยืนบนช่องตัวเลือกและหยิบได้ */ + function quizCarryGrabPickupAvailable() { + return getQuizCarryLocalGrabbableOptionIndex() != null; + } + + /** ถือป้ายแล้วยืนโซนส่งได้ */ + function quizCarryGrabSubmitAvailable() { + if (!quizCarryGrabCoreGatesOk()) return false; + if (me.quizCarryHeld == null) return false; + return quizCarryCanSubmitAnswerAt(mapData, me.x, me.y); + } + + /** true = กด F / ปุ่ม จะหยิบหรือส่งได้ทันที */ + function quizCarryGrabInteractionAvailable() { + return quizCarryGrabPickupAvailable() || quizCarryGrabSubmitAvailable(); + } + + function syncQuizCarryGrabButton() { + const btn = document.getElementById('quiz-carry-grab-btn'); + if (!btn) return; + if (!isQuizCarry() || !mapData) { + btn.classList.add('is-hidden'); + btn.classList.remove('quiz-carry-grab-btn--active'); + btn.classList.remove('quiz-carry-grab-btn--place'); + btn.setAttribute('aria-disabled', 'true'); + return; + } + const grabSrc = BASE + '/img/quiz-carry/btn-grab.png'; + const placeSrc = BASE + '/img/quiz-carry/btn-drop.png'; + const img = btn.querySelector('img'); + btn.classList.remove('is-hidden'); + const active = quizCarryGrabInteractionAvailable(); + btn.classList.toggle('quiz-carry-grab-btn--active', active); + btn.setAttribute('aria-disabled', active ? 'false' : 'true'); + btn.style.pointerEvents = active ? '' : 'none'; + if (img) { + if (active && quizCarryGrabSubmitAvailable()) { + img.src = placeSrc; + btn.classList.add('quiz-carry-grab-btn--place'); + btn.title = 'ส่ง / วางคำตอบ — เหมือนกด F'; + btn.setAttribute('aria-label', 'ส่งหรือวางคำตอบ'); + } else { + img.src = grabSrc; + btn.classList.remove('quiz-carry-grab-btn--place'); + btn.title = 'หยิบตัวเลือก — เหมือนกด F'; + btn.setAttribute('aria-label', 'หยิบตัวเลือก (Grab)'); + } + } + } + + /** Gauntlet (รวมพรมแดง): กระโดด — ใช้ร่วมกับ Space/W/↑ และปุ่ม UI */ + function tryRequestGauntletJumpPlay() { + if (!mapData || !isGauntlet() || isChatFocused()) return false; + if (isGauntletCrownPregameBlockingPlay()) return false; + if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return false; + const now = Date.now(); + if (now - lastGauntletJumpKey < 200) return false; + lastGauntletJumpKey = now; + socket.emit('gauntlet-jump'); + return true; + } + + function syncGauntletCrownJumpButton() { + const btn = document.getElementById('gauntlet-crown-jump-btn'); + if (!btn) return; + const show = !!(mapData && isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live' && !me.gauntletEliminated && gauntletCrownRunwayAvatarRunAllowedPlay()); + if (!show) { + btn.classList.add('is-hidden'); + btn.setAttribute('aria-hidden', 'true'); + btn.style.pointerEvents = 'none'; + return; + } + btn.classList.remove('is-hidden'); + btn.setAttribute('aria-hidden', 'false'); + btn.style.pointerEvents = ''; + } + + function syncStackTowerDropButtonPlay() { + const btn = document.getElementById('stack-tower-drop-btn'); + if (!btn) return; + if (!mapData || !isStack() || !stackMini) { + btn.classList.add('is-hidden'); + btn.setAttribute('aria-hidden', 'true'); + btn.style.pointerEvents = 'none'; + btn.classList.remove('stack-tower-drop-btn--active'); + return; + } + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') { + btn.classList.add('is-hidden'); + btn.setAttribute('aria-hidden', 'true'); + btn.style.pointerEvents = 'none'; + btn.classList.remove('stack-tower-drop-btn--active'); + return; + } + btn.classList.remove('is-hidden'); + btn.setAttribute('aria-hidden', 'false'); + const ok = stackHumanDropGateOkPlay(); + btn.classList.toggle('stack-tower-drop-btn--active', ok); + btn.style.pointerEvents = ok ? '' : 'none'; + } + + function resetQuizCarryPlayState() { + cancelQuizCarrySessionCompleteResultToSummary(); + const qcmo = document.getElementById('quiz-carry-mission-overlay'); + if (qcmo) qcmo.classList.add('is-hidden'); + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryPool = []; + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownStartAt = 0; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedPreOptionCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + quizCarryAnswerTimeupAwaitNext = false; + quizCarryRoundsCompleted = 0; + quizCarrySessionEnded = false; + hideQuizCarryEmbedCountdownOverlay(); + hideQuizCarryTimeupOnDeskLayer(); + hideQuizCarryResultEndLayer(); + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + } + + /** รวม carry จากดิสก์ — ใช้เฉพาะ GET ที่ Node รองรับ (path เดียวกับ server.js) ไม่เรียก .php เพื่อกัน 404 บน nginx/php-fpm */ + async function mergeQuizCarrySettingsFromDisk(s) { + const out = s && typeof s === 'object' ? s : {}; + try { + const rd = await fetch(BASE + '/api/quiz-carry-from-disk?_=' + Date.now(), { cache: 'no-store' }); + if (rd.ok) { + const disk = await rd.json(); + if (disk && typeof disk === 'object') { + if (disk.carryMapPanelTheme && typeof disk.carryMapPanelTheme === 'object') { + out.carryMapPanelTheme = disk.carryMapPanelTheme; + } + if (disk.quizMapPanelTheme && typeof disk.quizMapPanelTheme === 'object') { + out.quizMapPanelTheme = disk.quizMapPanelTheme; + } + if (disk.carryEmbedCountdownTheme && typeof disk.carryEmbedCountdownTheme === 'object') { + out.carryEmbedCountdownTheme = disk.carryEmbedCountdownTheme; + } + if (Array.isArray(disk.carryChoicePlaqueThemes) && disk.carryChoicePlaqueThemes.length) { + out.carryChoicePlaqueThemes = disk.carryChoicePlaqueThemes; + } else if (disk.carryChoicePlaqueTheme && typeof disk.carryChoicePlaqueTheme === 'object') { + out.carryChoicePlaqueTheme = disk.carryChoicePlaqueTheme; + } + if (disk.carryReadMs != null) out.carryReadMs = disk.carryReadMs; + if (disk.carryAnswerMs != null) out.carryAnswerMs = disk.carryAnswerMs; + if (disk.carrySessionLength != null) out.carrySessionLength = disk.carrySessionLength; + if (disk.carryWalkSpeedMultForMapId != null) { + out.carryWalkSpeedMultForMapId = String(disk.carryWalkSpeedMultForMapId).trim(); + } + if (disk.carryWalkSpeedMult != null) { + const wm = Number(disk.carryWalkSpeedMult); + if (Number.isFinite(wm)) out.carryWalkSpeedMult = wm; + } + const diskPlaqueScale = Number(disk.carryChoicePlaqueMapScale); + if (Number.isFinite(diskPlaqueScale)) { + out.carryChoicePlaqueMapScale = Math.max(0.85, Math.min(2.5, diskPlaqueScale)); + } + if (Array.isArray(disk.carryQuestions) && disk.carryQuestions.length > 0) { + out.carryQuestions = disk.carryQuestions; + } + } + } + } catch (e) { /* ignore */ } + return out; + } + + async function loadQuizCarryPoolAndStart() { + cancelQuizCarrySessionCompleteResultToSummary(); + quizCarryEmbedPreOptionCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryRoundsCompleted = 0; + quizCarrySessionEnded = false; + hideQuizCarryTimeupOnDeskLayer(); + hideQuizCarryResultEndLayer(); + quizCarrySessionLength = 0; + let pool = []; + let s = {}; + const snap = quizCarryJoinSettingsSnap && typeof quizCarryJoinSettingsSnap === 'object' ? quizCarryJoinSettingsSnap : null; + try { + const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' }); + if (r.ok) { + const raw = await r.text(); + const trimmed = (raw || '').trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + const parsed = JSON.parse(trimmed); + if (parsed && typeof parsed === 'object') s = parsed; + } + } + } catch (e) { /* Node/HTML error — ต่อจาก merge ดิสก์ */ } + try { + s = await mergeQuizCarrySettingsFromDisk(s); + } catch (e) { /* ignore */ } + if (snap) { + if (s.carryReadMs == null && snap.carryReadMs != null) s.carryReadMs = snap.carryReadMs; + if (s.carryAnswerMs == null && snap.carryAnswerMs != null) s.carryAnswerMs = snap.carryAnswerMs; + if (s.carrySessionLength == null && snap.carrySessionLength != null) s.carrySessionLength = snap.carrySessionLength; + } + if (snap && snap.carryMapPanelTheme && typeof snap.carryMapPanelTheme === 'object') { + s.carryMapPanelTheme = snap.carryMapPanelTheme; + } + if (snap && snap.carryEmbedCountdownTheme && typeof snap.carryEmbedCountdownTheme === 'object') { + s.carryEmbedCountdownTheme = snap.carryEmbedCountdownTheme; + } + /* ไม่ทับ carryChoicePlaqueThemes จาก snap ถ้ามีแล้ว — snapshot ตอน join อาจเก่ากว่าไฟล์ที่เพิ่งบันทึกใน Admin (รูปป้ายหาย) */ + if (snap && (!Array.isArray(s.carryChoicePlaqueThemes) || !s.carryChoicePlaqueThemes.length)) { + if (Array.isArray(snap.carryChoicePlaqueThemes) && snap.carryChoicePlaqueThemes.length) { + s.carryChoicePlaqueThemes = snap.carryChoicePlaqueThemes; + } else if (snap.carryChoicePlaqueTheme && typeof snap.carryChoicePlaqueTheme === 'object') { + s.carryChoicePlaqueTheme = snap.carryChoicePlaqueTheme; + } + } + if (snap && (s.carryChoicePlaqueMapScale == null || !Number.isFinite(Number(s.carryChoicePlaqueMapScale)))) { + const sm = Number(snap.carryChoicePlaqueMapScale); + if (Number.isFinite(sm)) s.carryChoicePlaqueMapScale = sm; + } + if (snap) { + if ((s.carryWalkSpeedMultForMapId == null || String(s.carryWalkSpeedMultForMapId).trim() === '') && snap.carryWalkSpeedMultForMapId != null) { + s.carryWalkSpeedMultForMapId = snap.carryWalkSpeedMultForMapId; + } + if (s.carryWalkSpeedMult == null && snap.carryWalkSpeedMult != null) s.carryWalkSpeedMult = snap.carryWalkSpeedMult; + } + applyQuizCarryWalkSpeedFromSettingsObj(s); + { + const sc = Number(s.carryChoicePlaqueMapScale); + quizCarryPlaqueMapScale = Number.isFinite(sc) ? Math.max(0.85, Math.min(2.5, sc)) : 1.25; + } + try { + quizCarryCarryTimingMs = { + carryReadMs: clampPreviewMs(s.carryReadMs, 3000, 0, 120000), + carryAnswerMs: clampPreviewMs(s.carryAnswerMs, 5000, 1000, 300000), + }; + quizCarrySessionLength = clampCarrySessionLen(s.carrySessionLength, 0); + setQuizCarryMapPanelThemeFromApi(s); + setQuizCarryEmbedCountdownThemeFromApi(s); + setQuizCarryChoicePlaqueThemeFromApi(s); + for (let ti = 0; ti < QUIZ_CARRY_MAX_OPTION_SLOTS; ti++) { + const tUrl = sanitizeQuizCarryImageUrlClient(getEffectiveCarryChoicePlaqueThemeForChoice(ti).plaqueImageUrl); + if (!tUrl || quizCarryChoiceImageCache.has(tUrl)) continue; + const pim = new Image(); + quizCarryApplyImageCrossOrigin(pim, tUrl); + pim.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(tUrl, pim); + try { + pim.src = tUrl; + } catch (e) { /* ignore */ } + } + const carryOnly = s && Array.isArray(s.carryQuestions) && s.carryQuestions.length > 0; + const source = carryOnly ? s.carryQuestions : (s && Array.isArray(s.questions) ? s.questions : []); + for (let i = 0; i < source.length; i++) { + const n = normalizeQuizCarryQuestionFromAny(source[i]); + if (n) pool.push(n); + } + } catch (e) { /* map only */ } + if (!pool.length && mapData) pool = buildQuizCarryPoolFromMap(mapData); + quizCarryPool = pool; + for (let pi = 0; pi < pool.length; pi++) preloadQuizCarryChoiceImages(pool[pi]); + playLiveQuizScores = {}; + if (myId != null) playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + if (!pool.length) { + playQuizText = 'ไม่มีคำถาม — ตั้งใน Admin → คำถามหลายตัวเลือก หรือใส่ในแมป (quizQuestions)'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + try { + syncPlayQuizMapPanel(); + } catch (e) { /* ignore */ } + quizCarryJoinSettingsSnap = null; + return; + } + if (quizCarryEmbedMissionFlowActive()) { + beginQuizCarryEmbedPregame(); + } else { + quizCarryPickNextQuestion(); + } + const nowJoin = Date.now(); + if (!quizCarryEmbedMissionFlowActive()) { + quizCarryRestoreEmbedPhaseLabel(); + } else if (!(quizCarryEmbedCountdownEndAt > nowJoin) && !(quizCarryEmbedPreOptionCountdownEndAt > nowJoin)) { + quizCarryRestoreEmbedPhaseLabel(); + } + try { + syncPlayQuizMapPanel(); + } catch (e) { /* ignore */ } + quizCarryJoinSettingsSnap = null; + } + + function setupPlayQuizCarryUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) { + if (isDetectiveMinigamePlay() || (previewMode && editorEmbedReturn)) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } else { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + } + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + const leg = document.getElementById('quiz-play-legend'); + if (leg) { + leg.textContent = isDetectiveMinigamePlay() + ? '' + : (quizCarryMapHasAnswerInteractive(mapData) + ? 'แต่ละตัวเลือกมีคนถือได้คนเดียว — ส่งคำตอบที่โซน interactive (เขียว) · โซนกลางม่วงเป็นกำแพง · กด F หรือปุ่ม GRAB มุมขวาล่าง' + : 'แต่ละตัวเลือกมีคนถือได้คนเดียว — โซนกลางเป็นกำแพง · ยืนชิดขอบแล้วกด F หรือปุ่ม GRAB ส่งเมื่อถือป้าย'); + } + resetQuizCarryPlayState(); + if (previewMode) { + loadQuizCarryPoolAndStart(); + } else { + playQuizText = isDetectiveMinigamePlay() ? '' : 'เข้าห้องแล้ว — คำถามจากแมป (เล่นจริงแนะนำซิงก์ผ่านโฮสต์ในอนาคต)'; + loadQuizCarryPoolAndStart(); + } + renderPlayQuizScoreboard(playLiveQuizScores); + syncQuizCarryGrabButton(); + } + + function pickRandomTileForQuizCarryOption(md, optionIndex, o) { + const w = md.width || 20, h = md.height || 15; + const want = optionIndex + 1; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.quizCarryOptionArea && md.quizCarryOptionArea[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === want && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileInQuizCarryHub(md, o) { + const w = md.width || 20, h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.quizCarryHubArea && md.quizCarryHubArea[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1 && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileInQuizCarryInteractive(md, o) { + const w = md.width || 20, h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.interactive && md.interactive[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1 && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileForQuizCarrySubmit(md, o) { + if (quizCarryMapHasAnswerInteractive(md)) { + const p = pickRandomTileInQuizCarryInteractive(md, o); + if (p) return p; + } + return pickRandomTileInQuizCarryHub(md, o); + } + + /** ช่วงไม่มีข้อ / รอตัวเลือก — ให้บอทเดินเล่นในแมปเหมือน preview ทั่วไป (กันยืนนิ่งทั้งเกม) */ + function quizCarryPreviewBotLobbyWanderStep(o, w, h) { + const now = Date.now(); + o.botIsWalking = false; + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600; + if (now >= o.botWanderNextTurn) { + o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200); + if (Math.random() < 0.55) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + } + const accX = o.botWanderDx; + const accY = o.botWanderDy; + if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up'; + else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left'; + const step = MOVE_SPEED * quizCarryWalkSpeedMultActive; + const nx = o.x + accX * step; + const ny = o.y + accY * step; + const ox = o.x; + const oy = o.y; + if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + } + if (!Number.isFinite(o.x)) o.x = 0.5; + if (!Number.isFinite(o.y)) o.y = 0.5; + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) o.botIsWalking = true; + } + + function stepQuizCarryPreviewBots() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!playBotsEnabled() || !isQuizCarry() || !mapData) return; + const w = mapData.width || 20, h = mapData.height || 15; + const nowPf = performance.now(); + const pathfindMinGap = editorEmbedReturn ? 280 : 140; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + if (quizCarryFootprintOverlapsHub(o.x, o.y)) { + const sn = snapPositionOutOfQuizCarryHubIfNeeded(o.x, o.y); + if (Math.abs(sn.x - o.x) > 0.02 || Math.abs(sn.y - o.y) > 0.02) { + o.x = sn.x; + o.y = sn.y; + o.botPath = []; + o.botPathStuckTicks = 0; + o.botQuizCarryPathfindAfter = 0; + } + } + if (!o.botPath || o.botPath.length === 0) { + tryQuizCarryInteractionForPlayer(id, o.x, o.y, { silent: true }); + } + /* ช่วงนับถอยหลัง / เปลี่ยนข้อ — ยังให้เดินได้ */ + if (!quizCarryCurrent) { + quizCarryPreviewBotLobbyWanderStep(o, w, h); + return; + } + const tier = o.botTier || 'avg'; + const pCorrect = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.35 : 0.65; + const rawWant = Math.random() < pCorrect ? quizCarryCurrent.correctIndex + : Math.floor(Math.random() * Math.max(2, (quizCarryCurrent.choices || []).length)); + const wantIdx = pickQuizCarryTargetOptionIndexAvoidingTaken(rawWant); + if (!o.botPath || !o.botPath.length) { + if (typeof o.botQuizCarryPathfindAfter !== 'number') o.botQuizCarryPathfindAfter = 0; + if (nowPf < o.botQuizCarryPathfindAfter) { + quizCarryPreviewBotLobbyWanderStep(o, w, h); + return; + } + const jitter = (String(id).length * 31 + ((o.x | 0) ^ (o.y | 0)) * 7) % 160; + if (quizCarryOptionsPickableNow()) { + if (o.quizCarryHeld == null && wantIdx != null) { + const dest = pickRandomTileForQuizCarryOption(mapData, wantIdx, o); + if (dest) { + const path = pathfindPlayForBot(o.x, o.y, dest.x + 0.5, dest.y + 0.5, o); + if (path && path.length > 1) o.botPath = path.slice(1); + } + } else { + const sub = pickRandomTileForQuizCarrySubmit(mapData, o); + if (sub) { + const path = pathfindPlayForBot(o.x, o.y, sub.x + 0.5, sub.y + 0.5, o); + if (path && path.length > 1) o.botPath = path.slice(1); + } + } + o.botQuizCarryPathfindAfter = nowPf + pathfindMinGap + jitter; + } else { + o.botQuizCarryPathfindAfter = nowPf + 120; + quizCarryPreviewBotLobbyWanderStep(o, w, h); + } + } else { + stepPreviewBotAlongPath(o, w, h); + } + clampPlayEntityFootprintToMap(o, mapData); + }); + separateClumpedPreviewBots(); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const ob = others.get(bid); + if (ob) clampPlayEntityFootprintToMap(ob, mapData); + }); + } + + function getStackAreaBoundsPlay(area, w, h) { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let any = false; + for (let y = 0; y < h; y++) { + const row = area && area[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1) { + any = true; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + } + if (!any) return null; + return { + minX, maxX, minY, maxY, + cx: (minX + maxX + 1) / 2, + cy: (minY + maxY + 1) / 2, + }; + } + + /** ปิดการตามความสูงหอจาก JSON เสมอ — เซิร์ฟเคยบังคับ enabled:true ใน policy ทำให้ boost เลื่อนโลก */ + function getStackTowerCameraFollowPlayConfig() { + return { enabled: false, pxPerLayer: 12, maxPx: 260 }; + } + + /** ยกจุดยึดเชือกขึ้นเมื่อหอสูง (y ลดลง = สูงขึ้นในโลก) — เฉพาะ mnn93hpi + เปิดกล้องตามหอ */ + function getStackTowerSwingLiftWorldPx(layerCount, layerWorldHPx) { + if (!isStackTowerMissionUiMapPlay() || !getStackTowerCameraFollowPlayConfig().enabled) return 0; + const n = Math.floor(Number(layerCount) || 0); + if (n < STACK_TOWER_SWING_LIFT_FROM_LAYER) return 0; + const lh = Math.max(10, Number(layerWorldHPx) || Math.max(14, (tileSize || 32) * 0.3)); + const tiers = n - (STACK_TOWER_SWING_LIFT_FROM_LAYER - 1); + return Math.min(lh * 20, tiers * lh * 0.98); + } + + /** + * progress ≥เกณฑ์ Tower + ชั้นพอ: ไม่วาดชั้นล่าง (ข้าม draw) — ตำแหน่ง Y ยังใช้ชั้นจริง (floorY-(i+1)*h) + * เพื่อไม่ให้กองลอยหลุดจากฐานแมปเมื่อเทียบกริด + */ + function getStackTowerVisualHiddenLayerCountPlay() { + if (!isStackTowerMissionHudActivePlay() || !stackMini) return 0; + const p = Number(stackMini.progressPct) || 0; + if (p < STACK_TOWER_POST50_PROGRESS_THRESH) return 0; + const n = stackMini.layers ? stackMini.layers.length : 0; + if (n < 4) return 0; + return Math.floor(n / 2); + } + + /** + * world px ตามความสูงหอจริง (mnn93hpi) — คูณ z แล้วได้พิกเซลจอสำหรับเลื่อนแถบ BG + * ใช้ n × layerWorldH (ความสูงชั้นตามบล็อก) คล้มด้วย maxPx จากแมป + */ + function getStackTowerBgScrollHeightBoostPx() { + if (!isStackTowerMissionUiMapPlay() || !stackMini) return 0; + const cfg = getStackTowerCameraFollowPlayConfig(); + if (!cfg.enabled || cfg.pxPerLayer <= 0) return 0; + const lh = Math.max(10, Number(stackMini.layerWorldH) || Math.max(14, (tileSize || 32) * 0.31)); + const n = stackMini.layers ? stackMini.layers.length : 0; + let layerCount = n; + if (stackFall) { + const dur = Math.max(1, stackFall.dur || 400); + const u = Math.min(1, Math.max(0, (performance.now() - stackFall.t0) / dur)); + layerCount = n + u * 0.9; + } + return Math.min(cfg.maxPx, layerCount * lh); + } + + /** กล้อง stack: จุดกลางจาก land / release ของแมป (Tower ไม่เลื่อน cy ตามกอง) */ + function getStackCameraCentersPx() { + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const rel = getStackAreaBoundsPlay(mapData.stackReleaseArea, w, h); + let cx = (w / 2) * ts; + let cy = (h / 2) * ts; + if (land && rel) { + cx = land.cx * ts; + cy = ((land.cy + rel.cy) / 2) * ts; + } else if (land) { + cx = land.cx * ts; + cy = land.cy * ts; + } else if (rel) { + cx = rel.cx * ts; + cy = rel.cy * ts; + } + return { px: cx, py: cy }; + } + + /** ใช้ playStackBlockWidthTiles + แผนที่ — เรียกหลังโหลด /api/game-timing เมื่อหอว่าง */ + function reapplyStackMiniSizingFromGlobals() { + if (!mapData || !isStack() || !stackMini) return; + if (stackMini.layers && stackMini.layers.length > 0) return; + if (stackFall || stackMini.settling) return; + const wMap = mapData.width || 20; + const hMap = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap); + const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2)); + const iw = playStackBlockWidthTiles != null && Number.isFinite(playStackBlockWidthTiles) + ? Math.max(0.85, Math.min(3.2, playStackBlockWidthTiles)) + : autoW; + stackMini.initialWidthTiles = iw; + stackMini.widthTiles = iw; + markStackNextDropVisualDirty(); + } + + function resetStackMinigameState() { + stackMini = null; + stackTowerPost50AnimStartMs = null; + stackTowerHeartMinusFx = null; + if (!mapData || !isStack()) return; + const w = mapData.width || 20, h = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const rel = getStackAreaBoundsPlay(mapData.stackReleaseArea, w, h); + let towerCX = w * 0.5; + let swingAmp = 1.85; + let floorWorldY = (h - 1) * tileSize; + let swingWorldY = floorWorldY - tileSize * 1.35; + if (land) { + towerCX = land.cx; + floorWorldY = (land.maxY + 1) * tileSize; + const span = Math.max(1, land.maxX - land.minX + 1); + swingAmp = Math.min(span * 0.44, Math.max(0.6, span * 0.36)); + } + if (rel) { + swingWorldY = (rel.cy + 0.5) * tileSize; + const spanR = Math.max(1, rel.maxX - rel.minX + 1); + swingAmp = Math.max(swingAmp, Math.min(spanR * 0.42, 4.2)); + } + const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2)); + const initW = playStackBlockWidthTiles != null && Number.isFinite(playStackBlockWidthTiles) + ? Math.max(0.85, Math.min(3.2, playStackBlockWidthTiles)) + : autoW; + stackFall = null; + const towerMission = isStackTowerMissionUiMapPlay(); + const towerMul = towerMission ? STACK_TOWER_BLOCK_WORLD_SCALE : 1; + const towerLayerStripH = Math.max(14, tileSize * 0.31) * towerMul; + const missMax = Math.max(1, Math.min(20, Math.floor(Number(playStackTeamMissesMax) || 3))); + stackMini = { + topCenterX: towerCX, + widthTiles: initW, + initialWidthTiles: initW, + craneWorldX: towerCX * tileSize, + swingAmp, + phase: Math.random() * Math.PI * 2, + phaseSpeed: playStackSwingHz, + floorWorldY, + swingWorldY, + layerWorldH: towerMission ? towerLayerStripH : Math.max(14, tileSize * 0.3), + blockStripH: towerMission ? towerLayerStripH : Math.max(16, tileSize * 0.32), + layers: [], + lives: 3, + teamMissesLeft: towerMission ? missMax : null, + progressPct: 0, + score: 0, + combo: 0, + lastDropAt: 0, + stackTiltRad: 0, + stackTiltVel: 0, + nextDropVisualDirty: true, + pendingDropSeat: 1, + pendingDropHeavy: false, + scorePopups: [], + perfectStackX2Fx: null, + }; + if (playBotsEnabled()) { + me.stackPreviewHumanPts = 0; + stackPreviewHudLog = []; + } + ensureStackTowerSlingImagePlay(); + if (towerMission || playBotsEnabled()) { + ensureStackTowerLifeHudImagesPlay(); + ensureStackTowerHeartMinusImgPlay(); + } + updateStackTowerSwingYForStripGapPlay(); + } + + function stackEaseDrop(t) { + t = Math.max(0, Math.min(1, t)); + return t * t; + } + + /** ตกช้า/นุ่มขึ้นเมื่อปล่อยเพี้ยนกลาง (คล้าย Tower of Babel — มีน้ำหนักก่อนนิ่ง) */ + function stackEaseDropPhys(t, sloppyN) { + t = Math.max(0, Math.min(1, t)); + if (sloppyN < 0.14) return t * t; + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; + } + + /** + * @param {object} hit + * @param {{ human?: boolean, botId?: string } | undefined} previewAttribution — โหมด preview: แยกคะแนนแสดงในแถบ (ตึกยังเป็นของร่วม) + */ + function applyStackHitFromDrop(hit, previewAttribution, layerVisualMeta) { + if (!stackMini || !hit) return; + const towerLive = isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'live'; + if (hit.miss) { + stackMini.combo = 0; + stackMini.scorePopups = []; + stackMini.perfectStackX2Fx = null; + if (stackMini.layers && stackMini.layers.length) { + for (let gi = 0; gi < stackMini.layers.length; gi++) { + const lay = stackMini.layers[gi]; + if (lay && lay.perfectGlowUntil != null) delete lay.perfectGlowUntil; + } + } + if (towerLive && stackMini.teamMissesLeft != null) { + stackMini.teamMissesLeft = Math.max(0, stackMini.teamMissesLeft - 1); + } else { + stackMini.lives = Math.max(0, stackMini.lives - 1); + if (stackMini.lives <= 0) { + stackMini.score = Math.max(0, stackMini.score - 2); + stackMini.lives = 3; + stackMini.layers = []; + stackMini.scorePopups = []; + stackMini.perfectStackX2Fx = null; + stackMini.widthTiles = stackMini.initialWidthTiles; + const lb = getStackAreaBoundsPlay(mapData.stackLandArea, mapData.width || 20, mapData.height || 15); + stackMini.topCenterX = lb ? lb.cx : (mapData.width || 20) * 0.5; + stackMini.combo = 0; + stackMini.stackTiltRad = 0; + stackMini.stackTiltVel = 0; + } + } + if (towerLive) triggerStackTowerHeartMinusFxPlay(); + } else { + const seatLay = layerVisualMeta && layerVisualMeta.seat != null ? layerVisualMeta.seat : getStackDropActorSeat(); + const heavyLay = !!(layerVisualMeta && layerVisualMeta.heavy); + const placedW = hit.placedW != null && Number.isFinite(hit.placedW) + ? Math.max(0.85, Math.min(3.2, hit.placedW)) + : stackMini.initialWidthTiles; + stackMini.layers.push({ w: placedW, cx: hit.placedCx, seat: seatLay, heavy: heavyLay }); + const nAfter = stackMini.layers.length; + maybeQueueStackTowerBgScrollStepPlay(nAfter); + const perfectFull = + towerLive && + hit.perfect && + !hit.miss && + nAfter >= 2 && + hit.supportRatio != null && + hit.supportRatio >= STACK_TOWER_PERFECT_SUPPORT_MIN; + if (perfectFull) { + const gUntil = performance.now() + STACK_TOWER_PERFECT_GLOW_MS; + stackMini.layers[nAfter - 1].perfectGlowUntil = gUntil; + stackMini.layers[nAfter - 2].perfectGlowUntil = gUntil; + } + if (towerLive && (perfectFull || hit.pts >= 20)) { + ensureStackTowerPerfectFxImagesPlay(); + stackMini.perfectStackX2Fx = { + until: performance.now() + STACK_TOWER_PERFECT_X2_MS, + topLayerIndex: nAfter - 1, + }; + } else if (towerLive) { + stackMini.perfectStackX2Fx = null; + } + stackMini.score += hit.pts; + if (towerLive && Number.isFinite(Number(hit.progressDelta)) && (hit.progressDelta || 0) > 0) { + let np = Number(stackMini.progressPct) + Number(hit.progressDelta); + np = Math.min(100, Math.max(0, np)); + if (np > 99.999) np = 100; + stackMini.progressPct = np; + markStackNextDropVisualDirty(); + } + if (playBotsEnabled() && previewAttribution) { + if (previewAttribution.human) { + me.stackPreviewHumanPts = (me.stackPreviewHumanPts || 0) + hit.pts; + } else if (previewAttribution.botId) { + const ob = others.get(previewAttribution.botId); + if (ob) ob.stackBotScore = (ob.stackBotScore || 0) + hit.pts; + } + } + stackMini.combo = hit.perfect ? stackMini.combo + 1 : 0; + stackMini.topCenterX = hit.placedCx; + if (towerLive && !hit.miss && hit.pts > 0) { + const pNow = Number(stackMini.progressPct); + const prog100 = Number.isFinite(pNow) && pNow >= 99.5; + const kind20 = hit.pts >= 20 || prog100; + if (!stackMini.scorePopups) stackMini.scorePopups = []; + const until = performance.now() + STACK_TOWER_SCORE_POPUP_MS; + stackMini.scorePopups.push({ until, layerIndex: nAfter - 1, kind: kind20 ? '20' : '10' }); + if (kind20 && perfectFull && nAfter >= 2) { + stackMini.scorePopups.push({ until, layerIndex: nAfter - 2, kind: '20' }); + } + while (stackMini.scorePopups.length > 12) stackMini.scorePopups.shift(); + } + } + } + + /** + * Editor embed เท่านั้น: ปรับ `stackMini.phase` ให้ sin·swingAmp ใกล้จุดรองรับก่อนปล่อย — บอทฉลาด/กลาง/พลาดบ่อย ต่างค่า aim error + */ + function previewBotAlignStackSwingPhasePlay(m, botPeer) { + if (!playBotsEnabled() || !m || !mapData) return; + const wMap = mapData.width || 20; + const hMap = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap); + const fallWRaw = m.widthTiles != null && Number.isFinite(m.widthTiles) ? m.widthTiles : m.initialWidthTiles; + const fallW = Math.max(0.85, Math.min(3.2, fallWRaw)); + let supportCx; + let supportW; + if (m.layers.length === 0) { + supportCx = land ? land.cx : wMap * 0.5; + supportW = land ? (land.maxX - land.minX + 1) : Math.max(8, fallW * 2.2); + } else { + const sup = m.layers[m.layers.length - 1]; + supportCx = sup.cx; + const swR = sup.w != null && Number.isFinite(sup.w) ? sup.w : m.initialWidthTiles; + supportW = Math.max(0.85, Math.min(3.2, swR)); + } + const tier = botPeer && botPeer.botTier === 'sharp' ? 'sharp' : botPeer && botPeer.botTier === 'weak' ? 'weak' : 'avg'; + const layerTight = 1 / Math.sqrt(1 + (m.layers.length || 0) * 1.05); + const errMax = tier === 'sharp' ? 0.016 : tier === 'avg' ? 0.038 : 0.17; + const tri = Math.random() + Math.random() - 1; + let errTiles = tri * errMax * layerTight; + if (m.layers.length >= 2) { + const L = m.layers.length; + const step = m.layers[L - 1].cx - m.layers[L - 2].cx; + errTiles -= step * 0.28; + } + let targetCx = supportCx + errTiles; + if (land && m.layers.length > 0) { + const pull = (land.cx - supportCx) * 0.1; + const cap = tier === 'sharp' ? 0.09 : tier === 'avg' ? 0.12 : 0.06; + targetCx += Math.max(-cap, Math.min(cap, pull)); + } + const rawWant = targetCx - m.topCenterX; + const amp = Math.max(1e-5, m.swingAmp); + const s = Math.max(-1, Math.min(1, rawWant / amp)); + const phi0 = Math.asin(s); + const phi1 = Math.PI - phi0; + const twoPi = Math.PI * 2; + function nearestPhase(anchor, p) { + let best = p; + let bestD = Infinity; + for (let k = -5; k <= 5; k++) { + const cand = p + k * twoPi; + const d = Math.abs(cand - anchor); + if (d < bestD) { + bestD = d; + best = cand; + } + } + return best; + } + const n0 = nearestPhase(m.phase, phi0); + const n1 = nearestPhase(m.phase, phi1); + m.phase = Math.abs(n0 - m.phase) <= Math.abs(n1 - m.phase) ? n0 : n1; + } + + /** + * ชั้นรองรับใช้ความกว้างจริงต่อชั้น (L.w) · บล็อกที่กำลังตกใช้ m.widthTiles (ปกติ vs heavy กว้างต่างกัน) + * พลาดเมื่อทับแท่นไม่พอ หรือจุดกลางหลุดขอบฐาน (STACK_MAX_*) + */ + function computeStackDropResult(m) { + const raw = m.swingAmp * Math.sin(m.phase); + const wMap = mapData.width || 20; + const hMap = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap); + const fallWRaw = m.widthTiles != null && Number.isFinite(m.widthTiles) ? m.widthTiles : m.initialWidthTiles; + const fallW = Math.max(0.85, Math.min(3.2, fallWRaw)); + const wMul = getStackTowerBlockWorldScalePlay(); + const fallWEff = fallW * wMul; + let supportCx; + let supportW; + if (m.layers.length === 0) { + supportCx = land ? land.cx : wMap * 0.5; + supportW = land ? (land.maxX - land.minX + 1) : Math.max(8, fallW * 2.2); + } else { + const sup = m.layers[m.layers.length - 1]; + supportCx = sup.cx; + const swR = sup.w != null && Number.isFinite(sup.w) ? sup.w : m.initialWidthTiles; + supportW = Math.max(0.85, Math.min(3.2, swR)); + } + const c1 = m.topCenterX + raw; + const half1 = fallWEff * 0.5; + const half0 = (m.layers.length === 0 ? supportW : supportW * wMul) * 0.5; + const left = Math.max(supportCx - half0, c1 - half1); + const right = Math.min(supportCx + half0, c1 + half1); + const overlap = right - left; + const missThreshold = + m.layers.length === 0 + ? Math.max(STACK_OVERLAP_MISS_MIN_ON_LAND, fallWEff * STACK_OVERLAP_MISS_FRAC_ON_LAND) + : Math.max(STACK_OVERLAP_MISS_MIN_ON_LAYER, fallWEff * STACK_OVERLAP_MISS_FRAC_ON_LAYER); + const overlapMiss = overlap < missThreshold; + const placedCx = c1; + let driftMiss = false; + if (!overlapMiss && m.layers.length > 0) { + const anchorCx = land ? land.cx : wMap * 0.5; + let maxDriftTiles; + if (land) { + const landSpan = Math.max(1, land.maxX - land.minX + 1); + maxDriftTiles = landSpan * STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND; + } else { + maxDriftTiles = Math.max(0.85, fallW * STACK_MAX_CENTER_DRIFT_NO_LAND_MULT); + } + if (Math.abs(placedCx - anchorCx) > maxDriftTiles) driftMiss = true; + } + const miss = overlapMiss || driftMiss; + const delta = c1 - supportCx; + const span = supportW * 0.5 + fallW * 0.5; + const offN = Math.abs(delta) / Math.max(0.01, span); + const perfect = !miss && offN < 0.055; + const comboBefore = m.combo || 0; + let pts; + let progressDelta = 0; + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'live') { + const mult = !miss && perfect && comboBefore >= 1 ? 2 : 1; + const progPerLayer = 100 / stackTowerProgressBlocksPlay(); + pts = miss ? 0 : (10 * mult); + progressDelta = miss ? 0 : (progPerLayer * mult); + } else { + const bonus = perfect ? Math.min(4, comboBefore) : 0; + pts = perfect ? (2 + bonus) : (miss ? 0 : 1); + } + const supportRatio = fallWEff > 0 ? overlap / fallWEff : 0; + return { + miss, + perfect, + pts, + progressDelta, + landOffsetTiles: raw, + placedW: fallW, + placedCx, + overlap, + supportRatio, + delta, + }; + } + + /** + * เริ่มแอนิเมชันบล็อกร่วง (ผู้เล่น/บอทใช้ path เดียวกัน) + * @param {object} hit จาก computeStackDropResult + * @param {{ human?: boolean, botId?: string } | null} previewActor โหมด preview: ใครเป็นคนปล่อย (สำหรับคะแนน + เลื่อนตา) + * @returns {boolean} + */ + function startStackFallAnimation(hit, previewActor) { + if (!stackMini || !hit || stackFall || stackMini.settling) return false; + ensureStackNextDropVisual(); + const m = stackMini; + const actorSeat = m.pendingDropSeat != null ? m.pendingDropSeat : getStackDropActorSeat(); + const actorHeavy = !!m.pendingDropHeavy; + const tw = m.widthTiles * tileSize * getStackTowerBlockWorldScalePlay(); + const swingXW = (m.topCenterX + m.swingAmp * Math.sin(m.phase)) * tileSize; + const lh = m.layerWorldH || Math.max(14, tileSize * 0.3); + const swingLift = getStackTowerSwingLiftWorldPx(m.layers.length, lh); + const swingAttachY = m.swingWorldY - swingLift; + let y1 = m.floorWorldY - (m.layers.length + 1) * lh; + if (y1 <= swingAttachY) y1 = swingAttachY + lh * 0.55; + const landOff = hit.landOffsetTiles || 0; + const landCxWorld = (m.topCenterX + landOff) * tileSize; + const xLeft0 = swingXW - tw / 2; + const xLeft1 = landCxWorld - tw / 2; + const offN = Math.min(1, Math.abs(landOff) / Math.max(0.01, m.swingAmp)); + const dur = Math.max(400, Math.min(1280, 240 + (y1 - swingAttachY) * 0.92 + offN * 560)); + const tiltMax = hit.miss + ? 0.28 * (0.25 + offN) + : 0.16 * offN * (hit.perfect ? 0.1 : 1); + const isHumanPreview = !!(previewActor && previewActor.human); + const botIdPreview = previewActor && previewActor.botId ? previewActor.botId : null; + const cableTopYRel = isStackTowerMissionUiMapPlay() + ? (swingAttachY - tileSize * 6) + : Math.max(0, swingAttachY - tileSize * 6); + const craneXRel = m.craneWorldX != null ? m.craneWorldX : m.topCenterX * tileSize; + const releaseRopeAng = Math.atan2(swingAttachY - cableTopYRel, swingXW - craneXRel); + stackFall = { + t0: performance.now(), + dur, + xLeft0, + xLeft1, + tw, + y0: swingAttachY, + y1, + hit, + tiltMax, + sloppyN: offN, + previewHumanDrop: isHumanPreview, + previewBotId: botIdPreview, + releaseRopeAng, + releaseAttachWorldX: swingXW, + releaseAttachWorldY: swingAttachY, + actorSeat, + actorHeavy, + }; + return true; + } + + function stackHumanDropGateOkPlay() { + if (!stackMini || !isStack() || stackFall || stackMini.settling) return false; + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') return false; + if (playBotsEnabled() && mapData && mapData.gameType === 'stack' && + (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT)) { + rebuildStackPreviewTurnOrder(); + } + if (playBotsEnabled() && !isStackPreviewHumanTurn()) return false; + if (Date.now() - stackMini.lastDropAt < 480) return false; + return true; + } + + /** @returns {boolean} เริ่มแอนิเมชันร่วงแล้วหรือไม่ */ + function tryHumanStackDrop() { + if (!stackHumanDropGateOkPlay()) return false; + ensureStackNextDropVisual(); + const hit = computeStackDropResult(stackMini); + if (!startStackFallAnimation(hit, playBotsEnabled() ? { human: true } : null)) return false; + if (playBotsEnabled()) { + lastStackPreviewActorId = '__human__'; + lastStackPreviewActorUntil = Date.now() + 900; + } + return true; + } + + /** บอท preview: ปล่อยแบบมีแอนิเมชันร่วงเหมือนผู้เล่น — apply หลังแอนิเมชันจบ */ + function simulatePreviewBotStackDrop(botId, o) { + if (!stackMini || stackFall || stackMini.settling) return; + ensureStackNextDropVisual(); + previewBotAlignStackSwingPhasePlay(stackMini, o); + const hit = computeStackDropResult(stackMini); + if (!startStackFallAnimation(hit, playBotsEnabled() ? { botId } : null)) return; + o.stackBotGlowUntil = Date.now() + Math.max(900, 400 + (stackFall && stackFall.dur ? stackFall.dur : 800)); + lastStackPreviewActorId = botId; + lastStackPreviewActorUntil = Date.now() + Math.max(1000, 500 + (stackFall && stackFall.dur ? stackFall.dur : 800)); + } + + /** Stack preview: สลับตา — เฉพาะบอทที่ถึงตาถึงจะจำลองปล่อยหนึ่งครั้งแล้วเลื่อนตา */ + function stepPreviewStackTurnBased(nowMs) { + if (!playBotsEnabled() || !isStack() || !stackMini || stackFall || stackMini.settling) return; + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') return; + if (mapData && mapData.gameType === 'stack' && + (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT)) { + rebuildStackPreviewTurnOrder(); + } + const order = stackPreviewTurnOrder; + if (!order || order.length !== STACK_PREVIEW_TURN_COUNT) return; + const cur = order[stackPreviewTurnIndex]; + if (!cur || cur.kind !== 'bot') return; + if (!cur.botId) { + advanceStackPreviewTurn(); + return; + } + const o = others.get(cur.botId); + if (!o) { + advanceStackPreviewTurn(); + return; + } + if (stackPreviewBotThinkUntil === 0) { + const tier = o.botTier === 'sharp' ? 'sharp' : o.botTier === 'weak' ? 'weak' : 'avg'; + const thinkLo = tier === 'sharp' ? 160 : tier === 'avg' ? 260 : 340; + const thinkHi = tier === 'sharp' ? 340 : tier === 'avg' ? 520 : 620; + stackPreviewBotThinkUntil = nowMs + thinkLo + Math.random() * (thinkHi - thinkLo); + return; + } + if (nowMs < stackPreviewBotThinkUntil) return; + stackPreviewBotThinkUntil = 0; + simulatePreviewBotStackDrop(cur.botId, o); + } + + function stackTickFrame() { + stackTowerMissionMaybeEndPlay(); + const now = performance.now(); + const dt = Math.min(0.06, (now - lastStackTickMs) / 1000); + lastStackTickMs = now; + if (stackMini && stackMini.settling) { + const s = stackMini.settling; + const u = (now - s.t0) / s.dur; + if (u >= 1) { + const wasHuman = !!s.previewHumanDrop; + const settleBotId = s.previewBotId || null; + applyStackHitFromDrop({ miss: true, perfect: false, pts: 0, landOffsetTiles: 0 }, undefined); + if (playBotsEnabled() && (wasHuman || settleBotId)) { + stackPreviewLogStackDrop({ miss: true, perfect: false, pts: 0 }, { human: wasHuman, botId: settleBotId }); + } + stackMini.settling = null; + stackMini.lastDropAt = Date.now(); + markStackNextDropVisualDirty(); + if (playBotsEnabled() && (wasHuman || settleBotId)) advanceStackPreviewTurn(); + } + } else if (stackFall) { + const t = (now - stackFall.t0) / stackFall.dur; + if (t >= 1) { + const wasHuman = !!stackFall.previewHumanDrop; + const fallBotId = stackFall.previewBotId || null; + const hit = stackFall.hit; + const stableMin = STACK_STABLE_SUPPORT_RATIO_MIN; + if (hit.miss) { + applyStackHitFromDrop(hit); + if (playBotsEnabled() && (wasHuman || fallBotId)) { + stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId }); + } + stackMini.lastDropAt = Date.now(); + stackFall = null; + markStackNextDropVisualDirty(); + if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn(); + } else if (hit.supportRatio != null && hit.supportRatio < stableMin) { + const stripH = stackMini.blockStripH || Math.max(16, tileSize * 0.32); + stackMini.settling = { + t0: performance.now(), + dur: Math.max(820, Math.min(2600, 720 + (1 - hit.supportRatio) * 3400)), + hit, + xLeft0: stackFall.xLeft1, + yTop0: stackFall.y1, + twWorld: stackFall.tw, + stripH, + delta: hit.delta || 0, + previewHumanDrop: wasHuman, + previewBotId: fallBotId, + actorSeat: stackFall.actorSeat, + actorHeavy: stackFall.actorHeavy, + }; + stackFall = null; + } else { + let previewAttr; + if (playBotsEnabled()) { + if (wasHuman) previewAttr = { human: true }; + else if (fallBotId) previewAttr = { botId: fallBotId }; + } + applyStackHitFromDrop(hit, previewAttr, { seat: stackFall.actorSeat, heavy: stackFall.actorHeavy }); + if (playBotsEnabled() && (wasHuman || fallBotId)) { + stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId }); + } + stackMini.lastDropAt = Date.now(); + stackFall = null; + markStackNextDropVisualDirty(); + if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn(); + } + } + } else if (stackMini) { + stackMini.phase += stackMini.phaseSpeed * dt * Math.PI * 2; + } + stepPreviewStackTurnBased(Date.now()); + } + + /** + * จุดบนจอที่เส้นก้าน→บล็อกตัดแนว y = -(ความสูงแคนวาส × STACK_TOWER_ROPE_TOP_ABOVE_CANVAS_FRAC) + * — ปลายเชือกอยู่เหนือขอบบน ~25% ของความสูงจอ (มองเห็นช่วงที่เข้ามาในจอ) + */ + function stackTowerRopeScreenTopAnchorPlay(slingBx, slingBy, tcx, tcy, canvasH) { + const ch = Math.max(80, Number(canvasH) || 1080); + const marginTop = -ch * STACK_TOWER_ROPE_TOP_ABOVE_CANVAS_FRAC; + const ddx = tcx - slingBx; + const ddy = tcy - slingBy; + if (Math.abs(ddy) < 1e-4) { + return { x: tcx, y: Math.min(marginTop, Math.min(tcy, slingBy)) }; + } + const vTop = (marginTop - slingBy) / ddy; + if (!Number.isFinite(vTop) || vTop < 0) { + return { x: tcx, y: tcy }; + } + const xTop = slingBx + vTop * ddx; + return { x: xTop, y: marginTop }; + } + + function applyStackPreviewSpawnLayout() { + if (!mapData || mapData.gameType !== 'stack') return; + const w = mapData.width || 20, h = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const cx = land ? land.cx : w / 2 - 0.5; + const cy = land ? land.cy : h / 2 - 0.5; + me.x = cx; + me.y = cy; + me.tx = cx; + me.ty = cy; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.x = cx + (Math.random() - 0.5) * 0.15; + o.y = cy + (Math.random() - 0.5) * 0.15; + o.tx = o.x; + o.ty = o.y; + o.stackBotScore = 0; + o.stackBotNextTry = Date.now() + 300 + Math.random() * 700; + }); + } + + function drawStackMinigame(ctx, worldToScreen, zoom) { + if (!stackMini) return; + const m = stackMini; + const towerHudLive = isStackTowerMissionHudActivePlay(); + const wMul = getStackTowerBlockWorldScalePlay(); + ensureStackNextDropVisual(); + const layerWorldH = m.layerWorldH || Math.max(14, tileSize * 0.3); + const stripH = m.blockStripH || Math.max(16, tileSize * 0.32); + const floorY = m.floorWorldY; + const swingLiftDraw = getStackTowerSwingLiftWorldPx(m.layers.length, layerWorldH); + const swingDrawY = m.swingWorldY - swingLiftDraw; + const hidLayers = towerHudLive ? getStackTowerVisualHiddenLayerCountPlay() : 0; + + const nLay = m.layers.length; + for (let i = hidLayers; i < nLay; i++) { + const L = m.layers[i]; + const lw = (L.w != null ? L.w : m.initialWidthTiles) * tileSize * wMul; + const cxW = L.cx * tileSize; + const yb = floorY - (i + 1) * layerWorldH; + const xl = cxW - lw / 2; + const [sx, sy] = worldToScreen(xl, yb); + const drawW = lw * zoom; + const drawH = layerWorldH * zoom; + const hue = (i * 47) % 360; + const seatL = L.seat != null ? L.seat : 1; + const heavyL = !!L.heavy; + if (towerHudLive && L.perfectGlowUntil != null) { + const ntGlow = performance.now(); + if (ntGlow < L.perfectGlowUntil) { + drawStackTowerPerfectYellowGlowBehindBlockPlay(ctx, sx, sy, drawW, drawH, ntGlow, L.perfectGlowUntil); + } + } + drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seatL, heavyL, hue, {}); + } + if (towerHudLive && m.perfectStackX2Fx) { + const fxX2 = m.perfectStackX2Fx; + const ntX = performance.now(); + if (ntX >= fxX2.until) { + m.perfectStackX2Fx = null; + } else { + const ti = fxX2.topLayerIndex; + if (ti >= hidLayers && ti >= 0 && ti < m.layers.length) { + const Ltop = m.layers[ti]; + const lwT = (Ltop.w != null ? Ltop.w : m.initialWidthTiles) * tileSize * wMul; + const cxWT = Ltop.cx * tileSize; + const ybT = floorY - (ti + 1) * layerWorldH; + const xlT = cxWT - lwT / 2; + const [sxT, syT] = worldToScreen(xlT, ybT); + const drawWT = lwT * zoom; + const drawHT = layerWorldH * zoom; + const fxImX = ensureStackTowerPerfectFxImagesPlay(); + if (fxImX.x2) { + drawStackTowerPerfectX2ClusterPlay(ctx, fxImX.light, fxImX.x2, sxT, syT, drawWT, drawHT, ntX, fxX2.until); + } + } + } + } + if (towerHudLive) { + drawStackTowerScorePopupsPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY); + } + if (m.settling) { + const s = m.settling; + const u = Math.min(1, (performance.now() - s.t0) / s.dur); + const ue = u * u; + const rotSign = s.delta >= 0 ? 1 : -1; + const rotRad = ue * 1.22 * rotSign; + const yFall = ue * ue * tileSize * 6.5; + const xSlide = rotSign * ue * tileSize * 0.62; + const cxW = s.xLeft0 + s.twWorld / 2 + xSlide; + const cyW = s.yTop0 + s.stripH * 0.5 + yFall; + const [cxs, cys] = worldToScreen(cxW, cyW); + const drawW = s.twWorld * zoom; + const drawH = s.stripH * zoom; + const hue = (m.layers.length * 47) % 360; + const seatS = s.actorSeat != null ? s.actorSeat : 1; + const heavyS = !!s.actorHeavy; + ctx.save(); + ctx.translate(cxs, cys); + ctx.rotate(rotRad); + drawStackBlockSpriteOrHue(ctx, -drawW / 2, -drawH / 2, drawW, drawH, seatS, heavyS, hue, {}); + ctx.restore(); + } else { + const twWorld = m.widthTiles * tileSize * wMul; + const drawStripPx = stripH * zoom; + const cableTopY = isStackTowerMissionUiMapPlay() + ? (swingDrawY - tileSize * 6) + : Math.max(0, swingDrawY - tileSize * 6); + const craneX = m.craneWorldX != null ? m.craneWorldX : m.topCenterX * tileSize; + let blockLeftWorld; + let blockTopWorld; + let fallTiltRad = 0; + if (stackFall) { + const tRaw = (performance.now() - stackFall.t0) / stackFall.dur; + const t = Math.min(1, tRaw); + const u = stackEaseDropPhys(t, stackFall.sloppyN); + blockLeftWorld = stackFall.xLeft0 + (stackFall.xLeft1 - stackFall.xLeft0) * u; + blockTopWorld = stackFall.y0 + (stackFall.y1 - stackFall.y0) * u; + fallTiltRad = stackFall.tiltMax * Math.sin(Math.PI * u); + } else { + const swingXW = (m.topCenterX + m.swingAmp * Math.sin(m.phase)) * tileSize; + blockLeftWorld = swingXW - twWorld / 2; + blockTopWorld = swingDrawY; + } + const cxMid = blockLeftWorld + twWorld / 2; + const [tcx, tcy] = worldToScreen(craneX, cableTopY); + const [bcx, bcy] = worldToScreen(cxMid, blockTopWorld); + let slingBx = bcx; + let slingBy = bcy; + if (stackFall && stackFall.releaseAttachWorldX != null) { + const attY = stackFall.releaseAttachWorldY; + const p = worldToScreen(stackFall.releaseAttachWorldX, attY); + slingBx = p[0]; + slingBy = p[1]; + } + let ropeSx0 = tcx; + let ropeSy0 = tcy; + let ropeSx1 = slingBx; + let ropeSy1 = slingBy; + if (isStackTowerMissionUiMapPlay()) { + const chCv = ctx.canvas && ctx.canvas.height ? ctx.canvas.height : STACK_TOWER_FIXED_RENDER_H; + const topA = stackTowerRopeScreenTopAnchorPlay(slingBx, slingBy, tcx, tcy, chCv); + ropeSx0 = topA.x; + ropeSy0 = topA.y; + ropeSx1 = slingBx; + ropeSy1 = slingBy; + const t1 = Math.max(0, Math.min(1, STACK_TOWER_ROPE_DRAW_T1)); + if (t1 < 1) { + ropeSx1 = ropeSx0 + (slingBx - ropeSx0) * t1; + ropeSy1 = ropeSy0 + (slingBy - ropeSy0) * t1; + } + } + drawStackSlingSegmentPlay(ctx, ropeSx0, ropeSy0, ropeSx1, ropeSy1, zoom); + const ropeAng = stackFall && stackFall.releaseRopeAng != null + ? stackFall.releaseRopeAng + : Math.atan2(bcy - ropeSy0, bcx - ropeSx0); + const swingTiltRad = ropeAng - Math.PI / 2; + const hitMissTint = stackFall && stackFall.hit && stackFall.hit.miss; + const seatDraw = stackFall && stackFall.actorSeat != null + ? stackFall.actorSeat + : (m.pendingDropSeat != null ? m.pendingDropSeat : getStackDropActorSeat()); + const heavyDraw = stackFall ? !!stackFall.actorHeavy : !!m.pendingDropHeavy; + const hueHint = (m.layers.length * 47) % 360; + ctx.save(); + ctx.translate(bcx, bcy); + ctx.rotate(fallTiltRad + swingTiltRad); + drawStackBlockSpriteOrHue(ctx, -twWorld * zoom / 2, 0, twWorld * zoom, drawStripPx, seatDraw, heavyDraw, hueHint, { missTint: !!hitMissTint, missOverlay: !!hitMissTint }); + ctx.lineWidth = 1; + ctx.restore(); + } + if (towerHudLive) { + drawStackTowerHeartMinusFxPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY, nLay); + } + } + + function applyGauntletTimingFromServer(payload) { + if (!payload || typeof payload !== 'object') return; + const tm = Number(payload.gauntletTickMs); + if (Number.isFinite(tm)) gauntletRuntimeTickMs = Math.max(80, Math.min(800, tm)); + const jt = Number(payload.gauntletJumpTicks); + if (Number.isFinite(jt)) gauntletRuntimeJumpTicks = Math.max(4, Math.min(40, jt)); + const tl = Number(payload.gauntletTimeLimitSec); + if (Number.isFinite(tl)) gauntletRuntimeTimeLimitSec = Math.max(0, Math.min(7200, tl)); + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletEndsAt')) { + if (payload.gauntletEndsAt == null) gauntletEndsAtMs = null; + else { + const ge = Number(payload.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(ge) ? ge : null; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletCrownRunHeld')) { + gauntletCrownRunHeldRemote = !!payload.gauntletCrownRunHeld; + if (!gauntletCrownRunHeldRemote && usesCrownLobbyShellPlay()) { + gauntletCrownPregamePhase = 'live'; + trySnapGauntletCrownRunwayScrollToSpawnsPlay(); + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaneImageUrls') && Array.isArray(payload.gauntletLaneImageUrls)) { + gauntletLaneImageUrls = payload.gauntletLaneImageUrls + .filter((x) => typeof x === 'string') + .map((x) => normalizeGauntletAssetUrlForPlay(x)) + .filter((x) => x) + .slice(0, 24); + gauntletLaneImageUrls.forEach((x) => ensureGauntletAssetImage(x)); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserTopUrl')) { + gauntletLaserTopUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserTopUrl === 'string' ? payload.gauntletLaserTopUrl : ''); + ensureGauntletAssetImage(gauntletLaserTopUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserBottomUrl')) { + gauntletLaserBottomUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserBottomUrl === 'string' ? payload.gauntletLaserBottomUrl : ''); + ensureGauntletAssetImage(gauntletLaserBottomUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserLineUrl')) { + gauntletLaserLineUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserLineUrl === 'string' ? payload.gauntletLaserLineUrl : ''); + ensureGauntletAssetImage(gauntletLaserLineUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserFillColor')) { + gauntletLaserFillColor = clampClientLaserColor(payload.gauntletLaserFillColor, gauntletLaserFillColor); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserStrokeColor')) { + gauntletLaserStrokeColor = clampClientLaserColor(payload.gauntletLaserStrokeColor, gauntletLaserStrokeColor); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserLineWidthPx')) { + const lw = Number(payload.gauntletLaserLineWidthPx); + if (Number.isFinite(lw)) gauntletLaserLineWidthPx = Math.max(0, Math.min(24, Math.round(lw))); + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackSwingHz')) { + const sh = Number(payload.stackSwingHz); + if (Number.isFinite(sh)) playStackSwingHz = Math.max(0.08, Math.min(2.8, sh)); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackBlockWidthTiles')) { + const raw = payload.stackBlockWidthTiles; + if (raw == null || raw === '') playStackBlockWidthTiles = null; + else { + const nw = Number(raw); + playStackBlockWidthTiles = Number.isFinite(nw) && nw >= 0.85 + ? Math.max(0.85, Math.min(3.2, Math.round(nw * 100) / 100)) + : null; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackTowerMissionTimeSec')) { + const st = Number(payload.stackTowerMissionTimeSec); + if (Number.isFinite(st) && st > 0) { + playStackTowerMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st))); + } else { + playStackTowerMissionTimeSec = 90; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackTeamMissesMax')) { + const sm = Number(payload.stackTeamMissesMax); + if (Number.isFinite(sm)) { + playStackTeamMissesMax = Math.max(1, Math.min(20, Math.floor(sm))); + } else { + playStackTeamMissesMax = 3; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackTowerProgressBlocks')) { + const pb = Number(payload.stackTowerProgressBlocks); + if (Number.isFinite(pb) && pb >= 1) { + playStackTowerProgressBlocks = Math.max(1, Math.min(500, Math.floor(pb))); + } else { + playStackTowerProgressBlocks = 50; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackBlockNormalImageUrls')) { + playStackBlockNormalUrls = normalizePlayStackSixUrlsFromTiming(payload.stackBlockNormalImageUrls); + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackBlockHeavyImageUrls')) { + playStackBlockHeavyUrls = normalizePlayStackSixUrlsFromTiming(payload.stackBlockHeavyImageUrls); + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackHeavyBlockPercent')) { + const hp = Number(payload.stackHeavyBlockPercent); + if (Number.isFinite(hp)) { + playStackHeavyBlockPercent = Math.max(0, Math.min(100, Math.floor(hp))); + } else { + playStackHeavyBlockPercent = 35; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurviveJumpHeightMult')) { + const jm = Number(payload.jumpSurviveJumpHeightMult); + if (Number.isFinite(jm)) { + playJumpSurviveJumpHeightMult = Math.round(Math.max(0.5, Math.min(4, jm)) * 100) / 100; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurviveMissionTimeSec')) { + const jt = Number(payload.jumpSurviveMissionTimeSec); + if (Number.isFinite(jt) && jt > 0) { + playJumpSurviveMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(jt))); + } else { + playJumpSurviveMissionTimeSec = 0; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurvivePlatformTiles') + || Object.prototype.hasOwnProperty.call(payload, 'jumpSurvivePlatformTileUrl')) { + const legacy = Object.prototype.hasOwnProperty.call(payload, 'jumpSurvivePlatformTileUrl') + ? normalizeGauntletAssetUrlForPlay(typeof payload.jumpSurvivePlatformTileUrl === 'string' ? payload.jumpSurvivePlatformTileUrl : '') + : ''; + const arr = Array.isArray(payload.jumpSurvivePlatformTiles) ? payload.jumpSurvivePlatformTiles : []; + const next = []; + for (let pi = 0; pi < 3; pi++) { + const o = arr[pi] && typeof arr[pi] === 'object' ? arr[pi] : {}; + let url = normalizeGauntletAssetUrlForPlay(typeof o.url === 'string' ? o.url : ''); + if (pi === 0 && !url && legacy) url = legacy; + let ww = Number(o.w); + let hh = Number(o.h); + if (!Number.isFinite(ww) || ww <= 0) ww = 0; + else ww = Math.max(8, Math.min(4096, Math.round(ww))); + if (!Number.isFinite(hh) || hh <= 0) hh = 0; + else hh = Math.max(8, Math.min(4096, Math.round(hh))); + next.push({ url, w: ww, h: hh }); + if (url) ensureGauntletAssetImage(url); + } + playJumpSurvivePlatformTiles = next; + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterMissionTimeSec')) { + const st = Number(payload.spaceShooterMissionTimeSec); + if (Number.isFinite(st) && st > 0) { + playSpaceShooterMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st))); + } else { + playSpaceShooterMissionTimeSec = 0; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterShipImageUrls')) { + const a = payload.spaceShooterShipImageUrls; + const next = ['', '', '', '', '', '']; + if (Array.isArray(a)) { + for (let si = 0; si < 6; si++) { + const u = normalizeGauntletAssetUrlForPlay(typeof a[si] === 'string' ? a[si] : ''); + next[si] = u; + if (u) ensureGauntletAssetImage(u); + } + } + playSpaceShooterShipImageUrls = next; + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterAsteroidSpriteUrls')) { + const raw = payload.spaceShooterAsteroidSpriteUrls; + const next = []; + if (Array.isArray(raw)) { + for (let ai = 0; ai < raw.length && next.length < 32; ai++) { + const u = normalizeGauntletAssetUrlForPlay(typeof raw[ai] === 'string' ? raw[ai] : ''); + if (u) next.push(u); + } + } + playSpaceShooterAsteroidSpriteUrls = next; + next.forEach((u) => { + if (u) ensureGauntletAssetImage(u); + }); + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterAsteroidExplodeFrameMs')) { + const ef = Number(payload.spaceShooterAsteroidExplodeFrameMs); + if (Number.isFinite(ef)) { + playSpaceShooterAsteroidExplodeFrameMs = Math.max(30, Math.min(500, Math.round(ef))); + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterAsteroidIntervalMs')) { + const iv = Number(payload.spaceShooterAsteroidIntervalMs); + if (Number.isFinite(iv) && iv >= 200) { + playSpaceShooterAsteroidIntervalMs = Math.min(10000, Math.floor(iv)); + } else { + playSpaceShooterAsteroidIntervalMs = 1040; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterShipDamageOverlayUrls')) { + const rawD = payload.spaceShooterShipDamageOverlayUrls; + const nextD = ['', '', '']; + if (Array.isArray(rawD)) { + for (let di = 0; di < 3; di++) { + const u = normalizeGauntletAssetUrlForPlay(typeof rawD[di] === 'string' ? rawD[di] : ''); + nextD[di] = u; + if (u) ensureGauntletAssetImage(u); + } + } + playSpaceShooterShipDamageOverlayUrls = nextD; + } + if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossMissionTimeSec')) { + const st = Number(payload.balloonBossMissionTimeSec); + if (Number.isFinite(st) && st > 0) { + playBalloonBossMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st))); + } else { + playBalloonBossMissionTimeSec = 0; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossBossImageUrl')) { + playBalloonBossBossImageUrl = normalizeGauntletAssetUrlForPlay( + typeof payload.balloonBossBossImageUrl === 'string' ? payload.balloonBossBossImageUrl : '' + ); + if (playBalloonBossBossImageUrl) ensureGauntletAssetImage(playBalloonBossBossImageUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossPlayerBalloonImageUrls')) { + const a = payload.balloonBossPlayerBalloonImageUrls; + const nextBb = ['', '', '', '', '', '']; + if (Array.isArray(a)) { + for (let si = 0; si < 6; si++) { + const u = normalizeGauntletAssetUrlForPlay(typeof a[si] === 'string' ? a[si] : ''); + nextBb[si] = u; + if (u) ensureGauntletAssetImage(u); + } + } + playBalloonBossPlayerBalloonImageUrls = nextBb; + } + if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossPlayerBalloonFallbackUrl')) { + playBalloonBossPlayerBalloonFallbackUrl = normalizeGauntletAssetUrlForPlay( + typeof payload.balloonBossPlayerBalloonFallbackUrl === 'string' ? payload.balloonBossPlayerBalloonFallbackUrl : '' + ); + if (playBalloonBossPlayerBalloonFallbackUrl) ensureGauntletAssetImage(playBalloonBossPlayerBalloonFallbackUrl); + } + if (isStack() && stackMini) reapplyStackMiniSizingFromGlobals(); + } + + /** เลอร์ปตำแหน่งตัวละครต่อเฟรม (ยิ่งสูงยิ่งตามเป้าเร็ว) */ + const GAUNTLET_VIS_LERP = 0.42; + + function gauntletObsSmoothstep(t) { + t = Math.max(0, Math.min(1, t)); + return t * t * (3 - 2 * t); + } + + function cloneGauntletObsSnap(arr) { + if (!Array.isArray(arr)) return []; + return arr.map((o) => { + if (!o || o.id == null) return null; + const row = { id: o.id, kind: o.kind, x: o.x, y: o.y }; + if (o.kind === 'laser') { + if (o.y0 != null) row.y0 = o.y0; + if (o.y1 != null) row.y1 = o.y1; + } + return row; + }).filter(Boolean); + } + + function gauntletLaserRowHitRange(ob, mapH) { + const h = Math.max(1, Math.floor(Number(mapH)) || 15); + let y0 = ob && ob.y0 != null && Number.isFinite(Number(ob.y0)) ? Math.floor(Number(ob.y0)) : 0; + let y1 = ob && ob.y1 != null && Number.isFinite(Number(ob.y1)) ? Math.floor(Number(ob.y1)) : h - 1; + y0 = Math.max(0, Math.min(h - 1, y0)); + y1 = Math.max(0, Math.min(h - 1, y1)); + if (y1 < y0) { + const t = y0; + y0 = y1; + y1 = t; + } + return { y0, y1 }; + } + + function gauntletLaserOverlapsPlayerRow(ob, py, mapH) { + const { y0, y1 } = gauntletLaserRowHitRange(ob, mapH); + return py >= y0 && py <= y1; + } + + /** โค้งยกตัว: ขึ้นถึงจุดสูงสุดเร็ว ลงชัน (รู้สึกลงพื้นเร็วกว่าแบบสมมาตร) */ + function gauntletLiftHeightNorm(air, jumpTicksMax) { + const jm = Math.max(4, jumpTicksMax || 16); + const a = Math.max(0, Math.min(jm, Number(air) || 0)); + const t = a / jm; + const peakAt = 0.28; + if (t >= peakAt) { + const span = 1 - peakAt; + const u = span > 0 ? (t - peakAt) / span : 0; + return Math.max(0, 1 - u * u * 1.2); + } + return Math.min(1, t / peakAt); + } + + function getGauntletObsDrawPositionsAt(nowMs) { + if (!gauntletObsRenderNext.length) return []; + const elapsed = nowMs - gauntletObsBlendT0; + const alpha = gauntletObsSmoothstep(elapsed / gauntletRuntimeTickMs); + const prevMap = new Map(gauntletObsRenderPrev.map((o) => [o.id, o])); + const out = []; + for (let i = 0; i < gauntletObsRenderNext.length; i++) { + const n = gauntletObsRenderNext[i]; + if (!n) continue; + const p = prevMap.get(n.id); + let drawX = n.x; + if (p && Number.isFinite(p.x) && Number.isFinite(n.x)) drawX = p.x + (n.x - p.x) * alpha; + const row = { id: n.id, kind: n.kind, drawX, y: n.y }; + if (n.kind === 'laser') { + if (n.y0 != null) row.y0 = n.y0; + if (n.y1 != null) row.y1 = n.y1; + } + out.push(row); + } + return out; + } + + function pushGauntletObsRenderFrame(newObs) { + const now = performance.now(); + const next = cloneGauntletObsSnap(newObs); + if (!gauntletObsRenderNext.length) { + gauntletObsRenderPrev = next.slice(); + gauntletObsRenderNext = next.slice(); + } else { + const curDraw = getGauntletObsDrawPositionsAt(now); + const curMap = new Map(curDraw.map((o) => [o.id, o.drawX])); + gauntletObsRenderPrev = next.map((n) => { + const row = { + id: n.id, + kind: n.kind, + x: curMap.has(n.id) ? curMap.get(n.id) : n.x, + y: n.y, + }; + if (n.kind === 'laser') { + if (n.y0 != null) row.y0 = n.y0; + if (n.y1 != null) row.y1 = n.y1; + } + return row; + }); + gauntletObsRenderNext = next; + } + gauntletObsBlendT0 = now; + } + + function lerpGauntletEntityPos(x, y, tx, ty) { + let nx = x; + let ny = y; + if (tx != null && Number.isFinite(tx)) { + nx += (tx - nx) * GAUNTLET_VIS_LERP; + if (Math.abs(tx - nx) < 0.02) nx = tx; + } + if (ty != null && Number.isFinite(ty)) { + ny += (ty - ny) * GAUNTLET_VIS_LERP; + if (Math.abs(ty - ny) < 0.02) ny = ty; + } + return { nx, ny }; + } + + /** + * บอท preview ใน Gauntlet: ตัดสินใจกระโดด (client-only; เซิร์ฟเวอร์ไม่รู้จักบอท) + * — ตอบสนองสิ่งกีดขวางที่ชนเซลล์เดียวกัน + กระโดดล่วงหน้าเมื่อ threat อยู่คอลัมน์ถัดไป + */ + function maybePreviewBotGauntletJump(o, obstacles, w, h) { + if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return; + let px = Math.floor(Number(o.x)) || 0; + let py = Math.floor(Number(o.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + if ((o.gauntletJumpTicks || 0) > 0) return; + const { ch: gauntletCh } = getCharacterFootprintWH(mapData); + let sameCell = false; + let incomingCol = false; + for (let i = 0; i < obstacles.length; i++) { + const obs = obstacles[i]; + if (!obs) continue; + if (obs.kind === 'lane' && typeof obs.y === 'number') { + if (obs.x === px && obs.y >= py && obs.y < py + gauntletCh) sameCell = true; + if (obs.x === px + 1 && obs.y >= py && obs.y < py + gauntletCh) incomingCol = true; + } + if (obs.kind === 'laser' && typeof obs.x === 'number') { + if (obs.x === px && gauntletLaserOverlapsPlayerRow(obs, py, h)) sameCell = true; + if (obs.x === px + 1 && gauntletLaserOverlapsPlayerRow(obs, py, h)) incomingCol = true; + } + } + const tier = o.botTier || 'avg'; + const roll = Math.random(); + const pSame = tier === 'sharp' ? 0.96 : tier === 'weak' ? 0.62 : 0.86; + const pAhead = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.48 : 0.72; + if (sameCell && roll < pSame) { + o.gauntletJumpTicks = gauntletRuntimeJumpTicks; + return; + } + if (incomingCol && roll < pAhead) o.gauntletJumpTicks = gauntletRuntimeJumpTicks; + } + + /** หนึ่ง tick เดียวกับ runGauntletTick ฝั่งเซิร์ฟเวอร์ (เฉพาะ collision + เลื่อน x) */ + function applyGauntletPhysicsToPreviewBot(o, obstacles, w, h) { + if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return; + let px = Math.floor(Number(o.x)) || 0; + let py = Math.floor(Number(o.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + const air = (o.gauntletJumpTicks || 0) > 0; + const { ch: gauntletCh2 } = getCharacterFootprintWH(mapData); + const crown = isGauntletCrownHeistMapPlay(); + let advanceX = false; + let hitBack = false; + for (let i = 0; i < obstacles.length; i++) { + const ob = obstacles[i]; + if (!ob) continue; + if (ob.kind === 'lane' && typeof ob.y === 'number' && ob.x === px && ob.y >= py && ob.y < py + gauntletCh2) { + if (air) advanceX = true; + else hitBack = true; + } + if (ob.kind === 'laser' && typeof ob.x === 'number' && ob.x === px) { + if (!gauntletLaserOverlapsPlayerRow(ob, py, h)) { + /* เลเซอร์ไม่ครอบแถวนี้ */ + } else if (air) { + advanceX = true; + } else { + hitBack = true; + } + } + } + if (advanceX) { + px = Math.min(w - 2, px + 1); + o.gauntletJumpTicks = 0; + if (!crown) { + o.gauntletScore = (o.gauntletScore || 0) + 1; + } + } else if (hitBack) { + if (crown) { + if (px <= 0) { + o.gauntletEliminated = true; + o.gauntletScore = 0; + } else { + o.gauntletScore = Math.max(0, (o.gauntletScore || 0) - 10); + o.gauntletCrownPenaltyFxUntil = Date.now() + 1100; + px = Math.max(0, px - 1); + } + } else { + px = Math.max(0, px - 1); + } + } + if (crown && mapData) { + const mx = getGauntletCrownHeistMaxEntityXPlay(mapData); + if (mx != null && Number.isFinite(mx)) { + const maxPx = Math.floor(mx + 1e-6); + px = Math.min(px, maxPx); + } + } + if ((o.gauntletJumpTicks || 0) > 0) o.gauntletJumpTicks--; + o.tx = px; + o.ty = py; + } + + function applyGauntletPreviewBotsAfterSync() { + if (!playBotsEnabled() || !mapData || mapData.gameType !== 'gauntlet') return; + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return; + const w = mapData.width || 20; + const h = mapData.height || 15; + others.forEach((o, id) => { + if (!isPreviewBotId(id) || !o) return; + maybePreviewBotGauntletJump(o, gauntletObstacles, w, h); + applyGauntletPhysicsToPreviewBot(o, gauntletObstacles, w, h); + }); + } + + /** แจ้งเซิร์ฟเวอร์แถว y ที่มีคุณ+บอททดสอบ — lane spawn ใช้เฉพาะแถวที่มี peer; บอทไม่ใช่ peer */ + function emitGauntletPreviewRowsToServer() { + if (!playBotsEnabled() || !mapData || mapData.gameType !== 'gauntlet') return; + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return; + const h = mapData.height || 15; + const ysSet = new Set(); + const addY = (v) => { + const fy = Math.floor(Number(v)); + if (Number.isFinite(fy) && fy >= 0 && fy < h) ysSet.add(fy); + }; + addY(me.y); + others.forEach((o, id) => { + if (isPreviewBotId(id)) addY(o.y); + }); + socket.emit('gauntlet-preview-rows', { ys: [...ysSet] }); + } + + /** ซ่อน overlay โหมดอื่นที่ค้างจาก embed / session ก่อน — กัน UI ซ้อนไม่ตรง mock */ + function hideConflictingOverlaysForGauntletCrown() { + hideStackTowerResultFlashDomOnlyPlay(); + hideQuizCarryPregameOverlay(); + quizCarryPregameActive = false; + const hideIds = [ + 'quiz-game-overlay', + 'quiz-carry-mission-overlay', + 'gauntlet-ended-overlay', + 'quiz-battle-mcq-overlay', + 'quiz-carry-embed-countdown', + 'gauntlet-crown-countdown', + 'quiz-carry-embed-q-strip', + 'play-quiz-scoreboard', + 'play-quiz-feedback', + ]; + for (let i = 0; i < hideIds.length; i++) { + const el = document.getElementById(hideIds[i]); + if (el) el.classList.add('is-hidden'); + } + const gcm = document.getElementById('gauntlet-crown-mission-overlay'); + if (gcm) gcm.classList.add('is-hidden'); + hideStackTowerHowtoRulesDom(); + const gchHowto = document.getElementById('gauntlet-crown-howto-overlay'); + if (gchHowto) gchHowto.classList.remove(GCHOWTO_STACK_TOWER_CLASS); + } + + function gcmRankBadgeUrl(rank) { + const r = Math.floor(Number(rank)) || 99; + if (r === 1) return BASE + '/img/gauntlet-assets/result-1st.png'; + if (r === 2) return BASE + '/img/gauntlet-assets/result-2nd.png'; + if (r === 3) return BASE + '/img/gauntlet-assets/result-3rd.png'; + return BASE + '/img/gauntlet-assets/result-txt-2.png'; + } + + function gauntletCrownPregameReadyNumerator() { + let n = quizCarryPregameBotCount(); + quizCarryPregameHumanIds().forEach((id) => { + if (gauntletCrownLobbyReadyMap[id]) n++; + }); + return n; + } + + function gauntletCrownSyncGuestReadyIfNeeded() { + const inQuizQuestionHowto = isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto'; + const inStackTowerHowto = isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto'; + const inJumpSurviveHowto = isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto'; + const inSpaceShooterHowto = isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto'; + if (gauntletCrownPregamePhase !== 'howto' && !inQuizQuestionHowto && !inStackTowerHowto && !inJumpSurviveHowto && !inSpaceShooterHowto) return; + if (myId == null || isMePlayHost()) return; + const sid = String(myId); + if (gauntletCrownLobbyReadyMap[sid]) return; + gauntletCrownLobbyReadyMap[sid] = true; + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: true }); + } + + /** ภารกิจคำถาม mng8a80o — Ready Status / ปุ่ม แบบเดียวกับ Mega Virus (gauntlet-crown-lobby-sync) */ + function updateQuizQuestionMissionHowtoHud() { + if (!isQuizQuestionMissionUiMapPlay() || quizQuestionMissionPhase !== 'howto') return; + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!st || !btn) return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + function updateGauntletCrownHowtoHud() { + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!btn || !usesCrownLobbyShellPlay() || gauntletCrownPregamePhase !== 'howto') return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + if (st) { + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + } + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + function beginGauntletCrownCountdownThenRun() { + if (!usesCrownLobbyShellPlay()) return; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + gauntletCrownPregamePhase = 'countdown'; + if (playBotsEnabled() && mapData && mapData.gameType === 'gauntlet') { + applyGauntletPreviewSpawnLayout(false); + } + trySnapGauntletCrownRunwayScrollToSpawnsPlay(); + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + function applyMegaBalloonLiveStartPlay() { + balloonBossSessionStartMs = performance.now(); + balloonBossLastTickMs = performance.now(); + balloonBossGameEnded = false; + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossBossFireAcc = 0; + balloonBossScorePopups = []; + me.balloonBossScore = 0; + me.balloonBossBossDmg = 0; + me.balloonBossBalloons = balloonBossBalloonsStartPlay(); + me.balloonBossEliminated = false; + others.forEach((o) => { + o.balloonBossScore = 0; + o.balloonBossBossDmg = 0; + o.balloonBossBalloons = balloonBossBalloonsStartPlay(); + o.balloonBossEliminated = false; + }); + if (mapData) applyBalloonBossSpawnLayoutPlay(); + } + const runFinish = () => { + if (cd) cd.classList.add('is-hidden'); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + if (socket && socket.connected) { + socket.emit('gauntlet-crown-begin-run', (res) => { + if (res && res.ok) { + gauntletCrownPregamePhase = 'live'; + trySnapGauntletCrownRunwayScrollToSpawnsPlay(); + if (isMegaVirusMissionShellMapPlay()) applyMegaBalloonLiveStartPlay(); + } + }); + } else { + gauntletCrownPregamePhase = 'live'; + trySnapGauntletCrownRunwayScrollToSpawnsPlay(); + if (isMegaVirusMissionShellMapPlay()) applyMegaBalloonLiveStartPlay(); + } + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + setCountdown321QuestionAssetGraphic(numEl, n); + const step = () => { + n--; + if (n > 0) { + setCountdown321QuestionAssetGraphic(numEl, n); + gauntletCrownCountdownTimer = setTimeout(step, 1000); + } else { + gauntletCrownCountdownTimer = null; + runFinish(); + } + }; + gauntletCrownCountdownTimer = setTimeout(step, 1000); + } + + function showGauntletCrownHowtoOverlay() { + if (!usesCrownLobbyShellPlay()) return; + hideConflictingOverlaysForGauntletCrown(); + if (isMegaVirusMissionShellMapPlay()) applyMegaVirusMissionPanelImages(); + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + gauntletCrownPregamePhase = 'howto'; + resetGauntletCrownRunwaySpawnScrollSnap(); + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateGauntletCrownHowtoHud(); + } + + /** Editor embed: quiz_carry / crown ใช้เกรด F = gameover — Jumper: ภาพจบใช้เฉพาะเมื่อไม่มีผู้รอด (0 คน) */ + function gauntletCrownEmbedMissionAnyOk(mission) { + if (!mission) return false; + if (mission.uiSkin === 'violent_crime') { + const sc = Number(mission.survivorCount); + if (Number.isFinite(sc) && sc <= 0) return false; + const g = String(mission.grade || '').trim().toUpperCase().charAt(0); + return g !== 'F'; + } + if (mission.uiSkin === 'jumper') { + const sc = Number(mission.survivorCount); + return Number.isFinite(sc) ? sc > 0 : (String(mission.grade || '').trim().toUpperCase().charAt(0) !== 'F'); + } + if (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower') { + const g = String(mission.grade || '').trim().toUpperCase().charAt(0); + return g !== 'F'; + } + if (mission.uiSkin === 'mega_virus') { + const g = String(mission.grade || '').trim().toUpperCase().charAt(0); + return g !== 'F'; + } + const g = String(mission.grade || '').trim().toUpperCase().charAt(0); + return g !== 'F'; + } + + function gauntletCrownRankBonusLocal(rankOrdinal) { + if (rankOrdinal === 1) return 100; + if (rankOrdinal === 2) return 80; + if (rankOrdinal === 3) return 60; + return 0; + } + + function gauntletCrownRollRewardCardLocal(grade) { + const r = Math.random(); + if (grade === 'A') { + if (r < 0.3) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' }; + if (r < 0.8) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'B') { + if (r < 0.2) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' }; + if (r < 0.5) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (r < 0.2) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + + /** พรีวิว: สรุปภารกิจ Last Light หลังรันเวย์ครบ — ให้ gauntletCrownBuildDisplayMission รวมบอท */ + function gauntletCrownHeistBuildLocalCrownMissionPlay() { + const baseRows = []; + if (myId != null) { + baseRows.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + characterId: me.characterId ?? null, + baseScore: me.gauntletEliminated ? 0 : Math.max(0, Number(me.gauntletScore) | 0), + eliminated: !!me.gauntletEliminated, + }); + } + others.forEach((o, id) => { + if (!o || isPreviewBotId(id)) return; + baseRows.push({ + id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id), + characterId: o.characterId ?? null, + baseScore: o.gauntletEliminated ? 0 : Math.max(0, Number(o.gauntletScore) | 0), + eliminated: !!o.gauntletEliminated, + }); + }); + baseRows.sort((a, b) => { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + const nick = String(a.nickname).localeCompare(String(b.nickname), 'th'); + if (nick !== 0) return nick; + return String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map((row, idx) => { + const pos = idx + 1; + const rankBonus = gauntletCrownRankBonusLocal(pos); + const bs = Math.max(0, Number(row.baseScore) | 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus, + finalScore: bs + rankBonus, + }; + }); + const totalSum = ranked.reduce((s, r) => s + r.finalScore, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const survivorCount = baseRows.filter((r) => !r.eliminated).length; + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (survivorCount <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked, + totalSum, + averageScore, + grade, + rewardCard, + totalParts: ranked.map((r) => r.finalScore), + participantCount: baseRows.length, + survivorCount, + }; + } + + /** + * Editor embed + preview bots: รวมแถวอันดับกับบอท (สูงสุด 5 ช่อง) และคำนวณรวม/เฉลี่ย/เกรดให้สอดคล้องกับแถวที่โชว์ + * — อันดับในช่องใช้ลำดับ 1..5 หลังเรียงคะแนน (เหมือนแนว quiz_carry mission row) + */ + function gauntletCrownBuildDisplayMission(mission) { + if (!mission || !Array.isArray(mission.ranked)) return mission; + if (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus') return mission; + if (!(playBotsEnabled() && isGauntletCrownHeistMapPlay() && others && typeof others.forEach === 'function')) return mission; + + const seen = new Set(); + const baseRows = []; + mission.ranked.forEach((r) => { + if (!r) return; + const sid = String(r.id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: r.id, + nickname: (r.nickname && String(r.nickname).trim()) ? String(r.nickname).trim() : 'ผู้เล่น', + characterId: r.characterId ?? null, + baseScore: r.eliminated ? 0 : Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + }); + }); + others.forEach((o, id) => { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: o.gauntletEliminated ? 0 : Math.max(0, Number(o.gauntletScore) | 0), + eliminated: !!o.gauntletEliminated, + }); + }); + + baseRows.sort((a, b) => { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + const nick = String(a.nickname).localeCompare(String(b.nickname), 'th'); + if (nick !== 0) return nick; + return String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map((row, idx) => { + const pos = idx + 1; + const rankBonus = gauntletCrownRankBonusLocal(pos); + const rankLabel = pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos); + const finalScore = row.baseScore + rankBonus; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: row.eliminated, + rank: pos, + rankLabel: rankLabel, + rankBonus: rankBonus, + finalScore: finalScore, + }; + }); + + const totalSum = ranked.reduce(function (s, r) { return s + r.finalScore; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + const rewardCard = gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: ranked.map(function (r) { return r.finalScore; }), + }; + } + + /* ===== ชุดคำถามพิเศษ (รับการ์ดพิเศษ) — โมดูลกลาง ใช้ได้หลายเกม ===== + * จุดต่อเข้าเกมจริง (trigger) จะ wire ภายหลังตามที่ผู้ใช้ระบุ + * วิธีใช้: SpecialQuiz.fetchSettings() -> getSet(level,caseId) -> pickQuestions(set,n) + * เล่นจบแล้ว awardCard(set, correct, total) -> set mission.specialCardAwarded = card + */ + let __specialQuizSettingsCache = null; + let __specialQuizFetchPromise = null; + + function specialQuizFetchSettings() { + if (__specialQuizSettingsCache) return Promise.resolve(__specialQuizSettingsCache); + if (__specialQuizFetchPromise) return __specialQuizFetchPromise; + __specialQuizFetchPromise = fetch(BASE + '/api/quiz-settings', { cache: 'no-store' }) + .then(function (r) { return r.json(); }) + .then(function (j) { __specialQuizSettingsCache = j || {}; return __specialQuizSettingsCache; }) + .catch(function () { __specialQuizFetchPromise = null; return {}; }); + return __specialQuizFetchPromise; + } + + function specialQuizCurrentLevelCase() { + const meta = window.__detectiveLobbyMeta || {}; + let level = Number(meta.level); + let caseId = Number(meta.caseId); + if (!(level >= 1 && level <= 5)) { + const lp = Number(params.get('lobbyLevel')); + if (lp >= 1 && lp <= 5) level = lp; + } + if (!(caseId >= 1 && caseId <= 3)) { + const cp = Number(params.get('case') || params.get('caseId')); + if (cp >= 1 && cp <= 3) caseId = cp; + } + return { + level: (level >= 1 && level <= 5) ? level : 1, + caseId: (caseId >= 1 && caseId <= 3) ? caseId : 1, + }; + } + + function specialQuizGetSet(settings, level, caseId) { + const sets = (settings && settings.specialQuizSets) || {}; + const key = 'L' + level + '_C' + caseId; + return sets[key] || sets.L1_C1 || { questions: [], specialCard: {} }; + } + + function specialQuizPickQuestions(set, count) { + const qs = (set && Array.isArray(set.questions)) ? set.questions.slice() : []; + for (let i = qs.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const t = qs[i]; qs[i] = qs[j]; qs[j] = t; + } + let n = Number(count); + if (!(n >= 1)) n = qs.length; + return qs.slice(0, Math.min(n, qs.length)); + } + + /** กติกาได้การ์ด: ตอบถูก >= ครึ่งหนึ่ง และชุดนั้นตั้งการ์ดไว้ — คืน card หรือ null */ + function specialQuizAwardCard(set, correctCount, totalCount) { + const card = (set && set.specialCard) || null; + if (!card || !(card.th || card.en || card.imageUrl)) return null; + const total = Number(totalCount) || 0; + const correct = Number(correctCount) || 0; + const passNeed = total > 0 ? Math.ceil(total / 2) : 1; + if (correct < passNeed) return null; + return card; + } + + /** วาดการ์ดพิเศษลงในโซนโบนัสของสรุปผล (#gcm-bonus / .sm-bonus-right) */ + function renderSpecialCardInBonus(bonusEl, card) { + if (!bonusEl || !card || !(card.th || card.en || card.imageUrl || card.cardId)) return; + const imgUrl = card.imageUrl || (card.cardId >= 1 && card.cardId <= 7 + ? '/Game/img/special-cards/card-' + card.cardId + '.png' : ''); + if (!imgUrl) return; + const isPresetAsset = imgUrl.indexOf('/img/special-cards/') >= 0 || imgUrl.indexOf('/trycss/10-Game-1-result/') >= 0; + const wrap = document.createElement('div'); + wrap.className = isPresetAsset + ? 'sm-bonus-right gcm-special-card-preset' + : 'gcm-special-card sm-bonus-special rarity-' + (card.rarity || 'rare'); + const img = document.createElement('img'); + img.className = isPresetAsset ? 'bail-card gcm-special-card-img' : 'gcm-special-card-img'; + img.src = imgUrl; + img.alt = card.th || card.en || ''; + wrap.appendChild(img); + if (!isPresetAsset) { + const cap = document.createElement('div'); + cap.className = 'gcm-special-card-cap'; + const strong = document.createElement('strong'); + strong.textContent = card.th || ''; + const span = document.createElement('span'); + span.textContent = card.en || ''; + cap.appendChild(strong); + cap.appendChild(span); + wrap.appendChild(cap); + } + bonusEl.appendChild(wrap); + } + + window.SpecialQuiz = { + fetchSettings: specialQuizFetchSettings, + currentLevelCase: specialQuizCurrentLevelCase, + getSet: specialQuizGetSet, + pickQuestions: specialQuizPickQuestions, + awardCard: specialQuizAwardCard, + renderCardInBonus: renderSpecialCardInBonus, + }; + + /* ===== Special Quiz ในเกม — ไอคอนในฉาก (เดินชน) + overlay คำถาม multiplayer ===== + * server เป็นผู้ตัดสิน: สุ่มไอคอน 1/3, เปิดคำถามให้ทุกคนพร้อมกัน, ตอบถูกทุกคนทุกข้อ -> ได้การ์ด + */ + const SPECIAL_QUIZ_CHOICE_LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; + /** Test Mode (Admin) เก็บใน localStorage — เปิด shortcut ทดสอบ เช่น Ctrl+Q ตอบถูก */ + function __isTestModeOnPlay() { + try { return localStorage.getItem('justiceTestMode') === '1'; } catch (e) { return false; } + } + let specialQuizIcon = null; // { iconType, x, y } (พิกัด tile-center) + let specialQuizIconScreenRect = null; // { x, y, r } พิกัดบนจอ (ไว้คลิก/แตะไอคอน) + let specialQuizCollideSent = false; + let specialQuizCollideCooldownUntil = 0; + let specialQuizAwardedCard = null; // การ์ดที่ได้รอบนี้ (จาก special-quiz-ended) + let specialQuizActiveQuestion = null; // { index, total, choices, endsAt, iconType } + let specialQuizAnswered = false; + let specialQuizTimerRaf = 0; + const specialQuizIconImgs = {}; + /** หยุดเกมทุกอย่างระหว่างตอบคำถามพิเศษ แล้ว resume หลังจบ */ + let specialQuizFreeze = false; + let specialQuizFreezeStartMs = 0; + let specialQuizRoster = []; // [{ id, nickname, isBot }] + let specialQuizAnswerStatus = {}; // id -> true (ตอบแล้ว) + + let specialQuizFreezeLastTickMs = 0; + + function beginSpecialQuizFreeze() { + if (specialQuizFreeze) return; + specialQuizFreeze = true; + specialQuizFreezeStartMs = Date.now(); + specialQuizFreezeLastTickMs = Date.now(); + try { for (const k in keys) keys[k] = false; } catch (e) { /* ignore */ } + try { playPath = []; } catch (e) { /* ignore */ } + if (me) me.isWalking = false; + } + + /** เลื่อน deadline/anchor เวลาทุกตัวไปข้างหน้าเท่า delta — ใช้เรียกทุกเฟรมระหว่าง freeze + เพื่อให้ "เวลาที่แสดง" หยุดนิ่ง (ไม่ใช่แค่ชดเชยตอนจบ) */ + function shiftSpecialQuizFrozenTimers(deltaMs) { + if (!(deltaMs > 0)) return; + const shift = (v) => (typeof v === 'number' && v > 0) ? v + deltaMs : v; + try { playQuizPhaseEndsAt = shift(playQuizPhaseEndsAt); } catch (e) {} + try { quizCarryEmbedCountdownStartAt = shift(quizCarryEmbedCountdownStartAt); } catch (e) {} + try { quizCarryEmbedCountdownEndAt = shift(quizCarryEmbedCountdownEndAt); } catch (e) {} + try { quizCarryEmbedPreOptionCountdownStartAt = shift(quizCarryEmbedPreOptionCountdownStartAt); } catch (e) {} + try { quizCarryEmbedPreOptionCountdownEndAt = shift(quizCarryEmbedPreOptionCountdownEndAt); } catch (e) {} + try { quizCarryOptionRevealAt = shift(quizCarryOptionRevealAt); } catch (e) {} + try { quizCarryAnswerCloseAt = shift(quizCarryAnswerCloseAt); } catch (e) {} + try { if (typeof jumpSurviveSessionStartMs === 'number' && jumpSurviveSessionStartMs > 0) jumpSurviveSessionStartMs += deltaMs; } catch (e) {} + try { if (typeof spaceShooterSessionStartMs === 'number' && spaceShooterSessionStartMs > 0) spaceShooterSessionStartMs += deltaMs; } catch (e) {} + } + + /** เรียกทุกเฟรมระหว่าง freeze — ดันเวลาไปข้างหน้าเท่าที่ผ่านไปจริง ทำให้ countdown หยุดนิ่ง */ + function tickSpecialQuizFreezeShift() { + if (!specialQuizFreeze) return; + const now = Date.now(); + shiftSpecialQuizFrozenTimers(now - specialQuizFreezeLastTickMs); + specialQuizFreezeLastTickMs = now; + } + + function endSpecialQuizFreeze() { + if (!specialQuizFreeze) return; + /* ชดเชย delta ที่เหลือตั้งแต่เฟรมสุดท้ายจนถึงตอนนี้ */ + shiftSpecialQuizFrozenTimers(Date.now() - specialQuizFreezeLastTickMs); + specialQuizFreeze = false; + specialQuizFreezeStartMs = 0; + specialQuizFreezeLastTickMs = 0; + } + + function specialQuizIconUrl(type) { + return '/Game/img/special-quiz/icon-' + (type === 'police' ? 'police' : 'lawyer') + '.png'; + } + function specialQuizGetIconImg(type) { + const key = type === 'police' ? 'police' : 'lawyer'; + if (specialQuizIconImgs[key]) return specialQuizIconImgs[key]; + const img = new Image(); + img.src = specialQuizIconUrl(key); + specialQuizIconImgs[key] = img; + return img; + } + function setSpecialQuizIconFromServer(payload) { + if (!payload || payload.x == null || payload.y == null) { specialQuizIcon = null; return; } + specialQuizIcon = { + iconType: payload.iconType === 'police' ? 'police' : 'lawyer', + x: Number(payload.x), + y: Number(payload.y), + }; + specialQuizCollideSent = false; + specialQuizCollideCooldownUntil = 0; + specialQuizGetIconImg(specialQuizIcon.iconType); + } + function clearSpecialQuizIcon() { + specialQuizIcon = null; + specialQuizIconScreenRect = null; + specialQuizCollideSent = false; + specialQuizCollideCooldownUntil = 0; + } + + /** ส่ง collide ไป server เพื่อเปิดคำถาม — ถ้า server ปฏิเสธ (เช่น race) ให้ลองใหม่ได้หลัง cooldown */ + function sendSpecialQuizCollide() { + if (specialQuizCollideSent || specialQuizActiveQuestion) return; + if (Date.now() < specialQuizCollideCooldownUntil) return; + specialQuizCollideSent = true; + specialQuizCollideCooldownUntil = Date.now() + 800; + try { + socket.emit('special-quiz-collide', {}, function (res) { + if (!(res && res.ok) && !specialQuizActiveQuestion) specialQuizCollideSent = false; + }); + } catch (e) { specialQuizCollideSent = false; } + } + + /** กด/แตะไอคอนควิซเพื่อเปิดคำถาม (ใช้ได้ทุกมินิเกม รวม space shooter ที่เดินไปแตะไม่ได้) */ + function trySpecialQuizClickAt(clientX, clientY) { + if (!specialQuizIcon || specialQuizCollideSent || specialQuizActiveQuestion) return false; + if (!specialQuizIconScreenRect || !canvas) return false; + const r = canvas.getBoundingClientRect(); + if (!r.width || !r.height) return false; + const px = (clientX - r.left) * (canvas.width / r.width); + const py = (clientY - r.top) * (canvas.height / r.height); + const dx = px - specialQuizIconScreenRect.x; + const dy = py - specialQuizIconScreenRect.y; + /* เผื่อระยะกดให้กว้างขึ้น — แตะใกล้ไอคอนก็เปิดได้ */ + const hitR = specialQuizIconScreenRect.r * 1.3; + if (dx * dx + dy * dy <= hitR * hitR) { + sendSpecialQuizCollide(); + return true; + } + return false; + } + + /** วาดไอคอนในฉาก — ใช้ worldToScreen + zDraw จาก draw() เพื่อให้ตรงทุกมินิเกม */ + function drawSpecialQuizIconWorld(w2s, zDraw) { + if (!specialQuizIcon || typeof w2s !== 'function') return; + const img = specialQuizGetIconImg(specialQuizIcon.iconType); + const sc = w2s(specialQuizIcon.x * tileSize, specialQuizIcon.y * tileSize); + const sx = sc[0]; + const sy = sc[1]; + if (!Number.isFinite(sx) || !Number.isFinite(sy)) return; + const size = Math.max(30, tileSize * zDraw * 1.75); + const bob = Math.sin(Date.now() / 320) * size * 0.06; + const cy = sy + bob; + ctx.save(); + const r = size * 0.62; + const grad = ctx.createRadialGradient(sx, cy, r * 0.18, sx, cy, r); + grad.addColorStop(0, 'rgba(122, 162, 247, 0.5)'); + grad.addColorStop(1, 'rgba(122, 162, 247, 0)'); + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.arc(sx, cy, r, 0, Math.PI * 2); + ctx.fill(); + /* เก็บตำแหน่งบนจอไว้ให้กด/แตะไอคอนได้ (สำหรับมินิเกมที่เดินไปแตะไม่ได้ เช่น space shooter) */ + specialQuizIconScreenRect = { x: sx, y: cy, r: Math.max(size * 0.7, 36) }; + if (img && img.complete && img.naturalWidth > 0) { + ctx.drawImage(img, sx - size / 2, cy - size / 2, size, size); + } else { + ctx.fillStyle = '#7aa2f7'; + ctx.fillRect(sx - size / 2, cy - size / 2, size, size); + } + ctx.fillStyle = '#ffd166'; + ctx.font = '700 ' + Math.round(size * 0.42) + 'px system-ui, "Kanit", sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('?', sx + size * 0.4, cy - size * 0.38); + ctx.restore(); + } + + /** เช็คผู้เล่นเดินชนไอคอน — ใครชนก่อนเป็นคนเปิดคำถามให้ทั้งห้อง (server กันซ้ำ) */ + function checkSpecialQuizCollision() { + if (!specialQuizIcon || specialQuizCollideSent || specialQuizActiveQuestion) return; + if (!Number.isFinite(me.x) || !Number.isFinite(me.y)) return; + const dx = me.x - specialQuizIcon.x; + const dy = me.y - specialQuizIcon.y; + /* รัศมีกว้างขึ้น (1.25 tile) — ไอคอนวาดใหญ่ 1.75 tile เดินเข้าใกล้ก็ทริกได้ ไม่ต้องทับ tile กลางพอดี */ + if (dx * dx + dy * dy <= 1.25 * 1.25) { + sendSpecialQuizCollide(); + } + } + + function specialQuizOverlayEl() { return document.getElementById('special-quiz-overlay'); } + + function specialQuizSelfId() { return myId != null ? String(myId) : null; } + + function specialQuizMemberLabel(m) { + if (m && m.id && m.id === specialQuizSelfId()) return (m.nickname || 'คุณ') + ' (คุณ)'; + return (m && m.nickname) ? m.nickname : '—'; + } + + function renderSpecialQuizMembers() { + const box = document.getElementById('sq-ov-members'); + if (!box) return; + box.innerHTML = ''; + specialQuizRoster.forEach(function (m, i) { + const row = document.createElement('div'); + row.className = 'sq-ov-member' + (m.id === specialQuizSelfId() ? ' sq-ov-member--me' : ''); + row.dataset.id = m.id; + const dot = document.createElement('span'); + dot.className = 'sq-ov-member-dot'; + dot.textContent = String(i + 1); + const name = document.createElement('span'); + name.className = 'sq-ov-member-name'; + name.textContent = specialQuizMemberLabel(m); + const tag = document.createElement('span'); + tag.className = 'sq-ov-member-tag'; + tag.textContent = m.isBot ? 'บอท' : ''; + const ans = document.createElement('span'); + ans.className = 'sq-ov-member-ans'; + ans.textContent = specialQuizAnswerStatus[m.id] ? '…' : ''; + if (specialQuizAnswerStatus[m.id]) row.classList.add('sq-ov-member--answered'); + row.appendChild(dot); + row.appendChild(name); + row.appendChild(tag); + row.appendChild(ans); + box.appendChild(row); + }); + } + + function markSpecialQuizMemberAnswered(id) { + if (!id) return; + specialQuizAnswerStatus[id] = true; + const box = document.getElementById('sq-ov-members'); + if (!box) return; + const row = box.querySelector('.sq-ov-member[data-id="' + (window.CSS && CSS.escape ? CSS.escape(id) : id) + '"]'); + if (!row) { renderSpecialQuizMembers(); return; } + row.classList.add('sq-ov-member--answered'); + const ans = row.querySelector('.sq-ov-member-ans'); + if (ans && !row.classList.contains('sq-ov-member--correct') && !row.classList.contains('sq-ov-member--wrong')) ans.textContent = '✓'; + } + + function openSpecialQuizQuestion(q) { + if (!q) return; + clearSpecialQuizIcon(); + beginSpecialQuizFreeze(); + specialQuizActiveQuestion = { + index: q.index | 0, + total: q.total | 0, + choices: Array.isArray(q.choices) ? q.choices : [], + endsAt: Number(q.endsAt) || 0, + iconType: q.iconType, + }; + if (Array.isArray(q.roster) && q.roster.length) specialQuizRoster = q.roster.slice(); + specialQuizAnswerStatus = {}; + specialQuizAnswered = false; + const ov = specialQuizOverlayEl(); + if (!ov) return; + const iconEl = document.getElementById('sq-ov-icon'); + if (iconEl) iconEl.src = specialQuizIconUrl(q.iconType); + const prog = document.getElementById('sq-ov-progress'); + if (prog) prog.textContent = 'ข้อ ' + ((q.index | 0) + 1) + ' / ' + (q.total | 0); + const qEl = document.getElementById('sq-ov-question'); + if (qEl) qEl.textContent = q.text || ''; + const result = document.getElementById('sq-ov-result'); + if (result) { result.classList.add('is-hidden'); result.textContent = ''; result.className = 'sq-ov-result is-hidden'; } + /* แสดงเฉพาะ layer คำถาม/คำตอบ — ซ่อน layer ผลรางวัล */ + const quizBody = document.getElementById('sq-ov-quiz-body'); + if (quizBody) quizBody.classList.remove('is-hidden'); + const awardLayer = document.getElementById('sq-ov-award'); + if (awardLayer) awardLayer.classList.add('is-hidden'); + const status = document.getElementById('sq-ov-status'); + if (status) status.textContent = 'เลือกคำตอบ — ทุกคนต้องตอบถูกทุกข้อจึงได้การ์ดพิเศษ'; + const choicesEl = document.getElementById('sq-ov-choices'); + if (choicesEl) { + choicesEl.innerHTML = ''; + specialQuizActiveQuestion.choices.forEach(function (c, i) { + const b = document.createElement('button'); + b.type = 'button'; + b.className = 'sq-ov-choice'; + b.dataset.idx = String(i); + const k = document.createElement('span'); + k.className = 'sq-ov-choice-key'; + k.textContent = SPECIAL_QUIZ_CHOICE_LABELS[i] || String(i + 1); + const t = document.createElement('span'); + t.className = 'sq-ov-choice-text'; + t.textContent = c; + b.appendChild(k); + b.appendChild(t); + b.onclick = function () { submitSpecialQuizAnswer(i); }; + choicesEl.appendChild(b); + }); + } + renderSpecialQuizMembers(); + ov.classList.remove('is-hidden'); + startSpecialQuizTimerLoop(); + } + + function submitSpecialQuizAnswer(i) { + if (specialQuizAnswered || !specialQuizActiveQuestion) return; + specialQuizAnswered = true; + const choicesEl = document.getElementById('sq-ov-choices'); + if (choicesEl) { + Array.prototype.forEach.call(choicesEl.children, function (b) { + b.classList.toggle('sq-ov-choice--picked', Number(b.dataset.idx) === i); + b.disabled = true; + }); + } + const status = document.getElementById('sq-ov-status'); + if (status) status.textContent = 'ส่งคำตอบแล้ว — รอผู้เล่นคนอื่น...'; + if (specialQuizSelfId()) markSpecialQuizMemberAnswered(specialQuizSelfId()); + try { socket.emit('special-quiz-answer', { choiceIndex: i }, function () {}); } catch (e) { /* ignore */ } + } + + function startSpecialQuizTimerLoop() { + cancelAnimationFrame(specialQuizTimerRaf); + function tick() { + if (!specialQuizActiveQuestion) return; + const timerEl = document.getElementById('sq-ov-timer'); + const ms = specialQuizActiveQuestion.endsAt - Date.now(); + const s = Math.max(0, Math.ceil(ms / 1000)); + if (timerEl) { + timerEl.textContent = String(s); + timerEl.classList.toggle('sq-ov-timer--low', s <= 5); + } + if (ms > 0) specialQuizTimerRaf = requestAnimationFrame(tick); + } + tick(); + } + + function handleSpecialQuizResult(r) { + if (!r || !specialQuizActiveQuestion) return; + const choicesEl = document.getElementById('sq-ov-choices'); + if (choicesEl) { + Array.prototype.forEach.call(choicesEl.children, function (b) { + const idx = Number(b.dataset.idx); + b.disabled = true; + if (idx === r.correctIndex) b.classList.add('sq-ov-choice--correct'); + else if (b.classList.contains('sq-ov-choice--picked')) b.classList.add('sq-ov-choice--wrong'); + }); + } + /* เฉลย — เปิดเผยว่าแต่ละคนตอบอะไร ถูก/ผิด */ + const answers = (r.answers && typeof r.answers === 'object') ? r.answers : {}; + const labels = SPECIAL_QUIZ_CHOICE_LABELS; + const box = document.getElementById('sq-ov-members'); + if (box) { + Array.prototype.forEach.call(box.children, function (row) { + const id = row.dataset.id; + const a = answers[id]; + const ansEl = row.querySelector('.sq-ov-member-ans'); + row.classList.remove('sq-ov-member--answered'); + if (Number.isInteger(a)) { + const ok = a === r.correctIndex; + row.classList.add(ok ? 'sq-ov-member--correct' : 'sq-ov-member--wrong'); + if (ansEl) ansEl.textContent = (ok ? '✓ ' : '✗ ') + (labels[a] || String(a + 1)); + } else { + row.classList.add('sq-ov-member--wrong'); + if (ansEl) ansEl.textContent = '—'; + } + }); + } + const result = document.getElementById('sq-ov-result'); + if (result) { + result.classList.remove('is-hidden'); + result.textContent = r.allCorrect + ? 'ทุกคนตอบถูก!' + : (r.timeout ? 'หมดเวลา — ตอบไม่ครบทุกคน' : 'มีคนตอบผิด — ไม่ได้การ์ดพิเศษ'); + result.className = 'sq-ov-result ' + (r.allCorrect ? 'sq-ov-result--ok' : 'sq-ov-result--fail'); + } + cancelAnimationFrame(specialQuizTimerRaf); + } + + /** การ์ดพิเศษถูก "ใช้" — โชว์ animation + ขยายเวลามินิเกมฝั่ง client (ถ้าเป็น Time Dilation) */ + function applySpecialCardClient(data) { + const card = data.card || {}; + const addSec = Number(data.addSec) || 0; + const coins = Number(data.coins) || 0; + if (addSec > 0 && card.effectKey === 'time_dilation') { + extendCurrentMinigameTimeClient(addSec); + } + if (coins > 0 && card.effectKey === 'fund') { + addOwnCoinsViaApi(coins); + } + /* ถ้า overlay ควิซยังเปิด (การ์ดเพิ่งได้จากควิซ) — แผง «เล่นต่อ» โชว์การ์ดอยู่แล้ว ไม่ต้องเด้งซ้ำ */ + const sqOv = specialQuizOverlayEl(); + if (sqOv && !sqOv.classList.contains('is-hidden')) return; + showSpecialCardUseAnimation(card, { addSec: addSec, coins: coins }); + } + + /** Card 6 Fund — แต่ละ client บวกเหรียญของตัวเอง (อ่าน jdPlayerKey จาก localStorage) */ + function addOwnCoinsViaApi(amount) { + let key = ''; + try { key = (localStorage.getItem('jdPlayerKey') || '').trim(); } catch (e) { key = ''; } + if (!key || key.length < 8) return; + const url = (typeof appPath === 'function' ? appPath('/Admin/api/player-coins-add.php') : '/Admin/api/player-coins-add.php'); + try { + fetch(url, { + method: 'POST', + credentials: 'omit', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ playerKey: key, amount: amount }), + }).then(function (r) { return r.json(); }).then(function (d) { + if (d && d.ok && typeof d.coins === 'number') { + try { localStorage.setItem('jdCoins', String(d.coins)); } catch (e) { /* ignore */ } + } + }).catch(function () { /* ignore */ }); + } catch (e) { /* ignore */ } + } + + /** +วินาที ให้มินิเกมที่กำลังเล่น (jump/shooter/quiz carry) — mng8a80o จัดการฝั่ง server */ + function extendCurrentMinigameTimeClient(sec) { + const ms = Math.max(0, sec * 1000); + if (!ms) return; + if (isJumpSurvive() && jumpSurviveSessionStartMs) jumpSurviveSessionStartMs += ms; + if (isSpaceShooter() && spaceShooterSessionStartMs) spaceShooterSessionStartMs += ms; + if (isQuizCarry()) { + if (quizCarryAnswerCloseAt > 0) quizCarryAnswerCloseAt += ms; + if (quizCarryOptionRevealAt > 0) quizCarryOptionRevealAt += ms; + } + } + + function showSpecialCardUseAnimation(card, opts) { + opts = opts || {}; + let ov = document.getElementById('special-card-use-overlay'); + if (!ov) { + ov = document.createElement('div'); + ov.id = 'special-card-use-overlay'; + ov.className = 'is-hidden'; + ov.innerHTML = + '
' + + '
'; + document.body.appendChild(ov); + } + const img = ov.querySelector('.scu-img'); + const nameEl = ov.querySelector('.scu-name'); + const fxEl = ov.querySelector('.scu-effect'); + if (img) img.src = card.imageUrl || (card.cardId ? '/Game/img/special-cards/card-' + card.cardId + '.png' : ''); + if (nameEl) nameEl.textContent = 'ใช้การ์ด: ' + (card.en || card.th || 'Special Card'); + if (fxEl) { + let fx = card.desc || card.th || ''; + if (opts.addSec > 0) fx = '+' + opts.addSec + ' วินาที'; + else if (opts.coins > 0) fx = 'ทุกคน +' + opts.coins + ' COINS'; + fxEl.textContent = fx; + } + ov.classList.remove('is-hidden'); + ov.classList.remove('scu-play'); + void ov.offsetWidth; + ov.classList.add('scu-play'); + clearTimeout(showSpecialCardUseAnimation._t); + showSpecialCardUseAnimation._t = setTimeout(function () { + ov.classList.add('is-hidden'); + ov.classList.remove('scu-play'); + }, 2600); + } + + /** ปิด overlay + คืนเวลาเล่นต่อ (เรียกตอนกด «เล่นต่อ» หรือ safety timeout) */ + function specialQuizContinueAndResume() { + clearTimeout(endSpecialQuiz._safety); + endSpecialQuiz._safety = 0; + const ov = specialQuizOverlayEl(); + const award = document.getElementById('sq-ov-award'); + if (award) award.classList.add('is-hidden'); + if (ov) ov.classList.add('is-hidden'); + /* แจ้ง server ให้เล่นรอบ quiz (mng8a80o) ต่อ — มินิเกมอื่น freeze ฝั่ง client พอ */ + try { socket.emit('special-quiz-continue', {}, function () {}); } catch (e) { /* ignore */ } + endSpecialQuizFreeze(); + } + + function endSpecialQuiz(data) { + cancelAnimationFrame(specialQuizTimerRaf); + specialQuizActiveQuestion = null; + specialQuizAnswered = false; + const success = !!(data && data.success && data.card); + const card = success ? data.card : null; + if (success) specialQuizAwardedCard = card; + const status = document.getElementById('sq-ov-status'); + if (status) status.textContent = ''; + const result = document.getElementById('sq-ov-result'); + if (result) { + result.classList.remove('is-hidden'); + result.textContent = success ? 'ได้รับการ์ดพิเศษ!' : 'รอบนี้ไม่ได้การ์ดพิเศษ'; + result.className = 'sq-ov-result ' + (success ? 'sq-ov-result--ok' : 'sq-ov-result--fail'); + } + /* แผงโชว์การ์ด + ปุ่ม «เล่นต่อ» — เกมหยุดค้างจนกว่าจะกด */ + const award = document.getElementById('sq-ov-award'); + const titleEl = document.getElementById('sq-ov-award-title'); + const imgEl = document.getElementById('sq-ov-award-img'); + const nameEl = document.getElementById('sq-ov-award-name'); + const fxEl = document.getElementById('sq-ov-award-effect'); + const btn = document.getElementById('sq-ov-award-continue'); + if (award) { + if (success) { + if (titleEl) titleEl.textContent = 'ได้รับการ์ดพิเศษ!'; + if (imgEl) { + imgEl.removeAttribute('hidden'); + imgEl.src = card.imageUrl || (card.cardId ? '/Game/img/special-cards/card-' + card.cardId + '.png' : ''); + } + if (nameEl) nameEl.textContent = card.en || card.th || ('การ์ด #' + (card.cardId || '')); + if (fxEl) fxEl.textContent = card.desc || card.th || ''; + } else { + if (titleEl) titleEl.textContent = 'รอบนี้ไม่ได้การ์ดพิเศษ'; + if (imgEl) { imgEl.setAttribute('hidden', ''); imgEl.removeAttribute('src'); } + if (nameEl) nameEl.textContent = ''; + if (fxEl) fxEl.textContent = 'ตอบถูกครบทุกคนเพื่อรับการ์ดพิเศษในครั้งหน้า'; + } + if (btn) btn.onclick = specialQuizContinueAndResume; + /* สลับ layer: ซ่อนคำถาม/คำตอบ แล้วโชว์ผลรางวัลเต็มหน้า */ + const quizBody = document.getElementById('sq-ov-quiz-body'); + if (quizBody) quizBody.classList.add('is-hidden'); + award.classList.remove('is-hidden'); + } else { + /* ไม่มี element สำรอง — เล่นต่อทันที */ + specialQuizContinueAndResume(); + return; + } + /* กันค้าง: ถ้าไม่กดภายใน 30 วิ ให้ไปต่อเอง */ + clearTimeout(endSpecialQuiz._safety); + endSpecialQuiz._safety = setTimeout(specialQuizContinueAndResume, 30000); + } + + function showGauntletCrownMissionOverlay(mission) { + const ov = document.getElementById('gauntlet-crown-mission-overlay'); + const rowEl = document.getElementById('gcm-rank-row'); + const totalEl = document.getElementById('gcm-total'); + const gradeEl = document.getElementById('gcm-grade'); + const bonusEl = document.getElementById('gcm-bonus'); + const btn = document.getElementById('btn-gcm-done'); + if (!ov || !rowEl || !mission || !Array.isArray(mission.ranked)) return; + if (!mission.specialCardAwarded && specialQuizAwardedCard) mission.specialCardAwarded = specialQuizAwardedCard; + let disp = gauntletCrownBuildDisplayMission(mission); + if (mission.uiSkin === 'jumper') { + disp = jumpSurviveMergePreviewBotsMission(disp) || disp; + } else if (mission.uiSkin === 'violent_crime') { + disp = spaceShooterMergePreviewBotsMission(disp) || disp; + } else if (mission.uiSkin === 'question_mission') { + disp = quizQuestionMissionMergePreviewBotsMission(disp) || disp; + } else if (mission.uiSkin === 'stack_tower') { + disp = stackTowerMissionMergePreviewBotsMission(disp) || disp; + } else if (mission.uiSkin === 'mega_virus') { + disp = balloonBossMergePreviewBotsMission(disp) || disp; + } + if (!disp || !Array.isArray(disp.ranked)) return; + /* mng8a80o เท่านั้น — สรุปผลภารกิจตาม mockup Minigame-1 (06-summary) */ + const tfMockSummary = mission.uiSkin === 'question_mission' && isQuizQuestionMissionUiMapPlay(); + ov.classList.toggle('gcm-quiz-tf-mock', tfMockSummary); + /* mng8a80o: ใช้ popup-result.png ของ QUESTION (เหมือน mockup) — แมปอื่นคง popup ของ gauntlet */ + const gcmBgImg = ov.querySelector('.gcm-bg'); + if (gcmBgImg) { + const gcmBgSrc = tfMockSummary + ? questionMissionAssetUrl('popup-result.png') + : (BASE + '/img/gauntlet-assets/popup-result.png'); + if (gcmBgImg.getAttribute('src') !== gcmBgSrc) gcmBgImg.setAttribute('src', gcmBgSrc); + } + if (tfMockSummary) { + ensureMePlayTintFromLobbyStorage(); + ensureQuizTfScaleResizeListener(); + updateGcmTfMockScale(ov); + /* preload tint ทุกคนก่อนวาด avatar — กันรูปว่าง/สีเดียว */ + disp.ranked.forEach(function (r) { + const cid = resolveRankedCharacterIdPlay(r); + if (!cid) return; + preloadPlayTintForAvatar(cid, resolveCyberHudAvatarTintPlay({ + id: r.id, + characterId: cid, + isMe: myId != null && r.id != null && String(r.id) === String(myId), + })); + }); + } else { + ov.style.removeProperty('--gcm-tf-scale'); + } + rowEl.innerHTML = ''; + disp.ranked.forEach((r) => { + const cell = document.createElement('div'); + cell.className = 'gcm-cell'; + const tag = document.createElement('div'); + tag.className = 'gcm-rank-tag'; + const rk = Math.floor(Number(r.rank)) || 0; + if (tfMockSummary) { + /* mockup: ทุกอันดับเป็นภาพ (result-1st/2nd/3rd, txt-1=4, txt-2=5+) */ + const badge = rk === 1 ? 'result-1st.png' : rk === 2 ? 'result-2nd.png' + : rk === 3 ? 'result-3rd.png' : rk === 4 ? 'result-txt-1.png' : 'result-txt-2.png'; + const tagImg = document.createElement('img'); + tagImg.src = questionMissionAssetUrl(badge); + tagImg.alt = '[' + String(r.rankLabel || rk) + ']'; + tagImg.style.maxWidth = '100%'; + tagImg.style.height = 'auto'; + tag.appendChild(tagImg); + } else if (rk >= 1 && rk <= 3) { + const tagImg = document.createElement('img'); + tagImg.src = gcmRankBadgeUrl(rk); + tagImg.alt = '[' + String(r.rankLabel || rk) + ']'; + tagImg.style.maxWidth = '100%'; + tagImg.style.height = 'auto'; + tag.appendChild(tagImg); + } else { + tag.textContent = '[' + String(r.rankLabel || rk) + ']'; + } + const frame = document.createElement('div'); + frame.className = 'gcm-frame'; + const img = document.createElement('img'); + img.className = 'gcm-av'; + img.alt = ''; + const cid = resolveRankedCharacterIdPlay(r); + const avRow = { + id: r.id, + characterId: cid, + isMe: myId != null && r.id != null && String(r.id) === String(myId), + playTint: resolveCyberHudAvatarTintPlay({ + id: r.id, + characterId: cid, + isMe: myId != null && r.id != null && String(r.id) === String(myId), + }), + }; + if (cid) { + applyCyberHudScoreAvatarImgWithRetry(img, avRow, 0); + } else { + img.src = defaultAvatarImg.src; + } + img.onerror = function () { + this.onerror = null; + applyCyberHudScoreAvatarImgWithRetry(this, avRow, 3); + }; + if (tfMockSummary) { + /* mng8a80o: portrait อยู่ใต้ player-frame.png (mockup .portrait + .frame z-index 3) */ + const avClip = document.createElement('div'); + avClip.className = 'gcm-av-clip'; + avClip.appendChild(img); + frame.appendChild(avClip); + const frameImg = document.createElement('img'); + frameImg.className = 'gcm-frame-img'; + frameImg.src = questionMissionAssetUrl('player-frame.png'); + frameImg.alt = ''; + frameImg.decoding = 'async'; + frame.appendChild(frameImg); + } else { + frame.appendChild(img); + } + const qMissionLose = mission && mission.uiSkin === 'question_mission' && r.hadQuizWrong; + if (tfMockSummary && (r.eliminated || qMissionLose)) cell.classList.add('gcm-cell--out'); + if (r.eliminated || qMissionLose) { + const st = document.createElement('img'); + st.className = 'gcm-stamp'; + const jSkin = mission && mission.uiSkin === 'jumper'; + let loseUrl = BASE + '/img/gauntlet-assets/result-Lose_stamp.png'; + let altLose = 'เสียชีวิต'; + if (jSkin && r.eliminated) { + loseUrl = jumperAssetUrl('stamp-sacrifice.png'); + altLose = 'ผู้เสียสละ'; + } else if (qMissionLose) { + loseUrl = BASE + '/img/QUESTION/result-Lose_stamp.png'; + altLose = 'ตอบผิด'; + } + st.src = loseUrl; + st.alt = altLose; + st.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/result-Lose_stamp.png'; + this.alt = 'ตอบผิด'; + }; + frame.appendChild(st); + } + const nick = document.createElement('div'); + nick.className = 'gcm-nick'; + nick.textContent = r.nickname || '—'; + const sc = document.createElement('div'); + sc.className = 'gcm-sc'; + const jumpPlain = mission && (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus'); + const botScorePlain = (playBotsEnabled() && isGauntletCrownHeistMapPlay() && isPreviewBotId(r.id)) + || (playBotsEnabled() && mission && mission.uiSkin === 'question_mission' && isPreviewBotId(r.id)) + || (playBotsEnabled() && mission && mission.uiSkin === 'stack_tower' && isPreviewBotId(r.id)) + || (playBotsEnabled() && mission && mission.uiSkin === 'mega_virus' && isPreviewBotId(r.id)); + sc.textContent = (jumpPlain || botScorePlain) + ? String(Math.max(0, Number(r.baseScore) || 0)) + : String(r.baseScore || 0) + '(+' + String(r.rankBonus || 0) + ')'; + cell.appendChild(tag); + cell.appendChild(frame); + cell.appendChild(nick); + cell.appendChild(sc); + rowEl.appendChild(cell); + }); + if (totalEl) { + const parts = (disp.totalParts || []).map((n) => String(Math.max(0, Number(n) || 0))); + const sum = Math.max(0, Number(disp.totalSum) || 0); + const avg = Math.max(0, Math.floor(Number(disp.averageScore) || 0)); + const jumperSkin = mission && mission.uiSkin === 'jumper'; + const violentSkin = mission && mission.uiSkin === 'violent_crime'; + const questionMissionSkin = mission && mission.uiSkin === 'question_mission'; + if (jumperSkin) { + const sc = Math.max(0, Math.floor(Number(disp.survivorCount) || 0)); + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้รอด ' + sc + '/' + pc + ' → เกรด'; + } else if (violentSkin) { + const sc = Math.max(0, Math.floor(Number(disp.survivorCount) || 0)); + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · รอดชีวิต ' + sc + '/' + pc + ' → เกรด'; + } else if (questionMissionSkin) { + if (tfMockSummary) { + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') =' + sum + ''; + } else { + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'คะแนนตอบคำถาม (' + parts.join('+') + ') = ' + sum + ' · ผู้เล่น ' + pc + ' → เกรด'; + } + } else if (mission && mission.uiSkin === 'stack_tower') { + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้เล่น ' + pc + ' → เกรด'; + } else if (mission && mission.uiSkin === 'mega_virus') { + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'ความเสียหายรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้เล่น ' + pc + ' → เกรด'; + } else { + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · เฉลี่ย ' + avg + ' → เกรด'; + } + } + if (gradeEl) { + const g = String(disp.grade || 'C').toUpperCase().charAt(0); + gradeEl.textContent = g; + gradeEl.className = 'gcm-grade gcm-grade--' + g.toLowerCase(); + } + if (bonusEl) { + bonusEl.innerHTML = ''; + const gLetter = String(disp.grade || 'C').toUpperCase().charAt(0); + const missionOk = gLetter !== 'F'; + if (missionOk) { + const row = document.createElement('div'); + row.className = 'gcm-bonus-okrow'; + row.style.display = 'flex'; + row.style.alignItems = 'center'; + row.style.gap = '8px'; + const chk = document.createElement('img'); + const stackTS = mission && mission.uiSkin === 'stack_tower'; + const megaVs = mission && mission.uiSkin === 'mega_virus'; + chk.src = stackTS ? stackTowerAssetUrl('result-check.png') : (megaVs ? megaVirusAssetUrl('result-check.png') : (BASE + '/img/gauntlet-assets/result-check.png')); + chk.alt = ''; + if (stackTS || megaVs) { + chk.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/result-check.png'; + }; + } + if (tfMockSummary) { + /* mockup: ใช้ป้าย "ภารกิจสำเร็จ" สำเร็จรูป mission-complete-select.png เท่านั้น */ + chk.src = questionMissionAssetUrl('mission-complete-select.png'); + chk.alt = 'ภารกิจสำเร็จ'; + chk.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/result-check.png'; + }; + row.appendChild(chk); + } else { + chk.width = 26; + chk.height = 26; + const sp = document.createElement('span'); + sp.style.color = '#9ece6a'; + sp.style.fontWeight = '700'; + sp.textContent = 'ภารกิจสำเร็จ'; + row.appendChild(chk); + row.appendChild(sp); + } + bonusEl.appendChild(row); + } + const rc = disp.rewardCard; + if (missionOk && rc && rc.th) { + const pl = document.createElement('div'); + pl.className = 'gcm-plaque'; + pl.innerHTML = '' + String(rc.th) + '' + String(rc.en || '') + ''; + bonusEl.appendChild(pl); + } else if (!missionOk) { + const pl = document.createElement('div'); + pl.className = 'gcm-plaque'; + pl.style.borderColor = 'rgba(148, 153, 168, 0.45)'; + pl.style.opacity = '0.95'; + pl.innerHTML = 'ไม่ได้รับการ์ดเกรด ' + gLetter + ' · ภารกิจไม่ผ่านเกณฑ์'; + bonusEl.appendChild(pl); + } + /* การ์ดพิเศษจากชุดคำถาม (ถ้ารอบนี้ได้รับ) — wire จุด trigger ภายหลัง */ + if (mission && mission.specialCardAwarded) { + renderSpecialCardInBonus(bonusEl, mission.specialCardAwarded); + } + } + const gcmHead = document.getElementById('gcm-heading'); + if (gcmHead) { + if (mission && (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus' || mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime')) gcmHead.classList.add('sr-only'); + else gcmHead.classList.remove('sr-only'); + } + ov.classList.remove('is-hidden'); + if (tfMockSummary) { + requestAnimationFrame(function () { + refreshGcmTfMockAvatarsPlay(rowEl, disp.ranked); + }); + setTimeout(function () { refreshGcmTfMockAvatarsPlay(rowEl, disp.ranked); }, 200); + setTimeout(function () { refreshGcmTfMockAvatarsPlay(rowEl, disp.ranked); }, 550); + setTimeout(function () { refreshGcmTfMockAvatarsPlay(rowEl, disp.ranked); }, 1200); + } + function goLobby() { + ov.classList.add('is-hidden'); + if (gcmHead) gcmHead.classList.remove('sr-only'); + if (tryFinishDetectiveMinigameAndReturnLobby()) return; + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = function () { + if (previewMode && editorEmbedReturn) { + ov.classList.add('is-hidden'); + if (gcmHead) gcmHead.classList.remove('sr-only'); + if (mission && (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus' || mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime')) { + cancelQuizCarryResultEndAfterTimeup(); + hideQuizCarryTimeupOnDeskLayer(); + hideQuizCarryResultEndLayer(); + scheduleEmbedPreviewReturnToLobbyAfterResultEnd(); + } else { + showQuizCarryTimeupOnDeskLayer(function () { return gauntletCrownEmbedMissionAnyOk(disp); }); + } + } else { + goLobby(); + } + }; + } + } + + function isLobby() { return mapData && mapData.gameType === 'lobby'; } + function isQuiz() { return mapData && mapData.gameType === 'quiz'; } + function getLane(y) { + if (!mapData || !mapData.lanes) return null; + const lane = mapData.lanes.find(l => l.y === y); + return lane || (mapData.lanes[y] != null ? mapData.lanes[y] : null); + } + function getVehiclePositions(lane, width, timeMs) { + if (!lane || (lane.type !== 'road' && lane.type !== 'water')) return []; + const speed = (lane.speed != null ? lane.speed : 1) * 0.05; + const dir = lane.dir === -1 ? -1 : 1; + const spacing = lane.spacing != null ? lane.spacing : (lane.type === 'road' ? 3 : 2.5); + const period = width + 2; + const count = Math.max(2, Math.floor(period / spacing)); + const positions = []; + for (let i = 0; i < count; i++) { + const p = i * spacing + (timeMs * speed * dir) / 60; + const pNorm = ((p % period) + period) % period; + const vx = pNorm - 1; + positions.push(Math.max(-1, Math.min(width, vx))); + } + return positions; + } + function checkFroggerCollision() { + const fy = Math.floor(me.y); + const lane = getLane(fy); + if (!lane) return false; + if (lane.type === 'road') { + const positions = getVehiclePositions(lane, mapData.width, Date.now()); + for (let i = 0; i < positions.length; i++) { + if (Math.abs(positions[i] - me.x) < 1.1) return true; + } + } + if (lane.type === 'water') { + const positions = getVehiclePositions(lane, mapData.width, Date.now()); + let onLog = false; + for (let i = 0; i < positions.length; i++) { + if (Math.abs(positions[i] - me.x) < 1.2) { onLog = true; break; } + } + if (!onLog) return true; + } + return false; + } + function respawnFrogger() { + const sp = mapData.spawn || { x: 1, y: mapData.height - 1 }; + me.x = sp.x; me.y = sp.y; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + + /** รองรับ x/y เป็น string จาก Socket — ถ้าเช็คแค่ typeof number จะร่วงไป mapData.spawn ทุกครั้ง (จุดเกิดไม่สุ่ม) */ + function peerXYFromJoin(peer, spawnFb) { + const sx = Number(spawnFb && spawnFb.x); + const sy = Number(spawnFb && spawnFb.y); + const defX = Number.isFinite(sx) ? sx : 1; + const defY = Number.isFinite(sy) ? sy : 1; + if (!peer) return { x: defX, y: defY }; + const x = Number(peer.x); + const y = Number(peer.y); + if (Number.isFinite(x) && Number.isFinite(y)) return { x, y }; + return { x: defX, y: defY }; + } + + /** Hub = 0/1 · กริดตัวเลือก = 0 หรือเลข 1..16 (ห้ามบังคับ === 1 เหมือน hub — เลข 2–16จะหาย) */ + function normalizeQuizCarryLayersInPlay(md) { + if (!md || md.gameType !== 'quiz_carry') return; + const w = md.width || 20, h = md.height || 15; + const maxOpt = QUIZ_CARRY_MAX_OPTION_SLOTS; + const srcHub = md.quizCarryHubArea || []; + const hubRows = []; + for (let y = 0; y < h; y++) { + const r = srcHub[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + hubRows.push(row); + } + md.quizCarryHubArea = hubRows; + const srcOpt = md.quizCarryOptionArea || []; + const optRows = []; + for (let y = 0; y < h; y++) { + const r = srcOpt[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= maxOpt ? Math.floor(n) : 0); + } + optRows.push(row); + } + md.quizCarryOptionArea = optRows; + const srcCd = md.carryEmbedCountdownArea || []; + const cdRows = []; + for (let y = 0; y < h; y++) { + const r = srcCd[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + row.push(Number(v) === 1 ? 1 : 0); + } + cdRows.push(row); + } + md.carryEmbedCountdownArea = cdRows; + } + + /** โดม = ช่องติดกัน (4 ทิศ) ต่อ 1 ข้อ · comp id เรียงตามการค้นพบบนแมป */ + function normalizeQuizBattleDomeInPlay(md) { + if (!md || md.gameType !== 'quiz_battle') return; + const w = md.width || 20, h = md.height || 15; + const src = md.quizBattleDomeArea || []; + const grid = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + grid.push(row); + } + md.quizBattleDomeArea = grid; + const comp = Array(h).fill(0).map(() => Array(w).fill(0)); + let nextId = 0; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (grid[y][x] !== 1 || comp[y][x]) continue; + nextId++; + const stack = [[x, y]]; + comp[y][x] = nextId; + while (stack.length) { + const c = stack.pop(); + const cx = c[0], cy = c[1]; + for (let i = 0; i < 4; i++) { + const dx = (i === 0 ? 1 : i === 1 ? -1 : 0); + const dy = (i === 2 ? 1 : i === 3 ? -1 : 0); + const nx = cx + dx, ny = cy + dy; + if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue; + if (grid[ny][nx] !== 1 || comp[ny][nx]) continue; + comp[ny][nx] = nextId; + stack.push([nx, ny]); + } + } + } + } + md.quizBattleDomeComp = comp; + } + + /** เส้นทาง Quiz Battle — วาดอย่างน้อย 1 ช่องแล้วจะเดินได้เฉพาะบนเส้นทาง (เปลี่ยนแค่รูปพื้นหลังได้) */ + function normalizeQuizBattlePathInPlay(md) { + if (!md || md.gameType !== 'quiz_battle') return; + const w = md.width || 20, h = md.height || 15; + const src = md.quizBattlePathArea || []; + const grid = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + grid.push(row); + } + md.quizBattlePathArea = grid; + } + + function quizBattlePathModeActive(md) { + if (!md || md.gameType !== 'quiz_battle' || !md.quizBattlePathArea) return false; + const g = md.quizBattlePathArea; + for (let y = 0; y < g.length; y++) { + const row = g[y]; + if (!row) continue; + for (let x = 0; x < row.length; x++) if (row[x] === 1) return true; + } + return false; + } + + /** ทุกช่องที่สไปรต์กินพื้นที่ต้องอยู่บนเลน — กันหลุดไปพื้นเปิด (path=0) หรือทับกำแพง (objects=1) */ + function quizBattleFootprintFullyOnPath(md, px, py) { + if (!md || !quizBattlePathModeActive(md)) return true; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return false; + const g = md.quizBattlePathArea; + const tiles = quizBattleSpriteBoundsTilesPlay(md, px, py); + if (tiles.size === 0) return false; + for (const k of tiles) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!g[ty] || g[ty][tx] !== 1) return false; + } + return true; + } + + /** ยืน/เดินได้บนเลนม่วง หรือทุกช่องสไปรต์อยู่ใน spawnArea (ฟ้า) — โซนรอก่อนลงเลน */ + function quizBattlePositionAllowedPlay(md, px, py) { + if (!md || !quizBattlePathModeActive(md)) return true; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return false; + if (quizBattleFootprintFullyOnPath(md, px, py)) return true; + const sa = md.spawnArea; + if (!sa || !Array.isArray(sa)) return false; + const tiles = quizBattleSpriteBoundsTilesPlay(md, px, py); + if (tiles.size === 0) return false; + for (const k of tiles) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!sa[ty] || sa[ty][tx] !== 1) return false; + } + return true; + } + + /** สปอว์นในพื้นที่ฟ้า — เช็คแค่ไม่ทับกำแพง ไม่บังคับอยู่บนเลน */ + function quizBattleSpawnWallClearPlay(md, nx, ny) { + if (!md || !md.objects) return false; + if (typeof nx !== 'number' || typeof ny !== 'number' || !Number.isFinite(nx) || !Number.isFinite(ny)) return false; + const w = md.width || 20, h = md.height || 15; + const prevMap = mapData; + mapData = md; + try { + for (const k of quizTilesWallCollisionFootprintPlay(nx, ny)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = md.objects[ty]; + if (!row || row[tx] === 1) return false; + } + return true; + } finally { + mapData = prevMap; + } + } + + /** เดินทีละย่อย — เลนบาง 1 ช่อง + กล่องสไปรต์ใหญ่กว่าเท้า ยังเดินได้โดยไม่หลุดเลน */ + function quizBattleTryStepMovePlay(fx, fy, tx, ty, canWalkFn) { + const SUB = 8; + let x = fx; + let y = fy; + for (let i = 1; i <= SUB; i++) { + const t = i / SUB; + const nx = fx + (tx - fx) * t; + const ny = fy + (ty - fy) * t; + if (canWalkFn(nx, ny, x, y)) { + x = nx; + y = ny; + } + } + return { x, y }; + } + + /** สแน็ปบนเลน — เช็คแค่กำแพง + อยู่บน path (ให้ตรงกับ server) ไม่ใช้ blockPlayer/ผู้เล่นคนอื่น เพราะจะทำให้หาจุดสแน็ปไม่ได้แล้วค้างนอกเลน */ + function quizBattleSnapTargetValidPlay(x, y) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + for (const k of quizTilesWallCollisionFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (quizBattlePathModeActive(mapData) && !quizBattlePositionAllowedPlay(mapData, x, y)) return false; + return true; + } + + function snapPositionOntoQuizBattlePathIfNeeded(px, py) { + if (!mapData || !isQuizBattle() || !quizBattlePathModeActive(mapData)) return { x: px, y: py }; + if (quizBattleSnapTargetValidPlay(px, py)) return { x: px, y: py }; + const w = mapData.width || 20, h = mapData.height || 15; + const g = mapData.quizBattlePathArea; + let bestX = null, bestY = null, bestD = Infinity; + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + const nx = tx + 0.01; + const ny = ty + 0.01; + if (!quizBattleSnapTargetValidPlay(nx, ny)) continue; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + /* footprint ใหญ่กว่าเลน: ลองจุดกลางช่อง — ต้องผ่านกล่องสไปรต์+path เหมือนก้อนบน (เดิมเช็คแค่ไม่ทับกำแพง → snap หลุดเลน) */ + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + const nx = tx + 0.5; + const ny = ty + 0.5; + if (!quizBattleSnapTargetValidPlay(nx, ny)) continue; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + const sa = mapData.spawnArea; + if (sa && Array.isArray(sa)) { + for (let ty = 0; ty < h; ty++) { + const row = sa[ty]; + if (!row) continue; + for (let tx = 0; tx < w; tx++) { + if (row[tx] !== 1) continue; + for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) { + if (!quizBattleSnapTargetValidPlay(nx, ny)) continue; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + return { x: px, y: py }; + } + + /** บังคับให้ผู้เล่นอยู่บนเลน — เรียกก่อน return กลาง tick (แชท / path หมด) เพราะเดิมข้ามบล็อกสแน็ปท้าย tick */ + function enforceQuizBattleLaneOnMePlay() { + if (!mapData || !isQuizBattle() || !quizBattlePathModeActive(mapData)) return; + if (quizBattleSnapTargetValidPlay(me.x, me.y)) return; + const snapped = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = snapped.x; + me.y = snapped.y; + } + + /** สแน็ป peer / บอทหลัง lerp — กันตัวละครล้ำกำแพงหรือหลุดเลนระหว่างเฟรม */ + function enforceQuizBattleLaneOnPeersPlay() { + if (!mapData || !isQuizBattle() || !quizBattlePathModeActive(mapData)) return; + others.forEach((o) => { + if (!o) return; + if (quizBattleSnapTargetValidPlay(o.x, o.y)) return; + const sn = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = sn.x; + o.y = sn.y; + o.tx = sn.x; + o.ty = sn.y; + }); + } + + function hideQuizBattleMcqModal() { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + } + + function flashQuizBattleFeedback(text, ok) { + const el = document.getElementById('play-quiz-feedback'); + if (!el) return; + el.textContent = text || ''; + el.classList.remove('is-hidden', 'play-quiz-feedback-ok', 'play-quiz-feedback-bad'); + el.classList.add(ok ? 'play-quiz-feedback-ok' : 'play-quiz-feedback-bad'); + if (typeof window.__quizBattleFbT === 'number') clearTimeout(window.__quizBattleFbT); + window.__quizBattleFbT = setTimeout(() => { el.classList.add('is-hidden'); }, 1800); + } + + function getQuizBattleDomeCompIdsUnderFootprint(px, py) { + if (!mapData || !isQuizBattle()) return []; + const comp = mapData.quizBattleDomeComp; + const grid = mapData.quizBattleDomeArea; + if (!comp || !grid) return []; + const ids = []; + const seen = Object.create(null); + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + const cid = comp[ty] && comp[ty][tx]; + if (grid[ty] && grid[ty][tx] === 1 && cid > 0 && !seen[cid]) { + seen[cid] = 1; + ids.push(cid); + } + } + return ids; + } + + function getPrimaryQuizBattleDomeCompAt(px, py) { + const arr = getQuizBattleDomeCompIdsUnderFootprint(px, py); + if (!arr.length) return null; + return Math.min.apply(null, arr); + } + + function showQuizBattleMcqModal(q) { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + const textEl = document.getElementById('quiz-battle-mcq-text'); + if (!ov || !textEl || !q) return; + textEl.textContent = q.text || ''; + const labels = ['A', 'B', 'C']; + for (let i = 0; i < 3; i++) { + const btn = ov.querySelector('.quiz-battle-choice[data-idx="' + i + '"]'); + if (btn) { + const ch = (q.choices && q.choices[i]) ? String(q.choices[i]) : ''; + btn.textContent = labels[i] + (ch ? (': ' + ch.slice(0, 96)) : ''); + } + } + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + + function trySubmitQuizBattleChoice(idx) { + if (quizBattleModalCompId == null || !mapData || !isQuizBattle()) return; + const compId = quizBattleModalCompId; + const pool = quizBattleMcqPool; + if (!pool.length) { + flashQuizBattleFeedback('ยังไม่มีคำถาม — ตั้ง battleQuizMcq ใน Admin → Quiz Battle', false); + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + return; + } + const q = pool[(compId - 1) % pool.length]; + if (!q) return; + const ok = Number(q.correctIndex) === Number(idx); + if (ok) { + quizBattleAnsweredComps.add(compId); + if (myId != null) { + if (!playLiveQuizScores) playLiveQuizScores = {}; + playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + 1; + renderPlayQuizScoreboard(playLiveQuizScores); + } + flashQuizBattleFeedback('ถูกต้อง · +1', true); + } else { + flashQuizBattleFeedback('ยังไม่ถูก — ลองใหม่ (กด E อีกครั้ง)', false); + } + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + } + + function tryOpenQuizBattleFromKey() { + if (!isQuizBattle() || !mapData || myId == null) return; + const compId = getPrimaryQuizBattleDomeCompAt(me.x, me.y); + if (compId == null) { + flashQuizBattleFeedback('ยังไม่ยืนบนโดมคำถาม', false); + return; + } + if (quizBattleAnsweredComps.has(compId)) { + flashQuizBattleFeedback('ตอบโดมนี้ถูกแล้ว', false); + return; + } + const pool = quizBattleMcqPool; + if (!pool.length) { + flashQuizBattleFeedback('ยังไม่มีคำถามในระบบ (Admin → Quiz Battle)', false); + return; + } + const q = pool[(compId - 1) % pool.length]; + if (!q) return; + quizBattleModalCompId = compId; + showQuizBattleMcqModal(q); + } + + function pushSanitizedBattleMcqFromPayload(s) { + if (!s || !Array.isArray(s.battleQuizMcq)) return 0; + let n = 0; + for (let i = 0; i < s.battleQuizMcq.length; i++) { + const raw = s.battleQuizMcq[i]; + if (!raw || !String(raw.text || '').trim()) continue; + const ch = Array.isArray(raw.choices) ? raw.choices : []; + if (ch.length !== 3) continue; + const a = String(ch[0] || '').trim(); + const b = String(ch[1] || '').trim(); + const c = String(ch[2] || '').trim(); + if (!a || !b || !c) continue; + let ci = Number(raw.correctIndex); + if (!Number.isFinite(ci)) ci = 0; + ci = Math.max(0, Math.min(2, Math.floor(ci))); + quizBattleMcqPool.push({ + text: String(raw.text).trim(), + choices: [a, b, c], + correctIndex: ci, + }); + n++; + } + return n; + } + + /** + * โหลดชุดข้อ A/B/C — ลอง Node ก่อน แล้ว fallback PHP อ่านจากดิสก์ (กรณี nginx ไม่ส่ง /api/quiz-settings ถึง Node) + */ + async function loadQuizBattleMcqPool() { + quizBattleMcqPool = []; + const bust = '_=' + Date.now(); + const tryUrls = [ + BASE + '/api/quiz-settings?' + bust, + BASE + '/api-quiz-battle-mcq.php?' + bust, + ]; + for (let u = 0; u < tryUrls.length; u++) { + try { + const r = await fetch(tryUrls[u], { cache: 'no-store' }); + if (!r.ok) continue; + const s = await r.json(); + if (pushSanitizedBattleMcqFromPayload(s) > 0) break; + } catch (e) { /* next url */ } + } + } + + function resetQuizBattlePlayState() { + quizBattleMcqPool = []; + quizBattleAnsweredComps = new Set(); + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + } + + function setupPlayQuizBattleUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + /* ทดสอบจากเอดิเตอร์: อย่าเปิดแผงคำถามเต็มจอล่าง — บังมองเห็นบอท/แผนที่ (overlay ยังอัปเดตข้อความไว้ถ้าเปิด preview แบบไม่ embed) */ + if (ov) { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else ov.classList.remove('is-hidden'); + } + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'Quiz Battle'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'เดินทับโดม (สีฟ้า–ขอบแดง) แล้วกด E เพื่อตอบ A / B / C'; + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + const leg = document.getElementById('quiz-play-legend'); + if (leg) { + leg.textContent = quizBattlePathModeActive(mapData) + ? 'โหมดเส้นทาง: เดินได้เฉพาะช่องเส้นทาง (ม่วงบนแผนที่) — วางโดมบนเส้นทาง · ไม่วาดเส้นทางใน Editor = เดินอิสระ · เปลี่ยนรูปพื้นหลังได้ทีหลัง' + : 'คำถามจาก Admin → Quiz Battle (battleQuizMcq) · ช่องโดมติดกัน = 1 ข้อ · ถูกแล้วได้ +1 ต่อโดม'; + } + quizBattleAnsweredComps = new Set(); + quizBattleModalCompId = null; + quizBattleMcqPool = []; + hideQuizBattleMcqModal(); + playLiveQuizScores = {}; + if (myId != null) playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + renderPlayQuizScoreboard(playLiveQuizScores); + loadQuizBattleMcqPool().then(() => { + const q2 = document.getElementById('quiz-game-question'); + if (!q2) return; + if (!quizBattleMcqPool.length) { + q2.textContent = 'ยังไม่มีคำถาม — ไปที่ Admin แท็บ Quiz Battle แล้วบันทึกข้อ A/B/C (ครบคำถาม + A + B + C) · หรือเช็คว่าเซิร์ฟเวอร์เกม (Node) รันอยู่ · ลองรีเฟรชแบบ hard refresh (Ctrl+F5)'; + } else { + const q0 = quizBattleMcqPool[0]; + const snippet = q0 && q0.text ? String(q0.text).trim().slice(0, 100) : ''; + const more = q0 && q0.text && String(q0.text).trim().length > 100 ? '…' : ''; + q2.textContent = snippet + ? ('โหลดคำถามแล้ว ' + quizBattleMcqPool.length + ' ข้อ · ตัวอย่าง: 「' + snippet + more + '」\nเข้าโดมสีฟ้า (ขอบแดง) แล้วกด E เพื่อเลือก A / B / C') + : ('โหลดคำถามแล้ว ' + quizBattleMcqPool.length + ' ข้อ — เดินเข้าโดม (ฟ้า–ขอบแดง) แล้วกด E เพื่อตอบ A / B / C'); + } + const ov2 = document.getElementById('quiz-game-overlay'); + if (ov2 && previewMode && editorEmbedReturn) ov2.classList.add('is-hidden'); + }); + } + + function normalizeJumpSurvivePlatformAreaInPlay(md) { + if (!md || md.gameType !== 'jump_survive') return; + const w = md.width || 20, h = md.height || 15; + const src = md.jumpSurvivePlatformArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + md.jumpSurvivePlatformArea = rows; + } + + function normalizeJumpSurvivePlatformVariantAreaInPlay(md) { + if (!md || md.gameType !== 'jump_survive') return; + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea || []; + const src = md.jumpSurvivePlatformVariantArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const row = []; + for (let x = 0; x < w; x++) { + const has = pa[y] && pa[y][x] === 1; + let v = has ? Math.floor(Number(src[y] && src[y][x])) : 0; + if (has && (!Number.isFinite(v) || v < 1)) v = 1; + if (v > 3) v = 3; + if (!has) v = 0; + row.push(v); + } + rows.push(row); + } + md.jumpSurvivePlatformVariantArea = rows; + } + + function jumpSurvivePlatformVariantIndexAtPlay(md, tx, ty) { + const pa = md && md.jumpSurvivePlatformArea; + if (!pa || !pa[ty] || pa[ty][tx] !== 1) return 0; + const va = md.jumpSurvivePlatformVariantArea; + let v = va && va[ty] ? Math.floor(Number(va[ty][tx])) : 1; + if (!Number.isFinite(v) || v < 1) v = 1; + if (v > 3) v = 3; + return v; + } + + function normalizeJumpSurviveHazardAreaInPlay(md) { + if (!md || md.gameType !== 'jump_survive') return; + const w = md.width || 20, h = md.height || 15; + const src = md.jumpSurviveHazardArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + md.jumpSurviveHazardArea = rows; + } + + /** โซนตาย jump_survive — กริดคงที่ในโลก (ไม่เลื่อนกับแพลตฟอร์ม) · ช่องที่มีแพลตฟอร์มอยู่ด้วย = ไม่นับ hazard (ยืนบนแพลตฟอร์มชนะ) */ + function jumpSurviveOverlapsHazardArea(md, left, top, right, bottom) { + const ha = md && md.jumpSurviveHazardArea; + if (!ha) return false; + const pa = md && md.jumpSurvivePlatformArea; + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const x0 = Math.max(0, Math.floor(left / ts)); + const x1 = Math.min(w - 1, Math.floor((right - 1e-6) / ts)); + const y0 = Math.max(0, Math.floor(top / ts)); + const y1 = Math.min(h - 1, Math.floor((bottom - 1e-6) / ts)); + for (let ty = y0; ty <= y1; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (!ha[ty] || ha[ty][tx] !== 1) continue; + if (pa && pa[ty] && pa[ty][tx] === 1) continue; + return true; + } + } + return false; + } + + function normalizeShooterSpawnSlotsInPlay(md) { + if (!md || (md.gameType !== 'space_shooter' && md.gameType !== 'jump_survive')) return; + const w = md.width || 20, h = md.height || 15; + const src = md.shooterSpawnSlots || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= 6 ? Math.floor(n) : 0); + } + rows.push(row); + } + md.shooterSpawnSlots = rows; + } + + function getShooterSpawnWorldCenterFromMap(md, slot1to6, ts) { + if (!md || !md.shooterSpawnSlots) return null; + const w = md.width || 20, h = md.height || 15; + const g = md.shooterSpawnSlots; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (g[y] && g[y][x] === slot1to6) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; + } + } + return null; + } + + function buildSpaceShooterParticipantRefsPlay() { + const refs = []; + if (me) refs.push({ id: myId, ref: me }); + const humanIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + humanIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + botIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + return refs.filter((r) => r.ref); + } + + function applySpaceShooterSpawnLayoutPlay() { + if (!mapData || mapData.gameType !== 'space_shooter') return; + normalizeShooterSpawnSlotsInPlay(mapData); + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const { cw, ch } = getCharacterFootprintWH(mapData); + const mw = w * ts, mh = h * ts; + const refs = buildSpaceShooterParticipantRefsPlay(); + const n = refs.length || 1; + refs.forEach((entry, idx) => { + const ent = entry.ref; + const slot = (idx % 6) + 1; + const cen = getShooterSpawnWorldCenterFromMap(mapData, slot, ts); + let cx, cy; + if (cen) { + cx = cen.cx; + cy = cen.cy; + } else { + cx = ((idx + 1) / (n + 1)) * mw; + cy = Math.min(mh - ts * 1.2, (h - 1.5) * ts); + } + cx = Math.max(ts * 0.5, Math.min(mw - ts * 0.5, cx)); + cy = clampSpaceShooterWorldCy(cy, mh); + ent.spaceShooterCx = cx; + ent.spaceShooterCy = cy; + ent.spaceShooterSlot = slot; + if (typeof ent.spaceShooterScore !== 'number' || !Number.isFinite(ent.spaceShooterScore)) ent.spaceShooterScore = 0; + ent.x = cx / ts - cw * 0.5; + ent.y = cy / ts - ch * 0.92; + ent.tx = ent.x; + ent.ty = ent.y; + ent.direction = 'up'; + }); + } + + function spaceShooterTimeLimitSecPlay() { + const t = Number(mapData && mapData.spaceShooterTimeSec); + if (Number.isFinite(t) && t === 0) return 0; + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + const g = Number(playSpaceShooterMissionTimeSec); + if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g); + return 90; + } + + function spaceShooterRemainingSecPlay() { + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return null; + const lim = spaceShooterTimeLimitSecPlay(); + if (lim <= 0) return null; + const elapsed = (performance.now() - spaceShooterSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function endSpaceShooterTimeUp() { + if (spaceShooterGameEnded || !mapData || mapData.gameType !== 'space_shooter') return; + if (isSpaceShooterMissionUiMapPlay()) { + endSpaceShooterMissionRound('time_up'); + return; + } + spaceShooterGameEnded = true; + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterAsteroidExplosions = []; + spaceShooterSpawnAccMs = 0; + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) titleEl.textContent = 'หมดเวลา · Time up'; + msgEl.textContent = 'ยิงยานอวกาศจบแล้ว · SPACE SHOOTER finished — อันดับจากคะแนนยิงหิน'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ id: myId, nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', score: Math.max(0, me.spaceShooterScore | 0) }); + } + others.forEach((o, id) => { + ranks.push({ id, nickname: (o && o.nickname) ? String(o.nickname).trim() : id, score: Math.max(0, o.spaceShooterScore | 0) }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + } + + function syncSpaceShooterFootFromShipCenter(ent) { + if (!ent || !mapData || ent.spaceShooterCx == null || ent.spaceShooterCy == null) return; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + ent.x = ent.spaceShooterCx / ts - cw * 0.5; + ent.y = ent.spaceShooterCy / ts - ch * 0.92; + } + + /** จำกัดกลางยานแนวตั้งให้อยู่แค่ครึ่งล่างของแมพ (ไม่ขึ้นเกินกึ่งกลางความสูง) */ + function clampSpaceShooterWorldCy(cy, mhPx) { + if (!Number.isFinite(cy) || !Number.isFinite(mhPx) || mhPx <= 0) return cy; + const edge = 20; + const lo = mhPx * 0.5; + const hi = Math.max(lo + 4, mhPx - edge); + return Math.max(lo, Math.min(hi, cy)); + } + + const SPACE_SHOOTER_ASTEROID_INTERVAL_MIN = 200; + /** ms ระหว่างเกิดอุกาบาต — แมปทับถ้าตั้ง ≥200 */ + function spaceShooterAsteroidSpawnIntervalMsPlay() { + const mapI = Number(mapData && mapData.spaceShooterAsteroidIntervalMs); + if (Number.isFinite(mapI) && mapI >= SPACE_SHOOTER_ASTEROID_INTERVAL_MIN) { + return Math.min(10000, Math.floor(mapI)); + } + const g = Number(playSpaceShooterAsteroidIntervalMs); + if (Number.isFinite(g) && g >= SPACE_SHOOTER_ASTEROID_INTERVAL_MIN) return Math.min(10000, Math.floor(g)); + return 1040; + } + + function stepSpaceShooterPreviewBots(dt) { + if (spaceShooterGameEnded || !playBotsEnabled() || !mapData || mapData.gameType !== 'space_shooter') return; + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return; + const ts = tileSize; + const mw = (mapData.width || 20) * ts; + const mh = (mapData.height || 15) * ts; + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o || o.spaceShooterCx == null || o.spaceShooterEliminated) return; + let nearest = null, nd = 1e9; + for (let i = 0; i < spaceShooterAsteroids.length; i++) { + const a = spaceShooterAsteroids[i]; + if (!a) continue; + const d = Math.abs(a.x - o.spaceShooterCx) + Math.abs(a.y - o.spaceShooterCy) * 0.15; + if (d < nd) { nd = d; nearest = a; } + } + let vx = 0; + if (nearest) vx = nearest.x > o.spaceShooterCx ? 1 : nearest.x < o.spaceShooterCx ? -1 : 0; + else vx = Math.sin(performance.now() / 900 + bid.length) > 0 ? 1 : -1; + let vy = 0; + if (nearest && Math.abs(nearest.y - o.spaceShooterCy) > ts * 0.25) { + vy = nearest.y > o.spaceShooterCy ? 1 : -1; + } else if (!nearest) { + vy = Math.sin(performance.now() / 700 + bid.charCodeAt(0)) > 0 ? 1 : -1; + } + const spd = (o.botTier === 'sharp' ? 195 : o.botTier === 'weak' ? 115 : 155); + const padX = spaceShooterShipEdgePadWorldPxPlay(); + o.spaceShooterCx = Math.max(padX, Math.min(mw - padX, o.spaceShooterCx + vx * spd * dt)); + o.spaceShooterCy = clampSpaceShooterWorldCy(o.spaceShooterCy + vy * spd * 0.7 * dt, mh); + syncSpaceShooterFootFromShipCenter(o); + o.tx = o.x; + o.ty = o.y; + if (typeof o.spaceShooterBotFireCd !== 'number') o.spaceShooterBotFireCd = 0; + o.spaceShooterBotFireCd -= dt; + if (o.spaceShooterBotFireCd <= 0 && nearest && nd < ts * 5) { + o.spaceShooterBotFireCd = o.botTier === 'sharp' ? 0.22 : o.botTier === 'weak' ? 0.48 : 0.34; + spaceShooterBullets.push({ x: o.spaceShooterCx, y: o.spaceShooterCy - 16, vy: -500, ownerId: bid }); + } + }); + } + + function spaceShooterTickFrame() { + if (!mapData || mapData.gameType !== 'space_shooter') return; + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return; + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const mw = w * ts, mh = h * ts; + const { cw, ch } = getCharacterFootprintWH(mapData); + const now = performance.now(); + const dt = Math.min(0.055, spaceShooterLastTickMs ? (now - spaceShooterLastTickMs) / 1000 : 0.016); + spaceShooterLastTickMs = now; + tickPlayScrollBg(dt); + spaceShooterTickAsteroidExplosions(dt); + + if (spaceShooterGameEnded) { + const wallClock = Date.now(); + for (let pi = spaceShooterPopups.length - 1; pi >= 0; pi--) { + if (wallClock >= spaceShooterPopups[pi].until) spaceShooterPopups.splice(pi, 1); + } + return; + } + const remSec = spaceShooterRemainingSecPlay(); + if (remSec != null && remSec <= 0) { + endSpaceShooterTimeUp(); + return; + } + + const interval = spaceShooterAsteroidSpawnIntervalMsPlay(); + spaceShooterSpawnAccMs += dt * 1000; + while (spaceShooterSpawnAccMs >= interval) { + spaceShooterSpawnAccMs -= interval; + const maxHp = 1 + Math.floor(Math.random() * SPACE_SHOOTER_ASTEROID_MAX_HP); + const hp0 = maxHp; + spaceShooterAsteroids.push({ + x: 36 + Math.random() * (mw - 72), + y: -45 - Math.random() * 90, + r: spaceShooterAsteroidRadiusFromHpPlay(hp0), + vy: 58 + Math.random() * 62, + maxHp: maxHp, + hp: hp0, + }); + } + + others.forEach((o, id) => { + if (isPreviewBotId(id)) return; + if (o.tx != null) o.x += (o.tx - o.x) * 0.28; + if (o.ty != null) o.y += (o.ty - o.y) * 0.28; + o.spaceShooterCx = (o.x + cw * 0.5) * ts; + o.spaceShooterCy = clampSpaceShooterWorldCy((o.y + ch * 0.92) * ts, mh); + }); + + let vx = 0; + let vy = 0; + if (!isChatFocused()) { + if (keys['ArrowLeft'] || keys['KeyA']) vx -= 1; + if (keys['ArrowRight'] || keys['KeyD']) vx += 1; + if (keys['ArrowUp'] || keys['KeyW']) vy -= 1; + if (keys['ArrowDown'] || keys['KeyS']) vy += 1; + } + const moveSpd = 280; + const canPilotMe = !(isSpaceShooterMissionUiMapPlay() && me.spaceShooterEliminated); + if (canPilotMe) { + if (me.spaceShooterCx != null) { + const padX = spaceShooterShipEdgePadWorldPxPlay(); + me.spaceShooterCx = Math.max(padX, Math.min(mw - padX, me.spaceShooterCx + vx * moveSpd * dt)); + } + if (me.spaceShooterCy != null) { + me.spaceShooterCy = clampSpaceShooterWorldCy(me.spaceShooterCy + vy * moveSpd * dt, mh); + } + syncSpaceShooterFootFromShipCenter(me); + } + + spaceShooterFireCd -= dt; + const autoFireOk = canPilotMe && !isChatFocused(); + if (autoFireOk && spaceShooterFireCd <= 0 && me.spaceShooterCy != null) { + spaceShooterFireCd = 0.21; + spaceShooterBullets.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 20, vy: -580, ownerId: myId }); + } + + stepSpaceShooterPreviewBots(dt); + + for (let i = spaceShooterBullets.length - 1; i >= 0; i--) { + const b = spaceShooterBullets[i]; + b.y += b.vy * dt; + if (b.y < -50 || b.x < -30 || b.x > mw + 30) spaceShooterBullets.splice(i, 1); + } + for (let ai = spaceShooterAsteroids.length - 1; ai >= 0; ai--) { + const a = spaceShooterAsteroids[ai]; + a.y += a.vy * dt; + if (a.y > mh + 100) spaceShooterAsteroids.splice(ai, 1); + } + + spaceShooterMissionResolveShipAsteroidHitsPlay(); + + for (let bi = spaceShooterBullets.length - 1; bi >= 0; bi--) { + const b = spaceShooterBullets[bi]; + let hit = -1; + for (let ai = 0; ai < spaceShooterAsteroids.length; ai++) { + const a = spaceShooterAsteroids[ai]; + const dx = b.x - a.x, dy = b.y - a.y; + const rr = (a.r + 7) * (a.r + 7); + if (dx * dx + dy * dy <= rr) { hit = ai; break; } + } + if (hit >= 0) { + const a = spaceShooterAsteroids[hit]; + const mh = Math.max(1, Math.floor(Number(a.maxHp)) || SPACE_SHOOTER_ASTEROID_MAX_HP); + a.hp = Math.max(0, Math.floor(Number(a.hp) || mh) - 1); + spaceShooterBullets.splice(bi, 1); + const add = 5; + if (b.ownerId === myId) { + me.spaceShooterScore = Math.max(0, (me.spaceShooterScore || 0) + add); + spaceShooterPopups.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 }); + } else { + const o = others.get(b.ownerId); + if (o) { + o.spaceShooterScore = Math.max(0, (o.spaceShooterScore || 0) + add); + spaceShooterPopups.push({ x: o.spaceShooterCx, y: o.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 }); + } + } + if (a.hp <= 0) { + spaceShooterSpawnAsteroidExplosion(a.x, a.y, a.r); + spaceShooterAsteroids.splice(hit, 1); + } + /** ขนาดคงที่จนแตก — ไม่ย่อ r ตาม HP ที่เหลือ (สลับสปรายต์ผ่าน spaceShooterAsteroidLiveSpriteIndexPlay) */ + } + } + + const wallClock = Date.now(); + for (let pi = spaceShooterPopups.length - 1; pi >= 0; pi--) { + /* until ใช้ Date.now() — ห้ามเทียบกับ performance.now() (สเกลคนละระบบ → ลบไม่ออก) */ + if (wallClock >= spaceShooterPopups[pi].until) spaceShooterPopups.splice(pi, 1); + } + + me.direction = 'up'; + me.isWalking = vx !== 0 || vy !== 0; + const tEmit = Date.now(); + if (tEmit - spaceShooterLastMoveEmit > 90 && socket && myId != null) { + spaceShooterLastMoveEmit = tEmit; + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + spaceShooterScore: Math.max(0, me.spaceShooterScore | 0), + }); + } + } + + function drawSpaceShooterCombatLayer(ctx, worldToScreen, zDraw, timeMs) { + if (!mapData || !isSpaceShooter()) return; + const refs = buildSpaceShooterParticipantRefsPlay(); + const wallNow = typeof timeMs === 'number' ? timeMs : Date.now(); + + const astUrls = playSpaceShooterAsteroidSpriteUrls; + + for (let i = 0; i < spaceShooterAsteroids.length; i++) { + const a = spaceShooterAsteroids[i]; + const [sx, sy] = worldToScreen(a.x, a.y); + const sr = Math.max(6, a.r * zDraw); + const si = spaceShooterAsteroidLiveSpriteIndexPlay(a); + let liveUrl = (astUrls && astUrls[si]) ? String(astUrls[si]).trim() : ''; + if (!liveUrl && si > 0 && astUrls && astUrls[0]) liveUrl = String(astUrls[0]).trim(); + const liveRec = liveUrl ? ensureGauntletAssetImage(liveUrl) : null; + const liveAstImg = liveRec && liveRec.ready && liveRec.img && liveRec.img.naturalWidth > 0 ? liveRec.img : null; + if (liveAstImg) { + const iw = liveAstImg.naturalWidth; + const ih = liveAstImg.naturalHeight; + const diam = sr * 2.15; + const sc = Math.min(diam / iw, diam / ih); + const dw = iw * sc; + const dh = ih * sc; + ctx.drawImage(liveAstImg, sx - dw * 0.5, sy - dh * 0.5, dw, dh); + } else { + const grd = ctx.createRadialGradient(sx - sr * 0.25, sy - sr * 0.25, sr * 0.1, sx, sy, sr); + grd.addColorStop(0, 'rgba(90, 85, 95, 0.95)'); + grd.addColorStop(0.55, 'rgba(35, 32, 42, 0.98)'); + grd.addColorStop(1, 'rgba(18, 16, 24, 1)'); + ctx.fillStyle = grd; + ctx.beginPath(); + ctx.arc(sx, sy, sr, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 90, 60, 0.75)'; + ctx.lineWidth = Math.max(1.5, zDraw * 1.2); + ctx.beginPath(); + ctx.arc(sx, sy, sr * 0.88, 0.6, 1.9); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(sx + sr * 0.15, sy + sr * 0.1, sr * 0.35, 0, Math.PI * 2); + ctx.stroke(); + ctx.shadowColor = 'rgba(255, 60, 40, 0.55)'; + ctx.shadowBlur = sr * 0.45; + ctx.strokeStyle = 'rgba(255, 120, 80, 0.35)'; + ctx.beginPath(); + ctx.arc(sx, sy, sr + 3 * zDraw, 0, Math.PI * 2); + ctx.stroke(); + ctx.shadowBlur = 0; + } + } + + if (astUrls && astUrls.length >= 2) { + for (let ei = 0; ei < spaceShooterAsteroidExplosions.length; ei++) { + const ex = spaceShooterAsteroidExplosions[ei]; + const u = astUrls[ex.fi + 1]; + if (!u) continue; + const rec = ensureGauntletAssetImage(u); + const img = rec && rec.ready && rec.img && rec.img.naturalWidth > 0 ? rec.img : null; + if (!img) continue; + const [sx, sy] = worldToScreen(ex.x, ex.y); + const sr = Math.max(6, ex.r * zDraw); + const iw = img.naturalWidth; + const ih = img.naturalHeight; + const diam = sr * 2.4; + const sc = Math.min(diam / iw, diam / ih); + const dw = iw * sc; + const dh = ih * sc; + ctx.drawImage(img, sx - dw * 0.5, sy - dh * 0.5, dw, dh); + } + } + + for (let i = 0; i < spaceShooterBullets.length; i++) { + const b = spaceShooterBullets[i]; + for (let k = 0; k < 4; k++) { + const t = k / 4; + const yy = b.y + t * 22; + const [bx, by] = worldToScreen(b.x, yy); + ctx.fillStyle = `rgba(255, ${180 - k * 35}, ${80 - k * 15}, ${0.95 - k * 0.18})`; + ctx.beginPath(); + ctx.arc(bx, by, Math.max(2, 3.2 * zDraw - k * 0.4), 0, Math.PI * 2); + ctx.fill(); + } + } + + const shipBody = spaceShooterShipBodyScreenPxPlay(zDraw); + const bodyW = shipBody.bodyW; + const bodyH = shipBody.bodyH; + refs.forEach((entry, idx) => { + const ent = entry.ref; + if (ent.spaceShooterCx == null || ent.spaceShooterCy == null) return; + const [sx, sy] = worldToScreen(ent.spaceShooterCx, ent.spaceShooterCy); + const col = SPACE_SHOOTER_SHIP_COLORS[idx % SPACE_SHOOTER_SHIP_COLORS.length]; + const elimShip = !!(isSpaceShooterMissionUiMapPlay() && ent.spaceShooterEliminated); + if (elimShip) return; + const slot = Math.max(1, Math.min(6, Number(ent.spaceShooterSlot) || ((idx % 6) + 1))); + const shipUrl = (playSpaceShooterShipImageUrls[slot - 1] || '').trim(); + const shipRec = shipUrl ? ensureGauntletAssetImage(shipUrl) : null; + const shipImg = shipRec && shipRec.ready && shipRec.img && shipRec.img.naturalWidth > 0 ? shipRec.img : null; + let shipDrawW = bodyW * 2.2; + let shipDrawH = bodyH * 2.2; + if (shipImg) { + ctx.save(); + ctx.translate(sx, sy); + if (elimShip) ctx.globalAlpha = 0.38; + const iw = shipImg.naturalWidth; + const ih = shipImg.naturalHeight; + const maxW = bodyW * 2.4; + const maxH = bodyH * 2.4; + const sc = Math.min(maxW / iw, maxH / ih); + const dw = iw * sc; + const dh = ih * sc; + shipDrawW = dw; + shipDrawH = dh; + ctx.drawImage(shipImg, -dw * 0.5, -dh * 0.5, dw, dh); + ctx.restore(); + } else { + ctx.save(); + ctx.translate(sx, sy); + if (elimShip) ctx.globalAlpha = 0.38; + ctx.fillStyle = col; + ctx.strokeStyle = 'rgba(180, 255, 255, 0.85)'; + ctx.lineWidth = Math.max(1.2, zDraw); + ctx.beginPath(); + ctx.moveTo(0, -bodyH * 0.55); + ctx.lineTo(-bodyW * 0.55, bodyH * 0.42); + ctx.lineTo(0, bodyH * 0.22); + ctx.lineTo(bodyW * 0.55, bodyH * 0.42); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = 'rgba(255, 220, 120, 0.95)'; + ctx.beginPath(); + ctx.moveTo(-bodyW * 0.22, bodyH * 0.38); + ctx.lineTo(0, bodyH * 0.72); + ctx.lineTo(bodyW * 0.22, bodyH * 0.38); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + if (isSpaceShooterMissionUiMapPlay()) { + const hits = Math.max(0, Math.min(3, Number(ent.spaceShooterHits) || 0)); + if (hits > 0 && playSpaceShooterShipDamageOverlayUrls && playSpaceShooterShipDamageOverlayUrls.length) { + const oUrl = (playSpaceShooterShipDamageOverlayUrls[hits - 1] || '').trim(); + if (oUrl) { + const oRec = ensureGauntletAssetImage(oUrl); + const oImg = oRec && oRec.ready && oRec.img && oRec.img.naturalWidth > 0 ? oRec.img : null; + if (oImg) { + ctx.save(); + if (elimShip) ctx.globalAlpha = 0.38; + const ow = oImg.naturalWidth; + const oh = oImg.naturalHeight; + const scO = Math.min(shipDrawW / ow, shipDrawH / oh); + const odw = ow * scO; + const odh = oh * scO; + ctx.drawImage(oImg, sx - odw * 0.5, sy - odh * 0.5, odw, odh); + ctx.restore(); + } + } + } + } + const name = (ent.nickname || '').slice(0, 10); + if (name) { + ctx.font = `${Math.max(9, 10 * zDraw)}px system-ui, "Kanit", sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(224, 242, 255, 0.92)'; + ctx.strokeStyle = 'rgba(0, 20, 40, 0.85)'; + ctx.lineWidth = 3; + ctx.strokeText(name, sx, sy - bodyH * 0.62); + ctx.fillText(name, sx, sy - bodyH * 0.62); + ctx.textAlign = 'left'; + } + }); + + for (let i = 0; i < spaceShooterPopups.length; i++) { + const p = spaceShooterPopups[i]; + if (wallNow >= p.until) continue; + const [px, py] = worldToScreen(p.x, p.y); + const dur = 700; + const age = 1 - Math.max(0, Math.min(1, (p.until - wallNow) / dur)); + ctx.save(); + ctx.globalAlpha = Math.max(0, 1 - age * 0.92); + ctx.font = `bold ${Math.max(12, 16 * zDraw)}px Orbitron, ui-sans-serif, sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = '#ffe066'; + ctx.strokeStyle = 'rgba(120, 60, 0, 0.6)'; + ctx.lineWidth = 3; + ctx.strokeText(p.text || '+5', px, py - age * 18); + ctx.fillText(p.text || '+5', px, py - age * 18); + ctx.restore(); + } + } + + const BALLOON_BOSS_PLAYER_COLORS = ['#ff79c6', '#7aa2f7', '#f9e2af', '#f7768e', '#9ece6a', '#bb9af7']; + + function normalizeBalloonBossPlayerSlotsInPlay(md) { + if (!md || md.gameType !== 'balloon_boss') return; + const w = md.width || 20, h = md.height || 15; + const src = md.balloonBossPlayerSlots || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= 6 ? Math.floor(n) : 0); + } + rows.push(row); + } + md.balloonBossPlayerSlots = rows; + } + + function getBalloonBossPlayerSpawnWorldCenterFromMap(md, slot1to6, ts) { + if (!md || !md.balloonBossPlayerSlots) return null; + const w = md.width || 20, h = md.height || 15; + const g = md.balloonBossPlayerSlots; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (g[y] && g[y][x] === slot1to6) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; + } + } + return null; + } + + function getBalloonBossBossWorldCenterPlay(md, ts) { + const bx = md && md.balloonBossBossSpawn; + if (bx && Number.isFinite(Number(bx.x)) && Number.isFinite(Number(bx.y))) { + return { cx: (Number(bx.x) + 0.5) * ts, cy: (Number(bx.y) + 0.5) * ts }; + } + const w = md.width || 20, h = md.height || 15; + return { cx: (w * 0.5) * ts, cy: (h * 0.38) * ts }; + } + + /** + * จุดกลางบอสระหว่างเล่น — ลอยแบบลูกโป่งจากผลรวม sine (deterministic ต่อเวลาในรอบ) + * เพื่อให้ทุก client ได้ตำแหน่งใกล้เคียงกัน (ไม่ใช้ state สุ่มต่อเครื่อง) + */ + function getBalloonBossBossLiveCenterPlay(md, ts) { + if (!md || md.gameType !== 'balloon_boss') return getBalloonBossBossWorldCenterPlay(md, ts); + const base = getBalloonBossBossWorldCenterPlay(md, ts); + const w = md.width || 20, h = md.height || 15; + const mw = w * ts, mh = h * ts; + const tSec = balloonBossSessionStartMs > 0 + ? (performance.now() - balloonBossSessionStartMs) / 1000 + : performance.now() / 1000; + const ax = Math.min(mw * 0.19, 240); + const ay = Math.min(mh * 0.17, 200); + const cx = base.cx + Math.sin(tSec * 0.33 + 0.75) * ax + Math.sin(tSec * 0.69 + 0.15) * (ax * 0.4); + const cy = base.cy + Math.cos(tSec * 0.29 + 1.05) * ay + Math.cos(tSec * 0.64 + 0.45) * (ay * 0.37); + return clampBalloonBossWorld(cx, cy, mw, mh); + } + + /** จุดโลกประมาณกลางกระจุกลูกโป่ง (ใช้ hit จากกระสุนบอส) — ไม่ใช่จุดเท้า */ + function balloonBossBalloonClusterWorldPlay(ent, bossCx, bossCy) { + if (!ent || ent.balloonBossCx == null) return { cx: 0, cy: 0 }; + const dx = ent.balloonBossCx - bossCx; + const dy = ent.balloonBossCy - bossCy; + const L = Math.hypot(dx, dy) || 1; + const ox = (dx / L) * 22; + const oy = (dy / L) * 12; + return { + cx: ent.balloonBossCx + ox, + cy: ent.balloonBossCy - 52 + oy * 0.35, + }; + } + + /** ผู้เล่นหลักเด้งกับวงชนบอส (บอสเป็นวงนิ่งใน world) */ + function balloonBossPlayerBossElasticPlay(meEnt, bossCx, bossCy, bossCollideR, mw, mh) { + if (!meEnt || meEnt.balloonBossEliminated || meEnt.balloonBossCx == null) return; + const rMe = 44; + const rB = Math.max(30, bossCollideR * 0.9); + const dx = meEnt.balloonBossCx - bossCx; + const dy = meEnt.balloonBossCy - bossCy; + const dist = Math.hypot(dx, dy) || 1e-6; + const minD = rMe + rB; + if (dist >= minD) return; + const nx = dx / dist; + const ny = dy / dist; + const push = (minD - dist) * 0.58; + meEnt.balloonBossCx += nx * push; + meEnt.balloonBossCy += ny * push; + const vn = meEnt.balloonBossVelX * nx + meEnt.balloonBossVelY * ny; + if (vn < 0) { + meEnt.balloonBossVelX -= 1.85 * vn * nx; + meEnt.balloonBossVelY -= 1.85 * vn * ny; + } + const cl = clampBalloonBossWorld(meEnt.balloonBossCx, meEnt.balloonBossCy, mw, mh); + meEnt.balloonBossCx = cl.cx; + meEnt.balloonBossCy = cl.cy; + } + + /** ผู้เล่นหลักเด้งกับคนอื่น (บอทขยับรับแรงปะทะเล็กน้อย) */ + function balloonBossPlayerElasticCollisionsPlay(meEnt, aliveRefs, mw, mh) { + if (!meEnt || meEnt.balloonBossEliminated || meEnt.balloonBossCx == null) return; + const rMe = 44; + const rO = 44; + const minD = rMe + rO - 2; + for (let ii = 0; ii < aliveRefs.length; ii++) { + const entry = aliveRefs[ii]; + if (!entry || entry.ref === meEnt) continue; + const o = entry.ref; + if (!o || o.balloonBossEliminated || o.balloonBossCx == null) continue; + const dx = meEnt.balloonBossCx - o.balloonBossCx; + const dy = meEnt.balloonBossCy - o.balloonBossCy; + const dist = Math.hypot(dx, dy); + if (!dist || dist >= minD) continue; + const nx = dx / dist; + const ny = dy / dist; + const push = (minD - dist) * 0.52; + meEnt.balloonBossCx += nx * push; + meEnt.balloonBossCy += ny * push; + const vn = meEnt.balloonBossVelX * nx + meEnt.balloonBossVelY * ny; + if (vn < 0) { + meEnt.balloonBossVelX -= 1.85 * vn * nx; + meEnt.balloonBossVelY -= 1.85 * vn * ny; + } + if (isPreviewBotId(entry.id)) { + const pushO = (minD - dist) * 0.48; + o.balloonBossCx -= nx * pushO; + o.balloonBossCy -= ny * pushO; + } + } + const cl2 = clampBalloonBossWorld(meEnt.balloonBossCx, meEnt.balloonBossCy, mw, mh); + meEnt.balloonBossCx = cl2.cx; + meEnt.balloonBossCy = cl2.cy; + } + + /** หมายเลขสล็อต 1–6 ที่มีบนแมป (เรียง) — ถ้ามีแค่ 3 ช่อง ผู้เล่นจะวนที่นั่ง 3 จุดรอบบอส · Occupied P-slots on map for spawn cycling */ + function getBalloonBossOccupiedSlotNumbersPlay(md) { + if (!md || !md.balloonBossPlayerSlots) return []; + const g = md.balloonBossPlayerSlots; + const w = md.width || 20, h = md.height || 15; + const seen = new Set(); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const v = g[y] && g[y][x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + if (Number.isFinite(n) && n >= 1 && n <= 6) seen.add(Math.floor(n)); + } + } + return Array.from(seen).sort((a, b) => a - b); + } + + function buildBalloonBossParticipantRefsPlay() { + const refs = []; + if (me) refs.push({ id: myId, ref: me }); + const humanIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + humanIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + botIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + return refs.filter((r) => r.ref); + } + + function balloonBossBalloonsStartPlay() { + const n = Number(mapData && mapData.balloonBossBalloonsPerPlayer); + /** ไม่มีค่าในแมป = 3 ลูก (Mega Virus mock) — ไม่ใช้ 5 เพราะบอท/preview ติด 5 */ + return Math.max(1, Math.min(12, Number.isFinite(n) ? Math.floor(n) : 3)); + } + + function balloonBossMaxHpPlay() { + const n = Number(mapData && mapData.balloonBossMaxHp); + return Math.max(20, Math.min(9999, Number.isFinite(n) ? Math.floor(n) : 100)); + } + + function balloonBossFireDelayMsPlay() { + const n = Number(mapData && mapData.balloonBossFireDelayMs); + return Math.max(120, Math.min(2000, Number.isFinite(n) ? Math.floor(n) : 380)); + } + + /** ความเร่งยาน (px/s²) — ต่ำ = คุมยาก ลื่น */ + function balloonBossShipAccelPlay() { + const n = Number(mapData && mapData.balloonBossShipAccel); + return Math.max(80, Math.min(520, Number.isFinite(n) ? n : 210)); + } + + /** ความเร็วสูงสุดยาน (px/s) */ + function balloonBossShipMaxSpeedPlay() { + const n = Number(mapData && mapData.balloonBossShipMaxSpeed); + return Math.max(60, Math.min(320, Number.isFinite(n) ? n : 158)); + } + + /** แรงหน่วงต่อวินาที (velocity *= 1 - min(1, drag*dt)) — สูง = หยุดเร็วขึ้น */ + function balloonBossShipDragPlay() { + const n = Number(mapData && mapData.balloonBossShipDrag); + return Math.max(0.4, Math.min(6, Number.isFinite(n) ? n : 1.65)); + } + + function balloonBossTimeLimitSecPlay() { + const t = Number(mapData && mapData.balloonBossTimeSec); + if (Number.isFinite(t) && t === 0) return 0; + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + const g = Number(playBalloonBossMissionTimeSec); + if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g); + return 120; + } + + function balloonBossRemainingSecPlay() { + const lim = balloonBossTimeLimitSecPlay(); + if (lim <= 0) return null; + if (isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay()) return lim; + if (!balloonBossSessionStartMs) return lim; + const elapsed = (performance.now() - balloonBossSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function balloonBossTeamDamagePlay() { + let s = Math.max(0, me.balloonBossScore | 0); + others.forEach((o) => { s += Math.max(0, o.balloonBossScore | 0); }); + return s; + } + + /** รวมดาเมจที่ทำกับบอส (ไม่เท่ากับคะแนน — คะแนน +10 ต่อครั้ง, HP บอส -2 ต่อครั้ง) */ + function balloonBossTeamBossDamagePlay() { + let s = Math.max(0, me.balloonBossBossDmg | 0); + others.forEach((o) => { s += Math.max(0, o.balloonBossBossDmg | 0); }); + return s; + } + + function syncBalloonBossFootFromCenter(ent) { + if (!ent || !mapData || ent.balloonBossCx == null || ent.balloonBossCy == null) return; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + ent.x = ent.balloonBossCx / ts - cw * 0.5; + ent.y = ent.balloonBossCy / ts - ch * 0.92; + } + + function clampBalloonBossWorld(cx, cy, mw, mh) { + const e = 22; + return { + cx: Math.max(e, Math.min(mw - e, cx)), + cy: Math.max(e, Math.min(mh - e, cy)), + }; + } + + function applyBalloonBossSpawnLayoutPlay() { + if (!mapData || mapData.gameType !== 'balloon_boss') return; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const { cw, ch } = getCharacterFootprintWH(mapData); + const mw = w * ts, mh = h * ts; + const refs = buildBalloonBossParticipantRefsPlay(); + const n = refs.length || 1; + const bStart = balloonBossBalloonsStartPlay(); + const slotCycle = getBalloonBossOccupiedSlotNumbersPlay(mapData); + refs.forEach((entry, idx) => { + const ent = entry.ref; + const slot = slotCycle.length ? slotCycle[idx % slotCycle.length] : ((idx % 6) + 1); + const cen = getBalloonBossPlayerSpawnWorldCenterFromMap(mapData, slot, ts); + let cx, cy; + if (cen) { + cx = cen.cx; + cy = cen.cy; + } else { + const t = (idx + 1) / (n + 1); + cx = t * mw; + cy = Math.min(mh - ts * 1.2, (h - 1.2) * ts); + } + const cl = clampBalloonBossWorld(cx, cy, mw, mh); + ent.balloonBossSkinSlot = slot; + ent.balloonBossCx = cl.cx; + ent.balloonBossCy = cl.cy; + if (typeof ent.balloonBossScore !== 'number' || !Number.isFinite(ent.balloonBossScore)) ent.balloonBossScore = 0; + if (typeof ent.balloonBossBossDmg !== 'number' || !Number.isFinite(ent.balloonBossBossDmg)) ent.balloonBossBossDmg = 0; + /** จำนวนลูกโป่งตามแมปเสมอเมื่อจัดตำแหน่ง — ไม่ค้าง 5 จาก preview bot · Always sync start count */ + ent.balloonBossBalloons = bStart; + if (typeof ent.balloonBossHitIframe !== 'number') ent.balloonBossHitIframe = 0; + ent.balloonBossVelX = 0; + ent.balloonBossVelY = 0; + ent.x = ent.balloonBossCx / ts - cw * 0.5; + ent.y = ent.balloonBossCy / ts - ch * 0.92; + ent.tx = ent.x; + ent.ty = ent.y; + ent.direction = 'down'; + if (typeof ent.balloonBossAimRad !== 'number' || !Number.isFinite(ent.balloonBossAimRad)) { + ent.balloonBossAimRad = Math.random() * Math.PI * 2; + } + }); + } + + function endBalloonBossGame(reason) { + if (balloonBossGameEnded || !mapData || mapData.gameType !== 'balloon_boss') return; + balloonBossGameEnded = true; + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossScorePopups = []; + if (isMegaVirusMissionShellMapPlay()) { + applyMegaVirusMissionPanelImages(); + beginBalloonBossMegaVirusResultFlashSequenceThenGcm(balloonBossBuildMissionPayloadPlay(reason), reason); + return; + } + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) { + titleEl.textContent = reason === 'victory' ? 'ชนะบอส! · Boss defeated' : 'หมดเวลา · Time up'; + } + msgEl.textContent = reason === 'victory' + ? 'MEGA VIRUS ถูกกำจัดแล้ว — อันดับจากคะแนนการยิงโดน' + : 'หมดเวลา — อันดับจากคะแนนการยิงโดน'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ id: myId, nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', score: Math.max(0, me.balloonBossScore | 0) }); + } + others.forEach((o, id) => { + ranks.push({ id, nickname: (o && o.nickname) ? String(o.nickname).trim() : id, score: Math.max(0, o.balloonBossScore | 0) }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)} pts`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + } + + function stepBalloonBossPreviewBots(dt, mw, mh, bossCx, bossCy, ts) { + if (!playBotsEnabled() || !mapData || mapData.gameType !== 'balloon_boss') return; + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o || o.balloonBossEliminated || o.balloonBossCx == null) return; + const wob = Math.sin(performance.now() / 800 + bid.length) * 0.55; + const wob2 = Math.cos(performance.now() / 1100 + bid.charCodeAt(0)) * 0.45; + const spd = 48; + let nx = o.balloonBossCx + wob * spd * dt; + let ny = o.balloonBossCy + wob2 * spd * 0.72 * dt; + const cl = clampBalloonBossWorld(nx, ny, mw, mh); + o.balloonBossCx = cl.cx; + o.balloonBossCy = cl.cy; + syncBalloonBossFootFromCenter(o); + o.direction = 'down'; + o.tx = o.x; + o.ty = o.y; + if (typeof o.balloonBossBotNextFire === 'number') o.balloonBossBotNextFire -= dt; + else o.balloonBossBotNextFire = 0.8 + Math.random() * 0.9; + if (o.balloonBossBotNextFire <= 0) { + o.balloonBossBotNextFire = 0.9 + Math.random() * 1.1; + const delayMs = balloonBossFireDelayMsPlay(); + const dx = bossCx - o.balloonBossCx; + const dy = bossCy - o.balloonBossCy; + const L = Math.hypot(dx, dy) || 1; + const bspd = 500 + Math.random() * 90; + balloonBossPendingShots.push({ + releaseAt: performance.now() + delayMs, + sx: o.balloonBossCx, sy: o.balloonBossCy, + vx: (dx / L) * bspd, + vy: (dy / L) * bspd, + ownerId: bid, + }); + } + }); + } + + function balloonBossTickFrame() { + if (!mapData || mapData.gameType !== 'balloon_boss') return; + if (isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay()) return; + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const mw = w * ts, mh = h * ts; + const { cw, ch } = getCharacterFootprintWH(mapData); + others.forEach((o, id) => { + if (isPreviewBotId(id)) return; + if (!o || o.balloonBossEliminated) return; + if (o.tx != null) o.x += (o.tx - o.x) * 0.24; + if (o.ty != null) o.y += (o.ty - o.y) * 0.24; + const cx = (o.x + cw * 0.5) * ts; + const cy = (o.y + ch * 0.92) * ts; + const cl = clampBalloonBossWorld(cx, cy, mw, mh); + o.balloonBossCx = cl.cx; + o.balloonBossCy = cl.cy; + }); + const now = performance.now(); + const dt = Math.min(0.055, balloonBossLastTickMs ? (now - balloonBossLastTickMs) / 1000 : 0.016); + balloonBossLastTickMs = now; + const bossC = getBalloonBossBossLiveCenterPlay(mapData, ts); + const bossR = Math.max(36, ts * 1.15); + const maxHp = balloonBossMaxHpPlay(); + const teamBossDmg = balloonBossTeamBossDamagePlay(); + + if (me.balloonBossEliminated) { + me.balloonBossVelX = 0; + me.balloonBossVelY = 0; + } + if (balloonBossGameEnded) { + for (let hi = balloonBossHitFx.length - 1; hi >= 0; hi--) { + balloonBossHitFx[hi].t -= dt; + if (balloonBossHitFx[hi].t <= 0) balloonBossHitFx.splice(hi, 1); + } + for (let si = balloonBossScorePopups.length - 1; si >= 0; si--) { + const sp = balloonBossScorePopups[si]; + sp.t -= dt; + if (sp.t <= 0) balloonBossScorePopups.splice(si, 1); + } + return; + } + const remSec = balloonBossRemainingSecPlay(); + if (remSec != null && remSec <= 0) { + endBalloonBossGame('time'); + return; + } + if (teamBossDmg >= maxHp) { + endBalloonBossGame('victory'); + return; + } + const allRefsEarly = buildBalloonBossParticipantRefsPlay(); + if (allRefsEarly.length > 0 && allRefsEarly.every((e) => e.ref && e.ref.balloonBossEliminated)) { + endBalloonBossGame('all_dead'); + return; + } + + for (let hi = balloonBossHitFx.length - 1; hi >= 0; hi--) { + balloonBossHitFx[hi].t -= dt; + if (balloonBossHitFx[hi].t <= 0) balloonBossHitFx.splice(hi, 1); + } + for (let si = balloonBossScorePopups.length - 1; si >= 0; si--) { + const sp = balloonBossScorePopups[si]; + sp.t -= dt; + if (sp.t <= 0) balloonBossScorePopups.splice(si, 1); + } + + const aliveRefs = allRefsEarly.filter((e) => e.ref && !e.ref.balloonBossEliminated); + + for (let pi = balloonBossPendingShots.length - 1; pi >= 0; pi--) { + const pnd = balloonBossPendingShots[pi]; + if (now >= pnd.releaseAt) { + balloonBossPlayerBullets.push({ x: pnd.sx, y: pnd.sy, vx: pnd.vx, vy: pnd.vy, ownerId: pnd.ownerId }); + balloonBossPendingShots.splice(pi, 1); + } + } + + const bossFireEvery = 1.42; + balloonBossBossFireAcc += dt; + while (balloonBossBossFireAcc >= bossFireEvery) { + balloonBossBossFireAcc -= bossFireEvery; + if (aliveRefs.length) { + const ang = Math.random() * Math.PI * 2; + const spd = 88 + Math.random() * 72; + balloonBossBossBullets.push({ + x: bossC.cx, y: bossC.cy, + vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd, + gy: 48 + Math.random() * 56, + }); + } + } + + function applyBalloonBossHitToEntity(ent) { + if (!ent || ent.balloonBossEliminated) return; + if (ent.balloonBossHitIframe > now) return; + ent.balloonBossHitIframe = now + 520; + ent.balloonBossBalloons = Math.max(0, (ent.balloonBossBalloons | 0) - 1); + if (ent.balloonBossBalloons <= 0) { + ent.balloonBossEliminated = true; + ent.balloonBossBalloons = 0; + } + if (ent === me && socket) { + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + balloonBossBalloons: me.balloonBossBalloons, + balloonBossEliminated: !!me.balloonBossEliminated, + balloonBossScore: Math.max(0, me.balloonBossScore | 0), + balloonBossBossDmg: Math.max(0, me.balloonBossBossDmg | 0), + }); + } + } + + for (let bi = balloonBossBossBullets.length - 1; bi >= 0; bi--) { + const b = balloonBossBossBullets[bi]; + const gy = typeof b.gy === 'number' && Number.isFinite(b.gy) ? b.gy : 62; + b.vy += gy * dt; + b.x += b.vx * dt; + b.y += b.vy * dt; + if (b.x < -80 || b.x > mw + 80 || b.y < -80 || b.y > mh + 80) { + balloonBossBossBullets.splice(bi, 1); + continue; + } + let hit = false; + aliveRefs.forEach((entry) => { + if (hit) return; + const ent = entry.ref; + if (!ent || ent.balloonBossCx == null) return; + const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy); + const dx = b.x - cc.cx, dy = b.y - cc.cy; + if (dx * dx + dy * dy <= (36 * 36)) { + hit = true; + applyBalloonBossHitToEntity(ent); + } + }); + if (hit) balloonBossBossBullets.splice(bi, 1); + } + + for (let i = balloonBossPlayerBullets.length - 1; i >= 0; i--) { + const b = balloonBossPlayerBullets[i]; + b.x += b.vx * dt; + b.y += b.vy * dt; + if (b.x < -60 || b.x > mw + 60 || b.y < -60 || b.y > mh + 60) { + balloonBossPlayerBullets.splice(i, 1); + continue; + } + let pHit = false; + for (let ji = 0; ji < aliveRefs.length; ji++) { + if (pHit) break; + const entry = aliveRefs[ji]; + const ent = entry.ref; + if (!ent || ent.balloonBossCx == null) continue; + if (String(entry.id) === String(b.ownerId)) continue; + const canBalloonHit = isPreviewBotId(entry.id) || ent === me; + if (!canBalloonHit) continue; + const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy); + const pdx = b.x - cc.cx, pdy = b.y - cc.cy; + if (pdx * pdx + pdy * pdy <= (34 * 34)) { + applyBalloonBossHitToEntity(ent); + balloonBossPlayerBullets.splice(i, 1); + pHit = true; + } + } + if (pHit) continue; + const dx = b.x - bossC.cx, dy = b.y - bossC.cy; + if (dx * dx + dy * dy <= bossR * bossR) { + balloonBossHitFx.push({ x: bossC.cx, y: bossC.cy, t: 0.22 }); + balloonBossScorePopups.push({ + ownerId: b.ownerId, + x: b.x, + y: b.y, + t: 0.85, + tMax: 0.85, + }); + const addScore = 10; + const addBossDmg = 2; + if (b.ownerId === myId) { + me.balloonBossScore = Math.max(0, (me.balloonBossScore | 0) + addScore); + me.balloonBossBossDmg = Math.max(0, (me.balloonBossBossDmg | 0) + addBossDmg); + if (socket && myId != null) { + balloonBossLastMoveEmit = Date.now(); + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + balloonBossScore: Math.max(0, me.balloonBossScore | 0), + balloonBossBossDmg: Math.max(0, me.balloonBossBossDmg | 0), + balloonBossBalloons: Math.max(0, me.balloonBossBalloons | 0), + balloonBossEliminated: !!me.balloonBossEliminated, + }); + } + } else { + const o = others.get(b.ownerId); + if (o) { + o.balloonBossScore = Math.max(0, (o.balloonBossScore | 0) + addScore); + o.balloonBossBossDmg = Math.max(0, (o.balloonBossBossDmg | 0) + addBossDmg); + } + } + balloonBossPlayerBullets.splice(i, 1); + } + } + + const tWind = performance.now() * 0.001; + const gustAx = Math.sin(tWind * 0.36 + 1.15) * 62 + Math.sin(tWind * 1.05) * 24; + const gustAy = Math.cos(tWind * 0.39 + 0.28) * 58 + Math.cos(tWind * 0.93) * 22; + const maxSpd = Math.min(228, balloonBossShipMaxSpeedPlay() * 1.32); + const dragPerSec = balloonBossShipDragPlay() * 0.4; + if (typeof me.balloonBossVelX !== 'number' || !Number.isFinite(me.balloonBossVelX)) me.balloonBossVelX = 0; + if (typeof me.balloonBossVelY !== 'number' || !Number.isFinite(me.balloonBossVelY)) me.balloonBossVelY = 0; + let nvx = me.balloonBossVelX; + let nvy = me.balloonBossVelY; + nvx += gustAx * dt; + nvy += gustAy * dt; + const drag = Math.min(1, dragPerSec * dt); + nvx *= (1 - drag); + nvy *= (1 - drag); + const curSpd = Math.sqrt(nvx * nvx + nvy * nvy); + if (curSpd > maxSpd) { + const s = maxSpd / curSpd; + nvx *= s; + nvy *= s; + } + me.balloonBossVelX = nvx; + me.balloonBossVelY = nvy; + if (!me.balloonBossEliminated && me.balloonBossCx != null) { + const ncx = me.balloonBossCx + nvx * dt; + const ncy = me.balloonBossCy + nvy * dt; + const cl = clampBalloonBossWorld(ncx, ncy, mw, mh); + if (Math.abs(cl.cx - ncx) > 0.5) me.balloonBossVelX *= -0.88; + if (Math.abs(cl.cy - ncy) > 0.5) me.balloonBossVelY *= -0.88; + me.balloonBossCx = cl.cx; + me.balloonBossCy = cl.cy; + } + syncBalloonBossFootFromCenter(me); + balloonBossPlayerElasticCollisionsPlay(me, aliveRefs, mw, mh); + balloonBossPlayerBossElasticPlay(me, bossC.cx, bossC.cy, bossR, mw, mh); + syncBalloonBossFootFromCenter(me); + /** ฉากนี้แสดงตัวหันหน้าเข้ากล้อง — sync กับ combat draw */ + me.direction = 'down'; + me.isWalking = Math.abs(me.balloonBossVelX) > 12 || Math.abs(me.balloonBossVelY) > 12; + + /** วงลูกศรหมุนเองเรื่อย ๆ (คุมไม่ได้) — Mega Virus ยิงตามมุมลูกศร ณ ตอนกด Space */ + aliveRefs.forEach((e) => { + const r = e.ref; + if (!r || r.balloonBossEliminated) return; + if (typeof r.balloonBossAimRad !== 'number' || !Number.isFinite(r.balloonBossAimRad)) { + r.balloonBossAimRad = Math.random() * Math.PI * 2; + } + r.balloonBossAimRad += 1.02 * dt; + }); + + balloonBossPlayerFireCd -= dt; + const wantFire = !isChatFocused() && !!keys['Space'] && !me.balloonBossEliminated; + if (wantFire && balloonBossPlayerFireCd <= 0 && me.balloonBossCy != null) { + balloonBossPlayerFireCd = 0.35; + const delayMs = balloonBossFireDelayMsPlay(); + const bspd = 480; + let vx; + let vy; + if (isMegaVirusMissionShellMapPlay()) { + const aim = (typeof me.balloonBossAimRad === 'number' && Number.isFinite(me.balloonBossAimRad)) + ? me.balloonBossAimRad + : Math.atan2(bossC.cy - me.balloonBossCy, bossC.cx - me.balloonBossCx); + vx = Math.cos(aim) * bspd; + vy = Math.sin(aim) * bspd; + } else { + const dx = bossC.cx - me.balloonBossCx; + const dy = bossC.cy - me.balloonBossCy; + const L = Math.hypot(dx, dy) || 1; + vx = (dx / L) * bspd; + vy = (dy / L) * bspd; + } + balloonBossPendingShots.push({ + releaseAt: now + delayMs, + sx: me.balloonBossCx, sy: me.balloonBossCy, + vx, + vy, + ownerId: myId, + }); + } + + stepBalloonBossPreviewBots(dt, mw, mh, bossC.cx, bossC.cy, ts); + + const tEmit = Date.now(); + if (tEmit - balloonBossLastMoveEmit > 95 && socket && myId != null) { + balloonBossLastMoveEmit = tEmit; + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + balloonBossScore: Math.max(0, me.balloonBossScore | 0), + balloonBossBossDmg: Math.max(0, me.balloonBossBossDmg | 0), + balloonBossBalloons: Math.max(0, me.balloonBossBalloons | 0), + balloonBossEliminated: !!me.balloonBossEliminated, + }); + } + } + + function drawBalloonBossCombatLayer(ctx, worldToScreen, zDraw, timeMs) { + if (!mapData || !isBalloonBoss()) return; + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const mw = w * ts, mh = h * ts; + const bossC = getBalloonBossBossLiveCenterPlay(mapData, ts); + const maxHp = balloonBossMaxHpPlay(); + const dmg = balloonBossTeamBossDamagePlay(); + const hpLeft = Math.max(0, maxHp - dmg); + const bossR = Math.max(36, ts * 1.15); + const refs = buildBalloonBossParticipantRefsPlay(); + const leaderId = (() => { + let best = null, bs = -1; + refs.forEach((e) => { + if (!e.ref || e.ref.balloonBossEliminated) return; + const sc = Math.max(0, e.ref.balloonBossScore | 0); + if (sc > bs) { bs = sc; best = e.id; } + }); + return best; + })(); + const bossImgUrl = normalizeGauntletAssetUrlForPlay(String(playBalloonBossBossImageUrl || '')); + const bossImgRec = bossImgUrl ? ensureGauntletAssetImage(bossImgUrl) : null; + + for (let hi = 0; hi < balloonBossHitFx.length; hi++) { + const fx = balloonBossHitFx[hi]; + const [hx, hy] = worldToScreen(fx.x, fx.y); + const pulse = 1 + (fx.t || 0) * 3; + ctx.save(); + ctx.globalAlpha = Math.min(1, (fx.t || 0) * 4); + ctx.strokeStyle = 'rgba(255, 220, 120, 0.9)'; + ctx.lineWidth = 3 * zDraw; + ctx.beginPath(); + ctx.arc(hx, hy, bossR * zDraw * 0.5 * pulse, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + } + + const score10PopUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/score+10.png'); + const score10PopRec = score10PopUrl ? ensureGauntletAssetImage(score10PopUrl) : null; + for (let si = 0; si < balloonBossScorePopups.length; si++) { + const sp = balloonBossScorePopups[si]; + let wx = sp.x; + let wy = sp.y; + if (sp.ownerId != null) { + const oid = sp.ownerId; + const ent = (myId != null && String(oid) === String(myId)) ? me : others.get(oid); + if (ent && ent.balloonBossCx != null && !ent.balloonBossEliminated) { + const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy); + const tMax = typeof sp.tMax === 'number' && sp.tMax > 0 ? sp.tMax : 0.95; + const rise = ((tMax - Math.max(0, sp.t || 0)) / tMax) * 58; + wx = cc.cx; + wy = cc.cy - 46 - rise; + } + } + const [sx, sy] = worldToScreen(wx, wy); + ctx.save(); + ctx.globalAlpha = Math.min(1, Math.max(0, sp.t || 0) * 1.12); + if (sp.dmgTxt) { + const fpx = Math.max(11, 14 * zDraw); + ctx.font = `bold ${fpx}px ui-sans-serif, system-ui, sans-serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.strokeStyle = 'rgba(0, 0, 0, 0.62)'; + ctx.lineWidth = Math.max(2, 2.4 * zDraw); + ctx.strokeText(String(sp.dmgTxt), sx, sy); + ctx.fillStyle = '#f8fafc'; + ctx.fillText(String(sp.dmgTxt), sx, sy); + } else if (score10PopRec && score10PopRec.ready && score10PopRec.img && score10PopRec.img.naturalWidth > 0) { + const iw = score10PopRec.img.naturalWidth; + const ih = score10PopRec.img.naturalHeight; + const mh = Math.max(14, 20 * zDraw); + const sc = Math.min(1.35, mh / ih); + const dw = iw * sc; + const dh = ih * sc; + ctx.drawImage(score10PopRec.img, sx - dw / 2, sy - dh / 2, dw, dh); + } + ctx.restore(); + } + + const bossAlive = hpLeft > 0; + if (bossAlive) { + const [bsx, bsy] = worldToScreen(bossC.cx, bossC.cy); + ctx.save(); + ctx.translate(bsx, bsy); + const limR = bossR * zDraw; + if (bossImgRec && bossImgRec.ready && bossImgRec.img && bossImgRec.img.naturalWidth > 0) { + const iw = bossImgRec.img.naturalWidth; + const ih = bossImgRec.img.naturalHeight; + const diam = limR * 2.1; + const scale = Math.min(diam / iw, diam / ih); + const dw = iw * scale; + const dh = ih * scale; + ctx.save(); + ctx.beginPath(); + ctx.arc(0, 0, limR * 1.05, 0, Math.PI * 2); + ctx.clip(); + ctx.drawImage(bossImgRec.img, -dw / 2, -dh / 2, dw, dh); + ctx.restore(); + } else { + ctx.beginPath(); + for (let k = 0; k < 6; k++) { + const ang = (k / 6) * Math.PI * 2 - Math.PI / 2; + const px = Math.cos(ang) * bossR * zDraw * 1.05; + const py = Math.sin(ang) * bossR * zDraw * 1.05; + if (k === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + } + ctx.closePath(); + ctx.fillStyle = 'rgba(0, 40, 60, 0.35)'; + ctx.fill(); + + const skullR = bossR * zDraw * 0.62; + const grd = ctx.createRadialGradient(-skullR * 0.2, -skullR * 0.25, skullR * 0.1, 0, 0, skullR); + grd.addColorStop(0, 'rgba(55, 50, 62, 0.98)'); + grd.addColorStop(1, 'rgba(22, 18, 30, 1)'); + ctx.fillStyle = grd; + ctx.beginPath(); + ctx.arc(0, 0, skullR, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = 'rgba(255, 40, 60, 0.95)'; + ctx.shadowColor = 'rgba(255, 0, 0, 0.8)'; + ctx.shadowBlur = 12 * zDraw; + ctx.beginPath(); + ctx.arc(-skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2); + ctx.arc(skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + } + + /** HUD บอส: แถบชิดเหนือหัวบอส; โลโก้ Artboard10 + [hp/max] เหนือแถบ — bbHudS ลดสเกลทั้งแถว (ขอให้เล็กลง ~ครึ่ง) */ + const bbHudS = 0.5; + const fontPx = Math.max(8, 12.5 * zDraw * bbHudS); + ctx.font = `bold ${fontPx}px ui-monospace, monospace`; + ctx.textBaseline = 'middle'; + const hpStr = '[ ' + hpLeft + ' / ' + maxHp + ' ]'; + const hpW = ctx.measureText(hpStr).width; + const gapHp = 5 * zDraw * bbHudS; + + const powBgUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/boss-power-bg.png'); + const powFillUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/boss-power.png'); + const powBgRec = powBgUrl ? ensureGauntletAssetImage(powBgUrl) : null; + const powFillRec = powFillUrl ? ensureGauntletAssetImage(powFillUrl) : null; + + /** ความสูงหลอด: ยึดความกว้างก่อน — คูณ bbHudS ให้สัดส่วนเทียบหัวบอสเล็กลง */ + const barHMin = Math.max(9, 10.5 * zDraw) * bbHudS; + const barHMax = Math.max(17, 21 * zDraw) * bbHudS; + + const titleImgUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/Artboard%2010.png'); + const titleImgRec = titleImgUrl ? ensureGauntletAssetImage(titleImgUrl) : null; + let imgW = 0; + let imgH = 0; + if (titleImgRec && titleImgRec.ready && titleImgRec.img && titleImgRec.img.naturalWidth > 0) { + const iw = titleImgRec.img.naturalWidth; + const ih = titleImgRec.img.naturalHeight; + let barHForTitle = barHMin; + if (powBgRec && powBgRec.ready && powBgRec.img && powBgRec.img.naturalWidth > 0) { + const biw = powBgRec.img.naturalWidth; + const bijh = powBgRec.img.naturalHeight; + const wEst = Math.max(limR * 2.12 * bbHudS, hpW + gapHp + iw * 0.35 * bbHudS); + barHForTitle = Math.min(barHMax, Math.max(barHMin, bijh * (wEst / biw))); + } + const capTitle = Math.min(17 * zDraw * bbHudS, Math.max(fontPx * 1.15, barHForTitle * 0.88)); + const maxH = Math.max(fontPx * 1.12, 14 * zDraw * bbHudS, capTitle); + const sc = Math.min(1.0, maxH / ih); + imgW = iw * sc; + imgH = ih * sc; + } + + let rowW = (imgW > 0 ? imgW + gapHp : 0) + hpW; + if (imgW <= 0) { + const legacyPre = 'MEGA VIRUS : '; + rowW = ctx.measureText(legacyPre).width + hpW; + } + const barPadRow = 8 * zDraw * bbHudS; + const barWTarget = Math.max(limR * 2.08 * bbHudS, rowW + barPadRow); + + let barDw = barWTarget; + let barDh = barHMin; + if (powBgRec && powBgRec.ready && powBgRec.img && powBgRec.img.naturalWidth > 0) { + const iw = powBgRec.img.naturalWidth; + const ih = powBgRec.img.naturalHeight; + const naturalH = ih * (barWTarget / iw); + barDh = Math.min(barHMax, Math.max(barHMin, naturalH)); + } + + const gapBarBoss = 4 * zDraw * bbHudS; + const gapTitleBar = 5 * zDraw * bbHudS; + const barBottomEdge = -limR * 1.0 - gapBarBoss; + const barTopY = barBottomEdge - barDh; + const bx = -barDw / 2; + const by = barTopY; + const ratHp = Math.max(0, Math.min(1, hpLeft / maxHp)); + + const titleRowH = Math.max(imgH, fontPx * 1.12); + const labelY = barTopY - gapTitleBar - titleRowH * 0.5; + + let xLeft = -(imgW + (imgW > 0 ? gapHp : 0) + hpW) / 2; + if (imgW > 0) { + ctx.drawImage(titleImgRec.img, xLeft, labelY - imgH / 2, imgW, imgH); + xLeft += imgW + gapHp; + } else { + ctx.textAlign = 'left'; + ctx.fillStyle = 'rgba(255, 100, 200, 0.9)'; + const legacyPre = 'MEGA VIRUS : '; + const pw = ctx.measureText(legacyPre).width; + xLeft = -(pw + hpW) / 2; + ctx.fillText(legacyPre, xLeft, labelY); + xLeft += pw; + } + ctx.textAlign = 'left'; + ctx.fillStyle = '#ffffff'; + ctx.strokeStyle = 'rgba(0, 0, 0, 0.22)'; + ctx.lineWidth = Math.max(0.5, 1 * zDraw * bbHudS); + ctx.strokeText(hpStr, xLeft, labelY); + ctx.fillText(hpStr, xLeft, labelY); + + if (powBgRec && powBgRec.ready && powBgRec.img && powBgRec.img.naturalWidth > 0) { + ctx.drawImage(powBgRec.img, bx, by, barDw, barDh); + if (ratHp > 0 && powFillRec && powFillRec.ready && powFillRec.img && powFillRec.img.naturalWidth > 0) { + const fiw = powFillRec.img.naturalWidth; + const fih = powFillRec.img.naturalHeight; + const padIn = barDw * 0.04; + const innerW = barDw - padIn * 2; + const innerH = barDh - padIn * 1.2; + const fsc2 = Math.min(innerW / fiw, innerH / fih); + const fdw2 = fiw * fsc2; + const fdh2 = fih * fsc2; + const fx0 = bx + (barDw - fdw2) * 0.5; + const fy0 = by + (barDh - fdh2) * 0.5; + const srcW = fiw * ratHp; + ctx.drawImage(powFillRec.img, 0, 0, srcW, fih, fx0, fy0, fdw2 * ratHp, fdh2); + } + } else { + ctx.fillStyle = 'rgba(0,0,0,0.55)'; + ctx.fillRect(bx, by, barDw, barDh); + ctx.fillStyle = 'rgba(255, 80, 200, 0.85)'; + ctx.fillRect(bx, by, barDw * ratHp, barDh); + ctx.strokeStyle = 'rgba(180, 255, 255, 0.5)'; + ctx.strokeRect(bx, by, barDw, barDh); + } + ctx.restore(); + } + + /** จุดก้นลูกโป่งหลังหมุน tilt (รัศมีจากกลางสู่ก้นใน local) */ + function balloonBossBalloonKnotScreen(bx, by, dw, dh, tiltRad) { + const lx = 0; + const ly = dh * 0.5; + const c = Math.cos(tiltRad); + const s = Math.sin(tiltRad); + return { x: bx + lx * c - ly * s, y: by + lx * s + ly * c }; + } + + /** ลูกโป่งด้านบน +50% — กระจุกเอียงซ้าย/กลางตรง/ขวา (tilt) + เชือกบางไปจุดผูบนวง · Bouquet tilt like mock */ + function drawBalloonsCluster(sx, sy, count, col, z, slot1to6, playerRingR) { + const slotIdx = Math.max(0, Math.min(5, (Math.floor(Number(slot1to6)) || 1) - 1)); + const perSeat = String((playBalloonBossPlayerBalloonImageUrls[slotIdx] || '')).trim(); + const fallback = String(playBalloonBossPlayerBalloonFallbackUrl || '').trim(); + const bRec = resolveBalloonBossBalloonSpriteRec(perSeat, fallback); + const n = Math.max(0, Math.min(8, count | 0)); + const ringRpx = playerRingR != null && playerRingR > 0 ? playerRingR : 22 * z; + const sz = 1.5; + const tw = 22 * sz * z; + const th = 27 * sz * z; + const stackAnchor = ringRpx + 10 * z; + const tieX = sx; + const tieY = sy - ringRpx * 0.98; + const tiltStep = 0.5; + const spreadX = 5.4 * z; + const baseY = sy - stackAnchor - 1.5 * z; + const items = []; + for (let b = 0; b < n; b++) { + const rel = b - (n - 1) / 2; + const tilt = rel * tiltStep; + const bx = sx + rel * spreadX; + const by = baseY + Math.abs(rel) * 2.1 * z; + const bImg = bRec && bRec.img; + let dw = 0; + let dh = 0; + if (bImg && bImg.complete && bImg.naturalWidth > 0) { + const iw = bImg.naturalWidth; + const ih = bImg.naturalHeight; + const sc = Math.min(tw / iw, th / ih); + dw = iw * sc; + dh = ih * sc; + } else { + const ry = 12 * sz * z; + dw = 10 * sz * z * 2; + dh = ry * 2; + } + const knot = balloonBossBalloonKnotScreen(bx, by, dw, dh, tilt); + items.push({ bx, by, dw, dh, bImg, tilt, knotX: knot.x, knotY: knot.y }); + } + if (n > 0) { + ctx.strokeStyle = 'rgba(55, 48, 42, 0.55)'; + ctx.lineWidth = Math.max(0.65, 0.78 * z); + ctx.lineCap = 'butt'; + ctx.lineJoin = 'bevel'; + for (let i = 0; i < items.length; i++) { + const it = items[i]; + ctx.beginPath(); + ctx.moveTo(tieX, tieY); + ctx.lineTo(it.knotX, it.knotY); + ctx.stroke(); + } + } + for (let i = 0; i < items.length; i++) { + const it = items[i]; + const bImg = it.bImg; + if (bImg && bImg.complete && bImg.naturalWidth > 0) { + const iw = bImg.naturalWidth; + const ih = bImg.naturalHeight; + const sc = Math.min(tw / iw, th / ih); + const dw = iw * sc; + const dh = ih * sc; + ctx.save(); + ctx.translate(it.bx, it.by); + ctx.rotate(it.tilt); + ctx.drawImage(bImg, -dw / 2, -dh / 2, dw, dh); + ctx.restore(); + } else { + ctx.save(); + ctx.translate(it.bx, it.by); + ctx.rotate(it.tilt); + ctx.fillStyle = col; + ctx.beginPath(); + ctx.ellipse(0, 0, 10 * sz * z, 12 * sz * z, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255,255,255,0.35)'; + ctx.lineWidth = 1; + ctx.stroke(); + ctx.restore(); + } + } + } + + refs.forEach((entry, idx) => { + const ent = entry.ref; + if (ent.balloonBossCx == null || ent.balloonBossCy == null) return; + if (ent.balloonBossEliminated) return; + const [sx, sy] = worldToScreen(ent.balloonBossCx, ent.balloonBossCy); + const col = BALLOON_BOSS_PLAYER_COLORS[idx % BALLOON_BOSS_PLAYER_COLORS.length]; + const z = zDraw; + const skinSlot = (typeof ent.balloonBossSkinSlot === 'number' && ent.balloonBossSkinSlot >= 1 && ent.balloonBossSkinSlot <= 6) + ? ent.balloonBossSkinSlot + : ((idx % 6) + 1); + const ringR = 25 * z; + /** จุดกลางฟองบนจอ — ยกจากจุด world เล็กน้อยให้ตัวอยู่กลางวง ลูกโป่งชิดขอบบน (ตาม mock) */ + const bubbleCy = sy - ringR * 0.42; + /** มุมลูกศร Artboard 9 — หมุนอัตโนมัติ; Mega Virus ยิงตามมุมนี้ ณ ตอนกด Space */ + const angRing = (typeof ent.balloonBossAimRad === 'number' && Number.isFinite(ent.balloonBossAimRad)) + ? ent.balloonBossAimRad + : ((timeMs * 0.0019 + idx * 1.73) % (Math.PI * 2)); + /** หันหน้าเข้ากล้อง (mock) — ไม่ใช้ทิศออกจากบอสในเลเยอร์นี้ */ + const faceDir = 'down'; + const slotIdxRing = Math.max(0, Math.min(5, skinSlot - 1)); + const perRing = String((playBalloonBossPlayerBalloonImageUrls[slotIdxRing] || '')).trim(); + const fbRing = String(playBalloonBossPlayerBalloonFallbackUrl || '').trim(); + /** กรอบฟองรอบตัว = fallback (Artboard 9) เท่านั้น — ลูกโป่งต่อที่นั่งใช้แค่กองด้านบน · Ring frame is never per-seat balloon art */ + const ringRec = fbRing + ? resolveBalloonBossBalloonSpriteRec('', fbRing) + : resolveBalloonBossBalloonSpriteRec(perRing, ''); + ctx.save(); + const ringImg = ringRec && ringRec.img; + const ringPainted = !!(ringImg && ringImg.complete && ringImg.naturalWidth > 0); + if (ringPainted) { + const iw = ringImg.naturalWidth; + const ih = ringImg.naturalHeight; + const maxBox = 2 * ringR * 1.14; + const sc = Math.min(maxBox / iw, maxBox / ih); + const dw = iw * sc; + const dh = ih * sc; + /** หางฟอง (Artboard +Y) หมุนตาม angRing — tail ชี้ทิศยิง */ + ctx.save(); + ctx.translate(sx, bubbleCy); + ctx.rotate(angRing - Math.PI / 2); + ctx.drawImage(ringImg, -dw / 2, -dh / 2, dw, dh); + ctx.restore(); + } + if (!ringPainted) { + ctx.strokeStyle = 'rgba(255, 255, 255, 0.72)'; + ctx.lineWidth = Math.max(1.2, 1.6 * z); + ctx.beginPath(); + ctx.arc(sx, bubbleCy, ringR, 0, Math.PI * 2); + ctx.stroke(); + } else { + ctx.strokeStyle = 'rgba(255, 255, 255, 0.22)'; + ctx.lineWidth = Math.max(1, 1.1 * z); + ctx.beginPath(); + ctx.arc(sx, bubbleCy, ringR, 0, Math.PI * 2); + ctx.stroke(); + } + drawBalloonsCluster(sx, bubbleCy, ent.balloonBossBalloons | 0, col, z, skinSlot, ringR); + const nm = String(ent.nickname || '').slice(0, 12); + ctx.font = `${Math.max(9, 10 * z)}px system-ui, "Kanit", sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(248, 250, 252, 0.95)'; + ctx.strokeStyle = 'rgba(0, 10, 30, 0.85)'; + ctx.lineWidth = 3; + let label = nm; + if (entry.id === leaderId && leaderId != null) label = '♔ ' + nm; + const labelY = bubbleCy + ringR + 8 * z; + ctx.strokeText(label, sx, labelY); + ctx.fillText(label, sx, labelY); + const charImg = resolvePlayDrawCharSource( + ent.characterId, faceDir, timeMs, !!ent.isWalking, + ent.playTint || resolvePlayTintForPeer(entry.id), + myId != null && entry.id != null && String(entry.id) === String(myId), + ); + const size = 26 * z; + if (charImg && ((charImg.tagName === 'CANVAS' && charImg.width > 0) || (charImg.complete && charImg.naturalWidth))) { + const iw = charImg.tagName === 'CANVAS' ? charImg.width : charImg.naturalWidth; + const ih = charImg.tagName === 'CANVAS' ? charImg.height : charImg.naturalHeight; + const sc = Math.min(size / iw, size / ih, 1); + const dw = iw * sc, dh = ih * sc; + const charTop = bubbleCy - dh * 0.48; + ctx.drawImage(charImg, 0, 0, iw, ih, sx - dw / 2, charTop, dw, dh); + } else { + ctx.fillStyle = col; + ctx.beginPath(); + ctx.arc(sx, bubbleCy, 9 * z, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 10, 30, 0.45)'; + ctx.lineWidth = Math.max(1, 1.2 * z); + ctx.stroke(); + } + ctx.restore(); + }); + + const bulletCodeUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/bullet-code.png'); + const bulletCodeRec = bulletCodeUrl ? ensureGauntletAssetImage(bulletCodeUrl) : null; + for (let i = 0; i < balloonBossPlayerBullets.length; i++) { + const b = balloonBossPlayerBullets[i]; + const [bx, by] = worldToScreen(b.x, b.y); + ctx.save(); + ctx.translate(bx, by); + /** bullet-code.png หัวลูกศรชี้ขวา (+x) — หมุนตามทิศความเร็วโดยไม่บวก π/2 (ก่อนหน้านี้ดูเหมือนยิงเฉียง) */ + ctx.rotate(Math.atan2(b.vy, b.vx)); + if (bulletCodeRec && bulletCodeRec.ready && bulletCodeRec.img && bulletCodeRec.img.naturalWidth > 0) { + const iw = bulletCodeRec.img.naturalWidth; + const ih = bulletCodeRec.img.naturalHeight; + const mh = Math.max(10, 15 * zDraw); + const sc = Math.min(1.4, mh / ih); + const dw = iw * sc; + const dh = ih * sc; + ctx.drawImage(bulletCodeRec.img, -dw / 2, -dh / 2, dw, dh); + } else { + ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.beginPath(); + ctx.moveTo(0, -8 * zDraw); + ctx.lineTo(-5 * zDraw, 6 * zDraw); + ctx.lineTo(5 * zDraw, 6 * zDraw); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = 'rgba(0, 255, 200, 0.5)'; + ctx.font = `${Math.max(7, 8 * zDraw)}px monospace`; + ctx.textAlign = 'center'; + ctx.fillText('01001', 0, 12 * zDraw); + } + ctx.restore(); + } + for (let i = 0; i < balloonBossBossBullets.length; i++) { + const b = balloonBossBossBullets[i]; + const [bx, by] = worldToScreen(b.x, b.y); + ctx.fillStyle = 'rgba(255, 60, 120, 0.92)'; + ctx.beginPath(); + ctx.arc(bx, by, Math.max(3, 4.5 * zDraw), 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 200, 255, 0.6)'; + ctx.stroke(); + } + } + + function jumpSurviveCollectPlatformCells(md) { + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + const out = []; + if (!pa) return out; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (pa[y] && pa[y][x] === 1) out.push({ x, y }); + } + } + return out; + } + + /** ความสูงแมปเป็นคาบวนของแพลตฟอร์ม — แถวที่เลื่อนออกล่างจะวนมาโผล่บน */ + function jumpSurvivePlatformPeriodPx(md) { + return (md.height || 15) * tileSize; + } + + /** + * ยืนบนหน้าแพลตฟอร์มช่อง (tx,ty) — เลือกชั้น k ที่ใกล้ hintY (กล้อง / กลางแมป) + */ + function jumpSurviveGridPosStandingOnPlatform(md, tx, ty, scrollPx) { + const ts = tileSize; + const h = md.height || 15; + const period = jumpSurvivePlatformPeriodPx(md); + const { cw, ch } = getCharacterFootprintWH(md); + const rawTop = ty * ts + scrollPx; + const hintY = (jumpSurviveCamCenterY > 1) + ? jumpSurviveCamCenterY + : h * ts * 0.72; + const k = Math.round((hintY - rawTop - ts * 0.35) / period); + const platTop = rawTop + k * period; + const feetY = platTop; + const pxc = (tx + 0.5) * ts; + return { + x: pxc / ts - cw * 0.5, + y: feetY / ts - ch, + }; + } + + function jumpSurviveGridPosFromTileCenter(md, tx, ty) { + return jumpSurviveGridPosStandingOnPlatform(md, tx, ty, 0); + } + + function jumpSurviveRandomStandGridPos(md) { + const cells = jumpSurviveCollectPlatformCells(md); + const sc = jumpSurvivePlatformScrollPx; + if (cells.length) { + const c = cells[Math.floor(Math.random() * cells.length)]; + return jumpSurviveGridPosStandingOnPlatform(md, c.x, c.y, sc); + } + const sp = md.spawn || { x: 1, y: 1 }; + return jumpSurviveGridPosStandingOnPlatform(md, Number(sp.x) || 1, Number(sp.y) || 1, sc); + } + + function jumpSurviveTileHasPlatformCellPlay(md, tx, ty) { + const pa = md && md.jumpSurvivePlatformArea; + const h = md.height || (pa && pa.length) || 15; + const w = md.width || 20; + if (!pa || ty < 0 || ty >= h) return false; + const row = pa[ty]; + if (!row || tx < 0 || tx >= w) return false; + return row[tx] === 1; + } + + /** หาแพลตฟอร์มในคอลัมน์ tx ใกล้ ty — กันจุดเกิดช่องว่างแล้วลอย */ + function jumpSurviveFindPlatformColumnPlay(md, tx, tyHint) { + const w = md.width || 20, h = md.height || 15; + const tcx = Math.max(0, Math.min(w - 1, Math.floor(tx))); + let ty = Math.max(0, Math.min(h - 1, Math.floor(tyHint))); + if (jumpSurviveTileHasPlatformCellPlay(md, tcx, ty)) return { x: tcx, y: ty }; + for (let d = 1; d < h; d++) { + const yb = ty + d; + if (yb < h && jumpSurviveTileHasPlatformCellPlay(md, tcx, yb)) return { x: tcx, y: yb }; + const ya = ty - d; + if (ya >= 0 && jumpSurviveTileHasPlatformCellPlay(md, tcx, ya)) return { x: tcx, y: ya }; + } + return { x: tcx, y: ty }; + } + + /** จุดเกิดตามลำดับ join / P1–P6 → world ยืนบนแพลตฟอร์ม (ไม่ทับสุ่ม pool) */ + function jumpSurviveSpawnWorldFromJoinOrderPlay(md, joinOrder) { + const sp = pickSpawnForJoinPlay(md, joinOrder | 0); + let tx = Math.floor(Number(sp.x)); + let ty = Math.floor(Number(sp.y)); + if (!Number.isFinite(tx) || !Number.isFinite(ty)) { + const fb = md.spawn || { x: 1, y: 1 }; + tx = Math.floor(Number(fb.x)) || 1; + ty = Math.floor(Number(fb.y)) || 1; + } + const cell = jumpSurviveFindPlatformColumnPlay(md, tx, ty); + const sc = jumpSurvivePlatformScrollPx; + const p = jumpSurviveGridPosStandingOnPlatform(md, cell.x, cell.y, sc); + return { + x: p.x + (Math.random() - 0.5) * 0.05, + y: p.y + (Math.random() - 0.5) * 0.05, + }; + } + + /** ทดสอบจากเอดิเตอร์: วางคน+บอทบนแพลตฟอร์ม (ไม่ให้เกิดบน spawnArea บนฟ้าแล้วลอย) */ + /** Quiz Battle พรีวิว: บอท spawn ตามโหมดแมป — ผู้เล่นจริงใช้ตำแหน่งจาก server แล้ว snap เท่าที่จำเป็น */ + function applyQuizBattlePreviewSpawnLayout(onlyBots) { + if (!mapData || mapData.gameType !== 'quiz_battle' || !quizBattlePathModeActive(mapData)) return; + + function stampQuizEnt(ent, joinOrd) { + if (!ent) return; + const jo = Number.isFinite(Number(joinOrd)) ? Math.max(0, Math.floor(Number(joinOrd))) : 0; + const pos = quizBattleSpawnWorldFromJoinOrderPlay(mapData, jo); + ent.x = pos.x; + ent.y = pos.y; + ent.tx = pos.x; + ent.ty = pos.y; + } + + if (!onlyBots) { + const sn = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = sn.x; + me.y = sn.y; + me.tx = sn.x; + me.ty = sn.y; + } + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + if (!onlyBots) { + realIds.forEach((rid) => { + const o = others.get(rid); + if (!o) return; + const jo = Number.isFinite(Number(o.spawnJoinOrder)) ? o.spawnJoinOrder : realIds.indexOf(rid); + stampQuizEnt(o, jo); + }); + } + botIds.forEach((bid, bi) => { + const o = others.get(bid); + if (!o) return; + const jo = Number.isFinite(Number(o.spawnJoinOrder)) ? o.spawnJoinOrder : countPlayHumans() + bi; + stampQuizEnt(o, jo); + }); + } + + function applyJumpSurvivePreviewSpawnLayout(onlyBots) { + if (!mapData || mapData.gameType !== 'jump_survive') return; + + function stampJumpBotState(o) { + if (!o) return; + o.jumpSurviveVy = 0; + o.jumpSurviveOnGround = true; + o.jumpSurviveEliminated = false; + o.tx = o.x; + o.ty = o.y; + } + + if (isJumpSurviveMissionUiMapPlay()) { + function snapEnt(ent) { + if (!ent) return; + const ordRaw = Number(ent.spawnJoinOrder); + const jo = Number.isFinite(ordRaw) ? Math.max(0, Math.floor(ordRaw)) : 0; + const pos = jumpSurviveSpawnWorldFromJoinOrderPlay(mapData, jo); + ent.x = pos.x; + ent.y = pos.y; + stampJumpBotState(ent); + } + if (!onlyBots) { + snapEnt(me); + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + realIds.forEach((rid) => snapEnt(others.get(rid))); + } + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + botIds.forEach((bid) => snapEnt(others.get(bid))); + jumpSurviveInitRuntime(); + return; + } + + const cells = jumpSurviveCollectPlatformCells(mapData); + cells.sort((a, b) => b.y - a.y || a.x - b.x); + const sc = jumpSurvivePlatformScrollPx; + const pool = cells.length + ? cells.map((c) => jumpSurviveGridPosStandingOnPlatform(mapData, c.x, c.y, sc)) + : [jumpSurviveGridPosStandingOnPlatform(mapData, Number(mapData.spawn && mapData.spawn.x) || 1, Number(mapData.spawn && mapData.spawn.y) || 1, sc)]; + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + + if (onlyBots) { + let idx = 1 + realIds.length; + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || !pool.length) return; + const p = pool[idx % pool.length]; + idx++; + o.x = p.x + (Math.random() - 0.5) * 0.08; + o.y = p.y + (Math.random() - 0.5) * 0.08; + stampJumpBotState(o); + }); + return; + } + + let idx = 0; + function assignEnt(ent) { + if (!ent || !pool.length) return; + const p = pool[idx % pool.length]; + idx++; + ent.x = p.x + (Math.random() - 0.5) * 0.08; + ent.y = p.y + (Math.random() - 0.5) * 0.08; + stampJumpBotState(ent); + } + assignEnt(me); + realIds.forEach((rid) => assignEnt(others.get(rid))); + botIds.forEach((bid) => assignEnt(others.get(bid))); + jumpSurviveInitRuntime(); + } + + /** + * ฟิสิกส์กระโดดร่วม (ผู้เล่น + บอท preview) + * ตายจาก: hazard ที่วาง, บีบระหว่าง solid+object wall, หรือตกออกนอกขอบโลกแมป (ไม่ใช้ขอบกล้อง) + * @returns {{ pxc: number, feetY: number, vy: number, onGround: boolean, died: boolean }} + */ + function jumpSurvivePhysicsIntegrate(md, opts) { + const { + dt, + cw, ch, + pxc0, feetY0, vy0, wasOnGround, + jumpQueued, + moveLeft, moveRight, + } = opts; + const w = md.width || 20, h = md.height || 15; + const ts = tileSize; + const bw = ts * 0.34; + const bh = ts * 0.9; + let pxc = pxc0; + let feetY = feetY0; + let vy = vy0; + let onGround = wasOnGround; + + const gFull = Number(md.jumpSurviveGravity) > 0 ? Number(md.jumpSurviveGravity) : 3200; + const g = gFull * dt; + let jumpV0; + if (Number(md.jumpSurviveJumpImpulse) > 0) { + jumpV0 = -Number(md.jumpSurviveJumpImpulse); + } else { + const mult = (typeof playJumpSurviveJumpHeightMult === 'number' && playJumpSurviveJumpHeightMult > 0) + ? playJumpSurviveJumpHeightMult + : 1.5; + const charHpixels = ch * ts; + const impulse = Math.sqrt(Math.max(1e-6, 2 * gFull * mult * charHpixels)); + jumpV0 = -Math.min(impulse, ts * 36); + } + let moveX = (Number(md.jumpSurviveMovePxPerSec) > 0 ? Number(md.jumpSurviveMovePxPerSec) : 200) * dt; + if (isJumpSurviveMissionUiMapPlay()) moveX *= 1.35; + + if (jumpQueued && onGround) { + vy = jumpV0; + onGround = false; + } + vy += g; + + let nx = pxc; + if (moveLeft) nx -= moveX; + if (moveRight) nx += moveX; + nx = Math.max(bw + 1, Math.min(w * ts - bw - 1, nx)); + + const headY0 = feetY - bh; + const tryL = nx - bw; + const tryR = nx + bw; + if (!jumpSurviveOverlapsSolidForHorzMove(md, tryL, headY0, tryR, feetY)) { + pxc = nx; + } + + feetY += vy * dt; + let headTop = feetY - bh; + if (jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + if (vy > 0) { + const step = ts * 0.035; + const penetrationEst = Math.abs(vy) * dt + ts * 0.55; + const maxSteps = Math.min(160, Math.max(28, Math.ceil(penetrationEst / step) + 6)); + for (let s = 0; s < maxSteps; s++) { + feetY -= step; + headTop = feetY - bh; + if (!jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + vy = 0; + onGround = true; + break; + } + } + } else if (vy < 0) { + const stepUp = ts * 0.035; + const penetrationEstUp = Math.abs(vy) * dt + ts * 0.55; + const maxStepsUp = Math.min(160, Math.max(28, Math.ceil(penetrationEstUp / stepUp) + 6)); + for (let s = 0; s < maxStepsUp; s++) { + feetY += stepUp; + headTop = feetY - bh; + if (!jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + vy = 0; + break; + } + } + } + } else { + onGround = false; + } + + headTop = feetY - bh; + const headBandBottom = headTop + bh * 0.42; + if (jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY) && + jumpSurviveOverlapsObjectWall(md, pxc - bw, headTop, pxc + bw, headBandBottom)) { + return { pxc, feetY, vy, onGround, died: true }; + } + + headTop = feetY - bh; + if (jumpSurviveOverlapsHazardArea(md, pxc - bw, headTop, pxc + bw, feetY)) { + return { pxc, feetY, vy, onGround, died: true }; + } + + headTop = feetY - bh; + /* ไม่ใช้ขอบกล้อง (cam topKill/botKill) — ตายเฉพาะ hazard ที่วาง + ตกออกนอกโลกแมปจริง (ไม่ตายในอากาศระหว่างแพลตฟอร์ม) */ + const mapHpx = h * ts; + if (feetY > mapHpx + ts * 8 || headTop < -ts * 32) { + return { pxc, feetY, vy, onGround, died: true }; + } + + return { pxc, feetY, vy, onGround, died: false }; + } + + function jumpSurviveEntityFromHitbox(o, md, pxc, feetYpx) { + const { cw, ch } = getCharacterFootprintWH(md); + o.x = pxc / tileSize - cw * 0.5; + o.y = feetYpx / tileSize - ch; + } + + function jumpSurviveInitRuntime() { + jumpSurviveLastTickMs = performance.now(); + jumpSurviveSessionStartMs = performance.now(); + jumpSurviveVy = 0; + jumpSurviveJumpQueued = false; + jumpSurviveOnGround = false; + jumpSurvivePlatformScrollPx = 0; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + if (isJumpSurviveMissionUiMapPlay()) { + const w = mapData.width || 20, h = mapData.height || 15; + jumpSurviveCamCenterX = (w * ts) * 0.5; + jumpSurviveCamCenterY = (h * ts) * 0.5; + } else { + jumpSurviveCamCenterX = (me.x + cw * 0.5) * ts; + jumpSurviveCamCenterY = (me.y + ch * 0.5) * ts; + } + } + + function jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const x0 = Math.floor(left / ts); + const x1 = Math.floor((right - 1e-6) / ts); + const y0 = Math.floor(top / ts); + const y1 = Math.floor((bottom - 1e-6) / ts); + for (let ty = y0; ty <= y1; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (tx < 0 || tx >= w) return true; + if (ty < 0) continue; + /* แพลตฟอร์มวนแนวตั้ง (period = h*ts) — worldY เกิน h*ts เป็นปกติ ห้ามถือ ty>=h เป็น “ซีลพื้นแมป” ไม่งั้น solid+กำแพงข้าง = ตายคาแอร์ */ + if (ty >= h) continue; + const ob = md.objects && md.objects[ty] && md.objects[ty][tx]; + if (ob === 1) return true; + } + } + return false; + } + + function jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, scrollPx) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + if (!pa) return false; + const period = jumpSurvivePlatformPeriodPx(md); + if (period < ts) return false; + const x0 = Math.max(0, Math.floor(left / ts)); + const x1 = Math.min(w - 1, Math.floor((right - 1e-6) / ts)); + const vm = ts * 2; + for (let ty = 0; ty < h; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (!pa[ty] || pa[ty][tx] !== 1) continue; + const rawTop = ty * ts + scrollPx; + let kMin = Math.ceil((top - rawTop - ts - vm) / period); + let kMax = Math.floor((bottom - rawTop + vm) / period); + /* ceil/floor กับช่วงสั้น ๆ อาจได้ kMin > kMax → ไม่ลอง k เลย แล้วตกทะลุแพลตฟอร์ม */ + if (kMin > kMax) { + const kGuess = Math.round((bottom - rawTop - ts * 0.5) / period); + kMin = kGuess; + kMax = kGuess; + } + for (let k = kMin; k <= kMax; k++) { + const platTop = rawTop + k * period; + const platBot = platTop + ts; + const tileLeft = tx * ts; + const tileRight = (tx + 1) * ts; + if (platBot > top && platTop < bottom && right > tileLeft && left < tileRight) return true; + } + } + } + return false; + } + + /** แถวแพลตฟอร์มบนสุด/ล่างสุดของแมป (มีช่องอย่างน้อยหนึ่งช่อง) — ใช้เป็นเพดาน/พื้นตาย */ + function jumpSurvivePlatformEdgeKillRows(md) { + const pa = md && md.jumpSurvivePlatformArea; + if (!pa) return null; + const h = Math.min(pa.length, md.height || pa.length || 15); + const wM = md.width || 20; + let minTy = h; + let maxTy = -1; + for (let ty = 0; ty < h; ty++) { + const row = pa[ty]; + if (!row) continue; + const wlim = Math.min(row.length || 0, wM); + let rowHas = false; + for (let tx = 0; tx < wlim; tx++) { + if (row[tx] === 1) { + rowHas = true; + break; + } + } + if (!rowHas) continue; + if (ty < minTy) minTy = ty; + if (ty > maxTy) maxTy = ty; + } + if (minTy > maxTy) return null; + if (minTy === maxTy) return null; + return { minTy, maxTy }; + } + + /** ชนแพลตฟอร์มแถวเพดานเท่านั้น (รวมเลื่อนแบบคาบ) = ตายทันที — ไม่ฆ่าที่แถวพื้นล่าง */ + function jumpSurviveOverlapsEdgeKillPlatforms(md, left, top, right, bottom, scrollPx) { + const edges = jumpSurvivePlatformEdgeKillRows(md); + if (!edges) return false; + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + if (!pa) return false; + const period = jumpSurvivePlatformPeriodPx(md); + if (period < ts) return false; + const x0 = Math.max(0, Math.floor(left / ts)); + const x1 = Math.min(w - 1, Math.floor((right - 1e-6) / ts)); + const vm = ts * 2; + const overlapsRowTy = function (ty) { + if (ty < 0 || ty >= h || !pa[ty]) return false; + for (let tx = x0; tx <= x1; tx++) { + if (pa[ty][tx] !== 1) continue; + const rawTop = ty * ts + scrollPx; + let kMin = Math.ceil((top - rawTop - ts - vm) / period); + let kMax = Math.floor((bottom - rawTop + vm) / period); + if (kMin > kMax) { + const kGuess = Math.round((bottom - rawTop - ts * 0.5) / period); + kMin = kGuess; + kMax = kGuess; + } + const zSlack = ts * 0.08; + for (let k = kMin; k <= kMax; k++) { + const platTop = rawTop + k * period; + const platBot = platTop + ts; + const tileLeft = tx * ts; + const tileRight = (tx + 1) * ts; + if (platBot > top - zSlack && platTop < bottom + zSlack && right > tileLeft && left < tileRight) return true; + } + } + return false; + }; + if (overlapsRowTy(edges.minTy)) return true; + return false; + } + + function jumpSurviveOverlapsSolid(md, left, top, right, bottom) { + if (jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom)) return true; + return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, jumpSurvivePlatformScrollPx); + } + + /** + * ชนแนวนอน: objects เต็มกล่อง; แพลตฟอร์มไม่นับช่วงปลายเท้าเล็กน้อย + * (ถ้าใช้กล่องเต็มกับแพลตฟอร์ม ขณะยืนบนแพลตฟอร์มมักทับช่องแนวตั้ง — เดินซ้ายขวาถูกปฏิเสธทั้งก้อน) + */ + function jumpSurviveOverlapsSolidForHorzMove(md, left, top, right, bottom) { + if (jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom)) return true; + const ts = tileSize; + const footSlop = ts * 0.16; + const platformBottom = bottom - footSlop; + if (platformBottom <= top + ts * 0.05) return false; + return jumpSurviveOverlapsPlatforms(md, left, top, right, platformBottom, jumpSurvivePlatformScrollPx); + } + + /** Head/body overlaps กำแพง (objects=1) เท่านั้น — ใช้ตรวจชนฝาบนแล้วตาย */ + function jumpSurviveOverlapsObjectWall(md, left, top, right, bottom) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const x0 = Math.floor(left / ts); + const x1 = Math.floor((right - 1e-6) / ts); + const y0 = Math.floor(top / ts); + const y1 = Math.floor((bottom - 1e-6) / ts); + for (let ty = y0; ty <= y1; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (tx < 0 || tx >= w || ty < 0 || ty >= h) continue; + const ob = md.objects && md.objects[ty] && md.objects[ty][tx]; + if (ob === 1) return true; + } + } + return false; + } + + function jumpSurviveSyncMeFromHitbox(pxc, feetYpx) { + jumpSurviveEntityFromHitbox(me, mapData, pxc, feetYpx); + } + + function jumpSurviveCameraTargetPx(md) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + if (isJumpSurviveMissionUiMapPlay()) { + return { px: (w * ts) * 0.5, py: (h * ts) * 0.5 }; + } + const { cw, ch } = getCharacterFootprintWH(md); + const cx = (e) => (e.x + cw * 0.5) * ts; + const cy = (e) => (e.y + ch * 0.5) * ts; + if (!jumpSurviveEliminated) return { px: cx(me), py: cy(me) }; + const aliveHumanIds = [...others.keys()].filter((id) => { + if (isPreviewBotId(id)) return false; + const o = others.get(id); + return o && !o.jumpSurviveEliminated; + }); + const aliveBotIds = [...others.keys()].filter((id) => { + if (!isPreviewBotId(id)) return false; + const o = others.get(id); + return o && !o.jumpSurviveEliminated; + }); + const useIds = aliveHumanIds.length ? aliveHumanIds : aliveBotIds; + if (!useIds.length) return { px: cx(me), py: cy(me) }; + let sx = 0, sy = 0; + useIds.forEach((id) => { + const o = others.get(id); + if (!o) return; + sx += cx(o); + sy += cy(o); + }); + const n = useIds.length; + return { px: sx / n, py: sy / n }; + } + + /** มีแพลตฟอร์มใต้จุดเล็กน้อยข้างหน้าแนว wishDir หรือไม่ — กันบอทเดินหลุมแล้วตกจอ */ + function jumpSurviveBotHasFloorAhead(md, pxc, feetY, wishDir, scrollPx) { + if (!wishDir) return true; + const ts = tileSize; + const bw = ts * 0.34; + const probeX = pxc + wishDir * (bw + ts * 0.82); + const pad = ts * 0.24; + const left = probeX - pad; + const right = probeX + pad; + const top = feetY - ts * 0.45; + const bottom = feetY + ts * 8; + return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, scrollPx); + } + + function stepJumpSurvivePreviewBots(dt, halfViewH) { + if (!playBotsEnabled() || !mapData || !isJumpSurvive()) return; + const md = mapData; + const { cw, ch } = getCharacterFootprintWH(md); + const ts = tileSize; + const wpx = (md.width || 20) * ts; + const centerPx = wpx * 0.5; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + if (o.jumpSurviveEliminated) return; + if (typeof o.jumpSurviveVy !== 'number') o.jumpSurviveVy = 0; + if (o.jumpSurviveOnGround == null) o.jumpSurviveOnGround = false; + if (typeof o.jumpSurviveNextJumpAi !== 'number') o.jumpSurviveNextJumpAi = Date.now() + 300 + Math.floor(Math.random() * 500); + if (typeof o.jumpSurviveWishNext !== 'number') o.jumpSurviveWishNext = Date.now() + 200; + if (o.jumpSurviveWishX == null) o.jumpSurviveWishX = 0; + + let pxc = (o.x + cw * 0.5) * ts; + let feetY = (o.y + ch) * ts; + + const camCy = jumpSurviveCamCenterY; + const mission = isJumpSurviveMissionUiMapPlay(); + const lowDanger = feetY > camCy + halfViewH - ts * 8.5; + + if (Date.now() >= o.jumpSurviveWishNext) { + const wMin = mission && lowDanger ? 180 : 380; + const wSpan = mission && lowDanger ? 480 : 700; + o.jumpSurviveWishNext = Date.now() + wMin + Math.floor(Math.random() * wSpan); + const tier = o.botTier || 'avg'; + if (mission && lowDanger && Math.random() < 0.55) { + o.jumpSurviveWishX = 0; + } else if (pxc < centerPx - ts * 1.8) o.jumpSurviveWishX = 1; + else if (pxc > centerPx + ts * 1.8) o.jumpSurviveWishX = -1; + else if (tier === 'sharp') { + const roam = mission ? 0.42 : 0.55; + o.jumpSurviveWishX = Math.random() < roam ? (Math.random() < 0.5 ? -1 : 1) : 0; + } else { + const roam = mission ? 0.28 : 0.4; + o.jumpSurviveWishX = Math.random() < roam ? (Math.random() < 0.5 ? -1 : 1) : 0; + } + } + + let moveLeft = o.jumpSurviveWishX < 0; + let moveRight = o.jumpSurviveWishX > 0; + let hazardJump = false; + if (o.jumpSurviveOnGround) { + const sc = jumpSurvivePlatformScrollPx; + if (moveRight && !jumpSurviveBotHasFloorAhead(md, pxc, feetY, 1, sc)) { + o.jumpSurviveWishX = Math.random() < 0.65 ? -1 : 0; + hazardJump = true; + } else if (moveLeft && !jumpSurviveBotHasFloorAhead(md, pxc, feetY, -1, sc)) { + o.jumpSurviveWishX = Math.random() < 0.65 ? 1 : 0; + hazardJump = true; + } + moveLeft = o.jumpSurviveWishX < 0; + moveRight = o.jumpSurviveWishX > 0; + } + + let jumpQ = false; + if (hazardJump) o.jumpSurviveNextJumpAi = Math.min(o.jumpSurviveNextJumpAi, Date.now()); + if (o.jumpSurviveOnGround && Date.now() >= o.jumpSurviveNextJumpAi) { + o.jumpSurviveNextJumpAi = Date.now() + 300 + Math.floor(Math.random() * (hazardJump ? 520 : 1100)); + const tier = o.botTier || 'avg'; + let jp = tier === 'sharp' ? 0.5 : tier === 'weak' ? 0.25 : 0.36; + if (mission) jp += 0.12; + if (lowDanger) jp += 0.22; + jp = Math.min(0.9, jp); + if (hazardJump || Math.random() < jp) jumpQ = true; + } + + const r = jumpSurvivePhysicsIntegrate(md, { + dt, + cw, + ch, + pxc0: pxc, + feetY0: feetY, + vy0: o.jumpSurviveVy, + wasOnGround: o.jumpSurviveOnGround, + jumpQueued: jumpQ, + moveLeft, + moveRight, + }); + + if (r.died) { + o.jumpSurviveEliminated = true; + o.jumpSurviveVy = 0; + jumpSurviveEntityFromHitbox(o, md, r.pxc, r.feetY); + o.tx = o.x; + o.ty = o.y; + o.botIsWalking = false; + return; + } + o.jumpSurviveVy = r.vy; + o.jumpSurviveOnGround = r.onGround; + jumpSurviveEntityFromHitbox(o, md, r.pxc, r.feetY); + o.tx = o.x; + o.ty = o.y; + if (moveRight) o.direction = 'right'; + else if (moveLeft) o.direction = 'left'; + o.botIsWalking = Math.abs(r.vy) > 40 || moveLeft || moveRight; + }); + } + + function jumpSurviveTickFrame() { + if (!mapData || !isJumpSurvive()) return; + if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) return; + const remEnd = jumpSurviveRemainingSecMission(); + if (remEnd !== null && remEnd <= 0) { + endJumpSurviveMissionRound('time_up'); + return; + } + } + const md = mapData; + const ts = tileSize; + const now = performance.now(); + const dt = Math.min(0.05, Math.max(0, (now - jumpSurviveLastTickMs) / 1000)); + jumpSurviveLastTickMs = now; + + let zJumpSurviveWorld = zoom; + if (previewMode && editorEmbedReturn && mapData) zJumpSurviveWorld *= playEmbedUserZoomMul; + const halfViewH = canvas.height / (2 * zJumpSurviveWorld); + const rise = (Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42) * dt; + jumpSurvivePlatformScrollPx += rise; + + const camTgt = jumpSurviveCameraTargetPx(md); + const ck = Math.min(1, dt * 7); + jumpSurviveCamCenterX += (camTgt.px - jumpSurviveCamCenterX) * ck; + jumpSurviveCamCenterY += (camTgt.py - jumpSurviveCamCenterY) * ck; + + if (jumpSurviveEliminated) { + stepJumpSurvivePreviewBots(dt, halfViewH); + jumpSurviveMissionMaybeEarlyFinish(); + return; + } + + const { cw, ch } = getCharacterFootprintWH(md); + let pxc = (me.x + cw * 0.5) * ts; + let feetY = (me.y + ch) * ts; + + const jq = jumpSurviveJumpQueued; + jumpSurviveJumpQueued = false; + + const r = jumpSurvivePhysicsIntegrate(md, { + dt, + cw, + ch, + pxc0: pxc, + feetY0: feetY, + vy0: jumpSurviveVy, + wasOnGround: jumpSurviveOnGround, + jumpQueued: jq, + moveLeft: !!(keys['ArrowLeft'] || keys['KeyA']), + moveRight: !!(keys['ArrowRight'] || keys['KeyD']), + }); + + if (r.died) { + jumpSurviveEliminated = true; + jumpSurviveVy = 0; + jumpSurviveSyncMeFromHitbox(r.pxc, r.feetY); + ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'KeyA', 'KeyW', 'KeyS', 'KeyD'].forEach((c) => { keys[c] = false; }); + } else { + jumpSurviveVy = r.vy; + jumpSurviveOnGround = r.onGround; + jumpSurviveSyncMeFromHitbox(r.pxc, r.feetY); + } + + stepJumpSurvivePreviewBots(dt, halfViewH); + + if (!jumpSurviveEliminated) { + me.isWalking = Math.abs(jumpSurviveVy) > 40 || !!(keys['ArrowLeft'] || keys['KeyA'] || keys['ArrowRight'] || keys['KeyD']); + if (keys['ArrowRight'] || keys['KeyD']) me.direction = 'right'; + else if (keys['ArrowLeft'] || keys['KeyA']) me.direction = 'left'; + + if (Date.now() - jumpSurviveLastEmitT > 80) { + jumpSurviveLastEmitT = Date.now(); + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + } + + jumpSurviveMissionMaybeEarlyFinish(); + } + + function gridImageLibIdleUrlPlay(entry) { + if (entry == null) return ''; + if (typeof entry === 'string') return entry; + if (typeof entry === 'object' && entry.idle) return String(entry.idle); + return ''; + } + function gridImageLibHeldUrlPlay(entry) { + if (entry == null || typeof entry === 'string') return ''; + if (typeof entry === 'object' && entry.held && String(entry.held).length) return String(entry.held); + return ''; + } + + function normalizeGridImageCellsOnMap(md) { + if (!md) return; + const w = md.width || 20, h = md.height || 15; + if (!Array.isArray(md.gridImageLibrary)) md.gridImageLibrary = []; + md.gridImageLibrary = md.gridImageLibrary + .map((raw) => { + if (typeof raw === 'string' && raw.length > 0 && raw.length < 30000000) return { idle: raw, held: null }; + if (raw && typeof raw === 'object' && typeof raw.idle === 'string' && raw.idle.length > 0 && raw.idle.length < 30000000) { + let held = typeof raw.held === 'string' && raw.held.length > 0 && raw.held.length < 30000000 ? raw.held : null; + if (held && held === raw.idle) held = null; + return { idle: raw.idle, held }; + } + return null; + }) + .filter(Boolean); + const libLen = md.gridImageLibrary.length; + let placements = []; + if (Array.isArray(md.gridImageSprites) && md.gridImageSprites.length) { + placements = md.gridImageSprites.map((raw) => { + const i = Math.max(0, Math.floor(Number(raw.i))); + const x = Math.floor(Number(raw.x)); + const y = Math.floor(Number(raw.y)); + const ww = Math.max(1, Math.floor(Number(raw.w)) || 1); + const hh = Math.max(1, Math.floor(Number(raw.h)) || 1); + const rawB = raw.bindCarryOption; + const bn = typeof rawB === 'number' ? rawB : parseInt(String(rawB), 10); + const base = { i, x, y, w: ww, h: hh }; + if (Number.isFinite(bn) && bn >= 1 && bn <= QUIZ_CARRY_MAX_OPTION_SLOTS) base.bindCarryOption = bn; + return base; + }).filter((s) => Number.isFinite(s.i) && s.i >= 0 && s.i < libLen && + Number.isFinite(s.x) && Number.isFinite(s.y) && s.w >= 1 && s.h >= 1 && + s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0); + placements = placements.map((s) => { + let x = s.x, y = s.y, ww = s.w, hh = s.h; + if (x < 0) { ww += x; x = 0; } + if (y < 0) { hh += y; y = 0; } + if (x + ww > w) ww = w - x; + if (y + hh > h) hh = h - y; + const out = { i: s.i, x, y, w: Math.max(1, ww), h: Math.max(1, hh) }; + if (s.bindCarryOption != null) out.bindCarryOption = s.bindCarryOption; + return out; + }).filter((s) => s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0); + } else { + const cells = md.gridImageCells; + if (Array.isArray(cells) && cells.length === h && libLen > 0) { + for (let yy = 0; yy < h; yy++) { + const row = cells[yy]; + if (!row) continue; + for (let xx = 0; xx < w; xx++) { + if (row[xx] == null) continue; + const iv = parseInt(row[xx], 10); + if (!Number.isFinite(iv) || iv < 0 || iv >= libLen) continue; + placements.push({ i: iv, x: xx, y: yy, w: 1, h: 1 }); + } + } + } + } + md.gridImagePlacements = placements; + const outCells = Array(h).fill(0).map(() => Array(w).fill(null)); + placements.forEach((sp) => { + for (let yy = sp.y; yy < sp.y + sp.h; yy++) { + for (let xx = sp.x; xx < sp.x + sp.w; xx++) { + if (yy >= 0 && yy < h && xx >= 0 && xx < w) outCells[yy][xx] = sp.i; + } + } + }); + md.gridImageCells = outCells; + } + + function loadMapGridImages() { + mapGridImageImgs = []; + mapGridImageHeldImgs = []; + if (!mapData || !Array.isArray(mapData.gridImageLibrary) || !mapData.gridImageLibrary.length) return; + mapData.gridImageLibrary.forEach((entry, i) => { + mapGridImageImgs[i] = null; + mapGridImageHeldImgs[i] = null; + const idle = gridImageLibIdleUrlPlay(entry); + if (!idle) return; + const im = new Image(); + im.onload = () => { try { draw(); } catch (e) {} }; + im.onerror = () => {}; + im.src = idle; + mapGridImageImgs[i] = im; + const held = gridImageLibHeldUrlPlay(entry); + if (held && held !== idle) { + const hm = new Image(); + hm.onload = () => { try { draw(); } catch (e) {} }; + hm.onerror = () => {}; + hm.src = held; + mapGridImageHeldImgs[i] = hm; + } + }); + } + + /** โซนตัวเลือกที่ทับสไปรต์มากที่สุด — quiz_carry สลับรูป idle/held · ถ้าไม่ทับเลยลองขยายขอบ 1 ช่อง (มาร์กเกอร์ชิดขอบสไปรต์) */ + function dominantQuizCarryOptionInSpriteRect(md, sp) { + if (!md || md.gameType !== 'quiz_carry' || !md.quizCarryOptionArea) return null; + const g = md.quizCarryOptionArea; + const mw = md.width || 20, mh = md.height || 15; + const bx = sp.x | 0, by = sp.y | 0, bw = sp.w | 0, bh = sp.h | 0; + const counts = new Map(); + function addOverlapRect(x0, y0, ww, hh) { + for (let yy = y0; yy < y0 + hh; yy++) { + if (yy < 0 || yy >= mh) continue; + const row = g[yy]; + if (!row) continue; + for (let xx = x0; xx < x0 + ww; xx++) { + if (xx < 0 || xx >= mw) continue; + const v = row[xx]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + if (!Number.isFinite(n) || n < 1 || n > QUIZ_CARRY_MAX_OPTION_SLOTS) continue; + const idx = n - 1; + counts.set(idx, (counts.get(idx) || 0) + 1); + } + } + } + addOverlapRect(bx, by, bw, bh); + if (!counts.size) { + const pad = 1; + const x0 = Math.max(0, bx - pad); + const y0 = Math.max(0, by - pad); + const x1 = Math.min(mw, bx + bw + pad); + const y1 = Math.min(mh, by + bh + pad); + addOverlapRect(x0, y0, x1 - x0, y1 - y0); + } + if (!counts.size) return null; + let best = null; + let bestN = -1; + counts.forEach((n, k) => { + if (best == null || n > bestN || (n === bestN && k < best)) { + bestN = n; + best = k; + } + }); + return best; + } + + /** 0-based choice index สำหรับสลับ idle/held — ใช้ bindCarryOption จากแมปก่อน แล้วค่อยนับทับโซน */ + function quizCarryOptionIndexForGridSprite(md, sp) { + if (!sp) return null; + const rawB = sp.bindCarryOption; + if (rawB != null && rawB !== '') { + const n = typeof rawB === 'number' ? rawB : parseInt(String(rawB), 10); + if (Number.isFinite(n) && n >= 1 && n <= QUIZ_CARRY_MAX_OPTION_SLOTS) return n - 1; + } + return dominantQuizCarryOptionInSpriteRect(md, sp); + } + + /** รูป held จากคลังกริดสำหรับข้อนี้ — ใช้วาดป้ายติดตัว (ไม่ผ่าน sanitize URL) */ + function findQuizCarryGridHeldImgForChoiceIndex(choiceIdx) { + if (choiceIdx == null || choiceIdx < 0 || !isQuizCarry() || !mapData) return null; + const pl = mapData.gridImagePlacements; + if (!Array.isArray(pl) || !pl.length) return null; + for (let gi = 0; gi < pl.length; gi++) { + const sp = pl[gi]; + const opt = quizCarryOptionIndexForGridSprite(mapData, sp); + if (opt !== choiceIdx) continue; + const gh = mapGridImageHeldImgs[sp.i]; + if (gh && gh.complete && gh.naturalWidth) return gh; + } + return null; + } + + /** รูป idle จากคลังกริดสำหรับข้อนี้ — วาดบนป้ายตัวเลือกบนพื้นเมื่อไม่มีรูปจากคำถาม/ธีม */ + function findQuizCarryGridIdleImgForChoiceIndex(choiceIdx) { + if (choiceIdx == null || choiceIdx < 0 || !isQuizCarry() || !mapData) return null; + const pl = mapData.gridImagePlacements; + if (!Array.isArray(pl) || !pl.length) return null; + for (let gi = 0; gi < pl.length; gi++) { + const sp = pl[gi]; + const opt = quizCarryOptionIndexForGridSprite(mapData, sp); + if (opt !== choiceIdx) continue; + const gid = mapGridImageImgs[sp.i]; + if (gid && gid.complete && gid.naturalWidth) return gid; + } + return null; + } + + function applyMapAndStart(gameMapData, res) { + 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; + } + { + const qMap = (playMapIdFromQuery || '').trim(); + const dataId = mapData && mapData.id != null && String(mapData.id).trim() !== '' ? String(mapData.id).trim() : ''; + const resMap = res && res.mapId != null && String(res.mapId).trim() !== '' ? String(res.mapId).trim() : ''; + if (qMap) playSessionMapId = qMap; + else if (dataId) playSessionMapId = dataId; + else if (resMap) playSessionMapId = resMap; + } + applyPlayEmbedZoomForCurrentMapPlay(); + if (res && res.hostId != null) playHostId = res.hostId; + if (!isQuizQuestionMissionUiMapPlay()) { + quizQuestionMissionPhase = null; + quizQuestionMissionDeferredPhase = null; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + } + if (mapData.gameType !== 'space_shooter') { + spaceShooterGameEnded = false; + if (prevWasSpaceShooter) { + const gend = document.getElementById('gauntlet-ended-overlay'); + if (gend) gend.classList.add('is-hidden'); + spaceShooterMissionPhase = null; + spaceShooterAsteroidExplosions = []; + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + const gccLeave = document.getElementById('gauntlet-crown-countdown'); + if (gccLeave) gccLeave.classList.add('is-hidden'); + } + } + if (mapData.gameType !== 'balloon_boss') { + balloonBossGameEnded = false; + if (prevWasBalloonBoss) { + const gend = document.getElementById('gauntlet-ended-overlay'); + if (gend) gend.classList.add('is-hidden'); + } + } + if (!mapData.lanes && mapData.gameType === 'frogger') mapData.lanes = []; + if (!mapData.interactive) mapData.interactive = []; + if (mapData.gameType === 'quiz') { + if (!mapData.quizTrueArea) mapData.quizTrueArea = []; + if (!mapData.quizFalseArea) mapData.quizFalseArea = []; + if (!mapData.quizQuestionArea) mapData.quizQuestionArea = []; + } + if (mapData.gameType === 'stack') { + if (!mapData.stackReleaseArea) mapData.stackReleaseArea = []; + if (!mapData.stackLandArea) mapData.stackLandArea = []; + } + if (mapData.gameType === 'quiz_carry') { + if (!mapData.quizCarryHubArea) mapData.quizCarryHubArea = []; + if (!mapData.quizCarryOptionArea) mapData.quizCarryOptionArea = []; + if (!mapData.carryEmbedCountdownArea) mapData.carryEmbedCountdownArea = []; + if (!mapData.quizQuestions) mapData.quizQuestions = []; + if (!mapData.quizQuestionArea) mapData.quizQuestionArea = []; + normalizeQuizCarryLayersInPlay(mapData); + applyQuizCarryWalkSpeedFromSettingsObj(quizCarryJoinSettingsSnap || {}); + } + if (mapData.gameType === 'quiz_battle') { + if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = []; + if (!mapData.quizBattlePathArea) mapData.quizBattlePathArea = []; + normalizeQuizBattlePathInPlay(mapData); + normalizeQuizBattleDomeInPlay(mapData); + } + tileSize = mapData.tileSize || 32; + mapBackgroundImg = null; + if (mapData.backgroundImage) { + mapBackgroundImg = new Image(); + mapBackgroundImg.src = mapData.backgroundImage; + } + reloadPlayScrollBgFromMap(); + reloadStackTowerScrollBgFromMap(); + reloadGauntletCrownRunwayBgFromMap(); + normalizeGridImageCellsOnMap(mapData); + loadMapGridImages(); + var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน' + : (isGauntlet() ? ' | พรมแดงสุดท้าย (Last Light)' + : (isLobby() ? ' | โถงรอ' : (isQuiz() ? ' | ตอบคำถาม' : (isQuizCarry() ? ' | หยิบคำตอบมาวางกลาง' : (isQuizBattle() ? ' | Quiz Battle · โดม E' : (isStack() ? ' | Stack ซ้อนตึก' : (isJumpSurvive() ? ' | กระโดดให้รอด' : (isSpaceShooter() ? ' | ยิงยานอวกาศ' : (isBalloonBoss() ? ' | ลูกโป้งยิงบอส' : ''))))))))); + var prevTag = previewMode ? '[ทดสอบ] ' : ''; + document.getElementById('room-id').textContent = prevTag + spaceId + modeLabel; + const plist = Array.isArray(res.peers) ? res.peers : []; + const myPeer = plist.find(p => p.id === myId); + const myPos = peerXYFromJoin(myPeer, mapData.spawn); + me.x = myPos.x; + me.y = myPos.y; + if (isQuizCarry()) { + const hubSnap = snapPositionOutOfQuizCarryHubIfNeeded(me.x, me.y); + me.x = hubSnap.x; + me.y = hubSnap.y; + } + if (isQuizBattle()) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = pathSnap.x; + me.y = pathSnap.y; + } + me.direction = myPeer?.direction ?? 'down'; + if (myPeer && myPeer.nickname) { + me.nickname = String(myPeer.nickname).trim() || me.nickname; + nick = me.nickname; + } + me.characterId = myPeer?.characterId || null; + meGauntletJumpTicks = typeof myPeer?.gauntletJumpTicks === 'number' ? myPeer.gauntletJumpTicks : 0; + meGauntletJumpVis = meGauntletJumpTicks; + { + const sc0 = Number(myPeer && myPeer.gauntletScore); + me.gauntletScore = Number.isFinite(sc0) ? Math.max(0, sc0) : 0; + me.gauntletEliminated = !!(myPeer && myPeer.gauntletEliminated); + } + { + const sh0 = Number(myPeer && myPeer.spaceShooterScore); + me.spaceShooterScore = Number.isFinite(sh0) ? Math.max(0, sh0) : 0; + } + { + const bSt = balloonBossBalloonsStartPlay(); + const bs0 = Number(myPeer && myPeer.balloonBossScore); + me.balloonBossScore = Number.isFinite(bs0) ? Math.max(0, bs0) : 0; + const bd0 = Number(myPeer && myPeer.balloonBossBossDmg); + me.balloonBossBossDmg = Number.isFinite(bd0) ? Math.max(0, bd0) : 0; + const bb0 = Number(myPeer && myPeer.balloonBossBalloons); + me.balloonBossBalloons = Number.isFinite(bb0) ? Math.max(0, Math.floor(bb0)) : bSt; + me.balloonBossEliminated = !!(myPeer && myPeer.balloonBossEliminated); + } + gauntletObstacles = []; + gauntletObsRenderPrev = []; + gauntletObsRenderNext = []; + gauntletObsBlendT0 = 0; + if (Array.isArray(res.lobbyBotThemes)) playLobbyBotThemes = res.lobbyBotThemes; + const myPeerJoin = (Array.isArray(plist) ? plist : []).find((q) => q && q.id === myId); + me.playTint = playTintFromJoinPeer(myPeerJoin) || resolvePlayTintForPeer(String((myId != null && myId !== '') ? myId : (nick || 'local')), myPeerJoin); + if (myPeerJoin && myPeerJoin.lobbyColorThemeIndex) { + try { + const ti = parsePlayLobbyThemeIndex(myPeerJoin.lobbyColorThemeIndex); + const si = parsePlayLobbySkinIndex(myPeerJoin.lobbySkinToneIndex) || 1; + if (ti) { + localStorage.setItem(LOBBY_PLAYER_TINT_KEY, JSON.stringify({ + themeRgb: PLAY_LOBBY_THEME_HEX[ti - 1], + skinRgb: PLAY_LOBBY_SKIN_HEX[si - 1], + })); + } + } catch (eTintStore) { /* ignore */ } + } + // อุ่น cache เลเยอร์สีตัวละครก่อนเข้าเกม (ตัวเรา + เพื่อนร่วมห้อง) กันกระตุกตอนโหลดสีกลางเกม + try { + preloadPlayCharacterLayers(me.characterId || getPlayCharacterId()); + (Array.isArray(plist) ? plist : []).forEach(function (p) { if (p && p.characterId) preloadPlayCharacterLayers(p.characterId); }); + preloadPlayOccupantsTints(); + preloadPlayLobbyIdleAvatarsForOccupants(); + } catch (e) { /* ignore */ } + { + const jo = Number(myPeer && myPeer.spawnJoinOrder); + me.spawnJoinOrder = Number.isFinite(jo) ? Math.max(0, Math.floor(jo)) : Math.max(0, plist.findIndex((q) => q && q.id === myId)); + } + others.clear(); + plist.forEach(p => { + if (p.id !== myId) { + const pos = peerXYFromJoin(p, mapData.spawn); + let px = pos.x, py = pos.y; + if (isQuizCarry()) { + const hubSnap = snapPositionOutOfQuizCarryHubIfNeeded(px, py); + px = hubSnap.x; + py = hubSnap.y; + } + if (isQuizBattle()) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(px, py); + px = pathSnap.x; + py = pathSnap.y; + } + const joP = Number(p && p.spawnJoinOrder); + const spawnOrd = Number.isFinite(joP) ? Math.max(0, Math.floor(joP)) : Math.max(0, plist.findIndex((q) => q && q.id === p.id)); + others.set(p.id, { + x: px, y: py, tx: px, ty: py, direction: p.direction, nickname: p.nickname, characterId: p.characterId, + spawnJoinOrder: spawnOrd, + playTint: playTintFromJoinPeer(p) || playTintForPreviewBotId(p.id) || resolvePlayTintForPeer(p.id, p), + gauntletJumpTicks: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0, + gauntletJumpVis: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0, + gauntletScore: (() => { const s = Number(p.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + gauntletEliminated: !!p.gauntletEliminated, + jumpSurviveEliminated: false, + spaceShooterScore: (() => { const s = Number(p.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossScore: (() => { const s = Number(p.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBossDmg: (() => { const s = Number(p.balloonBossBossDmg); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBalloons: (() => { + const s = Number(p.balloonBossBalloons); + return Number.isFinite(s) ? Math.max(0, Math.floor(s)) : balloonBossBalloonsStartPlay(); + })(), + balloonBossEliminated: !!p.balloonBossEliminated, + quizCarryHeld: null, + }); + } + }); + if (mapData.gameType === 'gauntlet') { + me.tx = me.x; + me.ty = me.y; + if (res.gauntletEndsAt != null) { + const geJoin = Number(res.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(geJoin) ? geJoin : null; + } else { + gauntletEndsAtMs = null; + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => {}); + } else { + me.tx = null; + me.ty = null; + gauntletEndsAtMs = null; + } + rebalancePreviewBots(); + if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) { + applyQuizBattlePreviewSpawnLayout(false); + try { socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } catch (eSync) { /* ignore */ } + } + if (mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + if (isStack()) { + lastStackTickMs = performance.now(); + resetStackMinigameState(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + else reapplyStackMiniSizingFromGlobals(); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => { + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }); + if (isStackTowerMissionUiMapPlay()) { + stackTowerMissionEndedOnce = false; + stackTowerMissionDeferredPhase = null; + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + stackTowerMissionPhase = 'howto'; + stackTowerSessionStartAt = 0; + hideConflictingOverlaysForGauntletCrown(); + applyStackTowerMissionPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + setTimeout(function () { showStackTowerMissionHowtoOverlay(); }, 50); + } else { + teardownStackTowerMissionUiPlay(); + } + } else { + stackMini = null; + teardownStackTowerMissionUiPlay(); + } + playPath = []; + me.isWalking = false; + if (mapData.gameType === 'jump_survive') { + if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = []; + if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = []; + if (!mapData.jumpSurviveHazardArea) mapData.jumpSurviveHazardArea = []; + if (!mapData.jumpSurvivePlatformVariantArea) mapData.jumpSurvivePlatformVariantArea = []; + normalizeJumpSurvivePlatformAreaInPlay(mapData); + normalizeJumpSurvivePlatformVariantAreaInPlay(mapData); + normalizeJumpSurviveHazardAreaInPlay(mapData); + normalizeShooterSpawnSlotsInPlay(mapData); + jumpSurviveEliminated = false; + jumpSurviveGameEnded = false; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + if (isJumpSurviveMissionUiMapPlay()) { + jumpSurviveMissionPhase = 'howto'; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + setTimeout(function () { showJumpSurviveMissionHowtoOverlay(); }, 50); + } else { + jumpSurviveMissionPhase = null; + jumpSurviveInitRuntime(); + } + } + if (mapData.gameType === 'space_shooter') { + normalizeShooterSpawnSlotsInPlay(mapData); + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + if (isSpaceShooterMissionUiMapPlay()) { + spaceShooterMissionPhase = 'howto'; + hideConflictingOverlaysForGauntletCrown(); + applySpaceShooterMissionPanelImages(); + setTimeout(function () { showSpaceShooterMissionHowtoOverlay(); }, 50); + } else { + spaceShooterMissionPhase = null; + spaceShooterGameEnded = false; + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterAsteroidExplosions = []; + spaceShooterPopups = []; + spaceShooterLastTickMs = performance.now(); + spaceShooterSpawnAccMs = 0; + spaceShooterFireCd = 0; + spaceShooterSessionStartMs = performance.now(); + spaceShooterLastMoveEmit = 0; + } + } + if (mapData.gameType === 'balloon_boss') { + balloonBossGameEnded = false; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossHitFx = []; + balloonBossScorePopups = []; + balloonBossLastTickMs = performance.now(); + balloonBossPlayerFireCd = 0; + balloonBossSessionStartMs = performance.now(); + balloonBossLastMoveEmit = 0; + balloonBossBossFireAcc = 0; + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + } + if (isQuiz()) { + setupPlayQuizUi(); + if (isQuizQuestionMissionUiMapPlay()) { + quizQuestionMissionDeferredPhase = null; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + quizQuestionMissionPhase = 'howto'; + hideConflictingOverlaysForGauntletCrown(); + applyQuizQuestionMissionPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + setTimeout(function () { showQuizQuestionMissionHowtoOverlay(); }, 50); + } + } else if (isQuizCarry()) setupPlayQuizCarryUi(); + else if (isQuizBattle()) setupPlayQuizBattleUi(); + else teardownPlayQuizUi(); + if (mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) { + hideConflictingOverlaysForGauntletCrown(); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccR = document.getElementById('gauntlet-crown-countdown'); + if (gccR) gccR.classList.add('is-hidden'); + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50); + } else if (mapData.gameType === 'balloon_boss' && isMegaVirusMissionShellMapPlay()) { + hideConflictingOverlaysForGauntletCrown(); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccRm = document.getElementById('gauntlet-crown-countdown'); + if (gccRm) gccRm.classList.add('is-hidden'); + if (res && Object.prototype.hasOwnProperty.call(res, 'gauntletCrownRunHeld')) { + applyGauntletTimingFromServer({ gauntletCrownRunHeld: res.gauntletCrownRunHeld }); + } + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50); + } else if (mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay()) { + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccRj = document.getElementById('gauntlet-crown-countdown'); + if (gccRj) gccRj.classList.add('is-hidden'); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + } else if (mapData.gameType === 'space_shooter' && isSpaceShooterMissionUiMapPlay()) { + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + const gccRs = document.getElementById('gauntlet-crown-countdown'); + if (gccRs) gccRs.classList.add('is-hidden'); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + } else if (isQuiz() && isQuizQuestionMissionUiMapPlay()) { + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + const gccRq = document.getElementById('gauntlet-crown-countdown'); + if (gccRq) gccRq.classList.add('is-hidden'); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + } else if (isStack() && isStackTowerMissionUiMapPlay()) { + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + const gccRst = document.getElementById('gauntlet-crown-countdown'); + if (gccRst) gccRst.classList.add('is-hidden'); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + } else { + const hov = document.getElementById('gauntlet-crown-howto-overlay'); + if (hov) { + hov.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + } + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccR2 = document.getElementById('gauntlet-crown-countdown'); + if (gccR2) gccR2.classList.add('is-hidden'); + teardownStackTowerMissionUiPlay(); + } + resizeCanvas(); draw(); tick(); + } + + socket.on('connect', () => { + firstCharacterDefaultPromise.then(() => { + const joinPlayMapId = (params.get('map') || '').trim(); + const joinNick = (typeof window.jdReadDisplayName === 'function' ? window.jdReadDisplayName() : null) + || readStoredDisplayName() + || nick; + socket.emit('join-space', { + spaceId, + nickname: joinNick, + characterId: getPlayCharacterId(), + playMapId: joinPlayMapId || undefined, + }, (res) => { + if (!res || !res.ok) { + const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้'; + alert(errMsg); + const caseLocked = /เริ่มคดี|ไม่รับผู้เล่น/.test(errMsg); + if (caseLocked) { + window.location.replace(buildRoomLobbyReturnHref()); + } else { + window.location.replace(playDesignMainMenuHref()); + } + return; + } + myId = socket.id; + if (specialQuizForceDebug) { + try { socket.emit('special-quiz-force', { on: true }); } catch (eF) { /* ignore */ } + } + if (__isTestModeOnPlay()) { + try { socket.emit('special-quiz-testmode', { on: true }); } catch (eT) { /* ignore */ } + } + let botSlots = parseInt(res.botSlotCount, 10); + if ((!botSlots || botSlots < 1) && res.maxPlayers > 0 && res.maxPlayers < 6) { + botSlots = 6 - res.maxPlayers; + } + if (botSlots > 0) { + detectiveCaseFillBots = true; + detectiveBotSlotCount = Math.min(6, Math.max(0, botSlots)); + } + if (isDetectiveMinigamePlay()) { + try { document.documentElement.classList.add('play-detective-minigame'); } catch (eD) { /* ignore */ } + } + if (res.quizSettingsSnap && typeof res.quizSettingsSnap === 'object') { + if (res.quizSettingsSnap.quizMapPanelTheme && typeof res.quizSettingsSnap.quizMapPanelTheme === 'object') { + setQuizMapPanelThemeFromApi({ quizMapPanelTheme: res.quizSettingsSnap.quizMapPanelTheme }); + } + } + if (res.quizCarrySettingsSnap && typeof res.quizCarrySettingsSnap === 'object') { + quizCarryJoinSettingsSnap = res.quizCarrySettingsSnap; + if (res.quizCarrySettingsSnap.carryMapPanelTheme && typeof res.quizCarrySettingsSnap.carryMapPanelTheme === 'object') { + setQuizCarryMapPanelThemeFromApi({ carryMapPanelTheme: res.quizCarrySettingsSnap.carryMapPanelTheme }); + } + if (res.quizCarrySettingsSnap.carryEmbedCountdownTheme && typeof res.quizCarrySettingsSnap.carryEmbedCountdownTheme === 'object') { + setQuizCarryEmbedCountdownThemeFromApi({ carryEmbedCountdownTheme: res.quizCarrySettingsSnap.carryEmbedCountdownTheme }); + } + if (Array.isArray(res.quizCarrySettingsSnap.carryChoicePlaqueThemes) && res.quizCarrySettingsSnap.carryChoicePlaqueThemes.length) { + setQuizCarryChoicePlaqueThemeFromApi({ carryChoicePlaqueThemes: res.quizCarrySettingsSnap.carryChoicePlaqueThemes }); + } else if (res.quizCarrySettingsSnap.carryChoicePlaqueTheme && typeof res.quizCarrySettingsSnap.carryChoicePlaqueTheme === 'object') { + setQuizCarryChoicePlaqueThemeFromApi({ carryChoicePlaqueTheme: res.quizCarrySettingsSnap.carryChoicePlaqueTheme }); + } + const joinSc = Number(res.quizCarrySettingsSnap.carryChoicePlaqueMapScale); + if (Number.isFinite(joinSc)) { + quizCarryPlaqueMapScale = Math.max(0.85, Math.min(2.5, joinSc)); + } + } + if (res.specialQuizIcon) setSpecialQuizIconFromServer(res.specialQuizIcon); + if (previewMode && editorEmbedReturn && res.mapData && res.mapData.gameType === 'lobby') { + const q = []; + q.push('space=' + encodeURIComponent(spaceId)); + q.push('nick=' + encodeURIComponent(nick)); + const mid = params.get('map'); + if (mid) q.push('map=' + encodeURIComponent(mid)); + q.push('preview=1'); + q.push('editorEmbed=1'); + if (params.get('defaultChar') != null) q.push('defaultChar=' + encodeURIComponent(params.get('defaultChar'))); + window.location.replace(BASE + '/room-lobby.html?' + q.join('&')); + return; + } + const playMapId = params.get('map'); + if (playMapId) { + fetch(BASE + '/api/maps/' + encodeURIComponent(playMapId)) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (data) applyMapAndStart(data, res); + else applyMapAndStart(res.mapData, res); + }) + .catch(() => applyMapAndStart(res.mapData, res)); + } else { + applyMapAndStart(res.mapData, res); + } + }); + }); + }); + + const LERP = 0.2; + + function applyPlayDisplayNameUpdate(data) { + if (!data || !data.id) return; + const next = String(data.nickname || '').trim(); + if (!next) return; + if (data.id === myId) { + me.nickname = next; + nick = next; + try { + localStorage.setItem('roomLobbyDisplayName', next); + localStorage.setItem('playerName', next); + } catch (e) { /* ignore */ } + if (window.jdDisplayName && typeof window.jdDisplayName.syncDom === 'function') { + window.jdDisplayName.syncDom(next); + } + } else { + const o = others.get(data.id); + if (o) o.nickname = next; + } + if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud(); + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud(); + if (typeof syncPlayCyberHud === 'function') syncPlayCyberHud(); + } + + socket.on('peer-display-name', applyPlayDisplayNameUpdate); + + socket.on('lobby-tint-sync', applyPlayLobbyTintSync); + + window.addEventListener('jd-display-name-changed', function (ev) { + const next = ev && ev.detail && ev.detail.displayName ? String(ev.detail.displayName).trim() : ''; + if (!next || !myId) return; + applyPlayDisplayNameUpdate({ id: myId, nickname: next }); + }); + + socket.on('user-joined', (data) => { + if (!data || isPreviewBotId(data.id)) return; + const x = Number(data.x); + const y = Number(data.y); + const px = Number.isFinite(x) ? x : 1; + const py = Number.isFinite(y) ? y : 1; + const joJ = Number(data && data.spawnJoinOrder); + const spawnOrdJ = Number.isFinite(joJ) ? Math.max(0, Math.floor(joJ)) : 0; + others.set(data.id, { + x: px, y: py, tx: px, ty: py, + direction: data.direction || 'down', nickname: data.nickname, + characterId: data.characterId ?? null, + spawnJoinOrder: spawnOrdJ, + playTint: playTintFromJoinPeer(data) || playTintForPreviewBotId(data.id) || resolvePlayTintForPeer(data.id, data), + gauntletJumpTicks: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0, + gauntletJumpVis: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0, + gauntletScore: (() => { const s = Number(data.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + jumpSurviveEliminated: false, + spaceShooterScore: (() => { const s = Number(data.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossScore: (() => { const s = Number(data.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBossDmg: (() => { const s = Number(data.balloonBossBossDmg); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBalloons: (() => { + const s = Number(data.balloonBossBalloons); + return Number.isFinite(s) ? Math.max(0, Math.floor(s)) : balloonBossBalloonsStartPlay(); + })(), + balloonBossEliminated: !!data.balloonBossEliminated, + quizCarryHeld: null, + }); + if (playBotsEnabled()) rebalancePreviewBots(); + if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud(); + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud(); + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud(); + if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') updateJumpSurviveMissionHowtoHud(); + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') updateSpaceShooterMissionHowtoHud(); + }); + socket.on('host-changed', (d) => { + if (d && d.hostId != null) playHostId = d.hostId; + if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud(); + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud(); + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud(); + if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') updateJumpSurviveMissionHowtoHud(); + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') updateSpaceShooterMissionHowtoHud(); + }); + socket.on('quiz-carry-lobby-sync', (d) => { + if (!d || typeof d.readyMap !== 'object') return; + quizCarryLobbyReadyMap = { ...d.readyMap }; + if (quizCarryPregameActive && isQuizCarry()) { + quizCarrySyncGuestReadyIfNeeded(); + updateQuizCarryPregameHud(); + } + }); + socket.on('quiz-carry-lobby-started', () => { + if (!quizCarryEmbedMissionFlowActive()) return; + endQuizCarryEmbedPregameAndStart(); + }); + socket.on('gauntlet-crown-lobby-sync', (d) => { + if (!d || typeof d.readyMap !== 'object') return; + gauntletCrownLobbyReadyMap = { ...d.readyMap }; + if (gauntletCrownPregamePhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateGauntletCrownHowtoHud(); + } else if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateQuizQuestionMissionHowtoHud(); + } else if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateStackTowerMissionHowtoHud(); + } else if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateJumpSurviveMissionHowtoHud(); + } else if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateSpaceShooterMissionHowtoHud(); + } + }); + socket.on('gauntlet-crown-lobby-started', () => { + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') { + beginQuizQuestionMissionCountdownThenRun(); + return; + } + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') { + beginStackTowerMissionCountdownThenRun(); + return; + } + if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') { + beginJumpSurviveMissionCountdownThenRun(); + return; + } + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') { + beginSpaceShooterMissionCountdownThenRun(); + return; + } + if (!usesCrownLobbyShellPlay()) return; + beginGauntletCrownCountdownThenRun(); + }); + socket.on('user-move', (data) => { + if (data && myId != null && data.id === myId) { + /* Gauntlet: ตำแหน่งอ้างอิงจาก gauntlet-sync + เลอร์ปเท่านั้น — ไม่งั้น echo จาก move จะสแนป me.x/y โดยไม่อัปเดต tx/ty ทำให้เลอร์ปผิดและชนเซิร์ฟเวอร์ไม่ตรง */ + if (mapData && mapData.gameType === 'gauntlet') { + return; + } + if (mapData && mapData.gameType === 'stack') { + return; + } + if (mapData && mapData.gameType === 'jump_survive') { + return; + } + if (mapData && mapData.gameType === 'space_shooter') { + if (data.spaceShooterScore != null) { + const s = Number(data.spaceShooterScore); + if (Number.isFinite(s)) me.spaceShooterScore = Math.max(0, s); + } + return; + } + if (mapData && mapData.gameType === 'balloon_boss') { + if (data.balloonBossScore != null) { + const s = Number(data.balloonBossScore); + if (Number.isFinite(s)) me.balloonBossScore = Math.max(0, s); + } + if (data.balloonBossBossDmg != null) { + const d = Number(data.balloonBossBossDmg); + if (Number.isFinite(d)) me.balloonBossBossDmg = Math.max(0, d); + } + if (data.balloonBossBalloons != null) { + const b = Number(data.balloonBossBalloons); + if (Number.isFinite(b)) me.balloonBossBalloons = Math.max(0, Math.floor(b)); + } + if (data.balloonBossEliminated != null) me.balloonBossEliminated = !!data.balloonBossEliminated; + return; + } + if (mapData && mapData.gameType === 'quiz_battle') { + if (data.x != null) { + const x = Number(data.x); + if (Number.isFinite(x)) me.x = x; + } + if (data.y != null) { + const y = Number(data.y); + if (Number.isFinite(y)) me.y = y; + } + const s = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = s.x; + me.y = s.y; + if (data.direction) me.direction = data.direction; + if (data.characterId != null) me.characterId = data.characterId; + playPath = []; + return; + } + if (data.x != null) { + const x = Number(data.x); + if (Number.isFinite(x)) { + me.x = x; + me.tx = me.x; + } + } + if (data.y != null) { + const y = Number(data.y); + if (Number.isFinite(y)) { + me.y = y; + me.ty = me.y; + } + } + if (data.direction) me.direction = data.direction; + if (data.characterId != null) me.characterId = data.characterId; + playPath = []; + return; + } + const o = others.get(data.id); + if (o) { + if (mapData && mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData) && !isPreviewBotId(data.id)) { + const px = Number(data.x); + const py = Number(data.y); + if (Number.isFinite(px) && Number.isFinite(py)) { + const sn = snapPositionOntoQuizBattlePathIfNeeded(px, py); + o.tx = sn.x; + o.ty = sn.y; + o.x = sn.x; + o.y = sn.y; + } else { + o.tx = data.x; + o.ty = data.y; + } + } else { + o.tx = data.x; + o.ty = data.y; + if (mapData && isQuiz()) { + const px = Number(data.x); + const py = Number(data.y); + if (Number.isFinite(px)) o.x = px; + if (Number.isFinite(py)) o.y = py; + } + } + o.direction = data.direction || o.direction; + if (data.characterId != null) o.characterId = data.characterId; + if (mapData && mapData.gameType === 'space_shooter' && data.spaceShooterScore != null) { + const s = Number(data.spaceShooterScore); + if (Number.isFinite(s)) o.spaceShooterScore = Math.max(0, s); + } + if (mapData && mapData.gameType === 'balloon_boss') { + if (data.balloonBossScore != null) { + const s = Number(data.balloonBossScore); + if (Number.isFinite(s)) o.balloonBossScore = Math.max(0, s); + } + if (data.balloonBossBossDmg != null) { + const d = Number(data.balloonBossBossDmg); + if (Number.isFinite(d)) o.balloonBossBossDmg = Math.max(0, d); + } + if (data.balloonBossBalloons != null) { + const b = Number(data.balloonBossBalloons); + if (Number.isFinite(b)) o.balloonBossBalloons = Math.max(0, Math.floor(b)); + } + if (data.balloonBossEliminated != null) o.balloonBossEliminated = !!data.balloonBossEliminated; + } + } + }); + socket.on('user-left', (data) => { + if (!data || isPreviewBotId(data.id)) return; + others.delete(data.id); + if (playBotsEnabled()) rebalancePreviewBots(); + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud(); + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud(); + if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') updateJumpSurviveMissionHowtoHud(); + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') updateSpaceShooterMissionHowtoHud(); + }); + socket.on('chat', (data) => { + const box = document.getElementById('chat-messages'); + if (!box) return; + const div = document.createElement('div'); + div.className = 'chat-msg'; + div.textContent = (data.nickname || '') + ': ' + (data.text || ''); + box.appendChild(div); + box.scrollTop = 1e9; + }); + + socket.on('quiz-phase', (p) => { + if (previewMode) return; + if (!p || !mapData || !isQuiz()) return; + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase !== 'live' && quizQuestionMissionPhase !== 'ended') { + quizQuestionMissionDeferredPhase = p; + return; + } + applyQuizPhaseFromServer(p); + }); + + socket.on('quiz-result', (r) => { + if (previewMode || !r) return; + if (r.scores) { + playLiveQuizScores = { ...r.scores }; + renderPlayQuizScoreboard(playLiveQuizScores); + } + if (isQuiz() && Array.isArray(r.results)) { + r.results.forEach(function (row) { + if (!row || row.id == null) return; + if (!row.right) { + playQuizEverWrong[String(row.id)] = true; + const o = others.get(row.id); + if (o) { + o.quizCannotTrue = true; + o.quizCannotFalse = true; + } + } + }); + } + if (isQuiz() && Array.isArray(r.results) && r.results.some(function (row) { return row && row.right; })) { + spawnQuizTrueFalseScorePopupPlay(!!r.correctTrue); + } + showPlayQuizFeedback(r); + }); + + socket.on('quiz-player-state', (st) => { + if (previewMode) return; + if (!st) return; + playQuizPlayerLocal = { + cannotTrue: !!st.cannotTrue, + cannotFalse: !!st.cannotFalse, + eliminated: !!st.eliminated, + score: typeof st.score === 'number' ? st.score : (playQuizPlayerLocal.score || 0), + }; + }); + + socket.on('quiz-ended', () => { + if (previewMode && isQuiz()) return; + let questionMissionEnd = null; + if (isQuizQuestionMissionUiMapPlay()) { + questionMissionEnd = quizQuestionMissionBuildPayload(); + } + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + playQuizPhaseLocal = null; + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) ov.classList.add('is-hidden'); + const panel = document.getElementById('quiz-map-question-panel'); + if (panel) { + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + } + playQuizText = ''; + playQuizPhaseEndsAt = 0; + playLiveQuizScores = {}; + hidePlayQuizHud(); + if (questionMissionEnd) { + quizQuestionMissionPhase = 'ended'; + quizQuestionMissionDeferredPhase = null; + applyQuizQuestionMissionPanelImages(); + showGauntletCrownMissionOverlay(questionMissionEnd); + } + }); + + /* ===== Special Quiz ในเกม — socket events ===== */ + socket.on('special-quiz-icon', (payload) => { + if (previewMode) return; + setSpecialQuizIconFromServer(payload); + }); + socket.on('special-quiz-icon-clear', () => { clearSpecialQuizIcon(); }); + socket.on('special-quiz-question', (q) => { + if (previewMode) return; + openSpecialQuizQuestion(q); + }); + socket.on('special-quiz-answer-update', (d) => { + if (previewMode || !d || !specialQuizActiveQuestion) return; + markSpecialQuizMemberAnswered(d.id); + }); + socket.on('special-quiz-result', (r) => { + if (previewMode) return; + handleSpecialQuizResult(r); + }); + socket.on('special-quiz-ended', (data) => { + if (previewMode) return; + endSpecialQuiz(data); + }); + socket.on('special-card-applied', (data) => { + if (previewMode || !data || !data.card) return; + applySpecialCardClient(data); + }); + + socket.on('gauntlet-sync', (data) => { + if (!data || typeof data !== 'object') return; + if (isMegaVirusMissionShellMapPlay()) { + applyGauntletTimingFromServer(data); + return; + } + if (!mapData || mapData.gameType !== 'gauntlet') return; + applyGauntletTimingFromServer(data); + if (Array.isArray(data.obstacles)) { + gauntletObstacles = data.obstacles; + pushGauntletObsRenderFrame(data.obstacles); + } + if (!Array.isArray(data.players)) return; + data.players.forEach((p) => { + if (!p || p.id == null || myId == null) return; + const pid = String(p.id); + const mid = String(myId); + if (pid === mid) { + const prevSc = Math.max(0, Number(me.gauntletScore) || 0); + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) me.tx = px; + if (Number.isFinite(py)) me.ty = py; + if (p.direction) me.direction = p.direction; + const jt = Number(p.gauntletJumpTicks); + meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + meGauntletJumpVis = meGauntletJumpTicks; + const sc = Number(p.gauntletScore); + const newSc = Number.isFinite(sc) ? Math.max(0, sc) : 0; + me.gauntletScore = newSc; + if (isGauntletCrownHeistMapPlay() && prevSc - newSc >= 10) { + me.gauntletCrownPenaltyFxUntil = Date.now() + 1100; + } + me.gauntletEliminated = !!p.gauntletEliminated; + clampGauntletCrownHeistEntityXInPlacePlay(me); + } else { + const o = others.get(p.id) ?? others.get(pid); + if (o) { + const prevSc = Math.max(0, Number(o.gauntletScore) || 0); + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) o.tx = px; + if (Number.isFinite(py)) o.ty = py; + if (p.direction) o.direction = p.direction; + const jt = Number(p.gauntletJumpTicks); + o.gauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks; + const sc = Number(p.gauntletScore); + const newSc = Number.isFinite(sc) ? Math.max(0, sc) : 0; + o.gauntletScore = newSc; + if (isGauntletCrownHeistMapPlay() && prevSc - newSc >= 10) { + o.gauntletCrownPenaltyFxUntil = Date.now() + 1100; + } + o.gauntletEliminated = !!p.gauntletEliminated; + clampGauntletCrownHeistEntityXInPlacePlay(o); + } + } + }); + if (playBotsEnabled()) { + applyGauntletPreviewBotsAfterSync(); + emitGauntletPreviewRowsToServer(); + } + }); + + socket.on('gauntlet-ended', (data) => { + cancelQuizCarryResultEndAfterTimeup(); + gauntletEndsAtMs = null; + gauntletCrownRunwayBgFinishLatched = false; + gauntletCrownRunwayBgStripFreezeSinceMs = 0; + gauntletCrownRunwayClientMissionShown = false; + gauntletCrownRunHeldRemote = false; + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + resetGauntletCrownRunwaySpawnScrollSnap(); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc = document.getElementById('gauntlet-crown-countdown'); + if (gcc) gcc.classList.add('is-hidden'); + const hto = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto) hto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + if (data && data.crownMission) { + showGauntletCrownMissionOverlay(data.crownMission); + return; + } + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) { + titleEl.textContent = data && data.reason === 'time' ? 'หมดเวลา · Time up' : 'เกมจบ · Game over'; + } + msgEl.textContent = (data && data.message) || 'เกมพรมแดงจบแล้ว'; + listEl.innerHTML = ''; + const ranks = (data && data.rankings) || []; + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + }); + + socket.on('game-start', (ev) => { + if (!ev || !ev.mapId) return; + const urlMap = (params.get('map') || '').trim(); + if (urlMap && ev.stayInRoomLobby && String(ev.mapId).trim() !== urlMap) { + return; + } + const applySnap = (md) => { + mapData = md; + playEmbedUserZoomMul = 1; + if (ev.mapId != null && String(ev.mapId).trim() !== '') { + playSessionMapId = String(ev.mapId).trim(); + } + applyPlayEmbedZoomForCurrentMapPlay(); + if (!isQuizQuestionMissionUiMapPlay()) { + quizQuestionMissionPhase = null; + quizQuestionMissionDeferredPhase = null; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + } + if (!isStackTowerMissionUiMapPlay()) { + teardownStackTowerMissionUiPlay(); + } + if (mapData.gameType !== 'space_shooter') { + spaceShooterGameEnded = false; + spaceShooterMissionPhase = null; + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + } + if (mapData.gameType !== 'balloon_boss') { + balloonBossGameEnded = false; + } + if (!mapData.lanes && mapData.gameType === 'frogger') mapData.lanes = []; + tileSize = mapData.tileSize || 32; + gauntletObstacles = []; + gauntletObsRenderPrev = []; + gauntletObsRenderNext = []; + gauntletObsBlendT0 = 0; + mapBackgroundImg = null; + if (mapData.backgroundImage) { + mapBackgroundImg = new Image(); + mapBackgroundImg.src = mapData.backgroundImage; + } + reloadPlayScrollBgFromMap(); + reloadStackTowerScrollBgFromMap(); + reloadGauntletCrownRunwayBgFromMap(); + normalizeGridImageCellsOnMap(mapData); + loadMapGridImages(); + if (mapData.gameType === 'gauntlet' && ev.peersSnap && Array.isArray(ev.peersSnap)) { + ev.peersSnap.forEach((p) => { + if (p.id != null && myId != null && String(p.id) === String(myId)) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) { me.x = px; me.tx = px; } + if (Number.isFinite(py)) { me.y = py; me.ty = py; } + me.direction = p.direction || me.direction; + const jt = Number(p.gauntletJumpTicks); + meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + meGauntletJumpVis = meGauntletJumpTicks; + const sc = Number(p.gauntletScore); + me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + me.gauntletEliminated = !!p.gauntletEliminated; + } else { + const o = others.get(p.id) ?? others.get(String(p.id)); + if (o) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) { o.x = px; o.tx = px; } + if (Number.isFinite(py)) { o.y = py; o.ty = py; } + o.direction = p.direction || o.direction; + const jt = Number(p.gauntletJumpTicks); + o.gauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + o.gauntletJumpVis = o.gauntletJumpTicks; + const sc = Number(p.gauntletScore); + o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + o.gauntletEliminated = !!p.gauntletEliminated; + } + } + }); + } else { + meGauntletJumpTicks = 0; + meGauntletJumpVis = 0; + me.gauntletScore = 0; + me.gauntletEliminated = false; + others.forEach((o) => { + o.gauntletJumpTicks = 0; + o.gauntletJumpVis = 0; + o.gauntletScore = 0; + o.gauntletEliminated = false; + }); + } + if (mapData.gameType === 'gauntlet') { + if (ev.gauntletEndsAt != null) { + const ge = Number(ev.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(ge) ? ge : null; + } else { + gauntletEndsAtMs = null; + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => {}); + if (String(ev.mapId || '') === GAUNTLET_FACE_RIGHT_MAP_ID) { + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc0 = document.getElementById('gauntlet-crown-countdown'); + if (gcc0) gcc0.classList.add('is-hidden'); + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60); + } else { + const hto2 = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto2) hto2.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc1 = document.getElementById('gauntlet-crown-countdown'); + if (gcc1) gcc1.classList.add('is-hidden'); + } + } else { + gauntletEndsAtMs = null; + const megaShellGs = mapData.gameType === 'balloon_boss' && String(ev.mapId || '').trim() === BALLOON_BOSS_MISSION_MAP_ID; + const skipHideCrownShell = (mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay()) + || (mapData.gameType === 'space_shooter' && isSpaceShooterMissionUiMapPlay()) + || (mapData.gameType === 'quiz' && isQuizQuestionMissionUiMapPlay()) + || (mapData.gameType === 'stack' && isStackTowerMissionUiMapPlay()) + || megaShellGs; + if (!skipHideCrownShell) { + const hto3 = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto3) hto3.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + } + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc2 = document.getElementById('gauntlet-crown-countdown'); + if (gcc2) gcc2.classList.add('is-hidden'); + } + if (mapData.gameType !== 'gauntlet') { + me.tx = null; + me.ty = null; + } + const gauntletOv = document.getElementById('gauntlet-ended-overlay'); + if (gauntletOv) gauntletOv.classList.add('is-hidden'); + const gcmOv = document.getElementById('gauntlet-crown-mission-overlay'); + if (gcmOv) gcmOv.classList.add('is-hidden'); + if (mapData.gameType === 'stack') { + if (!mapData.stackReleaseArea) mapData.stackReleaseArea = []; + if (!mapData.stackLandArea) mapData.stackLandArea = []; + rebalancePreviewBots(); + lastStackTickMs = performance.now(); + resetStackMinigameState(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + else reapplyStackMiniSizingFromGlobals(); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => { + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }); + if (isStackTowerMissionUiMapPlay()) { + stackTowerMissionEndedOnce = false; + stackTowerMissionDeferredPhase = null; + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + stackTowerMissionPhase = 'howto'; + stackTowerSessionStartAt = 0; + hideConflictingOverlaysForGauntletCrown(); + applyStackTowerMissionPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + setTimeout(function () { showStackTowerMissionHowtoOverlay(); }, 60); + } else { + teardownStackTowerMissionUiPlay(); + } + } else { + stackMini = null; + teardownStackTowerMissionUiPlay(); + } + if (mapData.gameType === 'jump_survive') { + if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = []; + if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = []; + if (!mapData.jumpSurviveHazardArea) mapData.jumpSurviveHazardArea = []; + if (!mapData.jumpSurvivePlatformVariantArea) mapData.jumpSurvivePlatformVariantArea = []; + normalizeJumpSurvivePlatformAreaInPlay(mapData); + normalizeJumpSurvivePlatformVariantAreaInPlay(mapData); + normalizeJumpSurviveHazardAreaInPlay(mapData); + normalizeShooterSpawnSlotsInPlay(mapData); + jumpSurviveEliminated = false; + jumpSurviveGameEnded = false; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + if (isJumpSurviveMissionUiMapPlay()) { + jumpSurviveMissionPhase = 'howto'; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + setTimeout(function () { showJumpSurviveMissionHowtoOverlay(); }, 60); + } else { + jumpSurviveMissionPhase = null; + jumpSurviveInitRuntime(); + } + } + if (mapData.gameType === 'space_shooter') { + normalizeShooterSpawnSlotsInPlay(mapData); + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + if (isSpaceShooterMissionUiMapPlay()) { + spaceShooterMissionPhase = 'howto'; + hideConflictingOverlaysForGauntletCrown(); + applySpaceShooterMissionPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + setTimeout(function () { showSpaceShooterMissionHowtoOverlay(); }, 60); + } else { + spaceShooterMissionPhase = null; + spaceShooterGameEnded = false; + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterAsteroidExplosions = []; + spaceShooterPopups = []; + spaceShooterLastTickMs = performance.now(); + spaceShooterSpawnAccMs = 0; + spaceShooterFireCd = 0; + spaceShooterSessionStartMs = performance.now(); + spaceShooterLastMoveEmit = 0; + } + } + if (mapData.gameType === 'quiz') { + if (!mapData.quizTrueArea) mapData.quizTrueArea = []; + if (!mapData.quizFalseArea) mapData.quizFalseArea = []; + if (!mapData.quizQuestionArea) mapData.quizQuestionArea = []; + } + if (mapData.gameType === 'balloon_boss') { + balloonBossGameEnded = false; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossHitFx = []; + balloonBossScorePopups = []; + balloonBossLastTickMs = performance.now(); + balloonBossPlayerFireCd = 0; + balloonBossLastMoveEmit = 0; + balloonBossBossFireAcc = 0; + me.balloonBossScore = 0; + me.balloonBossBossDmg = 0; + me.balloonBossBalloons = balloonBossBalloonsStartPlay(); + me.balloonBossEliminated = false; + others.forEach((o) => { + o.balloonBossScore = 0; + o.balloonBossBossDmg = 0; + o.balloonBossBalloons = balloonBossBalloonsStartPlay(); + o.balloonBossEliminated = false; + }); + if (String(ev.mapId || '').trim() === BALLOON_BOSS_MISSION_MAP_ID) { + balloonBossSessionStartMs = 0; + if (Object.prototype.hasOwnProperty.call(ev, 'gauntletCrownRunHeld')) { + applyGauntletTimingFromServer({ gauntletCrownRunHeld: ev.gauntletCrownRunHeld }); + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + if (Object.prototype.hasOwnProperty.call(ev, 'gauntletCrownRunHeld')) { + applyGauntletTimingFromServer({ gauntletCrownRunHeld: ev.gauntletCrownRunHeld }); + } + }) + .catch(() => {}); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccMv = document.getElementById('gauntlet-crown-countdown'); + if (gccMv) gccMv.classList.add('is-hidden'); + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60); + } else { + balloonBossSessionStartMs = performance.now(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + } + } + if (mapData.gameType === 'quiz_battle') { + if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = []; + if (!mapData.quizBattlePathArea) mapData.quizBattlePathArea = []; + normalizeQuizBattlePathInPlay(mapData); + normalizeQuizBattleDomeInPlay(mapData); + if (quizBattlePathModeActive(mapData)) { + const ms = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = ms.x; + me.y = ms.y; + others.forEach((o) => { + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + }); + } + } + var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน' + : (isGauntlet() ? ' | พรมแดงสุดท้าย (Last Light)' + : (isLobby() ? ' | โถงรอ' : (isQuiz() ? ' | ตอบคำถาม' : (isQuizCarry() ? ' | หยิบคำตอบมาวางกลาง' : (isQuizBattle() ? ' | Quiz Battle · โดม E' : (isStack() ? ' | Stack ซ้อนตึก' : (isJumpSurvive() ? ' | กระโดดให้รอด' : (isSpaceShooter() ? ' | ยิงยานอวกาศ' : (isBalloonBoss() ? ' | ลูกโป้งยิงบอส' : ''))))))))); + var prevTag = previewMode ? '[ทดสอบ] ' : ''; + document.getElementById('room-id').textContent = prevTag + spaceId + modeLabel; + if (isQuiz()) { + setupPlayQuizUi(); + if (isQuizQuestionMissionUiMapPlay()) { + quizQuestionMissionDeferredPhase = null; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + quizQuestionMissionPhase = 'howto'; + hideConflictingOverlaysForGauntletCrown(); + applyQuizQuestionMissionPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + setTimeout(function () { showQuizQuestionMissionHowtoOverlay(); }, 60); + } + } else if (isQuizCarry()) setupPlayQuizCarryUi(); + else if (isQuizBattle()) setupPlayQuizBattleUi(); + else teardownPlayQuizUi(); + resizeCanvas(); + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + if (playBotsEnabled() && mapData.gameType === 'gauntlet') { + applyGauntletPreviewSpawnLayout(true); + emitGauntletPreviewRowsToServer(); + } + if (playBotsEnabled() && mapData.gameType === 'jump_survive') { + applyJumpSurvivePreviewSpawnLayout(true); + } + if (playBotsEnabled() && mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (playBotsEnabled() && mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + }; + fetch(BASE + '/api/maps/' + encodeURIComponent(ev.mapId)) + .then((r) => (r.ok ? r.json() : null)) + .then((md) => { if (md) applySnap(md); }); + }); + + function resizeCanvas() { + if (mapData && isStackTowerMissionUiMapPlay() && canvas) { + canvas.width = Math.max(320, STACK_TOWER_FIXED_RENDER_W); + canvas.height = Math.max(240, STACK_TOWER_FIXED_RENDER_H); + /* รีคำนวน BG scroll หลังขนาดบัฟเฟอร์คงที่ — กันรูปโหลดเร็วตอนแคนวาสยังไม่ล็อกแล้วไม่ซิงก์ใหม่ */ + stackTowerScrollBgSrawWorldBaselinePlay = null; + stackTowerScrollBgSyncInitialScrollToBottom(); + syncQuizCarryEmbedCountdownLayout(); + return; + } + const vw = window.innerWidth || document.documentElement.clientWidth || 800; + const vh = window.innerHeight || document.documentElement.clientHeight || 600; + const header = document.querySelector('.game-header'); + const headerH = playHideTopChrome() ? 0 : (header && header.offsetHeight ? header.offsetHeight : 48); + const stage = document.getElementById('play-canvas-stage'); + const stackEl = document.getElementById('play-canvas-stack'); + 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); + } + /* embed: stage อาจยัง 0 แต่ stack วัดได้แล้ว — อย่าให้ fallback เป็น vw/vh เต็มจอแล้ว CSS ย่อเข้า stage = ดูชิดมุม */ + if (previewMode && editorEmbedReturn && stackEl) { + const sw = Math.max(stackEl.clientWidth || 0, stackEl.offsetWidth || 0); + const sh = Math.max(stackEl.clientHeight || 0, stackEl.offsetHeight || 0); + if (sw > 80) cw = Math.max(cw, sw); + if (sh > 80) ch = Math.max(ch, sh); + } + if (ch < 120) ch = Math.max(240, vh - headerH - 6); + if (cw < 120) { + if (previewMode && editorEmbedReturn && stackEl) { + const sw = Math.max(stackEl.clientWidth || 0, stackEl.offsetWidth || 0); + cw = sw > 80 ? Math.max(320, sw) : Math.max(320, vw); + } else cw = Math.max(320, vw); + } + canvas.width = Math.max(320, cw); + canvas.height = Math.max(240, ch); + syncQuizCarryEmbedCountdownLayout(); + } + + canvas.addEventListener('click', (e) => { + 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; + if (isStackTowerEmbedZoomLockedPlay()) return; + if (isQuizQuestionMissionHudActivePlay()) 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 }); + })(); + + /* กด/แตะไอคอนควิซพิเศษ — ทำงานทุกมินิเกม (กันเคสเดินไปแตะไม่ได้ เช่น space shooter) */ + canvas.addEventListener('pointerdown', (e) => { + if (trySpecialQuizClickAt(e.clientX, e.clientY)) { + e.preventDefault(); + e.stopPropagation(); + } + }, true); + + canvas.addEventListener('dblclick', (e) => { + if (!mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss() || isChatFocused() || isQuizCarryEmbedCountdownBlockingMovement()) return; + const r = canvas.getBoundingClientRect(); + const sx = e.clientX - r.left; + const sy = e.clientY - r.top; + const qmC = isQuizQuestionMissionHudActivePlay() ? getQuizQuestionMissionMapCenterWorldPxPlay() : null; + const carryCam = !qmC && isQuizCarry() ? getQuizCarryMapCameraWorldCenterPxPlay() : null; + const camX = qmC ? qmC.cx : (carryCam ? carryCam.cx : me.x * tileSize); + const camY = qmC ? qmC.cy : (carryCam ? carryCam.cy : me.y * tileSize); + 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; + if (tx < 0 || tx >= mw || ty < 0 || ty >= mh) return; + if (!canWalkLikeLobby(tx + 0.5, ty + 0.5)) return; + const path = pathfindPlay(me.x, me.y, tx + 0.5, ty + 0.5); + if (path.length <= 1) return; + playPath = path.slice(1); + }); + + function getCharacterFootprintWH(md) { + if (!md) return { cw: 1, ch: 1 }; + let cw = Math.floor(Number(md.characterCellsW)); + let ch = Math.floor(Number(md.characterCellsH)); + const hasW = Number.isFinite(cw) && cw >= 1; + const hasH = Number.isFinite(ch) && ch >= 1; + if (hasW && hasH) { + return { cw: Math.max(1, Math.min(4, cw)), ch: Math.max(1, Math.min(4, ch)) }; + } + const leg = Math.max(1, Math.min(4, Math.floor(Number(md.characterCells)) || 1)); + return { cw: leg, ch: leg }; + } + + /** Quiz Battle: กล่องชน/เส้นทางตามสไปรต์ที่วาดจริง (drawAvatar ใช้ cw×1.15, ch×1.35 จากเท้า) */ + const QUIZ_BATTLE_SPRITE_COL_SCALE_W = 1.15; + const QUIZ_BATTLE_SPRITE_COL_SCALE_H = 1.35; + + function quizBattleSpriteBoundsTilesPlay(md, px, py) { + const s = new Set(); + if (!md || md.gameType !== 'quiz_battle') return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const w = md.width || 20, h = md.height || 15; + const { cw, ch } = getCharacterFootprintWH(md); + const cx = px + cw * 0.5; + const feetY = py + ch; + const left = cx - (cw * QUIZ_BATTLE_SPRITE_COL_SCALE_W) / 2; + const right = cx + (cw * QUIZ_BATTLE_SPRITE_COL_SCALE_W) / 2; + const top = feetY - ch * QUIZ_BATTLE_SPRITE_COL_SCALE_H; + const bottom = feetY; + if (left < 0 || right > w || top < 0 || bottom > h) return s; + const minTx = Math.floor(left); + const maxTx = Math.min(w - 1, Math.floor(right - 1e-6)); + const minTy = Math.floor(top); + const maxTy = Math.min(h - 1, Math.floor(bottom - 1e-6)); + for (let ty = minTy; ty <= maxTy; ty++) { + for (let tx = minTx; tx <= maxTx; tx++) s.add(tx + ',' + ty); + } + return s; + } + + /** + * ขนาดช่องที่ใช้ชนกำแพง (objects=1) เท่านั้น — ชิดเท้า (ล่าง) + กลางแนวนอน · เล็กกว่า footprint = ส่วนบนไม่ติดกำแพง + */ + function getCharacterCollisionFootprintWH(md) { + const { cw, ch } = getCharacterFootprintWH(md); + let colW = Math.floor(Number(md.characterCollisionW)); + let colH = Math.floor(Number(md.characterCollisionH)); + if (!Number.isFinite(colW) || colW < 1) colW = cw; + if (!Number.isFinite(colH) || colH < 1) colH = ch; + colW = Math.max(1, Math.min(cw, colW)); + colH = Math.max(1, Math.min(ch, colH)); + return { cw, ch, colW, colH }; + } + + function getGauntletCrownHeistMaxEntityXPlay(md) { + if (!md || !isGauntletCrownHeistMapPlay()) return null; + const w = md.width || 20; + const { cw } = getCharacterFootprintWH(md); + const capRight = GAUNTLET_CROWN_HEIST_MAX_X_WORLD_FRAC * w - cw; + const fullMapMax = Math.max(0, w - cw + 0.999); + return Math.max(0, Math.min(fullMapMax, capRight)); + } + function clampGauntletCrownHeistEntityXInPlacePlay(ent) { + if (!ent || !mapData || !isGauntletCrownHeistMapPlay()) return; + const mx = getGauntletCrownHeistMaxEntityXPlay(mapData); + if (mx == null || !Number.isFinite(mx)) return; + if (Number.isFinite(ent.x)) ent.x = Math.max(0, Math.min(mx, ent.x)); + if (ent.tx != null && Number.isFinite(ent.tx)) ent.tx = Math.max(0, Math.min(mx, ent.tx)); + } + + /** กันตัวละคร footprint ล้นออกนอกแมป — เดิมใช้ w-0.01 ทำให้ cw/ch>1 ชนขอบแล้วตรรกะเดินค้าง */ + function clampPlayEntityFootprintToMap(oEnt, md) { + if (!oEnt || !md) return; + const w = md.width || 20, h = md.height || 15; + const { cw, ch } = getCharacterFootprintWH(md); + let maxX = Math.max(0, w - cw + 0.999); + const crownMx = getGauntletCrownHeistMaxEntityXPlay(md); + if (crownMx != null) maxX = Math.min(maxX, crownMx); + const maxY = Math.max(0, h - ch + 0.999); + if (!Number.isFinite(oEnt.x)) oEnt.x = 0.5; + if (!Number.isFinite(oEnt.y)) oEnt.y = 0.5; + oEnt.x = Math.max(0, Math.min(maxX, oEnt.x)); + oEnt.y = Math.max(0, Math.min(maxY, oEnt.y)); + } + + /** แยกบอท preview ที่ทับกันบนจุดเดียว (มักชนกันที่ขอบแมป) */ + function separateClumpedPreviewBots() { + if (!playBotsEnabled() || !mapData) return; + const ids = [...others.keys()].filter(isPreviewBotId); + const minD = 0.52; + const pushAmt = MOVE_SPEED * 0.5; + for (let i = 0; i < ids.length; i++) { + for (let j = i + 1; j < ids.length; j++) { + const a = others.get(ids[i]); + const b = others.get(ids[j]); + if (!a || !b) continue; + let ddx = b.x - a.x, ddy = b.y - a.y; + let d = Math.sqrt(ddx * ddx + ddy * ddy); + if (d >= minD) continue; + if (d < 1e-5) { + ddx = (i + j * 2) % 2 === 0 ? 1 : -1; + ddy = ((i + j) % 3) - 1; + d = Math.sqrt(ddx * ddx + ddy * ddy) || 1; + } + const ux = -ddx / d, uy = -ddy / d; + const vx = ddx / d, vy = ddy / d; + const ax2 = a.x + ux * pushAmt, ay2 = a.y + uy * pushAmt; + const bx2 = b.x + vx * pushAmt, by2 = b.y + vy * pushAmt; + if (canWalkLikeLobbyForBot(ax2, ay2, a.x, a.y, a)) { a.x = ax2; a.y = ay2; } + if (canWalkLikeLobbyForBot(bx2, by2, b.x, b.y, b)) { b.x = bx2; b.y = by2; } + clampPlayEntityFootprintToMap(a, mapData); + clampPlayEntityFootprintToMap(b, mapData); + } + } + } + + function quizTilesFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch } = getCharacterFootprintWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const maxTx = Math.min(w - 1, minTx + cw - 1); + const maxTy = Math.min(h - 1, minTy + ch - 1); + for (let ty = minTy; ty <= maxTy; ty++) { + for (let tx = minTx; tx <= maxTx; tx++) { + if (tx >= 0 && ty >= 0) s.add(tx + ',' + ty); + } + } + return s; + } + + /** ร่าง (cw×ch) ของผู้เล่นที่ (px,py) ครอบคลุมช่อง (tx,ty) หรือไม่ — ใช้กับ blockPlayer แทนการเช็คแค่มุมซ้ายบน */ + function playEntityFootprintContainsTile(px, py, tx, ty) { + return quizTilesFootprintPlay(px, py).has(tx + ',' + ty); + } + + /** + * กล่องตรวจโซนห้ามซ้อน (blockPlayer) — วางเหมือนชนกำแพง: กลางแนวนอน + ชิดเท้า + * blockPlayerSeparationW/H: 0 = ใช้เต็มความกว้าง/สูงตัว · ค่าอื่น = จำกัด 1..cw / 1..ch + * รองรับ legacy blockPlayerSeparation เมื่อยังไม่มี W/H ใน JSON + */ + function resolveBlockPlayerNoOverlapBoxWH(md) { + const m = md || mapData; + if (!m) return { cw: 1, ch: 1, boxW: 1, boxH: 1 }; + const { cw, ch } = getCharacterFootprintWH(m); + let w = m.blockPlayerSeparationW != null && m.blockPlayerSeparationW !== '' ? Math.floor(Number(m.blockPlayerSeparationW)) : NaN; + let h = m.blockPlayerSeparationH != null && m.blockPlayerSeparationH !== '' ? Math.floor(Number(m.blockPlayerSeparationH)) : NaN; + const leg = m.blockPlayerSeparation != null && m.blockPlayerSeparation !== '' ? Math.floor(Number(m.blockPlayerSeparation)) : NaN; + const hasW = Number.isFinite(w) && w >= 0; + const hasH = Number.isFinite(h) && h >= 0; + if (!hasW && !hasH && Number.isFinite(leg) && leg >= 1) { + w = Math.min(leg, cw); + h = Math.min(leg, ch); + } else { + if (!hasW) w = 0; + if (!hasH) h = 0; + } + const boxW = !w || w <= 0 ? cw : Math.max(1, Math.min(cw, w)); + const boxH = !h || h <= 0 ? ch : Math.max(1, Math.min(ch, h)); + return { cw, ch, boxW, boxH }; + } + + /** ช่องกริดที่ถือว่า “ผู้เล่นที่ (px,py) กินพื้นที่” สำหรับตรวจโซน blockPlayer — กล่องเดียวกับแนวชนกำแพง */ + function quizTilesBlockPlayerPeerFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch, boxW, boxH } = resolveBlockPlayerNoOverlapBoxWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const offX = minTx + Math.floor((cw - boxW) / 2); + const offY = minTy + (ch - boxH); + for (let ty = offY; ty < offY + boxH; ty++) { + for (let tx = offX; tx < offX + boxW; tx++) { + if (tx >= 0 && ty >= 0 && tx < w && ty < h) s.add(tx + ',' + ty); + } + } + return s; + } + + function playPeerBlockPlayerOccupiesTile(px, py, tx, ty) { + return quizTilesBlockPlayerPeerFootprintPlay(px, py).has(tx + ',' + ty); + } + + /** Footprint ชนกำแพง (objects=1) เท่านั้น — hub / interactive / blockPlayer / quiz ใช้ quizTilesFootprintPlay = เต็ม characterCells */ + function quizTilesWallCollisionFootprintPlay(px, py) { + if (mapData && mapData.gameType === 'quiz_battle') { + return quizBattleSpriteBoundsTilesPlay(mapData, px, py); + } + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch, colW, colH } = getCharacterCollisionFootprintWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const offX = minTx + Math.floor((cw - colW) / 2); + const offY = minTy + (ch - colH); + for (let ty = offY; ty < offY + colH; ty++) { + for (let tx = offX; tx < offX + colW; tx++) { + if (tx >= 0 && ty >= 0 && tx < w && ty < h) s.add(tx + ',' + ty); + } + } + return s; + } + + function quizAnswerTileForbiddenForLock(lock, tx, ty) { + if (!lock || lock.eliminated) return false; + if (!mapData) return false; + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + if (lock.cannotTrue && qt && qt[ty] && qt[ty][tx] === 1) return true; + if (lock.cannotFalse && qf && qf[ty] && qf[ty][tx] === 1) return true; + return false; + } + + function quizAnswerTileForbiddenPlay(tx, ty) { + if (!playQuizPlayerLocal) return false; + return quizAnswerTileForbiddenForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, tx, ty); + } + + /** ยืนทับโซนต้องห้ามเมื่อโดนล็อก — ใช้กับ pathfind ปลายทาง / คลิก */ + function quizLockFootprintBlocksForLock(lock, px, py) { + if (!mapData || !isQuiz() || !lock || lock.eliminated) return false; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const txi = +p[0], tyi = +p[1]; + if (quizAnswerTileForbiddenForLock(lock, txi, tyi)) return true; + } + return false; + } + + function quizLockFootprintBlocksPlay(px, py) { + if (!playQuizPlayerLocal) return false; + return quizLockFootprintBlocksForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, px, py); + } + + /** บล็อกเฉพาะการ «เข้า» ช่องตอบใหม่ — ให้เดินออกจากโซนได้ถ้าเคยยืนผิดแล้ว */ + function quizLockWouldEnterForbiddenForLock(lock, ox, oy, nx, ny) { + if (!mapData || !isQuiz() || !lock || lock.eliminated) return false; + const fromS = quizTilesFootprintPlay(ox, oy); + const toS = quizTilesFootprintPlay(nx, ny); + for (const k of toS) { + if (fromS.has(k)) continue; + const p = k.split(','); + const txi = +p[0], tyi = +p[1]; + if (quizAnswerTileForbiddenForLock(lock, txi, tyi)) return true; + } + return false; + } + + function quizLockWouldEnterForbiddenPlay(ox, oy, nx, ny) { + if (!playQuizPlayerLocal) return false; + return quizLockWouldEnterForbiddenForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, ox, oy, nx, ny); + } + + function botQuizLock(o) { + return { + cannotTrue: !!(o && o.quizCannotTrue), + cannotFalse: !!(o && o.quizCannotFalse), + eliminated: false, + }; + } + + /** Same walkability as room-lobby `canWalkLobby` (LobbyA / hall). */ + function canWalkLikeLobby(x, y, fromX, fromY) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + const wallTiles = quizTilesWallCollisionFootprintPlay(x, y); + if (wallTiles.size === 0) return false; + for (const k of wallTiles) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (!isQuizBattle()) { + const { colW, colH } = getCharacterCollisionFootprintWH(mapData); + if (wallTiles.size < colW * colH) return false; + } + const bp = mapData.blockPlayer; + if (bp) { + for (const k of quizTilesFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!bp[ty] || bp[ty][tx] !== 1) continue; + for (const [, o] of others) { + if (playPeerBlockPlayerOccupiesTile(o.x, o.y, tx, ty)) return false; + } + } + } + if (isQuiz() && playQuizPlayerLocal && !playQuizPlayerLocal.eliminated) { + const hasFrom = typeof fromX === 'number' && typeof fromY === 'number' && !Number.isNaN(fromX) && !Number.isNaN(fromY); + if (hasFrom) { + if (quizLockWouldEnterForbiddenPlay(fromX, fromY, x, y)) return false; + } else if (quizLockFootprintBlocksPlay(x, y)) { + return false; + } + } + if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false; + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + if (!quizBattlePositionAllowedPlay(mapData, x, y)) return false; + } + return true; + } + + function canWalkLikeLobbyForBot(x, y, fromX, fromY, o) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + const wallTilesB = quizTilesWallCollisionFootprintPlay(x, y); + for (const k of wallTilesB) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (wallTilesB.size === 0) return false; + if (!isQuizBattle()) { + const { colW, colH } = getCharacterCollisionFootprintWH(mapData); + if (wallTilesB.size < colW * colH) return false; + } + const bp = mapData.blockPlayer; + if (bp) { + for (const k of quizTilesFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!bp[ty] || bp[ty][tx] !== 1) continue; + for (const [, peer] of others) { + if (o && peer === o) continue; + if (playPeerBlockPlayerOccupiesTile(peer.x, peer.y, tx, ty)) return false; + } + if (o && playPeerBlockPlayerOccupiesTile(me.x, me.y, tx, ty)) return false; + } + } + if (isQuiz()) { + const lock = botQuizLock(o); + if (lock.cannotTrue || lock.cannotFalse) { + const hasFrom = typeof fromX === 'number' && typeof fromY === 'number' && !Number.isNaN(fromX) && !Number.isNaN(fromY); + if (hasFrom) { + if (quizLockWouldEnterForbiddenForLock(lock, fromX, fromY, x, y)) return false; + } else if (quizLockFootprintBlocksForLock(lock, x, y)) { + return false; + } + } + } + if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false; + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + if (!quizBattlePositionAllowedPlay(mapData, x, y)) return false; + } + return true; + } + + /** ถ้าสปอว์น/โหลดมาทับโซน hub ให้หาจุดใกล้ ๆ ที่ footprint ไม่ทับ hub */ + function snapPositionOutOfQuizCarryHubIfNeeded(x, y) { + if (!mapData || !isQuizCarry()) return { x, y }; + if (!quizCarryFootprintOverlapsHub(x, y)) return { x, y }; + const w = mapData.width || 20, h = mapData.height || 15; + const maxR = Math.max(w, h) + 6; + for (let r = 1; r <= maxR; r++) { + for (let dy = -r; dy <= r; dy++) { + for (let dx = -r; dx <= r; dx++) { + if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue; + const nx = x + dx * 0.45; + const ny = y + dy * 0.45; + if (nx < 0 || ny < 0 || nx > w - 0.02 || ny > h - 0.02) continue; + if (quizCarryFootprintOverlapsHub(nx, ny)) continue; + if (!canWalkLikeLobby(nx, ny, NaN, NaN)) continue; + return { x: nx, y: ny }; + } + } + } + return { x, y }; + } + + function stepPreviewBotAlongPath(o, w, h) { + const path = o.botPath; + if (!path || !path.length) return; + const way = path[0]; + const dx = way.x - o.x, dy = way.y - o.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= PATH_ARRIVE_THRESH) { + path.shift(); + while (path.length > 0) { + const w2 = path[0]; + const ux = w2.x - o.x, uy = w2.y - o.y; + if (Math.sqrt(ux * ux + uy * uy) > PATH_ARRIVE_THRESH) break; + path.shift(); + } + clampPlayEntityFootprintToMap(o, mapData); + return; + } + const len = dist || 1; + const carryWalk = isQuizCarry() ? quizCarryWalkSpeedMultActive : 1; + const baseEmbedCarryPathMul = 0.74; + const pathStepMul = (previewFillBots && editorEmbedReturn) + ? (isQuizCarry() ? baseEmbedCarryPathMul * (quizCarryWalkSpeedMultActive / QUIZ_CARRY_WALK_SPEED_MULT) : 0.56) + : 1; + const step = Math.min(MOVE_SPEED * 1.28 * pathStepMul * carryWalk, len); + const nx = o.x + (dx / len) * step; + const ny = o.y + (dy / len) * step; + if (Math.abs(dy) > Math.abs(dx)) o.direction = dy > 0 ? 'down' : 'up'; + else if (Math.abs(dx) > 1e-6) o.direction = dx > 0 ? 'right' : 'left'; + const ox = o.x, oy = o.y; + const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData); + if (pathStrict) { + const moved = quizBattleTryStepMovePlay(o.x, o.y, nx, ny, (px, py, fx, fy) => canWalkLikeLobbyForBot(px, py, fx, fy, o)); + o.x = moved.x; + o.y = moved.y; + } else if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + /* เส้นตรงไป waypoint อาจตัดมุม hub / block — ลองก้าวแนวแกนตามทิศหลักอย่างใดอย่างหนึ่ง */ + const cstep = Math.min(MOVE_SPEED * 1.2 * pathStepMul * carryWalk, Math.max(Math.abs(dx), Math.abs(dy), 1e-4)); + const sx = dx > 1e-4 ? 1 : dx < -1e-4 ? -1 : 0; + const sy = dy > 1e-4 ? 1 : dy < -1e-4 ? -1 : 0; + const tryAxisX = () => { + if (sx === 0) return false; + if (!canWalkLikeLobbyForBot(o.x + sx * cstep, o.y, o.x, o.y, o)) return false; + o.x += sx * cstep; + return true; + }; + const tryAxisY = () => { + if (sy === 0) return false; + if (!canWalkLikeLobbyForBot(o.x, o.y + sy * cstep, o.x, o.y, o)) return false; + o.y += sy * cstep; + return true; + }; + if (Math.abs(dx) >= Math.abs(dy)) { + if (!tryAxisX()) tryAxisY(); + } else { + if (!tryAxisY()) tryAxisX(); + } + } + } + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) { + o.botIsWalking = true; + o.botPathStuckTicks = 0; + } else { + o.botPathStuckTicks = (o.botPathStuckTicks | 0) + 1; + if ((o.botPathStuckTicks | 0) >= 20) { + o.botPathStuckTicks = 0; + if (path.length) path.shift(); + if (isQuizCarry() && playBotsEnabled()) o.botQuizCarryPathfindAfter = 0; + } + } + } + + function pickRandomPreviewBotWanderDir() { + const dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]]; + return dirs[Math.floor(Math.random() * dirs.length)]; + } + + function stepPreviewBots() { + if (!playBotsEnabled() || !mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss()) return; + if (isQuizCarry()) { + stepQuizCarryPreviewBots(); + return; + } + const w = mapData.width || 20, h = mapData.height || 15; + const now = Date.now(); + const inAnswerPhase = previewMode && isQuiz() && previewQuizStep === 'answer'; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botIsWalking = false; + /* บอทที่ตอบผิดไปแล้วในแมป mng8a80o → เป็นผีเดินมั่ว ไม่ต้องไปยืนรอที่โซนตอบ */ + const ghostWanderBot = isQuizQuestionMissionUiMapPlay() && isQuizWrongGhostActiveForEnt(id); + if (ghostWanderBot) { + /* ข้าม path-following → ตกไป wander logic ด้านล่าง */ + o.botPath = []; + o.botAnswerWander = true; + } + if (inAnswerPhase && o.botPath && o.botPath.length > 0 && !o.botAnswerWander) { + stepPreviewBotAlongPath(o, w, h); + return; + } + if (inAnswerPhase && o.botPath && o.botPath.length === 0 && !o.botAnswerWander) { + return; + } + /* ก่อนตอบ / พัก / บอทสับสน / ผี (ตอบผิดไปแล้ว): เดินทุกเฟรมตามทิศ */ + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600; + if (now >= o.botWanderNextTurn) { + o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200); + if (Math.random() < 0.55) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + } + const accX = o.botWanderDx; + const accY = o.botWanderDy; + if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up'; + else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left'; + const step = MOVE_SPEED; + const tox = o.x + accX * step; + const toy = o.y + accY * step; + const ox = o.x, oy = o.y; + const pathStrictW = isQuizBattle() && quizBattlePathModeActive(mapData); + if (pathStrictW) { + const moved = quizBattleTryStepMovePlay(o.x, o.y, tox, toy, (nx, ny, fx, fy) => canWalkLikeLobbyForBot(nx, ny, fx, fy, o)); + o.x = moved.x; + o.y = moved.y; + if (Math.abs(o.x - ox) < 1e-5 && Math.abs(o.y - oy) < 1e-5) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + } else if (canWalkLikeLobbyForBot(tox, toy, o.x, o.y, o)) { + o.x = tox; + o.y = toy; + } else { + if (canWalkLikeLobbyForBot(tox, o.y, o.x, o.y, o)) { + o.x = tox; + } else if (canWalkLikeLobbyForBot(o.x, toy, o.x, o.y, o)) { + o.y = toy; + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + } + if (!Number.isFinite(o.x)) o.x = 0.5; + if (!Number.isFinite(o.y)) o.y = 0.5; + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) o.botIsWalking = true; + }); + separateClumpedPreviewBots(); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const ob = others.get(bid); + if (ob) clampPlayEntityFootprintToMap(ob, mapData); + }); + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + o.tx = s.x; + o.ty = s.y; + }); + } + } + + /** A* เหมือน room-lobby — double-click ไปจุดบนแผนที่ */ + function pathfindPlay(fromX, fromY, toX, toY) { + if (!mapData) return []; + const w = mapData.width || 20, h = mapData.height || 15; + const fx = Math.floor(fromX), fy = Math.floor(fromY); + const tx = Math.floor(toX), ty = Math.floor(toY); + if (tx < 0 || tx >= w || ty < 0 || ty >= h || !canWalkLikeLobby(tx + 0.5, ty + 0.5)) return []; + if (fx === tx && fy === ty) return [{ x: tx + 0.5, y: ty + 0.5 }]; + const key = (gx, gy) => gx + ',' + gy; + const open = [{ gx: fx, gy: fy, f: 0, g: 0 }]; + const closed = new Set(); + const cameFrom = {}; + const gScore = { [key(fx, fy)]: 0 }; + const heuristic = (ax, ay) => Math.abs(ax - tx) + Math.abs(ay - ty); + const dirs = [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }]; + while (open.length) { + open.sort((a, b) => a.f - b.f); + const cur = open.shift(); + const ck = key(cur.gx, cur.gy); + if (closed.has(ck)) continue; + closed.add(ck); + if (cur.gx === tx && cur.gy === ty) { + const path = []; + let u = cur; + while (u) { + path.unshift({ x: u.gx + 0.5, y: u.gy + 0.5 }); + u = cameFrom[key(u.gx, u.gy)]; + } + return path; + } + for (const d of dirs) { + const nx = cur.gx + d.dx, ny = cur.gy + d.dy; + if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + if (!canWalkLikeLobby(nx + 0.5, ny + 0.5, cur.gx + 0.5, cur.gy + 0.5)) continue; + const nk = key(nx, ny); + if (closed.has(nk)) continue; + const g = (gScore[ck] ?? Infinity) + 1; + if (g >= (gScore[nk] ?? Infinity)) continue; + gScore[nk] = g; + cameFrom[nk] = cur; + open.push({ gx: nx, gy: ny, f: g + heuristic(nx, ny), g }); + } + } + return []; + } + + function pathfindPlayForBot(fromX, fromY, toX, toY, o) { + if (!mapData) return []; + const w = mapData.width || 20, h = mapData.height || 15; + const fx = Math.floor(fromX), fy = Math.floor(fromY); + const tx = Math.floor(toX), ty = Math.floor(toY); + if (tx < 0 || tx >= w || ty < 0 || ty >= h || !canWalkLikeLobbyForBot(tx + 0.5, ty + 0.5, NaN, NaN, o)) return []; + if (fx === tx && fy === ty) return [{ x: tx + 0.5, y: ty + 0.5 }]; + const key = (gx, gy) => gx + ',' + gy; + const open = [{ gx: fx, gy: fy, f: 0, g: 0 }]; + const closed = new Set(); + const cameFrom = {}; + const gScore = { [key(fx, fy)]: 0 }; + const heuristic = (ax, ay) => Math.abs(ax - tx) + Math.abs(ay - ty); + const dirs = [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }]; + while (open.length) { + open.sort((a, b) => a.f - b.f); + const cur = open.shift(); + const ck = key(cur.gx, cur.gy); + if (closed.has(ck)) continue; + closed.add(ck); + if (cur.gx === tx && cur.gy === ty) { + const path = []; + let u = cur; + while (u) { + path.unshift({ x: u.gx + 0.5, y: u.gy + 0.5 }); + u = cameFrom[key(u.gx, u.gy)]; + } + return path; + } + for (const d of dirs) { + const nx = cur.gx + d.dx, ny = cur.gy + d.dy; + if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + if (!canWalkLikeLobbyForBot(nx + 0.5, ny + 0.5, cur.gx + 0.5, cur.gy + 0.5, o)) continue; + const nk = key(nx, ny); + if (closed.has(nk)) continue; + const g = (gScore[ck] ?? Infinity) + 1; + if (g >= (gScore[nk] ?? Infinity)) continue; + gScore[nk] = g; + cameFrom[nk] = cur; + open.push({ gx: nx, gy: ny, f: g + heuristic(nx, ny), g }); + } + } + return []; + } + + let playPath = []; + + function drawGauntletLaserColumnScreen(rx, ry, rw, rh) { + ctx.save(); + const topRec = gauntletLaserTopUrl ? ensureGauntletAssetImage(gauntletLaserTopUrl) : null; + const botRec = gauntletLaserBottomUrl ? ensureGauntletAssetImage(gauntletLaserBottomUrl) : null; + const lineRec = gauntletLaserLineUrl ? ensureGauntletAssetImage(gauntletLaserLineUrl) : null; + let topH = 0; + let botH = 0; + if (topRec && topRec.img.complete && topRec.img.naturalWidth > 0) { + topH = Math.min(rh * 0.4, rw * topRec.img.naturalHeight / topRec.img.naturalWidth); + } + if (botRec && botRec.img.complete && botRec.img.naturalWidth > 0) { + botH = Math.min(rh * 0.4, rw * botRec.img.naturalHeight / botRec.img.naturalWidth); + } + const lineReady = !!(lineRec && lineRec.img.complete && lineRec.img.naturalWidth > 0 && rh > 1); + /* ไม่เติมสีคอลัมน์ทึบเมื่อมีรูปเส้นแล้ว — กันทึบโปร่งซ้าย/ขวาเลเซอร์ดูเหมือนแถบ UI/scrollbar */ + if (!lineReady) { + ctx.fillStyle = gauntletLaserFillColor; + ctx.fillRect(rx, ry, rw, rh); + } + /* เส้นกลาง tile ทั้งความสูงคอลัมน์ แล้วค่อยวาดหัว/ท้ายทับ — ให้ลำแสงต่อเนื่องแบบสินทรัพย์รวม (รูปอ้างอิง) */ + if (lineReady) { + const iw = lineRec.img.naturalWidth; + const ih = lineRec.img.naturalHeight; + const scale = rw / iw; + const step = Math.max(1, ih * scale); + let y = ry; + while (y < ry + rh) { + const piece = Math.min(step, ry + rh - y); + const srcH = piece / scale; + try { + ctx.drawImage(lineRec.img, 0, 0, iw, srcH, rx, y, rw, piece); + } catch (e) { /* ignore */ } + y += piece; + } + } + if (topH > 0 && topRec) { + try { ctx.drawImage(topRec.img, rx, ry, rw, topH); } catch (e) { /* ignore */ } + } + if (botH > 0 && botRec) { + try { ctx.drawImage(botRec.img, rx, ry + rh - botH, rw, botH); } catch (e) { /* ignore */ } + } + const lw = Number(gauntletLaserLineWidthPx) || 0; + if (lw > 0) { + ctx.strokeStyle = gauntletLaserStrokeColor; + ctx.lineWidth = lw; + ctx.strokeRect(rx + lw / 2, ry + lw / 2, rw - lw, rh - lw); + } + ctx.restore(); + } + + /** Stack preview: HUD แนว cyberdeck — SCORE ซ้าย, ตาปัจจุบันขวา, เทอร์มินัลล่างซ้าย */ + function drawStackPreviewCyberHud(ctx, cw, ch) { + if (!playBotsEnabled() || !stackMini || !mapData || !isStack()) return; + const now = Date.now(); + const pad = 12; + const currentSeat = getStackPreviewCurrentSeat(); + const order = stackPreviewTurnOrder; + const mono = '11px ui-monospace, "Cascadia Mono", Consolas, monospace'; + const monoSm = '9px ui-monospace, Consolas, monospace'; + const towerMapHud = isStackTowerMissionUiMapPlay(); + const lifeSlots = towerMapHud + ? Math.max(1, Math.min(20, Math.floor(Number(playStackTeamMissesMax) || 3))) + : 3; + const livesRem = towerMapHud && stackMini.teamMissesLeft != null + ? Math.max(0, Number(stackMini.teamMissesLeft) || 0) + : Math.max(0, stackMini.lives != null ? stackMini.lives : 3); + const teamScore = stackMini.score || 0; + const combo = stackMini.combo || 0; + + function roundRectPath(x, y, w, h, r) { + const rr = Math.min(r, w / 2, h / 2); + ctx.beginPath(); + ctx.moveTo(x + rr, y); + ctx.arcTo(x + w, y, x + w, y + h, rr); + ctx.arcTo(x + w, y + h, x, y + h, rr); + ctx.arcTo(x, y + h, x, y, rr); + ctx.arcTo(x, y, x + w, y, rr); + ctx.closePath(); + } + + /** @param {{ headBust?: boolean, headSrcFrac?: number, bustLiftPx?: number }} [avatarOpts] — headBust + bustLiftPx ดันรูปขึ้นในกรอบ */ + function drawHudAvatar(cx, baselineY, maxSize, characterId, playTint, avatarOpts) { + const timeMs = now; + const dir = 'down'; + const rawImg = getAvatarImg(characterId, dir, timeMs, false); + const charImg = playTint ? getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, false, playTint) : rawImg; + let iw = 0; let ih = 0; + if (charImg && charImg.tagName === 'CANVAS' && charImg.width > 0 && charImg.height > 0) { + iw = charImg.width; + ih = charImg.height; + } else if (charImg && charImg.complete && charImg.naturalWidth) { + iw = charImg.naturalWidth; + ih = charImg.naturalHeight; + } + const headBust = !!(avatarOpts && avatarOpts.headBust); + let sx = 0; + let sy = 0; + let sww = iw; + let shh = ih; + if (headBust && iw > 0 && ih > 0) { + const rawF = Number(avatarOpts && avatarOpts.headSrcFrac); + const frac = Number.isFinite(rawF) && rawF > 0.22 && rawF < 0.85 ? rawF : 0.4; + shh = Math.max(8, Math.round(ih * frac)); + } + const ax = cx - maxSize / 2; + const ay = baselineY - maxSize; + ctx.save(); + roundRectPath(ax, ay, maxSize, maxSize, 5); + ctx.clip(); + if (iw > 0 && ih > 0) { + const maxScale = headBust ? 2.45 : 1; + const scale = Math.min(maxSize / sww, maxSize / shh, maxScale); + const dw = sww * scale; + const dh = shh * scale; + const rawLift = headBust && avatarOpts ? Number(avatarOpts.bustLiftPx) : NaN; + const bustLift = headBust + ? (Number.isFinite(rawLift) ? Math.max(0, Math.min(maxSize * 0.45, rawLift)) : 22) + : 0; + ctx.drawImage(charImg, sx, sy, sww, shh, cx - dw / 2, baselineY - dh - bustLift, dw, dh); + } else { + ctx.fillStyle = 'rgba(80, 200, 255, 0.35)'; + ctx.fillRect(ax, ay, maxSize, maxSize); + } + ctx.restore(); + ctx.strokeStyle = 'rgba(0, 234, 255, 0.5)'; + ctx.lineWidth = 1.5; + roundRectPath(ax, ay, maxSize, maxSize, 5); + ctx.stroke(); + } + + function resolveEntryVisual(entry) { + if (entry.kind === 'human') { + return { + name: String(me.nickname || 'YOU').toUpperCase(), + score: me.stackPreviewHumanPts || 0, + characterId: me.characterId, + playTint: me.playTint || resolvePlayTintForPeer(String(myId || 'me')), + }; + } + const o = entry.botId ? others.get(entry.botId) : null; + return { + name: o ? String(o.nickname || 'BOT').replace(/\s+/g, ' ').toUpperCase().slice(0, 12) : '—', + score: o ? (o.stackBotScore || 0) : 0, + characterId: o ? o.characterId : null, + playTint: o ? (o.playTint || resolvePlayTintForPeer(entry.botId)) : null, + }; + } + + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + + const boardW = Math.min(248, Math.max(200, cw * 0.36)); + const rowH = 42; + const headH = 38; + const boardH = headH + STACK_PREVIEW_TURN_COUNT * rowH + 10; + + if (!towerMapHud) { + ctx.fillStyle = 'rgba(8, 12, 28, 0.9)'; + roundRectPath(pad, 8, boardW, boardH, 8); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 230, 255, 0.5)'; + ctx.lineWidth = 1.5; + roundRectPath(pad, 8, boardW, boardH, 8); + ctx.stroke(); + + ctx.fillStyle = '#5cefff'; + ctx.font = 'bold 12px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText('SCORE', pad + 14, 16); + ctx.fillStyle = 'rgba(94, 239, 255, 0.55)'; + ctx.font = monoSm; + ctx.fillText('shared stack · P1–P6', pad + 62, 18); + + let rowY = 8 + headH; + const drawRows = (entries) => { + entries.forEach((entry) => { + const isCurrent = entry.seat === currentSeat; + const v = resolveEntryVisual(entry); + let actionFlash = false; + if (entry.kind === 'human') { + actionFlash = lastStackPreviewActorId === '__human__' && now < lastStackPreviewActorUntil; + } else if (entry.botId) { + const ob = others.get(entry.botId); + actionFlash = !!(ob && ((now < (ob.stackBotGlowUntil || 0)) || (entry.botId === lastStackPreviewActorId && now < lastStackPreviewActorUntil))); + } + if (isCurrent) { + ctx.fillStyle = 'rgba(255, 0, 180, 0.1)'; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 255, 220, 0.9)'; + ctx.lineWidth = 2; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.stroke(); + } else if (actionFlash) { + ctx.strokeStyle = 'rgba(255, 214, 102, 0.65)'; + ctx.lineWidth = 1.5; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.stroke(); + } + const avS = 34; + drawHudAvatar(pad + 10 + avS / 2, rowY + avS - 3, avS, v.characterId, v.playTint); + ctx.fillStyle = isCurrent ? '#e0f7ff' : '#89b4fa'; + ctx.font = '600 10px ui-sans-serif, system-ui, sans-serif'; + ctx.textBaseline = 'middle'; + ctx.fillText('P' + entry.seat, pad + 14 + avS + 6, rowY + 12); + ctx.fillStyle = '#c0caf5'; + ctx.font = 'bold 10px ' + mono; + ctx.fillText(v.name.slice(0, 11), pad + 14 + avS + 6, rowY + 28); + ctx.fillStyle = actionFlash ? '#ffe066' : '#7dfcff'; + ctx.font = 'bold 13px ' + mono; + ctx.textAlign = 'right'; + ctx.fillText(String(v.score), pad + boardW - 12, rowY + rowH / 2); + ctx.textAlign = 'left'; + rowY += rowH; + }); + }; + + if (order && order.length === STACK_PREVIEW_TURN_COUNT) { + drawRows(order); + } else { + const bots = [...others.keys()].filter(isPreviewBotId).sort(); + const rows = [{ kind: 'human', seat: 1 }]; + for (let i = 0; i < STACK_PREVIEW_TURN_COUNT - 1; i++) { + rows.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null }); + } + drawRows(rows); + } + } + + const framePad = 10; + const bigAv = Math.min(72, Math.max(52, ch * 0.10)); + const avBox = bigAv + 10; + const integrityMinW = 120; + const rightW = Math.min(300, Math.max(236, Math.round(cw * 0.34))); + let rightX = cw - pad - rightW; + const topY = 8; + const leftReserve = towerMapHud ? (pad + 8) : (pad + boardW + 14); + if (rightX < leftReserve) { + rightX = Math.max(pad, cw - pad - rightW); + } + const integrityW = Math.max(integrityMinW, rightW - framePad * 2 - avBox - 10); + const innerTop = topY + framePad; + const integrityX = rightX + framePad; + const integrityH = avBox + 2; + const avFrameX = integrityX + integrityW + 10; + const avFrameY = innerTop; + const footerH = towerMapHud ? 0 : 26; + const turnPanelH = framePad + integrityH + (towerMapHud ? framePad : (10 + footerH + framePad)); + + ctx.fillStyle = 'rgba(8, 12, 28, 0.9)'; + roundRectPath(rightX, topY, rightW, turnPanelH, 8); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 230, 255, 0.55)'; + ctx.lineWidth = 1.5; + roundRectPath(rightX, topY, rightW, turnPanelH, 8); + ctx.stroke(); + + let curEntry = null; + if (order && order.length === STACK_PREVIEW_TURN_COUNT) { + curEntry = order.find((e) => e.seat === currentSeat) || null; + } + const curV = curEntry ? resolveEntryVisual(curEntry) : resolveEntryVisual({ kind: 'human', seat: 1 }); + + drawStackTowerLifeIntegrityBarPlay(ctx, integrityX, innerTop - 2, integrityW, integrityH + 4, lifeSlots, livesRem); + + const avCx = avFrameX + avBox / 2; + ctx.save(); + ctx.shadowColor = 'rgba(0, 234, 255, 0.5)'; + ctx.shadowBlur = 12; + ctx.strokeStyle = 'rgba(0, 234, 255, 0.92)'; + ctx.lineWidth = 2; + roundRectPath(avFrameX - 1, avFrameY - 1, avBox + 2, avBox + 2, 6); + ctx.stroke(); + ctx.restore(); + ctx.strokeStyle = 'rgba(0, 234, 255, 0.45)'; + ctx.lineWidth = 1; + roundRectPath(avFrameX - 1, avFrameY - 1, avBox + 2, avBox + 2, 6); + ctx.stroke(); + drawHudAvatar(avCx, avFrameY + avBox - 5, bigAv, curV.characterId, curV.playTint, towerMapHud ? { headBust: true } : undefined); + + if (!towerMapHud) { + const footY = innerTop + integrityH + 12; + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.fillStyle = livesRem <= 1 ? '#f7768e' : 'rgba(125, 252, 255, 0.88)'; + ctx.font = monoSm; + ctx.fillText( + 'P' + currentSeat + ' · ' + curV.name.slice(0, 9) + ' · TEAM ' + teamScore + ' · COMBO x' + combo, + rightX + rightW / 2, + footY, + ); + } + + const termW = Math.min(380, Math.max(220, cw * 0.42)); + const logLineH = 12; + const logMaxLines = 5; + const logBlockH = logMaxLines * logLineH; + /** หัวข้อ + บล็อกล็อก + ช่องว่าง + ป้าย TOWER_LINK + แถบ — อย่าให้ทับกัน (เดิม termH=86 แคบเกิน) */ + const termH = Math.max(118, 22 + logBlockH + 10 + 14 + 8 + 10); + const termY = Math.max((towerMapHud ? 28 : boardH + 24), ch - termH - 52); + ctx.fillStyle = 'rgba(18, 8, 32, 0.88)'; + roundRectPath(pad, termY, termW, termH, 6); + ctx.fill(); + ctx.strokeStyle = 'rgba(187, 100, 255, 0.45)'; + ctx.lineWidth = 1.5; + roundRectPath(pad, termY, termW, termH, 6); + ctx.stroke(); + + ctx.fillStyle = '#c099ff'; + ctx.font = '600 10px ' + mono; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText('// NODE_LOG', pad + 10, termY + 8); + ctx.fillStyle = '#7dcfff'; + ctx.font = monoSm; + const logTop = termY + 24; + let ly = logTop; + const logLines = stackPreviewHudLog.length + ? stackPreviewHudLog.slice(-logMaxLines) + : ['>>> Awaiting stack sync…', '>>> Space / click = release (your turn only)']; + logLines.forEach((ln) => { + ctx.fillText(ln, pad + 10, ly); + ly += logLineH; + }); + + const decryptPct = stackTowerDecryptPctPlay(); + const barPadX = 10; + const barH = 8; + const barW = termW - barPadX * 2; + const barY = termY + termH - barH - 10; + const labelY = barY - 14; + if (labelY >= logTop + 4) { + ctx.fillStyle = '#94e2d5'; + ctx.font = monoSm; + ctx.textBaseline = 'top'; + ctx.fillText('TOWER_LINK ' + decryptPct + '%', pad + barPadX, labelY); + } + ctx.fillStyle = 'rgba(0,0,0,0.35)'; + ctx.fillRect(pad + barPadX, barY, barW, barH); + ctx.fillStyle = 'rgba(0, 255, 200, 0.65)'; + ctx.fillRect(pad + barPadX, barY, Math.max(4, barW * decryptPct / 100), barH); + ctx.strokeStyle = 'rgba(0, 234, 255, 0.4)'; + ctx.strokeRect(pad + barPadX, barY, barW, barH); + + ctx.restore(); + } + + function useCyberPlayHud() { + return !!(mapData && (isJumpSurvive() || isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() + || isQuizQuestionMissionHudActivePlay() || isStackTowerMissionHudActivePlay())); + } + + /** Cyber HUD: quiz_carry ใช้ playLiveQuizScores (ถูก +QUIZ_CARRY_POINTS_PER_CORRECT ตอนส่งป้ายถูกที่ฮับ) */ + function cyberHudQuizCarryPeerScore(peerId) { + if (peerId == null) return 0; + return Math.max(0, Number(playLiveQuizScores[peerId]) || 0); + } + + /** แมป mng8a80o ช่วงเล่น: คำถามอยู่บนแผนที่ (โซนทอง) — HUD กลางเหลือ TIME + แผ่นเวลา */ + function syncQuizQuestionMissionCyberCenterHud() { + const root = document.getElementById('play-cyber-hud'); + const qBand = document.getElementById('play-cyber-quiz-mission-q-band'); + const qText = document.getElementById('play-cyber-quiz-mission-q-text'); + const qPlaque = document.getElementById('play-cyber-quiz-mission-q-plaque'); + const tb = root ? root.querySelector('.play-cyber-time-block') : null; + const on = isQuizQuestionMissionHudActivePlay() || isStackTowerMissionHudActivePlay() || isJumpSurviveMissionHudActivePlay() + || isSpaceShooterMissionHudActivePlay(); + if (isQuizQuestionMissionUiMapPlay()) { + if (qBand) { + qBand.classList.add('is-hidden'); + qBand.setAttribute('aria-hidden', 'true'); + } + if (qText) qText.textContent = ''; + if (qPlaque) { + qPlaque.classList.add('is-hidden'); + qPlaque.removeAttribute('src'); + delete qPlaque.dataset.qmQsrc; + } + } + const plaqueImg = root ? document.getElementById('play-cyber-time-plaque-img') : null; + const head = tb && tb.querySelector ? tb.querySelector('.play-cyber-time-head') : null; + const clearQmTimePlaque = () => { + if (plaqueImg) { + plaqueImg.onload = null; + plaqueImg.onerror = null; + plaqueImg.removeAttribute('src'); + plaqueImg.classList.add('is-hidden'); + plaqueImg.setAttribute('aria-hidden', 'true'); + delete plaqueImg.dataset.qmTimeSrc; + } + if (head) head.classList.remove('play-cyber-time-head--qm-plaque'); + if (tb) { + tb.style.backgroundImage = ''; + tb.style.backgroundSize = ''; + tb.style.backgroundPosition = ''; + tb.style.backgroundRepeat = ''; + tb.style.paddingTop = ''; + tb.style.minWidth = ''; + delete tb.dataset.qmTimeSrc; + } + }; + if (tb && plaqueImg && head) { + const tUrl = questionMissionHudAssetUrl('time.png'); + if (on) { + tb.style.backgroundImage = ''; + tb.style.backgroundSize = ''; + tb.style.backgroundPosition = ''; + tb.style.backgroundRepeat = ''; + tb.style.paddingTop = ''; + tb.style.minWidth = ''; + const applyPlaque = () => { + head.classList.add('play-cyber-time-head--qm-plaque'); + plaqueImg.classList.remove('is-hidden'); + plaqueImg.setAttribute('aria-hidden', 'false'); + }; + if (plaqueImg.dataset.qmTimeSrc === tUrl && plaqueImg.getAttribute('src') === tUrl + && plaqueImg.complete && (plaqueImg.naturalWidth || 0) > 0) { + applyPlaque(); + } else if (plaqueImg.dataset.qmTimeSrc !== tUrl || plaqueImg.getAttribute('src') !== tUrl) { + plaqueImg.dataset.qmTimeSrc = tUrl; + plaqueImg.onload = function () { + if ((plaqueImg.naturalWidth || 0) > 0) applyPlaque(); + else clearQmTimePlaque(); + }; + plaqueImg.onerror = function () { + clearQmTimePlaque(); + }; + plaqueImg.setAttribute('src', tUrl); + } + } else { + clearQmTimePlaque(); + } + } + } + + function playCyberHudEsc(t) { + return String(t == null ? '' : t) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function resolveCyberHudAvatarTintPlay(row) { + if (row && row.playTint && row.playTint.head) return row.playTint; + const peerId = row && row.id != null ? row.id : ''; + if (row && row.isMe) { + return me.playTint || resolvePlayTintForPeer(String(myId != null ? myId : 'me')); + } + const sid = peerId !== '' ? String(peerId) : ''; + const ox = sid ? (others.get(peerId) || others.get(sid)) : null; + if (ox && ox.playTint) return ox.playTint; + return resolvePlayTintForPeer(sid); + } + + function resolveRankedCharacterIdPlay(rankedRow) { + if (!rankedRow) return ''; + if (rankedRow.characterId) return String(rankedRow.characterId); + const id = rankedRow.id; + if (id == null) return ''; + const ox = others.get(id) || others.get(String(id)); + if (ox && ox.characterId) return String(ox.characterId); + if (myId != null && String(id) === String(myId)) { + return String(me.characterId || getPlayCharacterId() || ''); + } + return ''; + } + + /** กัน cache/แสดง canvas ว่าง (ทำให้ช่องอันดับ 1 หายรูป แต่ retry หยุดเพราะ naturalWidth > 0) */ + function playCanvasHasOpaquePixels(canvas) { + if (!canvas || canvas.width < 2 || canvas.height < 2) return false; + try { + const ctx = canvas.getContext('2d', { willReadFrequently: true }); + if (!ctx) return false; + const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + for (let i = 3; i < data.length; i += 16) { + if (data[i] > 12) return true; + } + } catch (e) { /* ignore */ } + return false; + } + + function ensureMePlayTintFromLobbyStorage() { + if (me.playTint && me.playTint.head && me.playTint.body) return; + const t = loadPlayTintFromLobbyPlayerStorage(); + if (t) me.playTint = t; + } + + function cyberHudScoreAvatarCacheKey(row) { + const cid = row.characterId ? String(row.characterId) : ''; + const tint = resolveCyberHudAvatarTintPlay(row); + return [row.isMe ? 'me' : String(row.id), cid, tint.head, tint.hair, tint.body, 'faceV2'].join('|'); + } + + function findPlayCyberHudScoreAvatarSrcForPeer(peerId) { + if (peerId == null) return ''; + const sid = String(peerId).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + const ul = document.getElementById('play-cyber-score-list'); + if (!ul) return ''; + const row = ul.querySelector('.play-cyber-score-row[data-peer-id="' + sid + '"]'); + if (!row) return ''; + const av = row.querySelector('img.play-cyber-score-av'); + if (!av || !av.src || av.src === defaultAvatarImg.src) return ''; + if (!av.complete || av.naturalWidth < 8) return ''; + return av.src; + } + + /** crop หน้า idle ลง (down) + tint — ใช้ทั้ง HUD และหน้าสรุปผล ให้ตรงสีในเกม */ + function composeCyberHudFacePortraitDataUrl(cid, tint) { + if (!cid || !tint) return ''; + const dir = 'down'; + const nowT = Date.now(); + preloadPlayTintForAvatar(cid, tint); + ensurePlayLayerProbesAllDirections(cid); + const raw = getCharacterFrame(cid, dir, nowT, false) || getCharacterImg(cid, dir); + if (!raw) return ''; + let srcCanvas = null; + if (raw.complete && raw.naturalWidth > 0 && raw.naturalHeight > 0) { + const composeFrame = isUploadedCharAssetId(cid) ? 0 : idleAnimPhaseIndex(nowT); + const packIdle = tryComposePlayLayersFromFiles(raw, cid, dir, composeFrame, tint, { idle: true }); + if (packIdle && packIdle.canvas && playCanvasHasOpaquePixels(packIdle.canvas)) { + srcCanvas = packIdle.canvas; + } + if (!srcCanvas) { + const tinted = getPlayTintedAvatarSource(raw, cid, dir, nowT, false, tint); + if (tinted && tinted.tagName === 'CANVAS' && tinted.width > 0 && playCanvasHasOpaquePixels(tinted)) { + srcCanvas = tinted; + } + } + } + /* ตัวละครที่ไม่มีไฟล์เลเยอร์สี (เช่น defaultChar) — compose คืน Image ดิบ → crop หน้าจากรูปดิบตรง ๆ กันช่องว่าง */ + let drawSrc = srcCanvas; + if (!drawSrc && raw && raw.complete && raw.naturalWidth > 0 && raw.naturalHeight > 0) { + drawSrc = raw; + } + if (!drawSrc) return ''; + const face = document.createElement('canvas'); + face.width = 120; + face.height = 120; + const ctx = face.getContext('2d'); + if (!ctx) return ''; + const sw = drawSrc.naturalWidth || drawSrc.width; + const sh = drawSrc.naturalHeight || drawSrc.height; + if (!sw || !sh) return ''; + const sx = sw * 0.1; + const sy = sh * 0.02; + const sw2 = sw * 0.8; + const sh2 = sh * 0.5; + ctx.clearRect(0, 0, 120, 120); + try { + ctx.drawImage(drawSrc, sx, sy, sw2, sh2, 0, 0, 120, 120); + } catch (e) { + return ''; + } + if (!playCanvasHasOpaquePixels(face)) return ''; + try { + return face.toDataURL('image/png'); + } catch (e) { + return ''; + } + } + + function cyberHudScoreAvatarImgLooksReady(img) { + return !!(img && img.src && img.src !== defaultAvatarImg.src && img.complete && img.naturalWidth > 8 && img.naturalHeight > 8); + } + + /** แถบ Cyber SCORE + สรุปผล — หน้า idle down + tint ต่อผู้เล่น */ + function setCyberHudScoreAvatarImg(avImg, row) { + const cid = row.characterId ? String(row.characterId) : ''; + if (!cid) { + avImg.src = defaultAvatarImg.src; + return; + } + const cacheKey = cyberHudScoreAvatarCacheKey(row); + const cached = cyberHudScoreAvatarUrlCache.get(cacheKey); + if (cached) { + avImg.src = cached; + return; + } + const hudSrc = findPlayCyberHudScoreAvatarSrcForPeer(row.id); + if (hudSrc) { + avImg.src = hudSrc; + cyberHudScoreAvatarUrlCache.set(cacheKey, hudSrc); + return; + } + const tint = resolveCyberHudAvatarTintPlay(row); + /* ย้อมสีตาม tint ปัจจุบันก่อนเสมอ (รวมตัวเรา) — อย่าใช้ lobby idle ต่อ characterId เพราะสีไม่ตรงที่เลือกใน lobby */ + const faceUrl = composeCyberHudFacePortraitDataUrl(cid, tint); + if (faceUrl) { + avImg.src = faceUrl; + if (cyberHudScoreAvatarUrlCache.size >= CYBER_HUD_AV_URL_CACHE_CAP) { + const first = cyberHudScoreAvatarUrlCache.keys().next(); + if (!first.done) cyberHudScoreAvatarUrlCache.delete(first.value); + } + cyberHudScoreAvatarUrlCache.set(cacheKey, faceUrl); + return; + } + if (row.isMe) { + const lobbySrc = playLobbyIdleAvatarSrcForCharacter(cid); + if (lobbySrc) { + avImg.src = lobbySrc; + cyberHudScoreAvatarUrlCache.set(cacheKey, lobbySrc); + return; + } + } + const dir = 'down'; + const nowT = Date.now(); + const frameIm = getCharacterFrame(cid, dir, nowT, false) || getCharacterImg(cid, dir); + if (frameIm && frameIm.src) { + if (!frameIm.complete || !frameIm.naturalWidth) { + avImg.src = frameIm.src; + frameIm.addEventListener('load', function onHudAvFrame() { + frameIm.removeEventListener('load', onHudAvFrame); + setCyberHudScoreAvatarImg(avImg, row); + }, { once: true }); + return; + } + } + avImg.src = defaultAvatarImg.src; + } + + /** ลองโหลด avatar ซ้ำจน sprite/tint พร้อม (กันรูปหายตอนเปิด overlay) */ + function applyCyberHudScoreAvatarImgWithRetry(avImg, row, attempt) { + attempt = attempt || 0; + setCyberHudScoreAvatarImg(avImg, row); + const ok = cyberHudScoreAvatarImgLooksReady(avImg) + && (avImg.src.indexOf('data:image/') === 0 || (row.isMe && avImg.src.indexOf('/img/') >= 0)); + if (ok) return; + if (attempt >= 16) return; + const cid = row.characterId ? String(row.characterId) : ''; + const delay = 40 + attempt * 90; + setTimeout(function () { + if (cid) preloadPlayTintForAvatar(cid, resolveCyberHudAvatarTintPlay(row)); + applyCyberHudScoreAvatarImgWithRetry(avImg, row, attempt + 1); + }, delay); + } + + function refreshGcmTfMockAvatarsPlay(rowEl, ranked) { + if (!rowEl || !Array.isArray(ranked)) return; + const imgs = rowEl.querySelectorAll('img.gcm-av'); + ranked.forEach(function (r, idx) { + const img = imgs[idx]; + if (!img) return; + const cid = resolveRankedCharacterIdPlay(r); + const avRow = { + id: r.id, + characterId: cid, + isMe: myId != null && r.id != null && String(r.id) === String(myId), + playTint: resolveCyberHudAvatarTintPlay({ + id: r.id, + characterId: cid, + isMe: myId != null && r.id != null && String(r.id) === String(myId), + }), + }; + applyCyberHudScoreAvatarImgWithRetry(img, avRow, 0); + }); + } + + function quizQuestionMissionJoystickResetVisual() { + quizQuestionMissionJoyVecX = 0; + quizQuestionMissionJoyVecY = 0; + const knob = document.getElementById('quiz-q-mission-joystick-knob'); + if (knob) knob.style.transform = 'translate(-50%, -50%)'; + } + + function quizQuestionMissionJoystickUpdateFromClientXY(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) { + quizQuestionMissionJoyVecX = 0; + quizQuestionMissionJoyVecY = 0; + knob.style.transform = 'translate(-50%, -50%)'; + return; + } + quizQuestionMissionJoyVecX = dx / maxKn; + quizQuestionMissionJoyVecY = dy / maxKn; + knob.style.transform = 'translate(calc(-50% + ' + dx + 'px), calc(-50% + ' + dy + 'px))'; + } + + function syncQuizQuestionMissionJoystickPlay() { + const root = document.getElementById('quiz-question-mission-joystick'); + if (!root) return; + const on = !!(isQuizQuestionMissionHudActivePlay() && !isChatFocused()); + root.classList.toggle('is-hidden', !on); + root.setAttribute('aria-hidden', on ? 'false' : 'true'); + if (!on) { + const base = document.getElementById('quiz-q-mission-joystick-base'); + if (base && quizQuestionMissionJoyPointerId != null) { + try { + base.releasePointerCapture(quizQuestionMissionJoyPointerId); + } catch (_e) { /* ignore */ } + } + quizQuestionMissionJoyPointerId = null; + quizQuestionMissionJoystickResetVisual(); + } + } + + /* mng8a80o: scale HUD ทั้งก้อน (fix 1920×1080) ให้พอดี container แบบรักษา 16:9 — เทียบ scale.js ของ mockup Minigame-1 + สำคัญ: คำนวณเฉพาะตอนเข้าโหมด/resize เท่านั้น (ห้ามอ่าน layout ทุกเฟรมใน draw loop → กระตุก) */ + let _quizTfScaleLast = 0; + let _quizTfMockActivePrev = false; + function updateQuizTfMockScale(root) { + root = root || document.getElementById('play-cyber-hud'); + if (!root) return; + let cw; + let ch; + if (document.documentElement.classList.contains('play-preview-editor-embed')) { + const host = root.offsetParent || root.parentElement || document.querySelector('.game-wrap'); + cw = (host && host.clientWidth) || window.innerWidth; + ch = (host && host.clientHeight) || window.innerHeight; + } else { + cw = window.innerWidth; + ch = window.innerHeight; + } + const s = Math.min(cw / 1920, ch / 1080); + if (!(s > 0 && isFinite(s))) return; + /* เขียนเฉพาะเมื่อค่าต่างจริง (กันการเขียน style ซ้ำ → invalidate ทุกเฟรม) */ + if (Math.abs(s - _quizTfScaleLast) < 0.0005) return; + _quizTfScaleLast = s; + root.style.setProperty('--qtf-scale', String(s)); + } + /* mng8a80o: สรุปผลภารกิจ (06-summary) — scale popup 1451×851 ตามสูตรเดียวกับ HUD (min vw/1920, vh/1080) */ + function updateGcmTfMockScale(ov) { + ov = ov || document.getElementById('gauntlet-crown-mission-overlay'); + if (!ov) return; + const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080); + if (s > 0 && isFinite(s)) ov.style.setProperty('--gcm-tf-scale', String(s)); + } + /* mng8a80o: popup HOW TO PLAY (01-howto) — scale stage 1920×1080 ตามสูตรเดียวกัน */ + function updateGchTfMockScale(ov) { + ov = ov || document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080); + if (s > 0 && isFinite(s)) ov.style.setProperty('--gch-tf-scale', String(s)); + } + let _quizTfScaleObserversBound = false; + function ensureQuizTfScaleResizeListener() { + if (_quizTfScaleObserversBound) return; + _quizTfScaleObserversBound = true; + const recompute = function () { + const r = document.getElementById('play-cyber-hud'); + if (r && r.classList.contains('play-cyber-hud--quiz-tf-mock')) updateQuizTfMockScale(r); + const ov = document.getElementById('gauntlet-crown-mission-overlay'); + if (ov && ov.classList.contains('gcm-quiz-tf-mock') && !ov.classList.contains('is-hidden')) updateGcmTfMockScale(ov); + const hv = document.getElementById('gauntlet-crown-howto-overlay'); + if (hv && hv.classList.contains('gch-quiz-tf-mock') && !hv.classList.contains('is-hidden')) updateGchTfMockScale(hv); + }; + window.addEventListener('resize', recompute); + window.addEventListener('orientationchange', recompute); + /* ResizeObserver จับ container ที่เปลี่ยนขนาดโดยไม่มี window resize (เช่น ลากแบ่งจอ editor) */ + if (typeof ResizeObserver === 'function') { + try { + const ro = new ResizeObserver(recompute); + const host = document.querySelector('.game-wrap') || document.documentElement; + if (host) ro.observe(host); + } catch (e) { /* ignore */ } + } + } + + function syncPlayCyberHud() { + const root = document.getElementById('play-cyber-hud'); + const stackEl = document.getElementById('play-canvas-stack'); + const on = useCyberPlayHud(); + if (root) { + root.classList.toggle('is-hidden', !on); + root.setAttribute('aria-hidden', on ? 'false' : 'true'); + if (!on) { + root.classList.remove('play-cyber-hud--question-mission'); + root.classList.remove('play-cyber-hud--score-flush-left'); + root.classList.remove('play-cyber-hud--jump-mission-hide-score'); + root.classList.remove('play-cyber-hud--ss-self-integrity-left'); + root.classList.remove('play-cyber-hud--last-light-no-time'); + } + } + if (stackEl) stackEl.classList.toggle('play-cyber-vignette', !!on); + const gw = document.querySelector('.game-wrap'); + if (gw) gw.classList.toggle('play-cyber-active', !!on); + if (!on || !root) return; + ensureMePlayTintFromLobbyStorage(); + root.classList.toggle( + 'play-cyber-hud--question-mission', + !!(isQuizQuestionMissionHudActivePlay() || isStackTowerMissionHudActivePlay() || isJumpSurviveMissionHudActivePlay() + || isSpaceShooterMissionHudActivePlay()), + ); + /* mng8a80o เท่านั้น — สไตล์ HUD ตาม mockup Minigame-1 (score-bar.png / Avartar.png / time.png) แยกจากคลาสร่วม */ + root.classList.toggle('play-cyber-hud--quiz-tf-mock', !!isQuizQuestionMissionUiMapPlay()); + const tfMockOn = root.classList.contains('play-cyber-hud--quiz-tf-mock'); + if (tfMockOn) { + /* คำนวณ scale เฉพาะตอนเพิ่งเข้าโหมด — ไม่อ่าน layout ทุกเฟรม (กันกระตุก); ที่เหลือพึ่ง resize/ResizeObserver */ + if (!_quizTfMockActivePrev) { + ensureQuizTfScaleResizeListener(); + updateQuizTfMockScale(root); + /* เฟรมถัดไป layout นิ่งแล้ว วัดอีกครั้งให้แม่น */ + requestAnimationFrame(function () { + const r = document.getElementById('play-cyber-hud'); + if (r && r.classList.contains('play-cyber-hud--quiz-tf-mock')) updateQuizTfMockScale(r); + }); + } + } else if (_quizTfMockActivePrev) { + root.style.removeProperty('--qtf-scale'); + _quizTfScaleLast = 0; + } + _quizTfMockActivePrev = tfMockOn; + root.classList.toggle( + 'play-cyber-hud--stack-tower-canvas-hud', + !!(playBotsEnabled() && stackMini && isStack() && isStackTowerMissionUiMapPlay()), + ); + root.classList.toggle('play-cyber-hud--ss-self-integrity-left', !!isSpaceShooterMissionHudActivePlay()); + /** แถบ SCORE แนวนอน (mno9kb07) — ใช้รูปแบบเดียวกับ quiz_carry / พรีวิว editor */ + root.classList.toggle( + 'play-cyber-hud--gauntlet-crown-strip', + !!(on && (isGauntletCrownHeistMapPlay() || isQuizCarry())), + ); + root.classList.toggle('play-cyber-hud--score-flush-left', !!(on && isQuizCarry())); + root.classList.toggle('play-cyber-hud--jump-mission-hide-score', !!isJumpSurviveMissionUiMapPlay()); + /** Last Light mno9kb07: ไม่แสดง TIME / ซูมใต้เวลา — จบด้วยรันเวย์ ไม่ใช้นาฬิกา */ + root.classList.toggle('play-cyber-hud--last-light-no-time', !!(on && isGauntletCrownHeistMapPlay())); + const timeLabelEl = root.querySelector('.play-cyber-time-label'); + if (timeLabelEl) timeLabelEl.style.display = isQuizCarry() ? 'none' : ''; + const timeVal = document.getElementById('play-cyber-time-val'); + const timeSub = document.getElementById('play-cyber-time-sub'); + const portrait = document.getElementById('play-cyber-portrait-img'); + const statusEl = document.getElementById('play-cyber-self-status'); + const hintEl = document.getElementById('play-cyber-hint'); + const previewEl = document.getElementById('play-cyber-preview-line'); + + if (timeSub) { + const bbMegaLiveHud = isBalloonBoss() && isMegaVirusMissionShellMapPlay() && !isGauntletCrownPregameBlockingPlay(); + timeSub.textContent = isQuizQuestionMissionHudActivePlay() + ? (playQuizPhaseLocal === 'read' ? 'READ · อ่านคำถาม' + : (playQuizPhaseLocal === 'answer' ? 'ANSWER · เดินไปโซน จริง / เท็จ' + : 'QUIZ MISSION · ประลองความรู้')) + : (isSpaceShooterMissionHudActivePlay() + ? 'SPACE SHOOTER · ARCADE — TIME (sec) · ภารกิจยิงยาน' + : (isStackTowerMissionHudActivePlay() + ? 'TOWER STACK · DECRYPT — TIME (sec) · เหลือเวลาถอดรหัส' + : (isQuizCarry() + ? 'QUIZ CARRY · COURT — หยิบป้ายถูกแล้วส่งที่ฮับ (F / Grab)' + : (isGauntlet() ? 'GAUNTLET · SURVIVAL RUN' + : (isSpaceShooter() ? 'SPACE SHOOTER · ARCADE' + : (isBalloonBoss() ? (bbMegaLiveHud ? '' : 'BALLOON BOSS · MEGA VIRUS') + : (isJumpSurviveMissionUiMapPlay() ? 'JUMPER · SURVIVE — เหลือเวลา (วินาที) · TIME (sec)' : 'JUMP SURVIVE · NODE UPLINK'))))))); + timeSub.style.display = bbMegaLiveHud ? 'none' : ''; + } + const zoomHintEl = document.getElementById('play-cyber-embed-zoom-hint'); + if (zoomHintEl) { + const showEmbedZoom = !!(previewMode && editorEmbedReturn && mapData && !isQuizQuestionMissionHudActivePlay() + && !(isBalloonBoss() && isMegaVirusMissionShellMapPlay())); + if (showEmbedZoom) { + const zNum = Number(playEmbedUserZoomMul.toFixed(2)); + zoomHintEl.textContent = '×' + String(zNum); + zoomHintEl.classList.remove('is-hidden'); + zoomHintEl.setAttribute('aria-hidden', 'false'); + } else { + zoomHintEl.textContent = ''; + zoomHintEl.classList.add('is-hidden'); + zoomHintEl.setAttribute('aria-hidden', 'true'); + } + } + if (timeVal) { + if (isQuiz() && isQuizQuestionMissionUiMapPlay()) { + if (quizQuestionMissionPhase !== 'live' || quizQuestionMissionPhase === 'ended') { + timeVal.textContent = '···'; + } else if (!playQuizPhaseEndsAt) { + timeVal.textContent = '—'; + } else { + timeVal.textContent = String(Math.max(0, Math.ceil((playQuizPhaseEndsAt - Date.now()) / 1000))); + } + } else if (isStack() && isStackTowerMissionUiMapPlay()) { + if (stackTowerMissionPhase === 'live') { + const remSt = Math.max(0, Math.ceil(stackTowerMissionTimeLimitSecPlay() - stackTowerMissionElapsedSecPlay())); + timeVal.textContent = String(remSt); + } else if (stackTowerMissionPhase === 'howto' || stackTowerMissionPhase === 'countdown') { + timeVal.textContent = '···'; + } else { + timeVal.textContent = '0'; + } + } else if (isQuizCarry()) { + if (quizCarrySessionEnded) { + timeVal.textContent = '0'; + } else if (quizCarryEmbedMissionFlowActive() && quizCarryEmbedCountdownEndAt > Date.now()) { + const rem = Math.max(0, Math.ceil((quizCarryEmbedCountdownEndAt - Date.now()) / 1000)); + timeVal.textContent = String(rem); + } else if (quizCarryEmbedMissionFlowActive() && quizCarryEmbedPreOptionCountdownEndAt > Date.now()) { + const rem2 = Math.max(0, Math.ceil((quizCarryEmbedPreOptionCountdownEndAt - Date.now()) / 1000)); + timeVal.textContent = String(rem2); + } else if (quizCarryPregameActive) { + timeVal.textContent = '···'; + } else if (!quizCarryCurrent) { + timeVal.textContent = '—'; + } else if (quizCarryOptionRevealAt > 0 && Date.now() < quizCarryOptionRevealAt) { + timeVal.textContent = String(Math.max(0, Math.ceil((quizCarryOptionRevealAt - Date.now()) / 1000))); + } else if (quizCarryAnswerCloseAt > Date.now()) { + timeVal.textContent = String(Math.max(0, Math.ceil((quizCarryAnswerCloseAt - Date.now()) / 1000))); + } else { + timeVal.textContent = '0'; + } + } else if (isGauntlet()) { + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) { + timeVal.textContent = '···'; + } else if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) { + const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000)); + const mm = Math.floor(rem / 60); + const ss = rem % 60; + timeVal.textContent = `${mm}:${String(ss).padStart(2, '0')}`; + } else if (gauntletRuntimeTimeLimitSec > 0) { + timeVal.textContent = '···'; + } else { + timeVal.textContent = '∞'; + } + } else if (isSpaceShooter()) { + if (isSpaceShooterMissionUiMapPlay()) { + if (spaceShooterMissionPhase !== 'live' || spaceShooterGameEnded) { + const limSs = spaceShooterTimeLimitSecPlay(); + if (spaceShooterMissionPhase === 'howto' || spaceShooterMissionPhase === 'countdown') { + timeVal.textContent = String(limSs > 0 ? limSs : 0); + } else { + timeVal.textContent = '0'; + } + } else { + const remSs = spaceShooterRemainingSecPlay(); + timeVal.textContent = remSs != null ? String(remSs) : '∞'; + } + } else if (spaceShooterGameEnded) { + timeVal.textContent = '0'; + } else { + const rem = spaceShooterRemainingSecPlay(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else if (isBalloonBoss()) { + if (isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay()) { + timeVal.textContent = '···'; + } else if (balloonBossGameEnded) { + timeVal.textContent = '0'; + } else { + const rem = balloonBossRemainingSecPlay(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) { + timeVal.textContent = jumpSurviveMissionPhase === 'howto' ? '···' : (jumpSurviveMissionPhase === 'countdown' ? '···' : '0'); + } else { + const rem = jumpSurviveRemainingSecMission(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else { + const t = jumpSurviveSessionStartMs > 0 + ? Math.max(0, Math.floor((performance.now() - jumpSurviveSessionStartMs) / 1000)) + : 0; + timeVal.textContent = String(t); + } + } + + if (portrait) { + const cid = me.characterId || getPlayCharacterId(); + setCyberHudScoreAvatarImg(portrait, { + id: myId, + characterId: cid, + isMe: true, + playTint: me.playTint || resolvePlayTintForPeer(String(myId != null ? myId : 'me')), + }); + } + if (statusEl) { + if (isSpaceShooterMissionHudActivePlay()) { + ensureStackTowerLifeHudImagesPlay(); + const nn = String((me.nickname || nick || 'PILOT')).trim().slice(0, 24); + const hits = Math.max(0, Math.min(SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS, Number(me.spaceShooterHits) || 0)); + const rem = SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS - hits; + const esc = playCyberHudEsc(nn); + const W = 220; + const H = 58; + const dpr = Math.min(2, Math.max(1, typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1)); + let canv = statusEl.querySelector('canvas.play-cyber-ss-integrity-canvas'); + let metaEl = statusEl.querySelector('.play-cyber-ss-meta'); + if (!canv || !metaEl) { + statusEl.textContent = ''; + canv = document.createElement('canvas'); + canv.className = 'play-cyber-ss-integrity-canvas'; + canv.setAttribute('aria-hidden', 'true'); + metaEl = document.createElement('div'); + metaEl.className = 'play-cyber-ss-meta'; + statusEl.appendChild(canv); + statusEl.appendChild(metaEl); + } + canv.width = Math.max(1, Math.floor(W * dpr)); + canv.height = Math.max(1, Math.floor(H * dpr)); + canv.style.width = W + 'px'; + canv.style.height = H + 'px'; + const ctxSs = canv.getContext('2d'); + if (ctxSs) { + ctxSs.setTransform(dpr, 0, 0, dpr, 0, 0); + ctxSs.clearRect(0, 0, W + 2, H + 2); + drawStackTowerLifeIntegrityBarPlay( + ctxSs, + 2, + 2, + W - 4, + H - 4, + SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS, + rem, + ); + } + metaEl.textContent = 'P1 · ' + esc + ' · TEAM 0 · COMBO x0'; + } else if (isQuizQuestionMissionHudActivePlay()) { + statusEl.textContent = 'QUIZ LINK · ประลองความรู้'; + } else if (isStackTowerMissionHudActivePlay()) { + statusEl.textContent = 'DECRYPT UPLINK · TOWER'; + } else if (isJumpSurviveMissionHudActivePlay()) { + statusEl.textContent = 'JUMPER LINK · SURVIVE'; + } else if (isQuizCarry() && quizCarrySessionEnded) { + statusEl.textContent = 'SESSION COMPLETE · จบชุดคำถาม'; + } else if (isJumpSurvive() && jumpSurviveEliminated) { + statusEl.textContent = 'SPECTATOR · LINK SEVERED'; + } else if (isBalloonBoss() && me.balloonBossEliminated) { + statusEl.textContent = 'SPECTATOR · BALLOONS GONE'; + } else if (isGauntletCrownHeistMapPlay()) { + statusEl.textContent = ''; + } else { + statusEl.textContent = 'OPERATOR ONLINE'; + } + } + if (hintEl) { + if (isQuizQuestionMissionHudActivePlay()) { + hintEl.textContent = ''; + hintEl.setAttribute('aria-hidden', 'true'); + } else { + hintEl.removeAttribute('aria-hidden'); + if (isStackTowerMissionHudActivePlay()) { + hintEl.textContent = 'กด SPACE / ENTER หรือคลิกที่จอ = DROP · วางต่อเนื่องแม่น = โบนัส · ถอดรหัสให้ครบ 100% ภายในเวลา'; + } else if (isQuizCarry()) { + hintEl.textContent = quizCarrySessionEnded + ? 'ชุดคำถามจบแล้ว · Session ended — ดูคะแนนด้าน SCORE' + : ('ยืนโซนตัวเลือกแล้วกด F/Grab หยิบ · ถือป้ายถูกแล้วไปฮับส่ง — ถูก +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' แต้ม (correct +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' on hub)'); + } else if (isJumpSurvive()) { + if (isJumpSurviveMissionUiMapPlay()) { + hintEl.textContent = jumpSurviveEliminated + ? 'คุณตกน้ำแล้ว — ดูเพื่อนต่อ — รอจบเพื่อรับคะแนน (ผู้รอดได้ 100 คะแนน)' + : 'กระโดดตามแท่น — หลีกหนาม/กับดักด้านล่าง · หมดเวลาแล้วสรุปคะแนน (ผู้รอด 100) · Space / W / ↑ กระโดด · A D / ← → เดิน'; + } else { + hintEl.textContent = jumpSurviveEliminated + ? 'Spectate remaining nodes · Exit room to reset session' + : 'Space / W / ↑ jump · A D / arrows move · Hit ceiling/floor = out (watch others)'; + } + } else if (isSpaceShooter()) { + hintEl.textContent = spaceShooterGameEnded + ? 'จบแล้ว · Session ended — ดูอันดับบน overlay / see rankings on overlay' + : 'A D / ← → = move · W S / ↑ ↓ = up/down (lower half of map only) · Space = fire · +5 per hit'; + } else if (isBalloonBoss()) { + hintEl.textContent = balloonBossGameEnded + ? 'จบแล้ว · Session ended — see overlay' + : (me.balloonBossEliminated + ? 'คุณตกรอบแล้ว — ดูเพื่อนเล่น · You are out — spectate' + : (isMegaVirusMissionShellMapPlay() + ? 'ลูกศรบนวงหมุนเอง — ยิงตามมุมลูกศร ณ ตอนกด Space · Ring spins · Space = fire along arrow' + : 'ยานมีแรงเฉื่อย — A D / arrows / W S เร่งทิศทาง · ปล่อยปุ่มแล้วยังไหล (แรงหน่วง) · Space = ยิง (ดีเลย์) · ลูกโป้งหมด = ตกรอบ')); + } else if (isGauntletCrownHeistMapPlay()) { + hintEl.textContent = 'Space / W / ↑ jump · Start 100 pts · Hit -10 · Fall off left edge = out · Rank bonus at end'; + } else { + hintEl.textContent = 'Space / W / ↑ jump · Lanes + lasers · Clear = right + score'; + } + } + } + if (previewEl) { + if (playBotsEnabled() && mapData) { + const human = countPlayHumans(); + const bots = [...others.keys()].filter(isPreviewBotId).length; + const botTarget = playBotTargetHeadcount(); + const simLabel = detectiveCaseFillBots ? 'CASE' : 'SIM'; + previewEl.textContent = `${simLabel} · humans ${human} + bots ${bots} · target ${botTarget}`; + previewEl.classList.remove('is-hidden'); + } else { + previewEl.textContent = ''; + previewEl.classList.add('is-hidden'); + } + } + + syncQuizQuestionMissionCyberCenterHud(); + + const ul = document.getElementById('play-cyber-score-list'); + if (!ul) return; + /* mnptfts2 — ไม่แสดงแผง SCORE (เหลือ TIME + โปรไฟล์) */ + if (isJumpSurviveMissionUiMapPlay()) { + ul.innerHTML = ''; + ul.dataset.cyberSig = ''; + const boardSkip = ul.closest('.play-cyber-scoreboard'); + if (boardSkip) boardSkip.classList.remove('play-cyber-scoreboard--crown-strip'); + ul.classList.remove('play-cyber-score-list--crown-strip'); + return; + } + const stripScoreHud = isGauntletCrownHeistMapPlay() || isQuizCarry(); + const board = ul.closest('.play-cyber-scoreboard'); + if (board) board.classList.toggle('play-cyber-scoreboard--crown-strip', !!stripScoreHud); + ul.classList.toggle('play-cyber-score-list--crown-strip', !!stripScoreHud); + const panelTitle = root.querySelector('.play-cyber-panel-title'); + const crownHead = document.getElementById('play-cyber-crown-score-head'); + const crownImg = document.getElementById('play-cyber-crown-score-img'); + if (panelTitle && crownHead && crownImg) { + if (stripScoreHud) { + crownHead.classList.add('is-hidden'); + crownHead.setAttribute('aria-hidden', 'true'); + crownImg.removeAttribute('src'); + delete crownImg.dataset.gauntletScoreSrc; + crownImg.alt = ''; + crownImg.onerror = null; + crownImg.onload = null; + panelTitle.textContent = 'SCORE :'; + panelTitle.classList.remove('is-hidden'); + } else { + crownHead.classList.add('is-hidden'); + crownHead.setAttribute('aria-hidden', 'true'); + crownImg.removeAttribute('src'); + delete crownImg.dataset.gauntletScoreSrc; + crownImg.alt = ''; + crownImg.onerror = null; + crownImg.onload = null; + panelTitle.textContent = 'SCORE'; + panelTitle.classList.remove('is-hidden'); + } + } + const safeYv = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + const jumpMissionHud = isJumpSurvive() && isJumpSurviveMissionUiMapPlay(); + const quizMissionHud = isQuizQuestionMissionHudActivePlay(); + const stackMissionHud = isStackTowerMissionHudActivePlay(); + const jumpMissionLiveHud = isJumpSurviveMissionHudActivePlay(); + const spaceMissionLiveHud = isSpaceShooterMissionHudActivePlay(); + /** แถบ SCORE แบบ mock mng8a80o (แนวนอน อวาตาร์+ชื่อ | คะแนน) — ภารกิจคำถาม + Stack Tower + Jumper mnptfts2 + Space mnpz6rkp */ + const cyberQmMockHud = quizMissionHud || stackMissionHud || jumpMissionLiveHud || spaceMissionLiveHud; + /** mng8a80o เท่านั้น — layout แถวแบบ mockup: [avatar | ชื่อบน/คะแนนล่าง] (ใช้ meta column) */ + const tfMockHud = isQuizQuestionMissionUiMapPlay(); + const stackTeamPts = Math.max(0, Number(stackMini && stackMini.score) || 0); + const rows = []; + rows.push({ + id: myId, + nickname: me.nickname || nick, + characterId: me.characterId || getPlayCharacterId(), + playTint: me.playTint || resolvePlayTintForPeer(String(myId != null ? myId : (nick || 'local'))), + score: (isQuizCarry() || quizMissionHud) ? cyberHudQuizCarryPeerScore(myId) + : (stackMissionHud + ? (playBotsEnabled() ? (me.stackPreviewHumanPts || 0) : stackTeamPts) + : (isGauntlet() ? (me.gauntletScore || 0) + : (isSpaceShooter() ? (me.spaceShooterScore || 0) + : (isBalloonBoss() ? (me.balloonBossScore || 0) + : (jumpMissionHud ? (jumpSurviveEliminated ? 0 : 100) : 0))))), + y: safeYv(me.y), + eliminated: !!(isJumpSurvive() && jumpSurviveEliminated) || !!(isBalloonBoss() && me.balloonBossEliminated) + || !!(isGauntletCrownHeistMapPlay() && me.gauntletEliminated), + isMe: true, + }); + others.forEach((o, id) => { + rows.push({ + id, + nickname: o.nickname || id.slice(0, 8), + characterId: o.characterId, + playTint: o.playTint || resolvePlayTintForPeer(id), + score: (isQuizCarry() || quizMissionHud) ? cyberHudQuizCarryPeerScore(id) + : (stackMissionHud + ? (playBotsEnabled() && isPreviewBotId(id) ? (o.stackBotScore || 0) : stackTeamPts) + : (isGauntlet() ? (o.gauntletScore || 0) + : (isSpaceShooter() ? (o.spaceShooterScore || 0) + : (isBalloonBoss() ? (o.balloonBossScore || 0) + : (jumpMissionHud ? (o.jumpSurviveEliminated ? 0 : 100) : 0))))), + y: safeYv(o.y), + eliminated: !!(isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated) + || !!(isBalloonBoss() && o.balloonBossEliminated) + || !!(isGauntletCrownHeistMapPlay() && o.gauntletEliminated), + isMe: false, + }); + }); + let leaderId = null; + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud || quizMissionHud || stackMissionHud) { + let mx = -1; + for (let i = 0; i < rows.length; i++) { + if (rows[i].score > mx) { + mx = rows[i].score; + leaderId = rows[i].id; + } + } + } else { + let best = Infinity; + for (let i = 0; i < rows.length; i++) { + if (!rows[i].eliminated && rows[i].y < best) { + best = rows[i].y; + leaderId = rows[i].id; + } + } + } + rows.sort((a, b) => { + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud || quizMissionHud || stackMissionHud) { + if (b.score !== a.score) return b.score - a.score; + if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1; + return String(a.nickname || '').localeCompare(String(b.nickname || ''), 'th'); + } + if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1; + return a.y - b.y; + }); + + const cap = stripScoreHud ? (isQuizCarry() ? 8 : 6) : 8; + let hudRows = rows.slice(0, Math.min(cap, rows.length)); + if (stripScoreHud) { + const mi = hudRows.findIndex((row) => row.isMe); + if (mi >= 0) { + const mine = hudRows.splice(mi, 1)[0]; + hudRows.push(mine); + } + } + /* กันกระตุก: rebuild DOM เฉพาะตอนเนื้อหา (id/score/order/สี/ตกรอบ) เปลี่ยน — ไม่ลบ-สร้างทุกเฟรม */ + const listSig = [ + cyberQmMockHud ? 'qm' : '', stripScoreHud ? 'strip' : '', leaderId == null ? '' : String(leaderId), + hudRows.map(function (rw) { + const t = rw.playTint || {}; + return String(rw.id) + ':' + String(rw.score) + ':' + (rw.eliminated ? 1 : 0) + + ':' + String(rw.characterId || '') + ':' + String(t.body || '') + ':' + String(t.head || ''); + }).join(','), + ].join('|'); + if (ul.dataset.cyberSig === listSig) { + const existImgs = ul.querySelectorAll('img.play-cyber-score-av'); + if (existImgs.length === hudRows.length) { + for (let i = 0; i < existImgs.length; i++) { + if (!cyberHudScoreAvatarImgLooksReady(existImgs[i])) setCyberHudScoreAvatarImg(existImgs[i], hudRows[i]); + } + return; + } + } + ul.dataset.cyberSig = listSig; + ul.innerHTML = ''; + for (let r = 0; r < hudRows.length; r++) { + const row = hudRows[r]; + const li = document.createElement('li'); + li.className = 'play-cyber-score-row' + (row.isMe ? ' is-me' : '') + (row.eliminated ? ' is-out' : '') + + (cyberQmMockHud ? ' play-cyber-score-row--qm-mock' : '') + + (stripScoreHud ? ' play-cyber-score-row--crown-strip' : ''); + if (row.id != null) li.setAttribute('data-peer-id', String(row.id)); + const av = document.createElement('img'); + av.className = 'play-cyber-score-av' + (cyberQmMockHud ? ' play-cyber-score-av--qm' : '') + + (stripScoreHud ? ' play-cyber-score-av--crown-strip' : ''); + av.alt = ''; + setCyberHudScoreAvatarImg(av, row); + const sc = document.createElement('span'); + sc.className = 'play-cyber-score-val' + (cyberQmMockHud ? ' play-cyber-qm-score' : '') + + (stripScoreHud ? ' play-cyber-crown-strip-val' : ''); + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud || quizMissionHud || stackMissionHud) { + sc.textContent = String(row.score); + } else { + sc.textContent = row.eliminated ? '—' : String(r + 1); + } + if (cyberQmMockHud) { + if (row.id === leaderId && leaderId != null) { + const crown = document.createElement('span'); + crown.className = 'play-cyber-lead-crown'; + crown.textContent = '♔'; + crown.title = 'Leader'; + sc.insertBefore(crown, sc.firstChild); + } + const qmLeft = document.createElement('div'); + qmLeft.className = 'play-cyber-qm-left'; + const avWrap = document.createElement('span'); + avWrap.className = 'play-cyber-qm-av-wrap'; + if (tfMockHud) { + /* mng8a80o: หน้าตัวละครต้องอยู่ "บน" กรอบ (กรอบกลางทึบ) + clip ให้อยู่ในขอบ */ + const avClip = document.createElement('span'); + avClip.className = 'play-cyber-qm-av-clip'; + avClip.appendChild(av); + avWrap.appendChild(avClip); + } else { + avWrap.appendChild(av); + } + const qmName = document.createElement('span'); + qmName.className = 'play-cyber-qm-name'; + qmName.textContent = String(row.nickname || '').trim().slice(0, 28); + qmLeft.appendChild(avWrap); + if (tfMockHud) { + /* mockup Minigame-1: avatar ซ้าย | (ชื่อบน / คะแนนล่าง) ในคอลัมน์ meta */ + const qmMeta = document.createElement('div'); + qmMeta.className = 'play-cyber-qm-meta'; + qmMeta.appendChild(qmName); + qmMeta.appendChild(sc); + qmLeft.appendChild(qmMeta); + li.appendChild(qmLeft); + } else { + qmLeft.appendChild(qmName); + li.appendChild(qmLeft); + li.appendChild(sc); + } + } else if (stripScoreHud) { + const cell = document.createElement('div'); + cell.className = 'play-cyber-crown-strip-cell'; + const avWrap = document.createElement('div'); + avWrap.className = 'play-cyber-crown-strip-av-wrap'; + avWrap.appendChild(av); + const meta = document.createElement('div'); + meta.className = 'play-cyber-crown-strip-meta'; + const name = document.createElement('span'); + name.className = 'play-cyber-crown-strip-name'; + if (row.id === leaderId && leaderId != null) { + const crown = document.createElement('span'); + crown.className = 'play-cyber-lead-crown'; + crown.textContent = '♔'; + crown.title = 'Leader'; + name.appendChild(crown); + } + name.appendChild(document.createTextNode(String(row.nickname || '').trim().slice(0, 16))); + meta.appendChild(name); + meta.appendChild(sc); + cell.appendChild(avWrap); + cell.appendChild(meta); + li.appendChild(cell); + } else { + const mid = document.createElement('div'); + mid.className = 'play-cyber-score-mid'; + const name = document.createElement('span'); + name.className = 'play-cyber-score-name'; + if (row.id === leaderId && leaderId != null) { + const crown = document.createElement('span'); + crown.className = 'play-cyber-lead-crown'; + crown.textContent = '♔'; + crown.title = 'Leader'; + name.appendChild(crown); + } + name.appendChild(document.createTextNode(String(row.nickname || '').slice(0, 14))); + const st = document.createElement('span'); + st.className = 'play-cyber-score-state'; + st.textContent = (isQuizCarry() || quizMissionHud) + ? '' + : (row.eliminated + ? (isBalloonBoss() ? 'OUT' : (isGauntletCrownHeistMapPlay() ? 'เสียชีวิต' : 'OFFLINE')) + : (isBalloonBoss() + ? ('♥' + Math.max(0, row.isMe ? (me.balloonBossBalloons | 0) : ((() => { + const ox = others.get(row.id); + return ox ? (ox.balloonBossBalloons | 0) : 0; + })()))) + : 'LINK_OK')); + mid.appendChild(name); + mid.appendChild(st); + li.appendChild(av); + li.appendChild(mid); + li.appendChild(sc); + } + ul.appendChild(li); + } + } + + function draw() { + try { + if (!mapData) { + document.documentElement.classList.remove('play-stack-tower-pixel-canvas'); + return; + } + document.documentElement.classList.toggle('play-stack-tower-pixel-canvas', isStackTowerMissionUiMapPlay()); + } catch (e) { /* ignore */ } + if (isGauntletCrownHeistMapPlay()) ensureGauntletCrownScorePenaltyImgPlay(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + const w = mapData.width, h = mapData.height; + + let zDraw = computePlayCameraZDrawPlay(); + const stackCam = isStack() ? getStackCameraCentersPx() : null; + let camX = stackCam ? stackCam.px : me.x * tileSize; + let camY = stackCam ? stackCam.py : me.y * tileSize; + if (isJumpSurvive()) { + camX = jumpSurviveCamCenterX; + camY = jumpSurviveCamCenterY; + } + if (isSpaceShooter() || isBalloonBoss()) { + const mwPx = w * tileSize, mhPx = h * tileSize; + camX = mwPx * 0.5; + camY = mhPx * 0.5; + } else if (isQuizCarry()) { + const qc = getQuizCarryMapCameraWorldCenterPxPlay(); + if (qc) { + camX = qc.cx; + camY = qc.cy; + } + } else if (isQuizQuestionMissionHudActivePlay()) { + const qmc = getQuizQuestionMissionMapCenterWorldPxPlay(); + if (qmc) { + camX = qmc.cx; + camY = qmc.cy; + } + } + const gauntletGroupCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zDraw); + if (gauntletGroupCam) { + camX = gauntletGroupCam.px; + camY = gauntletGroupCam.py; + } + /* Stack Tower (mnn93hpi): ไม่เลื่อนกล้องตาม floorWorldY — ใช้ cam จาก getStackCameraCentersPx() เหมือน Stack ทั่วไป (วางบล็อกแล้วจอไม่ไหล) */ + lastPlayZDrawForInput = zDraw; + const stackTowerWorldScrollScreenY = getStackTowerWorldLayerScrollScreenOffsetYPlay(zDraw); + const halfW = canvas.width / (2 * zDraw); + const halfH = canvas.height / (2 * zDraw); + + // world bounds ที่กล้องมองเห็น (เป็นพิกัดพิกเซลของ map) + const worldMinX = camX - halfW; + const worldMaxX = camX + halfW; + const worldMinY = camY - halfH; + const worldMaxY = camY + halfH; + + const mapWpx = w * tileSize, mapHpx = h * tileSize; + const visibleW = worldMaxX - worldMinX, visibleH = worldMaxY - worldMinY; + const showGrid = mapData.showMapInGame !== false && mapData.showMapInGame !== 'false'; + const timeMs = Date.now(); + + if (playScrollBgDrawActive()) { + ctx.fillStyle = '#0a0e22'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawPlayScrollBgFullCanvas(canvas.width, canvas.height); + } else if (gauntletCrownRunwayBgDrawActivePlay()) { + ctx.fillStyle = '#1a1b26'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawGauntletCrownRunwayBgFullCanvasPlay(canvas.width, canvas.height, { + worldMinX, + worldMaxX, + camX, + camY, + zDraw: zDraw, + mapWpx, + mapHpx, + }); + } else if (stackTowerScrollBgDrawActive()) { + ctx.fillStyle = '#0a0e22'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawStackTowerScrollBgFullCanvas(canvas.width, canvas.height, zDraw); + } else if (mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) { + /* กล้องเห็นพื้นที่นอก [0,map] ได้ — ห้ามสุ่มต้นทาง drawImage เกินขอบภาพ (จะเกิดซ้ำ/เส้นตัด/ซ้อนกันที่ขอบจอ) */ + ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawStackTowerLoopSkyFillAboveMap(ctx, worldMinY, zDraw, canvas.width, canvas.height); + const natW = mapBackgroundImg.naturalWidth; + const natH = mapBackgroundImg.naturalHeight; + const bgMinX = Math.max(0, worldMinX); + const bgMaxX = Math.min(mapWpx, worldMaxX); + const bgMinY = Math.max(0, worldMinY); + const bgMaxY = Math.min(mapHpx, worldMaxY); + if (bgMaxX > bgMinX && bgMaxY > bgMinY) { + const srcX = (bgMinX / mapWpx) * natW; + const srcY = (bgMinY / mapHpx) * natH; + const srcW = ((bgMaxX - bgMinX) / mapWpx) * natW; + const srcH = ((bgMaxY - bgMinY) / mapHpx) * natH; + const destX = (bgMinX - camX) * zDraw + canvas.width / 2; + const destY = (bgMinY - camY) * zDraw + canvas.height / 2 + - (isStackTowerMissionUiMapPlay() ? stackTowerWorldScrollScreenY : 0); + const destW = (bgMaxX - bgMinX) * zDraw; + const destH = (bgMaxY - bgMinY) * zDraw; + if (srcW > 0.25 && srcH > 0.25 && destW > 0.25 && destH > 0.25) { + ctx.drawImage(mapBackgroundImg, srcX, srcY, srcW, srcH, destX, destY, destW, destH); + } + } + } else { + ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + if (isSpaceShooter()) { + ctx.save(); + for (let s = 0; s < 120; s++) { + const sx = (Math.sin(s * 12.9898 + w) * 0.5 + 0.5) * canvas.width; + const sy = (Math.cos(s * 4.1415 + h) * 0.5 + 0.5) * canvas.height; + const br = (s % 4 === 0) ? 1.6 : 0.9; + ctx.fillStyle = s % 7 === 0 ? 'rgba(200, 240, 255, 0.35)' : 'rgba(255, 255, 255, 0.12)'; + ctx.beginPath(); + ctx.arc(sx, sy, br, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); + } else if (isBalloonBoss()) { + ctx.save(); + ctx.fillStyle = 'rgba(30, 45, 120, 0.25)'; + for (let col = 0; col < 24; col++) { + const x = (col + 0.5) * (canvas.width / 24); + for (let r = 0; r < 18; r++) { + const bit = (col * 17 + r * 31 + Math.floor(timeMs / 80)) % 2; + ctx.fillStyle = bit ? 'rgba(0, 255, 200, 0.14)' : 'rgba(180, 200, 255, 0.08)'; + ctx.fillRect(x - 1, (r / 18) * canvas.height + ((timeMs / 40 + col * 3) % 40), 2.2, 10); + } + } + ctx.fillStyle = 'rgba(240, 248, 255, 0.55)'; + ctx.beginPath(); + ctx.moveTo(0, canvas.height); + const baseY = canvas.height * 0.88; + for (let k = 0; k <= 40; k++) { + const bx = (k / 40) * canvas.width; + const by = baseY - Math.abs(Math.sin(k * 1.7 + w)) * canvas.height * 0.08 - (k % 3) * 4; + ctx.lineTo(bx, by); + } + ctx.lineTo(canvas.width, canvas.height); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + } + function worldToScreen(wx, wy) { + const sx = (wx - camX) * zDraw + canvas.width / 2; + const sy = (wy - camY) * zDraw + canvas.height / 2 - stackTowerWorldScrollScreenY; + return [sx, sy]; + } + + function drawGridImagePlacementsLayer() { + if (!mapData.gridImagePlacements || !mapData.gridImagePlacements.length || !mapData.gridImageLibrary || !mapData.gridImageLibrary.length) return; + for (let gi = 0; gi < mapData.gridImagePlacements.length; gi++) { + const sp = mapData.gridImagePlacements[gi]; + const wx0 = sp.x * tileSize; + const wy0 = sp.y * tileSize; + const ww = sp.w * tileSize; + const wh = sp.h * tileSize; + if (wx0 + ww <= worldMinX || wx0 >= worldMaxX || wy0 + wh <= worldMinY || wy0 >= worldMaxY) continue; + let gim = mapGridImageImgs[sp.i]; + let carrySpriteAlpha = 1; + if (isQuizCarry()) { + const domOpt = quizCarryOptionIndexForGridSprite(mapData, sp); + const onHub = spriteOverlapsQuizCarryHubArea(mapData, sp); + const meHolding = me && me.quizCarryHeld != null; + let carryVisualActive = false; + if (domOpt != null && quizCarryOptionHeldByAnyone(domOpt)) carryVisualActive = true; + else if (domOpt == null && onHub && meHolding) carryVisualActive = true; + if (carryVisualActive) { + const gh = mapGridImageHeldImgs[sp.i]; + if (gh && gh.complete && gh.naturalWidth) gim = gh; + else carrySpriteAlpha = 0.32; + } + } + if (!gim || !gim.complete || !gim.naturalWidth) continue; + const sx0 = (wx0 - camX) * zDraw + canvas.width / 2; + const sy0 = (wy0 - camY) * zDraw + canvas.height / 2 - stackTowerWorldScrollScreenY; + const sw = ww * zDraw; + const sh = wh * zDraw; + ctx.save(); + if (carrySpriteAlpha < 1) ctx.globalAlpha = carrySpriteAlpha; + ctx.beginPath(); + ctx.rect(sx0, sy0, sw, sh); + ctx.clip(); + ctx.drawImage(gim, sx0, sy0, sw, sh); + ctx.restore(); + ctx.strokeStyle = 'rgba(255,255,255,0.18)'; + ctx.lineWidth = 1; + ctx.strokeRect(sx0 + 0.5, sy0 + 0.5, sw - 1, sh - 1); + } + } + + const startTileX = Math.max(0, Math.floor(worldMinX / tileSize)); + const endTileX = Math.min(w - 1, Math.ceil(worldMaxX / tileSize)); + const startTileY = Math.max(0, Math.floor(worldMinY / tileSize)); + const endTileY = Math.min(h - 1, Math.ceil(worldMaxY / tileSize)); + if (showGrid) { + for (let y = startTileY; y <= endTileY; y++) { + const lane = isFrogger() ? getLane(y) : null; + let rowFill = null; + if (lane) { + if (lane.type === 'goal') rowFill = 'rgba(158,206,106,0.4)'; + else if (lane.type === 'spawn') rowFill = 'rgba(187,154,247,0.35)'; + else if (lane.type === 'road') rowFill = 'rgba(80,70,60,0.6)'; + else if (lane.type === 'water') rowFill = 'rgba(125,207,255,0.5)'; + } else if (isGauntlet()) { + rowFill = (y % 2 === 0) ? 'rgba(247,118,190,0.08)' : 'rgba(180,90,140,0.06)'; + } else if (isStack()) { + rowFill = (y % 2 === 0) ? 'rgba(122, 162, 247, 0.06)' : 'rgba(187, 154, 247, 0.05)'; + } else if (isJumpSurvive()) { + rowFill = (y % 2 === 0) ? 'rgba(90, 200, 255, 0.07)' : 'rgba(60, 140, 200, 0.06)'; + } else if (isSpaceShooter()) { + rowFill = (y % 2 === 0) ? 'rgba(30, 45, 110, 0.22)' : 'rgba(20, 32, 78, 0.28)'; + } else if (isBalloonBoss()) { + rowFill = (y % 2 === 0) ? 'rgba(40, 25, 80, 0.2)' : 'rgba(25, 18, 60, 0.24)'; + } else if (isQuizBattle()) { + rowFill = (y % 2 === 0) ? 'rgba(110, 175, 255, 0.06)' : 'rgba(230, 110, 140, 0.055)'; + } + for (let x = startTileX; x <= endTileX; x++) { + const wx = x * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw; + const ob = mapData.objects?.[y]?.[x] ?? 0; + if (ob === 1) { + ctx.fillStyle = 'rgba(65,72,104,0.92)'; + ctx.fillRect(sx, sy, size, size); + ctx.strokeStyle = '#565f89'; + ctx.strokeRect(sx, sy, size, size); + } else { + const cellColor = showGrid && mapData.cellColors && mapData.cellColors[y] && mapData.cellColors[y][x]; + if (cellColor) { ctx.fillStyle = cellColor; ctx.fillRect(sx, sy, size, size); } + else if (rowFill) { ctx.fillStyle = rowFill; ctx.fillRect(sx, sy, size, size); } + else if (!mapBackgroundImg || !mapBackgroundImg.complete) { + ctx.fillStyle = (x + y) % 2 === 0 ? '#24283b' : '#1f2335'; + ctx.fillRect(sx, sy, size, size); + } + } + } + } + } + drawGridImagePlacementsLayer(); + if (showGrid) { + for (let y = startTileY; y <= endTileY; y++) { + for (let x = startTileX; x <= endTileX; x++) { + const wx = x * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw; + const isInter = mapData.interactive && mapData.interactive[y] && mapData.interactive[y][x] === 1; + if (isInter) { + ctx.fillStyle = 'rgba(158,206,106,0.35)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(158,206,106,0.8)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + if (isQuiz()) { + const isQuizQ = mapData.quizQuestionArea && mapData.quizQuestionArea[y] && mapData.quizQuestionArea[y][x] === 1; + if (isQuizQ) { + ctx.fillStyle = 'rgba(255, 214, 102, 0.32)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(224, 185, 70, 0.78)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isQuizT = mapData.quizTrueArea && mapData.quizTrueArea[y] && mapData.quizTrueArea[y][x] === 1; + if (isQuizT) { + ctx.fillStyle = 'rgba(86, 202, 255, 0.38)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(122, 220, 255, 0.85)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isQuizF = mapData.quizFalseArea && mapData.quizFalseArea[y] && mapData.quizFalseArea[y][x] === 1; + if (isQuizF) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.38)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(255, 130, 200, 0.85)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + } + if (isQuizCarry()) { + const hub = mapData.quizCarryHubArea && mapData.quizCarryHubArea[y] && mapData.quizCarryHubArea[y][x] === 1; + if (hub) { + ctx.fillStyle = 'rgba(187, 154, 247, 0.35)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(200, 170, 255, 0.75)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isCarryQ = mapData.quizQuestionArea && mapData.quizQuestionArea[y] && mapData.quizQuestionArea[y][x] === 1; + if (isCarryQ) { + ctx.fillStyle = 'rgba(255, 214, 102, 0.3)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(224, 185, 70, 0.82)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const ov = mapData.quizCarryOptionArea && mapData.quizCarryOptionArea[y] && mapData.quizCarryOptionArea[y][x]; + if (ov >= 1 && ov <= QUIZ_CARRY_MAX_OPTION_SLOTS) { + ctx.fillStyle = quizCarryMinimapOptionFillCss(ov); + ctx.fillRect(sx + 3, sy + 3, size - 6, size - 6); + } + } + if (isQuizBattle() && showGrid && mapData.quizBattlePathArea && mapData.quizBattlePathArea[y] && mapData.quizBattlePathArea[y][x] === 1) { + ctx.fillStyle = 'rgba(186, 130, 255, 0.4)'; + ctx.fillRect(sx + 1, sy + 1, size - 2, size - 2); + ctx.strokeStyle = 'rgba(220, 170, 255, 0.55)'; + ctx.strokeRect(sx + 1, sy + 1, size - 2, size - 2); + } + if (isQuizBattle() && showGrid && mapData.quizBattleDomeArea && mapData.quizBattleDomeArea[y] && mapData.quizBattleDomeArea[y][x] === 1) { + const cid = mapData.quizBattleDomeComp && mapData.quizBattleDomeComp[y] && mapData.quizBattleDomeComp[y][x]; + const done = cid > 0 && quizBattleAnsweredComps.has(cid); + ctx.fillStyle = done ? 'rgba(90, 90, 110, 0.4)' : 'rgba(100, 200, 255, 0.36)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = done ? 'rgba(140, 140, 160, 0.65)' : 'rgba(255, 100, 130, 0.82)'; + ctx.lineWidth = 2; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.lineWidth = 1; + } + if (isStack()) { + const sr = mapData.stackReleaseArea, sl = mapData.stackLandArea; + if (sr && sr[y] && sr[y][x] === 1) { + ctx.fillStyle = 'rgba(125, 207, 255, 0.28)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(122, 220, 255, 0.55)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + if (sl && sl[y] && sl[y][x] === 1) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.26)'; + ctx.fillRect(sx + 3, sy + 3, size - 6, size - 6); + ctx.strokeStyle = 'rgba(255, 130, 200, 0.5)'; + ctx.strokeRect(sx + 3, sy + 3, size - 6, size - 6); + } + } + /* ไม่วาด P1–P6 บนกริดระหว่างเล่น — ตำแหน่งไทล์คงที่ แต่ตัวละครอยู่ world px + บอทขยับ จึงไม่ตรง design · Slot labels belong in Map Editor only */ + if (isSpaceShooter() && showGrid && mapData.shooterSpawnSlots) { + const sv = mapData.shooterSpawnSlots[y] && mapData.shooterSpawnSlots[y][x]; + if (sv >= 1 && sv <= 6) { + ctx.fillStyle = 'rgba(0, 255, 240, 0.14)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(120, 255, 255, 0.45)'; + ctx.lineWidth = 1.5; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.lineWidth = 1; + ctx.fillStyle = 'rgba(220, 250, 255, 0.9)'; + ctx.font = `bold ${Math.max(8, 10 * zDraw)}px sans-serif`; + ctx.textAlign = 'center'; + ctx.fillText('P' + sv, sx + size / 2, sy + size / 2 + 4); + ctx.textAlign = 'left'; + } + } + } + } + } + if (isJumpSurvive() && mapData.jumpSurvivePlatformArea) { + const pa = mapData.jumpSurvivePlatformArea; + const sc = jumpSurvivePlatformScrollPx; + const tsz = tileSize; + const period = h * tsz; + const visTop = worldMinY; + const visBot = worldMaxY; + const eps = 1e-6; + for (let py = 0; py < h; py++) { + for (let px = 0; px < w; px++) { + if (!pa[py] || pa[py][px] !== 1) continue; + const vIdx = jumpSurvivePlatformVariantIndexAtPlay(mapData, px, py); + if (px > 0 && pa[py][px - 1] === 1 && jumpSurvivePlatformVariantIndexAtPlay(mapData, px - 1, py) === vIdx) { + continue; + } + let runLen = 1; + while (px + runLen < w && pa[py][px + runLen] === 1 + && jumpSurvivePlatformVariantIndexAtPlay(mapData, px + runLen, py) === vIdx) runLen += 1; + const cfg = playJumpSurvivePlatformTiles[vIdx - 1] || { url: '', w: 0, h: 0 }; + const jumpPlatU = normalizeGauntletAssetUrlForPlay(cfg.url || ''); + const jumpPlatRec = jumpPlatU ? ensureGauntletAssetImage(jumpPlatU) : null; + const jumpPlatImg = jumpPlatRec && jumpPlatRec.ready && jumpPlatRec.img && jumpPlatRec.img.naturalWidth > 0 ? jumpPlatRec.img : null; + const tileWorldLeft = px * tsz; + const tileWorldRight = (px + runLen) * tsz; + if (tileWorldRight <= worldMinX || tileWorldLeft >= worldMaxX) continue; + const rawTop = py * tsz + sc; + const kMin = Math.ceil((visTop - rawTop - tsz + eps) / period); + const kMax = Math.floor((visBot - rawTop - eps) / period); + if (kMax < kMin) continue; + for (let kk = kMin; kk <= kMax; kk++) { + const platTop = rawTop + kk * period; + if (platTop + tsz <= visTop || platTop >= visBot) continue; + const [psx, psy] = worldToScreen(tileWorldLeft, platTop); + const psz = tsz * zDraw; + const segScreenW = runLen * psz; + let dw; + let dh = cfg.h > 0 ? cfg.h * zDraw : psz - 4; + if (runLen > 1) { + dw = segScreenW; + } else { + dw = cfg.w > 0 ? cfg.w * zDraw : psz - 4; + const maxDim = psz * 4; + if (dw > maxDim) dw = maxDim; + } + const maxDh = psz * 4; + if (dh > maxDh) dh = maxDh; + const marginBot = 2; + const dx = runLen > 1 ? psx : psx + (psz - dw) / 2; + const dy = psy + psz - dh - marginBot; + if (jumpPlatImg) { + ctx.save(); + try { ctx.imageSmoothingEnabled = true; } catch (e) { /* ignore */ } + ctx.drawImage(jumpPlatImg, dx, dy, dw, dh); + ctx.restore(); + } else { + const fillW = runLen > 1 ? segScreenW - 6 : psz - 6; + ctx.fillStyle = 'rgba(0, 40, 52, 0.72)'; + ctx.fillRect(psx + 3, psy + 3, fillW, psz - 6); + const grad = ctx.createLinearGradient(psx, psy, psx, psy + psz); + grad.addColorStop(0, 'rgba(0, 255, 240, 0.12)'); + grad.addColorStop(0.5, 'rgba(0, 0, 0, 0)'); + grad.addColorStop(1, 'rgba(0, 180, 200, 0.08)'); + ctx.fillStyle = grad; + ctx.fillRect(psx + 4, psy + 4, fillW - 2, psz - 8); + ctx.lineWidth = 1; + if (zDraw >= 1.15 && psz > 22) { + ctx.save(); + ctx.font = `600 ${Math.max(7, Math.min(11, psz * 0.14))}px "Share Tech Mono", ui-monospace, monospace`; + ctx.fillStyle = 'rgba(180, 255, 255, 0.5)'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + const cx = psx + (runLen > 1 ? segScreenW * 0.5 : psz * 0.5); + ctx.fillText('DATA', cx, psy + psz * 0.38); + ctx.font = `500 ${Math.max(5, Math.min(8, psz * 0.1))}px "Share Tech Mono", ui-monospace, monospace`; + ctx.fillStyle = 'rgba(120, 230, 255, 0.38)'; + ctx.fillText('/SEC/BLOCK', cx, psy + psz * 0.62); + ctx.restore(); + } + } + } + } + } + } + if (isJumpSurvive() && showGrid && mapData.jumpSurviveHazardArea) { + const ha = mapData.jumpSurviveHazardArea; + const tsz = tileSize; + for (let py = startTileY; py <= endTileY; py++) { + for (let px = startTileX; px <= endTileX; px++) { + if (!ha[py] || ha[py][px] !== 1) continue; + const [psx, psy] = worldToScreen(px * tsz, py * tsz); + const psz = tsz * zDraw; + ctx.fillStyle = 'rgba(200, 28, 55, 0.38)'; + ctx.fillRect(psx + 1, psy + 1, psz - 2, psz - 2); + ctx.strokeStyle = 'rgba(255, 90, 110, 0.82)'; + ctx.lineWidth = 2; + ctx.strokeRect(psx + 1, psy + 1, psz - 2, psz - 2); + ctx.lineWidth = 1; + } + } + } + if (showGrid) { + if (isFrogger() && mapData.lanes) { + for (let y = startTileY; y <= endTileY; y++) { + const lane = getLane(y); + if (!lane || (lane.type !== 'road' && lane.type !== 'water')) continue; + const positions = getVehiclePositions(lane, mapData.width, timeMs); + const isRoad = lane.type === 'road'; + for (let i = 0; i < positions.length; i++) { + const vx = positions[i]; + const wx = vx * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw * (isRoad ? 1.2 : 1.5); + ctx.fillStyle = isRoad ? '#e0a060' : '#8b7355'; + ctx.fillRect(sx, sy, size, size * 0.7); + ctx.strokeStyle = isRoad ? '#c0caf5' : '#9ece6a'; + ctx.strokeRect(sx, sy, size, size * 0.7); + } + } + } + } + + if (isQuizCarry()) { + drawQuizCarryChoiceLabels(ctx, worldToScreen, zDraw); + } + + if (isStack() && stackMini) { + drawStackMinigame(ctx, worldToScreen, zDraw); + } + + const gauntletObsAlpha = isGauntlet() ? gauntletCrownRunwayObstacleDrawAlphaPlay() : 1; + const gauntletObsDraw = (isGauntlet() && gauntletObsAlpha > 0.001) + ? getGauntletObsDrawPositionsAt(performance.now()) + : []; + if (isGauntlet() && gauntletObsDraw.length) { + ctx.save(); + ctx.globalAlpha = gauntletObsAlpha; + const stx = Math.max(0, Math.floor(worldMinX / tileSize)); + const enx = Math.min(w - 1, Math.ceil(worldMaxX / tileSize)); + const sty = Math.max(0, Math.floor(worldMinY / tileSize)); + const eny = Math.min(h - 1, Math.ceil(worldMaxY / tileSize)); + for (let i = 0; i < gauntletObsDraw.length; i++) { + const o = gauntletObsDraw[i]; + if (!o) continue; + if (o.kind === 'lane' && typeof o.y === 'number') { + if (o.drawX < stx - 2 || o.drawX > enx + 2 || o.y < sty || o.y > eny) continue; + const size = tileSize * zDraw; + const inner = Math.max(2, size - 4); + const [sxC, syCellBottom] = worldToScreen((o.drawX + 0.5) * tileSize, (o.y + 1) * tileSize); + const dx = sxC - inner / 2; + const dy = syCellBottom - inner; + const laneRec = pickGauntletLaneImageRec(o.id); + if (laneRec && laneRec.img.complete && laneRec.img.naturalWidth > 0) { + try { + ctx.drawImage(laneRec.img, dx, dy, inner, inner); + } catch (e) { + ctx.fillStyle = '#f7768e'; + ctx.fillRect(dx, dy, inner, inner); + } + } else { + ctx.fillStyle = '#f7768e'; + ctx.fillRect(dx, dy, inner, inner); + } + } else if (o.kind === 'laser' && typeof o.drawX === 'number') { + if (o.drawX < stx - 2 || o.drawX > enx + 2) continue; + const { y0: laserRow0, y1: laserRow1 } = gauntletLaserRowHitRange(o, h); + const wx0 = o.drawX * tileSize; + const wx1 = (o.drawX + 1) * tileSize; + const wy0 = laserRow0 * tileSize; + const wy1 = (laserRow1 + 1) * tileSize; + const [sx0, syTop] = worldToScreen(wx0, wy0); + const [sx1, syBot] = worldToScreen(wx1, wy1); + const rx = Math.min(sx0, sx1); + const rw = Math.max(2, Math.abs(sx1 - sx0)); + const ry = Math.min(syTop, syBot); + const rh = Math.abs(syBot - syTop); + drawGauntletLaserColumnScreen(rx, ry, rw, rh); + } + } + ctx.restore(); + } + + /** ป้ายติดตัว — กึ่งกลางลำตัว (ทับอก/เอว) แบบเดียวกับป้ายบนพื้น ไม่มีเส้นเชื่อม */ + function drawQuizCarryHeldSignBoard(sxFeet, feetSy, halfSpriteW, spriteTopY, signText, _faceDir, choiceIndex, imageUrl, imageElementOpt) { + const imgUrlSan = sanitizeQuizCarryImageUrlClient(imageUrl); + const gridHeldDraw = imageElementOpt && imageElementOpt.complete && imageElementOpt.naturalWidth > 0 ? imageElementOpt : null; + const raw = String(signText || '').trim(); + if (!raw && !imgUrlSan && !gridHeldDraw) return; + ctx.save(); + const heldPs = quizCarryPlaqueMapScaleClampedPlay(); + const bodyH = Math.max(18, feetSy - spriteTopY); + const midBodyY = spriteTopY + bodyH * 0.48; + const bodyCx = sxFeet; + const halfW = Math.max(10, halfSpriteW || 0); + const baseMaxW = Math.min(280, Math.max(100, halfW * 2.4 + tileSize * zDraw * 0.5)); + const maxPlaqueW = Math.min(340, Math.round(baseMaxW * heldPs)); + + const fontPx = Math.max(12, Math.min(22, tileSize * zDraw * 0.22 * heldPs)); + ctx.font = `600 ${fontPx}px system-ui, "Segoe UI", "Kanit", sans-serif`; + const padX = 12; + const padY = 9; + const maxInner = Math.min(maxPlaqueW - padX * 2, Math.max(72, tileSize * zDraw * 4.2)); + let lines; + if (!raw) { + lines = []; + } else { + lines = canvasWordWrapLines(ctx, raw, maxInner); + if (!lines.length) lines = [raw]; + if (lines.length > 3) lines = lines.slice(0, 3); + for (let i = 0; i < lines.length; i++) { + let s = lines[i]; + if (ctx.measureText(s).width <= maxInner) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxInner) s = s.slice(0, -1); + lines[i] = s + '…'; + } + } + let maxLineW = 0; + for (let i = 0; i < lines.length; i++) { + const lw = ctx.measureText(lines[i]).width; + if (lw > maxLineW) maxLineW = lw; + } + const lineH = fontPx * 1.22; + let w; + let h; + if (!lines.length && (imgUrlSan || gridHeldDraw)) { + w = Math.ceil(Math.min(maxPlaqueW, Math.max(104, maxInner + padX * 2))); + h = Math.ceil(Math.max(56, fontPx * 2.8 + padY * 2)); + } else { + w = Math.ceil(Math.max(maxLineW, 56) + padX * 2); + h = Math.ceil(lines.length * lineH + padY * 2); + w = Math.min(maxPlaqueW, Math.max(96, w)); + h = Math.max(40, h); + } + + let signX = bodyCx - w / 2; + let signY = midBodyY - h / 2; + + if (signX + w > canvas.width - 6) signX = canvas.width - w - 6; + if (signX < 6) signX = 6; + if (signY < 6) signY = 6; + if (signY + h > feetSy + 8) signY = feetSy + 8 - h; + + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, null, { imageUrl: imgUrlSan, imageElement: gridHeldDraw }); + ctx.restore(); + } + + function drawAvatar(ax, ay, isMe, name, characterId, direction, isWalking, playTint, gauntletAirTicks, gauntletScoreLabel, quizCarrySignPayload, crownPenaltyFxUntil) { + const { cw, ch } = getCharacterFootprintWH(mapData); + const air = Number(gauntletAirTicks) || 0; + let liftWorldY = 0; + if (isGauntlet() && air > 0) { + liftWorldY = gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSize * 0.52; + } + const cxWorld = (ax + cw * 0.5) * tileSize; + /* ใช้ ay แบบต่อเนื่อง — ห้าม floor(ay) เดิมทำให้เท้ากระโดดทีละช่องขณะเดิน (กระตุกแนวตั้ง) */ + const cyBottomWorld = (ay + ch) * tileSize - liftWorldY; + const [sx, sy] = worldToScreen(cxWorld, cyBottomWorld); + const cellSpan = Math.max(cw, ch); + const boxW = tileSize * zDraw * Math.max(cw, 1) * 1.15; + const boxH = tileSize * zDraw * Math.max(ch, 1) * 1.35; + const r = Math.max(14, (tileSize * zDraw * cellSpan) / 2 - 2); + const dir = direction || 'down'; + const charImg = resolvePlayDrawCharSource(characterId, dir, timeMs, isWalking, playTint, !!isMe); + /* รองรับทั้ง และ canvas จากย้อมสี — กล่อง W×H ตาม footprint; อนุญาต upscale เลเยอร์เล็ก (ห้ามจำกัด scale≤1 ไม่งั้นตัวหดเป็นจุด) */ + let iw = 0, ih = 0; + if (charImg && charImg.tagName === 'CANVAS' && charImg.width > 0 && charImg.height > 0) { + iw = charImg.width; + ih = charImg.height; + } else if (charImg && charImg.complete && charImg.naturalWidth > 0 && charImg.naturalHeight > 0) { + iw = charImg.naturalWidth; + ih = charImg.naturalHeight; + } + let halfSpriteW = r; + let spriteTopY = sy - 2 * r; + if (iw > 0 && ih > 0) { + const scale = Math.min(boxW / iw, boxH / ih); + let drawW = iw * scale; + let drawH = ih * scale; + if (isQuiz()) { + const minPx = Math.max(28, r * 1.85); + if (drawW < minPx || drawH < minPx) { + const up = Math.max(minPx / Math.max(drawW, 0.01), minPx / Math.max(drawH, 0.01)); + drawW *= up; + drawH *= up; + } + } + halfSpriteW = drawW / 2; + spriteTopY = sy - drawH; + ctx.drawImage(charImg, 0, 0, iw, ih, sx - drawW / 2, sy - drawH, drawW, drawH); + } else { + const cy = sy - r; + ctx.fillStyle = isMe ? '#7aa2f7' : '#9ece6a'; + ctx.beginPath(); + ctx.arc(sx, cy, r, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = '#c0caf5'; + ctx.lineWidth = 2; + ctx.stroke(); + spriteTopY = cy - r; + } + ctx.fillStyle = '#c0caf5'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'center'; + const nameBase = name || ''; + const withScore = isGauntlet() && typeof gauntletScoreLabel === 'number' && gauntletScoreLabel >= 0 + ? (nameBase + ' · ' + gauntletScoreLabel) + : nameBase; + ctx.fillText(withScore, sx, sy + 10); + if (isGauntletCrownHeistMapPlay() + && typeof crownPenaltyFxUntil === 'number' + && crownPenaltyFxUntil > timeMs) { + const penImg = ensureGauntletCrownScorePenaltyImgPlay(); + if (penImg && penImg.complete && penImg.naturalWidth > 0) { + const ref = Math.max(18, r * 2); + const scale = Math.min(1.15, ref / Math.max(penImg.naturalWidth, penImg.naturalHeight, 1)); + const dw = penImg.naturalWidth * scale; + const dh = penImg.naturalHeight * scale; + ctx.drawImage(penImg, 0, 0, penImg.naturalWidth, penImg.naturalHeight, sx - dw * 0.5, spriteTopY - dh - 5, dw, dh); + } + } + if (isQuizCarry() && quizCarrySignPayload) { + drawQuizCarryHeldSignBoard( + sx, sy, halfSpriteW, spriteTopY, + quizCarrySignPayload.text, dir, quizCarrySignPayload.choiceIndex, + quizCarrySignPayload.imageUrl, + quizCarrySignPayload.imageElement, + ); + } + } + function quizCarrySignForEntity(ent) { + if (!isQuizCarry() || !quizCarryCurrent || !ent || ent.quizCarryHeld == null) return null; + const idx = ent.quizCarryHeld; + const ch = quizCarryCurrent.choices; + if (!Array.isArray(ch) || idx < 0 || idx >= ch.length) return null; + const t = String(ch[idx] != null ? ch[idx] : '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(quizCarryCurrent, idx); + const gridHeldEl = findQuizCarryGridHeldImgForChoiceIndex(idx); + if (!t && !imgUrl && !gridHeldEl) return null; + return { text: t, choiceIndex: idx, imageUrl: imgUrl, imageElement: gridHeldEl || undefined }; + } + const safeX = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + const safeY = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + function peerVisualOffset(id) { + let h = 0; + for (let i = 0; i < (id || '').length; i++) h = (h * 31 + (id || '').charCodeAt(i)) >>> 0; + return { ax: ((h % 5) - 2) * 0.1, ay: ((Math.floor(h / 5) % 5) - 2) * 0.1 }; + } + function shouldGauntletCrownHeistSkipAvatarDrawPlay(ent, isMeFlag, offAx, offAy) { + if (!isGauntletCrownHeistMapPlay() || gauntletCrownPregamePhase !== 'live') return false; + if (!ent || ent.gauntletEliminated) return true; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ax = safeX(ent.x) + (offAx || 0); + const ay = safeY(ent.y) + (offAy || 0); + const airTicks = isMeFlag ? (meGauntletJumpTicks || 0) : (Number(ent.gauntletJumpTicks) || 0); + const airVis = isMeFlag ? (meGauntletJumpVis || 0) : (Number(ent.gauntletJumpVis) || 0); + const air = airVis > 0.08 ? airVis : airTicks; + let liftWorldY = 0; + if (isGauntlet() && air > 0) { + liftWorldY = gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSize * 0.52; + } + const cxWorld = (ax + cw * 0.5) * tileSize; + const cyBottomWorld = (ay + ch) * tileSize - liftWorldY; + const [sx, sy] = worldToScreen(cxWorld, cyBottomWorld); + const cellSpan = Math.max(cw, ch); + const r = Math.max(14, (tileSize * zDraw * cellSpan) / 2 - 2); + const pad = Math.max(28, r * 0.4); + if (sx + r + pad < 0 || sx - r - pad > canvas.width) return true; + if (sy + 52 < 0 || sy - r * 4 - pad > canvas.height) return true; + return false; + } + const { ch: sortCh } = getCharacterFootprintWH(mapData); + /** ใช้แถวกริด (floor y) เป็นหลัก — ลดการสลับลำดับวาดทุกเฟรมเมื่อ y เป็น float เลอร์ป */ + function avatarDrawDepth(ent) { + return Math.floor(safeY(ent.y)) + sortCh; + } + if (!isStack() && !isSpaceShooter() && !isBalloonBoss()) { + let mt = me.playTint; + if (!mt || typeof mt.head !== 'string' || typeof mt.hair !== 'string' || typeof mt.body !== 'string') { + mt = resolvePlayTintForPeer(String((myId != null && myId !== '') ? myId : (nick || 'local'))); + me.playTint = mt; + } + const meTag = isFrogger() ? (' (กบ) ' + froggerScore) : (isGauntlet() && meGauntletJumpTicks > 0 ? ' (กระโดด)' : (isJumpSurvive() && jumpSurviveEliminated ? ' (ตกรอบ)' : ' (คุณ)')); + const crownRunStripLive = isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live' && gauntletCrownRunwayAvatarRunAllowedPlay(); + const meWalking = isFrogger() + ? false + : (isGauntlet() + ? ((crownRunStripLive && !me.gauntletEliminated) || meGauntletJumpTicks > 0 || meGauntletJumpVis > 0.08) + : !!me.isWalking); + const drawRows = []; + others.forEach((o, id) => { + if (!o) return; + drawRows.push({ isMe: false, id, o }); + }); + drawRows.push({ isMe: true, id: '__me', o: me }); + drawRows.sort((a, b) => { + const da = avatarDrawDepth(a.o); + const db = avatarDrawDepth(b.o); + if (da !== db) return da - db; + const xa = Math.floor(safeX(a.o.x) * 4); + const xb = Math.floor(safeX(b.o.x) * 4); + if (xa !== xb) return xa - xb; + const sa = a.isMe ? '\uFFFFme' : String(a.id); + const sb = b.isMe ? '\uFFFFme' : String(b.id); + return sa < sb ? -1 : sa > sb ? 1 : 0; + }); + drawRows.forEach((row) => { + if (!row.isMe) { + const id = row.id; + const o = row.o; + const off = isGauntlet() ? { ax: 0, ay: 0 } : peerVisualOffset(id); + const otherWalk = isGauntlet() + ? ((crownRunStripLive && !o.gauntletEliminated) || (o.gauntletJumpTicks || 0) > 0 || (o.gauntletJumpVis || 0) > 0.08) + : (isPreviewBotId(id) + ? !!o.botIsWalking + : !!((o.tx != null && Math.abs((o.tx || o.x) - o.x) > 0.02) || (o.ty != null && Math.abs((o.ty || o.y) - o.y) > 0.02))); + const ot = o.playTint || resolvePlayTintForPeer(id); + const faceDirOther = isGauntletFaceRightMapMno9kb07() ? 'right' : o.direction; + const botOut = isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated; + const peerName = botOut ? (o.nickname + ' (ตกรอบ)') : o.nickname; + if (shouldGauntletCrownHeistSkipAvatarDrawPlay(o, false, off.ax, off.ay)) return; + const axO = safeX(o.x) + off.ax; + const ayO = safeY(o.y) + off.ay; + /* ผีต้องโชว์เสมอ แม้ทับโซนคำตอบ (เดินไปไหนก็ได้) */ + const ghostActiveO = isQuizWrongGhostActiveForEnt(id); + if (!ghostActiveO && playQuizAvatarHideWhileOverlappingAnswerZone(o, id, axO, ayO)) return; + if (botOut) ctx.save(); + if (botOut) ctx.globalAlpha = 0.4; + /* ตอบผิดในแมป mng8a80o → แทน avatar ด้วยผีเลย */ + if (ghostActiveO) { + const ga = _quizGhostScreenAnchorPlay(axO, ayO, worldToScreen, zDraw); + drawQuizWrongGhostOverlayPlay(ga.sx, ga.sy, ga.sizeH, id, peerName); + } else { + drawAvatar(axO, ayO, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o), isGauntletCrownHeistMapPlay() ? (o.gauntletCrownPenaltyFxUntil || 0) : 0); + } + if (botOut) ctx.restore(); + } else { + if (shouldGauntletCrownHeistSkipAvatarDrawPlay(me, true, 0, 0)) return; + const axMe = safeX(me.x); + const ayMe = safeY(me.y); + const ghostActiveMe = isQuizWrongGhostActiveForEnt(myId); + if (!ghostActiveMe && playQuizAvatarHideWhileOverlappingAnswerZone(me, myId, axMe, ayMe)) return; + if (isJumpSurvive() && jumpSurviveEliminated) ctx.save(); + if (isJumpSurvive() && jumpSurviveEliminated) ctx.globalAlpha = 0.4; + const faceDirMe = isGauntletFaceRightMapMno9kb07() ? 'right' : me.direction; + if (ghostActiveMe) { + const gam = _quizGhostScreenAnchorPlay(axMe, ayMe, worldToScreen, zDraw); + drawQuizWrongGhostOverlayPlay(gam.sx, gam.sy, gam.sizeH, myId, me.nickname + meTag); + } else { + drawAvatar(axMe, ayMe, true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me), isGauntletCrownHeistMapPlay() ? (me.gauntletCrownPenaltyFxUntil || 0) : 0); + } + if (isJumpSurvive() && jumpSurviveEliminated) ctx.restore(); + } + }); + } + /* ไอคอนคำถามพิเศษในฉาก (วาดทับ map/ผู้เล่น) + ตรวจการเดินชน */ + if (specialQuizIcon) { + drawSpecialQuizIconWorld(worldToScreen, zDraw); + checkSpecialQuizCollision(); + } + if (isFrogger()) { + ctx.fillStyle = '#7aa2f7'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('โหมดกบ | คะแนน: ' + froggerScore, 10, 24); + } + if (isJumpSurvive() && !useCyberPlayHud()) { + ctx.fillStyle = 'rgba(148, 226, 213, 0.95)'; + ctx.font = '600 12px system-ui, "Segoe UI", "Kanit", sans-serif'; + ctx.textAlign = 'left'; + if (jumpSurviveEliminated) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.95)'; + ctx.fillText('คุณตกรอบแล้ว (ชนหรือหลุดเพดาน/พื้นจอ) · เล่นต่อไม่ได้ — ดูเพื่อนได้ · รอรอบใหม่หรือออกจากห้อง', 10, 22); + } else { + ctx.fillText('กระโดดให้รอด · Space / W / ↑ · A D / ← → · แพลตฟอร์มวน · ชนหรือหลุดบน/ล่างจอ = ตกรอบ (ดูเพื่อนต่อ)', 10, 22); + } + } + if (isGauntlet() && !useCyberPlayHud()) { + ctx.fillStyle = '#f7768e'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + if (isGauntletCrownHeistMapPlay()) { + ctx.fillText('พรมแดงสุดท้าย | คะแนน ' + (me.gauntletScore || 0) + ' · ชน -10 · ขอบซ้ายตาย · สูงสุด 6 คน', 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · จบเกมมีโบนัสอันดับ + เกรดทีม', 10, 42); + } else { + ctx.fillText('พรมแดงสุดท้าย | คะแนน: ' + (me.gauntletScore || 0) + ' (ข้าม 1 ชิ้น +1) · สูงสุด 6 คน', 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · ข้ามสำเร็จ → ขวา 1 + คะแนน · ชน → ซ้าย 1', 10, 42); + } + ctx.fillStyle = '#e0af68'; + if (!(isGauntletCrownHeistMapPlay() && gauntletCrownRunwayBgDrawActivePlay())) { + if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) { + const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000)); + const mm = Math.floor(rem / 60); + const ss = rem % 60; + const clock = `${mm}:${String(ss).padStart(2, '0')}`; + ctx.fillText(`เหลือเวลา ${clock} · Time left ${clock}`, 10, 60); + } else if (gauntletRuntimeTimeLimitSec > 0) { + ctx.fillText('เวลา: กำลังซิงก์จากเซิร์ฟเวอร์... · Syncing timer...', 10, 60); + } else { + ctx.fillText('เวลา: ไม่จำกัด · No time limit (ตั้งได้ที่ Admin → เวลาเกม)', 10, 60); + } + } + } + if (isStack() && stackMini) { + if (playBotsEnabled()) { + if (!playHideTopChrome()) { + ctx.fillStyle = 'rgba(125, 220, 255, 0.9)'; + ctx.font = '600 12px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Stack · shared tower · Space / click = release on your turn', 10, 22); + } + /* Tower mnn93hpi: แผงขวา (ชีวิตทีม + อวตาร์) วาดบน canvas — ต้องวาดแม้ซ่อนแถบห้องบน */ + if (!playHideTopChrome() || (isStackTowerMissionUiMapPlay() && useCyberPlayHud())) { + drawStackPreviewCyberHud(ctx, canvas.width, canvas.height); + } + } else { + ctx.fillStyle = '#7dcfff'; + ctx.font = 'bold 15px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Stack | คะแนน ' + stackMini.score + ' · ชีวิต ' + stackMini.lives + ' · combo x' + (stackMini.combo || 0), 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / คลิก = ปล่อย', 10, 44); + } + } + if (isQuiz() || isQuizCarry() || isQuizBattle()) { + syncPlayQuizMapPanel(); + syncQuizCarryEmbedQuestionStrip(); + } + /* timeup ต้องซิงก์ทุกเฟรมแม้จบเซสชัน — อย่าให้อยู่ในเงื่อนไขด้านบนอย่างเดียว */ + syncQuizCarryTimeupDeskLayerPosition(); + if (playBotsEnabled() && mapData && !useCyberPlayHud()) { + const human = countPlayHumans(); + const bots = [...others.keys()].filter(isPreviewBotId).length; + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = 'rgba(26,27,38,0.78)'; + const hudW = isStack() ? Math.min(canvas.width - 12, 520) : Math.min(canvas.width - 12, 320); + ctx.fillRect(6, canvas.height - 38, hudW, 30); + ctx.fillStyle = '#a9b1d6'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'left'; + const botTarget = playBotTargetHeadcount(); + let line = detectiveCaseFillBots + ? ('คดีสืบสวน: คนจริง ' + human + ' + บอท ' + bots + ' (เป้า ' + botTarget + ' คน)') + : ('ทดสอบ: คนจริง ' + human + ' + บอท ' + bots + ' (เป้า ' + botTarget + ' คน)'); + if (isStack()) { + line += ' · ตึกร่วม + HUD ซ้าย/ขวา'; + } + ctx.fillText(line, 12, canvas.height - 18); + ctx.restore(); + } + if (isSpaceShooter()) { + drawSpaceShooterCombatLayer(ctx, worldToScreen, zDraw, timeMs); + } + if (isBalloonBoss()) { + drawBalloonBossCombatLayer(ctx, worldToScreen, zDraw, timeMs); + } + if (isQuizCarry()) { + drawQuizCarryEmbedCountdownHighlight(ctx, worldToScreen, zDraw, timeMs); + } + if (quizCarryEmbedMissionFlowActive() + && (quizCarryEmbedCountdownEndAt > Date.now() || quizCarryEmbedPreOptionCountdownEndAt > Date.now())) { + syncQuizCarryEmbedCountdownLayout(); + } + drawQuizTfScorePopupsLayer(ctx, worldToScreen, zDraw, timeMs); + syncPlayCyberHud(); + syncQuizQuestionMissionJoystickPlay(); + syncQuizCarryGrabButton(); + syncGauntletCrownJumpButton(); + syncStackTowerDropButtonPlay(); + } + + document.addEventListener('keydown', (e) => { + /* Test Mode shortcut: Ctrl+Q = ตอบถูกใน special quiz (server เติมคำตอบให้) */ + if (e.ctrlKey && (e.code === 'KeyQ' || e.key === 'q' || e.key === 'Q')) { + if (specialQuizActiveQuestion && !specialQuizAnswered && __isTestModeOnPlay()) { + e.preventDefault(); + specialQuizAnswered = true; + const status = document.getElementById('sq-ov-status'); + if (status) status.textContent = '[Test] ส่งคำตอบที่ถูกอัตโนมัติ — รอผู้เล่นคนอื่น...'; + if (specialQuizSelfId()) markSpecialQuizMemberAnswered(specialQuizSelfId()); + const choicesEl = document.getElementById('sq-ov-choices'); + if (choicesEl) Array.prototype.forEach.call(choicesEl.children, function (b) { b.disabled = true; }); + try { socket.emit('special-quiz-answer', { debugCorrect: true }, function () {}); } catch (eQ) { /* ignore */ } + return; + } + } + if (isMovementKey(e.code) && isChatFocused()) return; + if (previewMode && editorEmbedReturn && mapData && !isChatFocused() && !isQuizQuestionMissionHudActivePlay() && !isStackTowerEmbedZoomLockedPlay()) { + 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(); + } + return; + } + if (isQuiz() && mapData && !isChatFocused() && isQuizQuestionMissionPregameBlockingPlay()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + return; + } + if (isStack() && mapData && !isChatFocused() && isStackTowerMissionPregameBlockingPlay()) { + if (['Space', 'Enter', 'NumpadEnter', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + return; + } + if (isBalloonBoss() && isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay() && mapData && !isChatFocused()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + return; + } + if ((isSpaceShooter() || isBalloonBoss()) && mapData && !isChatFocused()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + } + if (isStack() && mapData && !isChatFocused()) { + if (e.code === 'Space' || e.code === 'Enter' || e.code === 'NumpadEnter') { + e.preventDefault(); + tryHumanStackDrop(); + return; + } + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + return; + } + } + if (isGauntlet() && mapData && !isChatFocused()) { + if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { + e.preventDefault(); + tryRequestGauntletJumpPlay(); + return; + } + if (['ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + return; + } + } + if (isJumpSurvive() && mapData && !isChatFocused()) { + if (isJumpSurviveMissionPregameBlockingPlay()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) e.preventDefault(); + return; + } + if (jumpSurviveEliminated) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) e.preventDefault(); + return; + } + if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { + e.preventDefault(); + if (!e.repeat) jumpSurviveJumpQueued = true; + return; + } + if (e.code === 'ArrowDown' || e.code === 'KeyS') { + e.preventDefault(); + return; + } + } + if (isQuizBattle() && mapData && !isChatFocused()) { + if (e.code === 'KeyE') { + e.preventDefault(); + if (!e.repeat && myId != null) tryOpenQuizBattleFromKey(); + return; + } + } + if (isQuizCarry() && mapData && !isChatFocused()) { + if (e.code === 'KeyF') { + e.preventDefault(); + if (!e.repeat && myId != null) tryQuizCarryInteractionForPlayer(myId, me.x, me.y, { fromKey: true }); + return; + } + } + keys[e.code] = true; + keys[e.key] = true; + if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault(); + if (isFrogger() && mapData && !isChatFocused()) { + const now = Date.now(); + if (now - lastFroggerKey < 280) return; + let dx = 0, dy = 0; + if (e.code === 'ArrowUp' || e.code === 'KeyW') { dy = -1; me.direction = 'up'; } + else if (e.code === 'ArrowDown' || e.code === 'KeyS') { dy = 1; me.direction = 'down'; } + else if (e.code === 'ArrowLeft' || e.code === 'KeyA') { dx = -1; me.direction = 'left'; } + else if (e.code === 'ArrowRight' || e.code === 'KeyD') { dx = 1; me.direction = 'right'; } + if (dx !== 0 || dy !== 0) { + const nx = Math.round(me.x) + dx, ny = Math.round(me.y) + dy; + if (nx >= 0 && nx < mapData.width && ny >= 0 && ny < mapData.height) { + const row = mapData.objects && mapData.objects[ny]; + if (!row || row[nx] !== 1) { + me.x = nx; me.y = ny; + lastFroggerKey = now; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + const lane = getLane(Math.floor(me.y)); + if (lane && lane.type === 'goal') { froggerScore++; respawnFrogger(); } + else if (checkFroggerCollision()) respawnFrogger(); + } + } + } + } + }); + document.addEventListener('keyup', (e) => { keys[e.code] = false; keys[e.key] = false; }); + + let lastSend = 0; + function tick() { + if (!mapData) { requestAnimationFrame(tick); return; } + /* คำถามพิเศษเปิดอยู่ — หยุดเกมทุกอย่าง (input/sim/timer) เห็นแต่ภาพค้างไว้ + ดันเวลาทุกตัวไปข้างหน้าเท่า delta ทุกเฟรม → countdown ที่แสดงหยุดนิ่งจริง */ + if (specialQuizFreeze) { + tickSpecialQuizFreezeShift(); + if (me) me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isStack()) { + if (isStackTowerEmbedZoomLockedPlay()) { + playEmbedUserZoomMul = PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL; + } + const nowStBg = performance.now(); + const dtStBg = Math.min(0.06, (nowStBg - lastStackTowerScrollBgTickMs) / 1000); + lastStackTowerScrollBgTickMs = nowStBg; + tickStackTowerScrollBg(dtStBg); + if (isStackTowerMissionPregameBlockingPlay()) { + updateStackTowerSwingYForStripGapPlay(); + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + stackTickFrame(); + updateStackTowerSwingYForStripGapPlay(); + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isJumpSurvive()) { + if (isJumpSurviveMissionPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + jumpSurviveTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isQuiz() && isQuizQuestionMissionUiMapPlay() && isQuizQuestionMissionPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isSpaceShooter()) { + if (isSpaceShooterMissionPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + spaceShooterTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isBalloonBoss()) { + balloonBossTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isFrogger()) { + if (checkFroggerCollision()) respawnFrogger(); + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isGauntlet()) { + { + const nowBg = performance.now(); + const dtBg = Math.min(0.06, lastGauntletCrownRunwayBgTickMs ? (nowBg - lastGauntletCrownRunwayBgTickMs) / 1000 : 0.016); + lastGauntletCrownRunwayBgTickMs = nowBg; + tickGauntletCrownRunwayBgPlay(dtBg); + } + if (isGauntletCrownPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + me.isWalking = (isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live' && !me.gauntletEliminated && gauntletCrownRunwayAvatarRunAllowedPlay()) || meGauntletJumpTicks > 0; + const stepGauntletJumpVis = (vis, tgt) => { + const t = Number(tgt) || 0; + let v = Number(vis) || 0; + const d = t - v; + const k = d < 0 ? 0.68 : 0.4; + return v + d * k; + }; + meGauntletJumpVis = stepGauntletJumpVis(meGauntletJumpVis, meGauntletJumpTicks); + const mp = lerpGauntletEntityPos(me.x, me.y, me.tx, me.ty); + me.x = mp.nx; + me.y = mp.ny; + others.forEach((o) => { + const op = lerpGauntletEntityPos(o.x, o.y, o.tx, o.ty); + o.x = op.nx; + o.y = op.ny; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks || 0; + o.gauntletJumpVis = stepGauntletJumpVis(o.gauntletJumpVis, o.gauntletJumpTicks || 0); + }); + clampPlayEntityFootprintToMap(me, mapData); + others.forEach((o) => { + if (o) clampPlayEntityFootprintToMap(o, mapData); + }); + 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(); + requestAnimationFrame(tick); + return; + } + /* บอท preview ขยับที่ o.x/o.y โดยตรง — ห้าม LERP กับ tx/ty เพราะ stepPreviewBots เคยตั้ง tx=ก่อนเดิน ทำให้โดนดึงถอย ~20%/เฟรม กระตุกและแทบไม่ไป */ + others.forEach((o, id) => { + if (playBotsEnabled() && isPreviewBotId(id)) return; + if (o.tx != null) o.x += (o.tx - o.x) * LERP; + if (o.ty != null) o.y += (o.ty - o.y) * LERP; + }); + tickQuizCarryEmbedCountdown(); + tickQuizCarryRoundTimers(); + updateQuizCarryCarryPhaseHud(); + stepPreviewBots(); + if (isChatFocused()) { + me.isWalking = false; + enforceQuizBattleLaneOnMePlay(); + enforceQuizBattleLaneOnPeersPlay(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isQuizCarryEmbedLobbyBlockingMovement() || isQuizCarryEmbedCountdownBlockingMovement()) { + me.isWalking = false; + playPath = []; + enforceQuizBattleLaneOnMePlay(); + draw(); + requestAnimationFrame(tick); + return; + } + const w = mapData.width || 20, h = mapData.height || 15; + let accX = 0, accY = 0; + let usePath = false; + const keyPressed = keys['ArrowUp'] || keys['KeyW'] || keys['ArrowDown'] || keys['KeyS'] || keys['ArrowLeft'] || keys['KeyA'] || keys['ArrowRight'] || keys['KeyD']; + if (playPath.length > 0 && keyPressed) playPath = []; + if (playPath.length > 0) { + const way = playPath[0]; + const dx = way.x - me.x, dy = way.y - me.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= PATH_ARRIVE_THRESH) { + playPath.shift(); + /* ข้าม waypoint ที่ซ้ำตำแหน่ง — กัน acc=0 ทั้งที่ยังมี path (เดินขึ้น/ลงแล้วแอนิเมชันค้าง) */ + while (playPath.length > 0) { + const w2 = playPath[0]; + const ux = w2.x - me.x, uy = w2.y - me.y; + if (Math.sqrt(ux * ux + uy * uy) > PATH_ARRIVE_THRESH) break; + playPath.shift(); + } + if (playPath.length === 0) { + me.isWalking = false; + clampPlayEntityFootprintToMap(me, mapData); + enforceQuizBattleLaneOnMePlay(); + const t = Date.now(); + if (t - lastSend > 80) { lastSend = t; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } + draw(); + requestAnimationFrame(tick); + return; + } + usePath = true; + const next = playPath[0]; + accX = next.x - me.x; + accY = next.y - me.y; + } else { + usePath = true; + accX = dx; + accY = dy; + } + if (usePath && (accX !== 0 || accY !== 0)) { + if (Math.abs(accY) > Math.abs(accX)) me.direction = accY > 0 ? 'down' : 'up'; + else me.direction = accX > 0 ? 'right' : 'left'; + } + } + if (!usePath) { + let kx = 0; + let ky = 0; + if (keys['ArrowLeft'] || keys['KeyA']) kx -= 1; + if (keys['ArrowRight'] || keys['KeyD']) kx += 1; + if (keys['ArrowUp'] || keys['KeyW']) ky -= 1; + if (keys['ArrowDown'] || keys['KeyS']) ky += 1; + let jx = 0; + let jy = 0; + if (isQuizQuestionMissionHudActivePlay()) { + jx = quizQuestionMissionJoyVecX; + jy = quizQuestionMissionJoyVecY; + } + accX = kx + jx; + accY = ky + jy; + if (accX !== 0 || accY !== 0) { + if (Math.abs(accY) > Math.abs(accX)) me.direction = accY > 0 ? 'down' : 'up'; + else if (accX !== 0) me.direction = accX > 0 ? 'right' : 'left'; + } + } + const preWalkX = me.x, preWalkY = me.y; + if (accX !== 0 || accY !== 0) { + const len = Math.sqrt(accX * accX + accY * accY) || 1; + const step = Math.min(moveSpeedTilesThisFrameForWalk(), len); + const tox = me.x + (accX / len) * step; + const toy = me.y + (accY / len) * step; + const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData); + if (pathStrict) { + const moved = quizBattleTryStepMovePlay(me.x, me.y, tox, toy, (nx, ny, ox, oy) => canWalkLikeLobby(nx, ny, ox, oy)); + me.x = moved.x; + me.y = moved.y; + } else if (canWalkLikeLobby(tox, toy, me.x, me.y)) { + me.x = tox; + me.y = toy; + } else { + if (canWalkLikeLobby(tox, me.y, me.x, me.y)) { + me.x = tox; + } else if (canWalkLikeLobby(me.x, toy, me.x, me.y)) { + me.y = toy; + } + } + } + clampPlayEntityFootprintToMap(me, mapData); + enforceQuizBattleLaneOnMePlay(); + enforceQuizBattleLaneOnPeersPlay(); + const movedThisTick = Math.abs(me.x - preWalkX) > 1e-5 || Math.abs(me.y - preWalkY) > 1e-5; + me.isWalking = !!(accX !== 0 || accY !== 0) || playPath.length > 0 || movedThisTick; + const now = Date.now(); + if (now - lastSend > 80) { lastSend = now; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } + draw(); + requestAnimationFrame(tick); + } + + const chatForm = document.getElementById('chat-form'); + if (chatForm) { + chatForm.addEventListener('submit', (e) => { + e.preventDefault(); + const input = document.getElementById('chat-input'); + const text = (input && input.value || '').trim(); + if (text) { socket.emit('chat', text); if (input) input.value = ''; } + }); + } + document.getElementById('btn-leave').addEventListener('click', () => { + socket.emit('leave-space'); + const mid = params.get('map'); + if (previewMode && mid) { + var edQ = '?id=' + encodeURIComponent(mid) + (editorEmbedReturn ? '&embed=1' : ''); + window.location.replace(BASE + '/editor.html' + edQ); + } else { + window.location.replace(playDesignMainMenuHref()); + } + }); + (function wireQuizBattleMcqModal() { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + if (!ov) return; + ov.addEventListener('click', (e) => { + const t = e.target; + if (!t) return; + if (t.classList && t.classList.contains('quiz-battle-choice')) { + const idx = parseInt(t.getAttribute('data-idx'), 10); + if (Number.isFinite(idx) && idx >= 0 && idx <= 2) trySubmitQuizBattleChoice(idx); + return; + } + if (t.id === 'quiz-battle-mcq-close') { + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + return; + } + if (t.classList && t.classList.contains('quiz-battle-mcq-backdrop')) { + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + } + }); + })(); + (function wireQuizCarryGrabButton() { + const btn = document.getElementById('quiz-carry-grab-btn'); + if (!btn) return; + btn.addEventListener('click', (ev) => { + if (!isQuizCarry() || !mapData || isChatFocused()) return; + if (!btn.classList.contains('quiz-carry-grab-btn--active')) return; + ev.preventDefault(); + if (myId == null) return; + tryQuizCarryInteractionForPlayer(myId, me.x, me.y, { fromKey: true }); + }); + })(); + + (function wireGauntletCrownJumpButton() { + const btn = document.getElementById('gauntlet-crown-jump-btn'); + if (!btn) return; + btn.addEventListener('click', (ev) => { + ev.preventDefault(); + if (!mapData || !isGauntlet() || isChatFocused()) return; + tryRequestGauntletJumpPlay(); + }); + })(); + + (function wireStackTowerDropButton() { + const btn = document.getElementById('stack-tower-drop-btn'); + if (!btn) return; + btn.addEventListener('click', (ev) => { + ev.preventDefault(); + if (!mapData || !isStack() || isChatFocused()) return; + if (!btn.classList.contains('stack-tower-drop-btn--active')) return; + tryHumanStackDrop(); + }); + })(); + + (function wireQuizQuestionMissionJoystick() { + 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.onerror = function () { + this.onerror = null; + this.removeAttribute('src'); + }; + } + if (knobImg) { + knobImg.src = questionMissionAssetUrl('btn-joystick-2.png'); + knobImg.onerror = function () { + this.onerror = null; + this.removeAttribute('src'); + }; + } + function endJoy(e) { + if (quizQuestionMissionJoyPointerId == null || e.pointerId !== quizQuestionMissionJoyPointerId) return; + try { + base.releasePointerCapture(e.pointerId); + } catch (_err) { /* ignore */ } + quizQuestionMissionJoyPointerId = null; + quizQuestionMissionJoystickResetVisual(); + } + base.addEventListener('pointerdown', (e) => { + if (!isQuizQuestionMissionHudActivePlay() || isChatFocused()) return; + if (e.button != null && e.button !== 0) return; + e.preventDefault(); + try { + base.setPointerCapture(e.pointerId); + } catch (_err) { /* ignore */ } + quizQuestionMissionJoyPointerId = e.pointerId; + quizQuestionMissionJoystickUpdateFromClientXY(e.clientX, e.clientY); + }); + base.addEventListener('pointermove', (e) => { + if (quizQuestionMissionJoyPointerId == null || e.pointerId !== quizQuestionMissionJoyPointerId) return; + e.preventDefault(); + quizQuestionMissionJoystickUpdateFromClientXY(e.clientX, e.clientY); + }); + base.addEventListener('pointerup', endJoy); + base.addEventListener('pointercancel', endJoy); + base.addEventListener('lostpointercapture', (e) => { + if (quizQuestionMissionJoyPointerId == null || e.pointerId !== quizQuestionMissionJoyPointerId) return; + quizQuestionMissionJoyPointerId = null; + quizQuestionMissionJoystickResetVisual(); + }); + })(); + + (function wireGauntletCrownHowtoPrimary() { + const btn = document.getElementById('btn-gch-ready'); + if (!btn) return; + btn.addEventListener('click', () => { + if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'howto') return; + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + if (totPlayers === 1) { + if (!(humans.length === 1 || isMePlayHost())) return; + beginJumpSurviveMissionCountdownThenRun(); + return; + } + if (!isMePlayHost()) { + if (myId == null) return; + const sid = String(myId); + const next = !gauntletCrownLobbyReadyMap[sid]; + gauntletCrownLobbyReadyMap[sid] = next; + updateJumpSurviveMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: next }); + return; + } + if (myId == null) return; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReady) { + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } }); + return; + } + const sidH = String(myId); + const nextH = !gauntletCrownLobbyReadyMap[sidH]; + gauntletCrownLobbyReadyMap[sidH] = nextH; + updateJumpSurviveMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextH }); + return; + } + if (isSpaceShooterMissionUiMapPlay()) { + if (spaceShooterMissionPhase !== 'howto') return; + const humansSs = quizCarryPregameHumanIds(); + const totPlayersSs = Math.max(1, quizCarryPregameTotalPlayers()); + if (totPlayersSs === 1) { + if (!(humansSs.length === 1 || isMePlayHost())) return; + beginSpaceShooterMissionCountdownThenRun(); + return; + } + if (!isMePlayHost()) { + if (myId == null) return; + const sidSs = String(myId); + const nextSs = !gauntletCrownLobbyReadyMap[sidSs]; + gauntletCrownLobbyReadyMap[sidSs] = nextSs; + updateSpaceShooterMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextSs }); + return; + } + if (myId == null) return; + const humansReadySs = humansSs.length > 0 && humansSs.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReadySs) { + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } }); + return; + } + const sidHostSs = String(myId); + const nextHostSs = !gauntletCrownLobbyReadyMap[sidHostSs]; + gauntletCrownLobbyReadyMap[sidHostSs] = nextHostSs; + updateSpaceShooterMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextHostSs }); + return; + } + if (isQuizQuestionMissionUiMapPlay()) { + if (quizQuestionMissionPhase !== 'howto') return; + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + if (totPlayers === 1) { + beginQuizQuestionMissionCountdownThenRun(); + return; + } + if (!isMePlayHost()) { + if (myId == null) return; + const sid = String(myId); + const next = !gauntletCrownLobbyReadyMap[sid]; + gauntletCrownLobbyReadyMap[sid] = next; + updateQuizQuestionMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: next }); + return; + } + if (myId == null) return; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReady) { + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } }); + return; + } + const sidH = String(myId); + const nextH = !gauntletCrownLobbyReadyMap[sidH]; + gauntletCrownLobbyReadyMap[sidH] = nextH; + updateQuizQuestionMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextH }); + return; + } + if (isStackTowerMissionUiMapPlay()) { + if (stackTowerMissionPhase !== 'howto') return; + const humansSt = quizCarryPregameHumanIds(); + const totPlayersSt = Math.max(1, quizCarryPregameTotalPlayers()); + if (totPlayersSt === 1) { + if (!(humansSt.length === 1 || isMePlayHost())) return; + beginStackTowerMissionCountdownThenRun(); + return; + } + if (!isMePlayHost()) { + if (myId == null) return; + const sidSt = String(myId); + const nextSt = !gauntletCrownLobbyReadyMap[sidSt]; + gauntletCrownLobbyReadyMap[sidSt] = nextSt; + updateStackTowerMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextSt }); + return; + } + if (myId == null) return; + const humansReadySt = humansSt.length > 0 && humansSt.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReadySt) { + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } }); + return; + } + const sidHostSt = String(myId); + const nextHostSt = !gauntletCrownLobbyReadyMap[sidHostSt]; + gauntletCrownLobbyReadyMap[sidHostSt] = nextHostSt; + updateStackTowerMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextHostSt }); + return; + } + if (!usesCrownLobbyShellPlay()) return; + if (gauntletCrownPregamePhase !== 'howto') return; + if (myId == null || !isMePlayHost()) return; + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReady) { + if (humans.length === 1 && isMePlayHost()) { + beginGauntletCrownCountdownThenRun(); + return; + } + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { + if (!r || !r.ok) { /* ignore */ } + }); + return; + } + const sid = String(myId); + const next = !gauntletCrownLobbyReadyMap[sid]; + gauntletCrownLobbyReadyMap[sid] = next; + updateGauntletCrownHowtoHud(); + socket.emit('gauntlet-crown-lobby-ready', { ready: next }); + }); + })(); + + (function wireQuizCarryPregameOverlay() { + const primary = document.getElementById('quiz-carry-pregame-primary'); + if (!primary) return; + primary.addEventListener('click', () => { + if (!quizCarryPregameActive || myId == null) return; + if (!isMePlayHost()) return; + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]); + if (humansReady) { + /* ผู้เล่นจริงคนเดียว + พรีวิว: ไม่ต้องรอ ACK เซิร์ฟ (ห้องที่ map บนเซิร์ฟยังไม่ใช่ quiz_carry จะเคยล้มเหลว) */ + if (humans.length === 1 && isMePlayHost()) { + endQuizCarryEmbedPregameAndStart(); + return; + } + socket.emit('quiz-carry-lobby-start', {}, (r) => { + if (!r || !r.ok) showQuizCarryToast((r && r.error) || 'START ไม่ได้', false); + }); + return; + } + const sid = String(myId); + const next = !quizCarryLobbyReadyMap[sid]; + quizCarryLobbyReadyMap[sid] = next; + updateQuizCarryPregameHud(); + socket.emit('quiz-carry-lobby-ready', { ready: next }); + }); + })(); + window.addEventListener('beforeunload', () => socket.emit('leave-space')); + window.addEventListener('resize', () => { + if (!mapData || !canvas) return; + 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); + })(); +})(); diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index 50f8a8c..38b10ac 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -3974,7 +3974,7 @@ - +
v —
diff --git a/www/html/Game/public/play.html.bak-045630 b/www/html/Game/public/play.html.bak-045630 new file mode 100644 index 0000000..50f8a8c --- /dev/null +++ b/www/html/Game/public/play.html.bak-045630 @@ -0,0 +1,3980 @@ + + + + + + + เล่น — Game + + + + + + + + + +
+
+ + +
+
+
+ +
+ + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + +
v —
+ + diff --git a/www/html/Game/public/play.js b/www/html/Game/public/play.js index 25fe357..21a21a0 100644 --- a/www/html/Game/public/play.js +++ b/www/html/Game/public/play.js @@ -6454,10 +6454,21 @@ hideQuizBattleMcqModal(); } + /** หา comp โดมที่ตัวละครเหยียบอยู่ — เช็คหลายจุด (กลาง/เท้า/หัว) ให้ติดง่ายเมื่อเดินถึงสถานี */ + function quizBattleDomeAtMe() { + if (!isQuizBattle()) return null; + const pts = [[me.x, me.y], [me.x, me.y + 0.45], [me.x, me.y - 0.25], [me.x + 0.3, me.y], [me.x - 0.3, me.y]]; + for (let i = 0; i < pts.length; i++) { + const c = getPrimaryQuizBattleDomeCompAt(pts[i][0], pts[i][1]); + if (c != null) return c; + } + return null; + } + /** ZEP-style: เดินถึงสถานี (โดม) ที่ยังไม่ตอบ → เปิดคำถามอัตโนมัติ (ครั้งเดียวต่อการเข้า · เดินออกแล้วกลับมาใหม่ถึงเด้งอีก) */ function maybeAutoOpenQuizBattle() { if (!isQuizBattle() || myId == null) return; - const compNow = getPrimaryQuizBattleDomeCompAt(me.x, me.y); + const compNow = quizBattleDomeAtMe(); if (compNow == null) { qbAutoLastComp = null; return; } const ov = document.getElementById('quiz-battle-mcq-overlay'); const modalOpen = quizBattleModalCompId != null || (ov && !ov.classList.contains('is-hidden')); @@ -6473,6 +6484,56 @@ showQuizBattleMcqModal(q); } + function qbRoundRect(ctx, x, y, w, h, r) { + r = Math.min(r, w / 2, h / 2); + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); + } + /** วาดหีบสมบัติบนสถานีคำถาม — done=ปลดล็อก (เขียว+ติ๊กถูก) / ยังไม่ตอบ=ล็อก (น้ำตาล+กุญแจ) + เลขสถานี */ + function drawQuizBattleChest(ctx, cx, cy, size, done, num) { + const w = size * 0.84, h = size * 0.64; + const x0 = cx - w / 2, y0 = cy - h / 2; + ctx.save(); + ctx.shadowColor = done ? 'rgba(52,211,153,0.95)' : 'rgba(255,200,70,0.95)'; + ctx.shadowBlur = Math.max(4, size * 0.3); + ctx.fillStyle = done ? '#3f7d57' : '#8a5a2b'; + qbRoundRect(ctx, x0, y0 + h * 0.34, w, h * 0.66, size * 0.08); ctx.fill(); + ctx.fillStyle = done ? '#57a875' : '#a86f33'; + qbRoundRect(ctx, x0, y0, w, h * 0.44, size * 0.1); ctx.fill(); + ctx.shadowBlur = 0; + ctx.fillStyle = done ? '#d6f5e3' : '#f1c66a'; + ctx.fillRect(x0, y0 + h * 0.30, w, h * 0.10); + ctx.fillRect(cx - size * 0.045, y0, size * 0.09, h); + if (done) { + ctx.strokeStyle = '#eafff3'; ctx.lineWidth = Math.max(2, size * 0.08); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; + ctx.beginPath(); + ctx.moveTo(cx - size * 0.13, cy + size * 0.04); + ctx.lineTo(cx - size * 0.02, cy + size * 0.15); + ctx.lineTo(cx + size * 0.17, cy - size * 0.12); + ctx.stroke(); + } else { + ctx.strokeStyle = '#f1c66a'; ctx.lineWidth = Math.max(2, size * 0.05); + ctx.beginPath(); ctx.arc(cx, cy + h * 0.04, size * 0.085, Math.PI, 0); ctx.stroke(); + ctx.fillStyle = '#f1c66a'; + qbRoundRect(ctx, cx - size * 0.085, cy + h * 0.04, size * 0.17, size * 0.13, size * 0.03); ctx.fill(); + } + if (num) { + ctx.fillStyle = 'rgba(8,12,28,0.82)'; + ctx.beginPath(); ctx.arc(cx, y0 - size * 0.14, size * 0.17, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = '#fff'; + ctx.font = 'bold ' + Math.max(9, size * 0.23) + 'px Kanit, sans-serif'; + ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText(String(num), cx, y0 - size * 0.14); + ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic'; + } + ctx.restore(); + } + function tryOpenQuizBattleFromKey() { if (!isQuizBattle() || !mapData || myId == null) return; const compId = getPrimaryQuizBattleDomeCompAt(me.x, me.y); @@ -6497,10 +6558,13 @@ function pushSanitizedBattleMcqFromPayload(s) { if (!s || !Array.isArray(s.battleQuizMcq)) return 0; + /* กรองเฉพาะคลังคำถามของหัวข้อห้องนี้ (mapData.quizBattleCategoryId เช่น topic1..topic10) — ถ้าไม่ระบุ ใช้ทั้งหมด */ + const roomCat = (mapData && mapData.quizBattleCategoryId) ? String(mapData.quizBattleCategoryId).trim() : ''; let n = 0; for (let i = 0; i < s.battleQuizMcq.length; i++) { const raw = s.battleQuizMcq[i]; if (!raw || !String(raw.text || '').trim()) continue; + if (roomCat && String(raw.categoryId || '').trim() !== roomCat) continue; const ch = Array.isArray(raw.choices) ? raw.choices : []; if (ch.length !== 3) continue; const a = String(ch[0] || '').trim(); @@ -10884,20 +10948,20 @@ } } if (isQuizBattle() && showGrid && mapData.quizBattlePathArea && mapData.quizBattlePathArea[y] && mapData.quizBattlePathArea[y][x] === 1) { - ctx.fillStyle = 'rgba(186, 130, 255, 0.4)'; + /* ไฮไลต์เส้นทางบาง ๆ (โชว์เฉพาะตอนเปิดกริด) — ให้เห็นถนนจาก design ทะลุได้ */ + ctx.fillStyle = 'rgba(150, 195, 255, 0.12)'; ctx.fillRect(sx + 1, sy + 1, size - 2, size - 2); - ctx.strokeStyle = 'rgba(220, 170, 255, 0.55)'; - ctx.strokeRect(sx + 1, sy + 1, size - 2, size - 2); } - if (isQuizBattle() && showGrid && mapData.quizBattleDomeArea && mapData.quizBattleDomeArea[y] && mapData.quizBattleDomeArea[y][x] === 1) { + if (isQuizBattle() && mapData.quizBattleDomeArea && mapData.quizBattleDomeArea[y] && mapData.quizBattleDomeArea[y][x] === 1) { const cid = mapData.quizBattleDomeComp && mapData.quizBattleDomeComp[y] && mapData.quizBattleDomeComp[y][x]; - const done = cid > 0 && quizBattleAnsweredComps.has(cid); - ctx.fillStyle = done ? 'rgba(90, 90, 110, 0.4)' : 'rgba(100, 200, 255, 0.36)'; - ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); - ctx.strokeStyle = done ? 'rgba(140, 140, 160, 0.65)' : 'rgba(255, 100, 130, 0.82)'; - ctx.lineWidth = 2; - ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); - ctx.lineWidth = 1; + const comp = mapData.quizBattleDomeComp; + /* วาดหีบครั้งเดียวต่อโดม (ช่องบน-ซ้ายสุดของกลุ่ม comp) — กันวาดซ้อนเมื่อโดมหลายช่อง */ + const upSame = cid > 0 && comp && comp[y - 1] && comp[y - 1][x] === cid; + const leftSame = cid > 0 && comp && comp[y] && comp[y][x - 1] === cid; + if (!upSame && !leftSame) { + const done = cid > 0 && quizBattleAnsweredComps.has(cid); + drawQuizBattleChest(ctx, sx + size / 2, sy + size / 2, size, done, cid || 0); + } } if (isStack()) { const sr = mapData.stackReleaseArea, sl = mapData.stackLandArea; diff --git a/www/html/Game/public/play.js.bak-115137 b/www/html/Game/public/play.js.bak-115137 new file mode 100644 index 0000000..e4858e7 --- /dev/null +++ b/www/html/Game/public/play.js.bak-115137 @@ -0,0 +1,11780 @@ +(function () { + const BASE = '/Game'; + /** แผงคำถามบนแมป quiz_carry — ไฟล์อยู่ public/img/quiz-carry */ + const QUIZ_CARRY_MAP_FEEDBACK_IMG = { + correct: BASE + '/img/quiz-carry/icon-Correct.png', + incorrect: BASE + '/img/quiz-carry/icon-Incorrect.png', + scorePlus: BASE + '/img/quiz-carry/score+10.png', + }; + const QUIZ_MAP_CARRY_FEEDBACK_MS = 1400; + const params = new URLSearchParams(window.location.search); + const spaceId = params.get('space'); + const nick = params.get('nick') || 'ผู้เล่น'; + const previewMode = params.get('preview') === '1'; + const forceDefaultCharacter = params.get('defaultChar') === '1'; + const editorEmbedReturn = params.get('editorEmbed') === '1'; + const playMapIdFromQuery = (params.get('map') || '').trim(); + /** mapId จาก join / game-start — ใช้จับฉาก crown เมื่อ URL ไม่มี ?map= */ + let playSessionMapId = playMapIdFromQuery; + /** สรุปภารกิจแบบ mock (popup-result ฯลฯ) — ใช้เฉพาะฉากนี้จาก editor (id ใน URL เช่น editor.html?id=mnorwqx1) */ + const quizCarryMissionSummaryMapId = 'mnorwqx1'; + const quizCarryUseMissionSummaryOverlay = playMapIdFromQuery === quizCarryMissionSummaryMapId; + /** Gauntlet พรมแดง — ฉากนี้จาก editor (?map=mno9kb07): วาดตัวหันขวา + ใช้รูป lane/laser จาก game-timing */ + const GAUNTLET_FACE_RIGHT_MAP_ID = 'mno9kb07'; + /** Jump Survival ฉาก Jumper — UI สรุปภารกิจแบบ crown + รูปใน /img/Jumper/ (editor ?map=mnptfts2) */ + const JUMP_SURVIVE_MISSION_MAP_ID = 'mnptfts2'; + /** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */ + const LEGACY_PLACEHOLDER_CHARACTER_ID = ''; + let firstCharacterDefaultResolved = null; + const firstCharacterDefaultPromise = fetch(BASE + '/api/characters') + .then((r) => r.json()) + .then((list) => { + if (Array.isArray(list) && list.length > 0 && list[0] && list[0].id) { + firstCharacterDefaultResolved = String(list[0].id); + } else { + firstCharacterDefaultResolved = ''; + } + }) + .catch(() => { + firstCharacterDefaultResolved = ''; + }); + const lobbyLevelParam = params.get('lobbyLevel'); + const lobbyCaseParam = params.get('case'); + if (lobbyLevelParam || lobbyCaseParam) { + try { + window.__detectiveLobbyMeta = { level: lobbyLevelParam, caseId: lobbyCaseParam }; + } catch (e) { /* ignore */ } + } + + const POST_CASE_LOBBY_MAP_ID = 'mn8nx46h'; + + function isDetectiveMinigamePlay() { + if (params.get('detectiveReturn') === '1') return true; + try { + return sessionStorage.getItem('detectiveMinigameReturn') === '1'; + } catch (e) { + return false; + } + } + + function buildRoomLobbyReturnHref() { + let h = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + h += '&map=' + encodeURIComponent(POST_CASE_LOBBY_MAP_ID); + try { + const dr = localStorage.getItem('lastCreatedSpaceName'); + if (dr) h += '&displayRoom=' + encodeURIComponent(dr); + } catch (e) { /* ignore */ } + try { + const meta = window.__detectiveLobbyMeta; + if (meta && meta.level != null && String(meta.level).trim()) { + h += '&lobbyLevel=' + encodeURIComponent(String(meta.level).trim()); + } + if (meta && meta.caseId != null && String(meta.caseId).trim()) { + h += '&caseId=' + encodeURIComponent(String(meta.caseId).trim()); + } + } catch (e2) { /* ignore */ } + try { + if (sessionStorage.getItem('detectiveMinigameReturn') === '1') { + h += '&detectiveReturn=1'; + sessionStorage.removeItem('detectiveMinigameReturn'); + } + } catch (e3) { /* ignore */ } + if (params.get('detectiveReturn') === '1' && h.indexOf('detectiveReturn') < 0) { + h += '&detectiveReturn=1'; + } + return h; + } + + function finishDetectiveMinigameAndReturnLobby() { + quizCarrySessionEnded = true; + try { sessionStorage.setItem('detectiveMinigameReturn', '1'); } catch (e) { /* ignore */ } + const go = function () { + window.location.href = buildRoomLobbyReturnHref(); + }; + if (socket && socket.connected) { + socket.emit('detective-minigame-finished', {}, function (res) { + if (res && res.ok) go(); + else go(); + }); + } else { + go(); + } + } + /** ทดสอบจากเอดิเตอร์: เติมบอทให้ครบจำนวน (รวมผู้เล่นจริง) — ?fillTotal=6 (ค่าเริ่ม 6) */ + const previewFillBots = previewMode && editorEmbedReturn; + const previewTargetHeadcount = Math.min(24, Math.max(1, parseInt(params.get('fillTotal'), 10) || 6)); + let detectiveCaseFillBots = false; + let detectiveBotSlotCount = 0; + function playBotsEnabled() { + return previewFillBots || detectiveCaseFillBots; + } + function playBotTargetHeadcount() { + if (previewFillBots) return previewTargetHeadcount; + if (detectiveCaseFillBots) return countPlayHumans() + detectiveBotSlotCount; + return countPlayHumans(); + } + const PREVIEW_BOT_PREFIX = '__pv_bot_'; + let previewBotSeq = 0; + /** Stack preview: บอทไม่วาบบนแผนที่ — ใช้ไฮไลต์แถบแข่งแทน */ + let lastStackPreviewActorId = null; + let lastStackPreviewActorUntil = 0; + /** Stack preview: สลับตา Player 1 = มนุษย์, 2–6 = บอท (เฉพาะตาปัจจุบันเล่นได้) */ + let stackTurnCount = 6; + let stackPreviewTurnOrder = null; + let stackPreviewTurnIndex = 0; + let stackPreviewBotThinkUntil = 0; + + function rebuildStackPreviewTurnOrder() { + if (!playBotsEnabled() || !mapData || mapData.gameType !== 'stack') { + stackPreviewTurnOrder = null; + stackTurnCount = 1; + return; + } + const bots = [...others.keys()].filter(isPreviewBotId).sort(); + stackTurnCount = Math.max(1, Math.min(12, playBotTargetHeadcount())); + stackPreviewTurnOrder = [{ kind: 'human', seat: 1 }]; + for (let i = 0; i < stackTurnCount - 1; i++) { + stackPreviewTurnOrder.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null }); + } + stackPreviewTurnIndex = 0; + stackPreviewBotThinkUntil = 0; + } + + function advanceStackPreviewTurn() { + if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount) return; + stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % stackTurnCount; + stackPreviewBotThinkUntil = 0; + } + + function isStackPreviewHumanTurn() { + if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount) return true; + const cur = stackPreviewTurnOrder[stackPreviewTurnIndex]; + return !!(cur && cur.kind === 'human'); + } + + function getStackPreviewCurrentSeat() { + if (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount) return 1; + const cur = stackPreviewTurnOrder[stackPreviewTurnIndex]; + return cur && cur.seat ? cur.seat : stackPreviewTurnIndex + 1; + } + if (!spaceId) { window.location.replace(BASE + '/lobby.html'); return; } + + const socket = io({ path: BASE + '/socket.io' }); + const canvas = document.getElementById('game-canvas'); + const ctx = canvas.getContext('2d'); + let mapData = null, tileSize = 32, myId = null; + let stackMini = null; + let stackFall = null; + let lastStackTickMs = performance.now(); + let mapBackgroundImg = null; + /** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */ + let mapGridImageImgs = []; + let mapGridImageHeldImgs = []; + let me = { x: 1, y: 1, direction: 'down', nickname: nick, isWalking: false, playTint: null, gauntletScore: 0, gauntletEliminated: false, tx: null, ty: null, quizCarryHeld: null }; + const others = new Map(); + /** Stack preview HUD: เทอร์มินัลล็อก (cyber UI) */ + const STACK_PREVIEW_HUD_LOG_MAX = 8; + let stackPreviewHudLog = []; + function stackPreviewPushHudLog(line) { + if (!previewFillBots) return; + stackPreviewHudLog.push(String(line || '').slice(0, 96)); + while (stackPreviewHudLog.length > STACK_PREVIEW_HUD_LOG_MAX) stackPreviewHudLog.shift(); + } + function stackPreviewLogStackDrop(hit, actor) { + if (!previewFillBots || !hit) return; + const bid = actor && actor.botId; + let nm = 'NODE'; + if (actor && actor.human) nm = (me.nickname || 'YOU').toUpperCase().replace(/\s+/g, '_').slice(0, 12); + else if (bid && others.get(bid)) nm = String(others.get(bid).nickname || 'BOT').toUpperCase().replace(/\s+/g, '_').slice(0, 12); + if (hit.miss) stackPreviewPushHudLog(`>>> ${nm}: SECTOR_MISS`); + else if (hit.perfect) stackPreviewPushHudLog(`>>> ${nm}: PERFECT +${hit.pts} SYN`); + else stackPreviewPushHudLog(`>>> ${nm}: LOCK +${hit.pts}`); + } + const keys = {}; + const characterImages = {}; + function createDefaultAvatarImg() { + const c = document.createElement('canvas'); + c.width = 64; c.height = 64; + const ctx = c.getContext('2d'); + ctx.fillStyle = '#7aa2f7'; + ctx.beginPath(); + ctx.arc(32, 22, 14, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#9ece6a'; + ctx.beginPath(); + ctx.arc(32, 48, 18, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#1a1b26'; + ctx.beginPath(); + ctx.arc(28, 20, 3, 0, Math.PI * 2); + ctx.arc(36, 20, 3, 0, Math.PI * 2); + ctx.fill(); + const img = new Image(); + img.src = c.toDataURL('image/png'); + return img; + } + const defaultAvatarImg = createDefaultAvatarImg(); + const characterAnimations = {}; + const characterIdleAnimations = {}; + const CHARACTER_ANIM_FRAMES = 4; + const CHARACTER_ANIM_FRAME_MS = 200; + + /** จังหวะเดินลูปคงที่ 4 เฟรม — อย่า modulo ด้วยจำนวนเฟรมที่โหลดได้แล้ว (ไม่งั้นเฟรมกลางหาย/ลูปสั้นลง) */ + function walkAnimPhaseIndex(now, isWalking) { + const t = isWalking ? (typeof now === 'number' ? now : Date.now()) : 0; + return Math.floor(t / CHARACTER_ANIM_FRAME_MS) % CHARACTER_ANIM_FRAMES; + } + + /** เลือกเฟรมสูงสุดที่โหลดแล้วและ <= phase (เดิมถอย) */ + function pickLoadedWalkFrameIndex(anim, phase) { + if (!anim || !anim.frames || !anim.frames.length) return -1; + const maxK = Math.min(phase, anim.frames.length - 1, CHARACTER_ANIM_FRAMES - 1); + for (let k = maxK; k >= 0; k--) { + const f = anim.frames[k]; + if (f && f.complete && f.naturalWidth) return k; + } + return -1; + } + + /** ตัวละครอัปโหลดจากระบบมักเป็น char- + ตัวเลข (อาจมี suffix) — ไม่มี idle strip / idle layer ครบทุกทิศ */ + function isUploadedCharAssetId(id) { + if (!id) return false; + const s = String(id).trim(); + return /^char-\d/i.test(s); + } + + function useMultiFrameIdleSpriteSheets(id) { + if (!id) return true; + return !isUploadedCharAssetId(id); + } + + function ensureCharacterIdleAnim(id, dir) { + const d = dir || 'down'; + /* char-* ไม่มี *_idle_0.png — ใช้ strip เดิน dir_0..3 เป็น idle ต่อทิศ (มีลูป) แทนรูปเดียวที่มักไม่มี */ + if (!useMultiFrameIdleSpriteSheets(id)) { + const key = String(id) + '_' + d + '_idle'; + let anim = characterIdleAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterIdleAnimations[key] = anim; + const enc = encodeURIComponent(id); + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + enc + '_' + d + '_' + i + '.png'; + anim.frames.push(img); + } + const fb = new Image(); + fb.src = BASE + '/img/characters/' + enc + '_' + d + '_idle.png'; + anim.fallback = fb; + } + return anim; + } + const key = id + '_' + d + '_idle'; + let anim = characterIdleAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterIdleAnimations[key] = anim; + const enc = encodeURIComponent(id); + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + enc + '_' + d + '_idle_' + i + '.png'; + anim.frames.push(img); + } + const fb = new Image(); + fb.src = BASE + '/img/characters/' + enc + '_' + d + '_idle.png'; + anim.fallback = fb; + } + return anim; + } + + /** ลูป idle ขณะยืน — ใช้จังหวะเวลาเหมือนเดิน (CHARACTER_ANIM_FRAME_MS) */ + function idleAnimPhaseIndex(now) { + const t = typeof now === 'number' ? now : Date.now(); + return Math.floor(t / CHARACTER_ANIM_FRAME_MS) % CHARACTER_ANIM_FRAMES; + } + + function characterIdleSpritesVisible(id, dir) { + if (!id) return false; + const anim = ensureCharacterIdleAnim(id, dir || 'down'); + if (anim.fallback && anim.fallback.complete && anim.fallback.naturalWidth) return true; + for (let i = 0; i < anim.frames.length; i++) { + const f = anim.frames[i]; + if (f && f.complete && f.naturalWidth) return true; + } + return false; + } + + /** สุ่มสีตามเลเยอร์เดียวกับ character.html: bodyColor / hairColor / headColor (ลำดับซ้อนเหมือน composeLayeredFrame) */ + const PLAY_LAYER_ORDER = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face']; + const PLAY_LAYER_TINT_KEY = { bodyColor: 'body', headColor: 'head', hairColor: 'hair' }; + const PLAY_TINT_HEAD = ['#eaa78a', '#fbd5c4', '#fae9e1']; + const PLAY_TINT_HAIR = ['#d72520', '#ef8508', '#efe237', '#5bb443', '#2585cb', '#3f4ead', '#b53fd6', '#ef62b9']; + const PLAY_TINT_BODY = ['#fb4941', '#feaa11', '#fefe6d', '#adfd85', '#45fbfd', '#799afe', '#f87dff', '#fec1fe']; + const playLayerMode = {}; + /** คิว probe ครบ up/down/left/right แล้ว — กันทิศที่ไม่มีไฟล์ layer ไปตก fallback แถบทั้งทั้งที่ทิศอื่นมี */ + const playLayerAllDirsQueued = {}; + /** จาก GET /api/characters — hasLayerFiles สแกนจากชื่อไฟล์จริง (แม่นกว่า probe จาก อย่างเดียว) */ + let playCharLayerApi = { status: 'idle', map: null }; + /** id → Set เลเยอร์ที่มีไฟล์จริง (จาก GET /api/characters) — กันยิง URL เลเยอร์ที่ไม่เคยอัปโหลด → 404 รกคอนโซล */ + const playCharDiskLayersById = Object.create(null); + + function ensurePlayCharLayerListFetch() { + if (playCharLayerApi.status !== 'idle') return; + playCharLayerApi.status = 'loading'; + fetch(BASE + '/api/characters') + .then((r) => r.json()) + .then((list) => { + const map = Object.create(null); + if (Array.isArray(list)) { + list.forEach((c) => { + if (!c || !c.id) return; + map[c.id] = !!c.hasLayerFiles; + if (Array.isArray(c.layers) && c.layers.length) { + const st = new Set(); + c.layers.forEach((n) => { + if (typeof n === 'string' && n.length) st.add(n); + }); + if (st.size) playCharDiskLayersById[c.id] = st; + else delete playCharDiskLayersById[c.id]; + } else { + delete playCharDiskLayersById[c.id]; + } + }); + } + playCharLayerApi = { status: 'done', map }; + }) + .catch(() => { + playCharLayerApi = { status: 'done', map: Object.create(null) }; + }); + } + + /** @returns {boolean|null} true/false จาก API, null = ยังไม่โหลดหรือไม่มีรายการ id นี้ */ + function playCharLayerFromApi(characterId) { + if (playCharLayerApi.status !== 'done' || !characterId) return null; + if (Object.prototype.hasOwnProperty.call(playCharLayerApi.map, characterId)) { + return playCharLayerApi.map[characterId]; + } + return null; + } + + /** ถ้าเซิร์ฟเวอร์ส่งรายชื่อเลเยอร์ที่มีบนดิสก์ — ไม่โหลดเลเยอร์อื่น (ลด 404) */ + function playCharDiskLayersSet(characterId) { + if (!characterId || playCharLayerApi.status !== 'done') return null; + const s = playCharDiskLayersById[characterId]; + return s && s.size ? s : null; + } + + const playLayerImageCache = new Map(); + const playLayerCompositeCache = new Map(); + function pickRandomPlayTint() { + return { + head: PLAY_TINT_HEAD[Math.floor(Math.random() * PLAY_TINT_HEAD.length)], + hair: PLAY_TINT_HAIR[Math.floor(Math.random() * PLAY_TINT_HAIR.length)], + body: PLAY_TINT_BODY[Math.floor(Math.random() * PLAY_TINT_BODY.length)], + }; + } + + /** FNV-1a 32-bit — แยกดัชนี head/hair/body ได้กระจายกว่า hash*31 เดิม (กันบอท __pv_bot_N สีซ้ำ) */ + function hashStringFnv1a32(str) { + let h = 2166136261 >>> 0; + const s = String(str || ''); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619) >>> 0; + } + return h >>> 0; + } + + function playTintFromPeerId(id) { + const s = String(id || 'x'); + const h0 = hashStringFnv1a32(s); + const h1 = hashStringFnv1a32(s + '\n1hair'); + const h2 = hashStringFnv1a32(s + '\n2body'); + return { + head: PLAY_TINT_HEAD[h0 % PLAY_TINT_HEAD.length], + hair: PLAY_TINT_HAIR[h1 % PLAY_TINT_HAIR.length], + body: PLAY_TINT_BODY[h2 % PLAY_TINT_BODY.length], + }; + } + + function hexToRgb01(hex) { + const h = (hex || '').replace('#', '').trim(); + if (h.length !== 6) return [1, 1, 1]; + return [ + parseInt(h.slice(0, 2), 16) / 255, + parseInt(h.slice(2, 4), 16) / 255, + parseInt(h.slice(4, 6), 16) / 255, + ]; + } + + function tintPlayLayerImageData(imageData, tintHex) { + const rgb = hexToRgb01(tintHex); + const tr = rgb[0], tg = rgb[1], tb = rgb[2]; + const d = imageData.data; + for (let i = 0; i < d.length; i += 4) { + if (d[i + 3] < 12) continue; + const r = d[i], g = d[i + 1], b = d[i + 2]; + const L = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + d[i] = Math.min(255, Math.round(tr * 255 * L)); + d[i + 1] = Math.min(255, Math.round(tg * 255 * L)); + d[i + 2] = Math.min(255, Math.round(tb * 255 * L)); + } + } + + function drawPlayTintedLayer(ctx, w, h, img, tintHex) { + if (!tintHex) { + ctx.drawImage(img, 0, 0, w, h); + return; + } + const c = document.createElement('canvas'); + c.width = w; + c.height = h; + const x = c.getContext('2d'); + x.drawImage(img, 0, 0, w, h); + try { + const idata = x.getImageData(0, 0, w, h); + tintPlayLayerImageData(idata, tintHex); + x.putImageData(idata, 0, 0); + } catch (e) { + x.clearRect(0, 0, w, h); + x.drawImage(img, 0, 0, w, h); + } + ctx.drawImage(c, 0, 0, w, h); + } + + function ensurePlayLayerImage(url) { + let img = playLayerImageCache.get(url); + if (!img) { + img = new Image(); + img.src = url; + playLayerImageCache.set(url, img); + } + return img; + } + + /** ลำดับลอง URL: เฟรมปัจจุบันแบบ multi → เฟรม 0 → แบบ single (ไม่มี _0_) เพื่อกันชื่อไฟล์ไม่ตรงกับที่เซิร์ฟเวอร์เขียน */ + function layerUrlCandidates(id, dir, layerName, frameIndex) { + const enc = encodeURIComponent(id); + const base = BASE + '/img/characters/' + enc + '_' + dir; + const out = []; + function add(u) { + if (out.indexOf(u) === -1) out.push(u); + } + add(base + '_' + frameIndex + '_layer_' + layerName + '.png'); + if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png'); + add(base + '_layer_' + layerName + '.png'); + return out; + } + + /** เลเยอร์ idle: id__idle__layer_.png หรือ id__idle_layer_.png */ + function layerUrlCandidatesIdle(id, dir, layerName, frameIndex) { + const enc = encodeURIComponent(id); + const base = BASE + '/img/characters/' + enc + '_' + dir + '_idle'; + const out = []; + function add(u) { + if (out.indexOf(u) === -1) out.push(u); + } + add(base + '_' + frameIndex + '_layer_' + layerName + '.png'); + if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png'); + add(base + '_layer_' + layerName + '.png'); + return out; + } + + function shadowUrlCandidates(id, dir, frameIndex) { + const c = layerUrlCandidates(id, dir, 'shadow', frameIndex); + const def = BASE + '/img/default-shadow-' + dir + '.png'; + if (c.indexOf(def) === -1) c.push(def); + return c; + } + + function shadowUrlCandidatesIdle(id, dir, frameIndex) { + const c = layerUrlCandidatesIdle(id, dir, 'shadow', frameIndex); + const def = BASE + '/img/default-shadow-' + dir + '.png'; + if (c.indexOf(def) === -1) c.push(def); + return c; + } + + /** ลองทีละ URL — อย่า set src พร้อมกันหลายรูป (เดิมทำให้ legacy `id_dir_layer_x` โดน 404 ทุกเลเยอร์ทุกทิศทั้งที่มีแค่ `id_dir_0_layer_x`) */ + function resolvePlayLayerImage(urls) { + for (let i = 0; i < urls.length; i++) { + const img = ensurePlayLayerImage(urls[i]); + if (!img.complete) return { status: 'pending' }; + if (img.naturalWidth > 0) return { status: 'ok', img }; + } + return { status: 'missing' }; + } + + function ensurePlayLayerProbesAllDirections(characterId) { + if (!characterId || playLayerAllDirsQueued[characterId]) return; + playLayerAllDirsQueued[characterId] = true; + ['up', 'down', 'left', 'right'].forEach((d) => ensurePlayLayerProbe(characterId, d)); + } + + function playCharAnyDirectionLayered(characterId) { + if (!characterId) return false; + return ['up', 'down', 'left', 'right'].some((d) => playLayerMode[characterId + '|' + d] === 'layered'); + } + + /** รอเฉพาะตอนยังไม่เจอ layered เลย — ถ้ามีทิศหนึ่ง layered แล้ว ไม่ต้องรอทิศอื่น */ + function playCharLayerDiscoveryPending(characterId) { + if (!characterId) return true; + const api = playCharLayerFromApi(characterId); + if (api === true || api === false) return false; + if (playCharAnyDirectionLayered(characterId)) return false; + return ['up', 'down', 'left', 'right'].some((d) => { + const m = playLayerMode[characterId + '|' + d]; + return m === undefined || m === 'pending'; + }); + } + + function ensurePlayLayerProbe(characterId, dir) { + const key = characterId + '|' + dir; + if (playLayerMode[key] !== undefined && playLayerMode[key] !== 'pending') return; + if (playLayerMode[key] === 'pending') return; + playLayerMode[key] = 'pending'; + const cands = layerUrlCandidates(characterId, dir, 'bodyColor', 0); + let ci = 0; + function tryNextProbe() { + if (playLayerMode[key] !== 'pending') return; + if (ci >= cands.length) { + playLayerMode[key] = 'none'; + return; + } + const url = cands[ci++]; + const img = ensurePlayLayerImage(url); + const fin = () => { + if (playLayerMode[key] !== 'pending') return; + if (img.naturalWidth > 0) { + playLayerMode[key] = 'layered'; + return; + } + tryNextProbe(); + }; + img.onload = fin; + img.onerror = fin; + if (img.complete) fin(); + } + tryNextProbe(); + } + + function getCharacterAnimFrameIndex(id, dir, now, isWalking) { + if (!id) return 0; + const key = id + '_' + dir; + const anim = characterAnimations[key]; + if (!anim) return 0; + const phase = walkAnimPhaseIndex(now, isWalking); + const fi = pickLoadedWalkFrameIndex(anim, phase); + return fi >= 0 ? fi : 0; + } + + function tryComposePlayLayersFromFiles(rawImg, id, dir, frameIndex, tint, opts) { + const useIdle = opts && opts.idle; + if (!rawImg || !rawImg.naturalWidth || !rawImg.naturalHeight) return null; + const w = rawImg.naturalWidth, h = rawImg.naturalHeight; + const c = document.createElement('canvas'); + c.width = w; + c.height = h; + const cctx = c.getContext('2d'); + let anyTintColorLayer = false; + let skippedShadowWhilePending = false; + const diskLayers = playCharDiskLayersSet(id); + /* ยืน (opts.idle): ลอง compose จาก *_idle_*_layer_* เหมือนหน้า character upload — รวม char-* ที่อัปโหลด idle เลเยอร์; ถ้าไม่มีไฟล์จะ missing → ไม่มีเลเยอร์ย้อม → getPlayTintedAvatarSource fallback ไป walk compose */ + const idleLayerStripes = !!useIdle; + const resDir = dir; + const resFi = frameIndex; + for (let li = 0; li < PLAY_LAYER_ORDER.length; li++) { + const layerName = PLAY_LAYER_ORDER[li]; + if (diskLayers && layerName !== 'shadow' && !diskLayers.has(layerName)) continue; + /* char-* หันหลัง (up): ไม่มี face ทิศนี้ — ข้ามกันตาหน้าซ้อนหลังหัว; ซ้าย/ขวา/หน้าใช้ face ตามทิศได้ */ + if (layerName === 'face' && isUploadedCharAssetId(id) && dir === 'up') { + continue; + } + let urls; + if (layerName === 'shadow') { + urls = idleLayerStripes + ? shadowUrlCandidatesIdle(id, dir, frameIndex) + : shadowUrlCandidates(id, resDir, resFi); + } else { + urls = idleLayerStripes + ? layerUrlCandidatesIdle(id, dir, layerName, frameIndex) + : layerUrlCandidates(id, resDir, layerName, resFi); + } + const r = resolvePlayLayerImage(urls); + /* เงาโหลดช้า/404 บ่อย — อย่าให้บล็อกทั้งคอมโพส (ไม่งั้นตกไป fallback แถบแนวตั้งแทนเลเยอร์จริง) */ + if (r.status === 'pending' && layerName === 'shadow') { + skippedShadowWhilePending = true; + continue; + } + if (r.status === 'pending') return null; + if (r.status === 'missing') continue; + const tintHex = PLAY_LAYER_TINT_KEY[layerName] ? tint[PLAY_LAYER_TINT_KEY[layerName]] : null; + if (PLAY_LAYER_TINT_KEY[layerName]) anyTintColorLayer = true; + drawPlayTintedLayer(cctx, w, h, r.img, tintHex); + } + /* ถ้าโหลดได้แต่ไม่มีเลเยอร์สีเลย จะเหลือแต่ stroke → ตัวขาว — ใช้ PNG รวมแทน */ + if (!anyTintColorLayer) return null; + return { canvas: c, skipCache: skippedShadowWhilePending }; + } + + function getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, isWalking, tint) { + if (!tint || !characterId || !rawImg) return rawImg; + ensurePlayCharLayerListFetch(); + ensurePlayLayerProbesAllDirections(characterId); + const dirn = dir || 'down'; + const frameIndexWalk = getCharacterAnimFrameIndex(characterId, dirn, timeMs, isWalking); + const idlePhase = idleAnimPhaseIndex(typeof timeMs === 'number' ? timeMs : Date.now()); + + if (playCharLayerDiscoveryPending(characterId)) return rawImg; + + const apiFlag = playCharLayerFromApi(characterId); + /* เหมือนเดิม: มีเลเยอร์แยกไฟล์เมื่อ API บอก hasLayerFiles หรือ probe เจอ — gen สีทำบน canvas จาก *_layer_* เท่านั้น */ + const charHasLayerFiles = apiFlag === true + || (apiFlag == null && playCharAnyDirectionLayered(characterId)); + + /* มี PNG idle รวม — ใช้เมื่อไม่มีระบบเลเยอร์สี (ถ้ามีเลเยอร์ต้อง compose ไม่ return raw ขาว) */ + if (!charHasLayerFiles && !isWalking && characterIdleSpritesVisible(characterId, dirn)) { + return rawImg; + } + const cacheKeyWalk = [characterId, dirn, frameIndexWalk, tint.head, tint.hair, tint.body, 'ct3'].join('|'); + + if (charHasLayerFiles) { + if (!isWalking) { + const composeFrame = isUploadedCharAssetId(characterId) ? 0 : idlePhase; + const cacheKeyIdle = [characterId, dirn, composeFrame, tint.head, tint.hair, tint.body, 'ct3', 'idleL'].join('|'); + const hitI = playLayerCompositeCache.get(cacheKeyIdle); + if (hitI) return hitI; + const packI = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, composeFrame, tint, { idle: true }); + if (packI && packI.canvas) { + if (!packI.skipCache) playLayerCompositeCache.set(cacheKeyIdle, packI.canvas); + return packI.canvas; + } + } + const hit = playLayerCompositeCache.get(cacheKeyWalk); + if (hit) return hit; + const pack = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, frameIndexWalk, tint, { idle: false }); + if (pack && pack.canvas) { + if (!pack.skipCache) playLayerCompositeCache.set(cacheKeyWalk, pack.canvas); + return pack.canvas; + } + return rawImg; + } + + /* + * ไม่มีไฟล์เลเยอร์ตาม API/probe — แสดง PNG รวม (ไม่ย้อมทั้งตัว: ไม่เหมือนระบบ gen สีแบบเลเยอร์เดิม) + */ + return rawImg; + } + + function getCharacterImg(id, direction) { + if (!id) return null; + const key = id + '_' + (direction || 'down'); + if (characterImages[key]) return characterImages[key]; + const img = new Image(); + img.src = BASE + '/img/characters/' + encodeURIComponent(id) + '_' + (direction || 'down') + '.png'; + characterImages[key] = img; + return img; + } + + function getCharacterFrame(id, direction, now, isWalking) { + if (!id) return null; + const dir = direction || 'down'; + const t = typeof now === 'number' ? now : Date.now(); + if (!isWalking) { + const idleAnim = ensureCharacterIdleAnim(id, dir); + let idlePhase = idleAnimPhaseIndex(t); + if (isUploadedCharAssetId(id)) idlePhase = 0; + const idleFi = pickLoadedWalkFrameIndex(idleAnim, idlePhase); + if (idleFi >= 0) return idleAnim.frames[idleFi]; + const idfb = idleAnim.fallback; + if (idfb && idfb.complete && idfb.naturalWidth) return idfb; + } + const key = id + '_' + dir; + let anim = characterAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterAnimations[key] = anim; + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + encodeURIComponent(id) + '_' + dir + '_' + i + '.png'; + anim.frames.push(img); + } + anim.fallback = getCharacterImg(id, dir); + } + const phase = walkAnimPhaseIndex(now, isWalking); + const fi = pickLoadedWalkFrameIndex(anim, phase); + if (fi >= 0) return anim.frames[fi]; + const fb = anim.fallback; + if (fb && fb.complete && fb.naturalWidth) return fb; + return null; + } + + function getAvatarImg(characterId, direction, now, isWalking) { + const img = characterId ? getCharacterFrame(characterId, direction, now, isWalking) : null; + if (img) return img; + return defaultAvatarImg; + } + function getStoredCharacterId() { + try { + let v = localStorage.getItem('gameCharacterId'); + if (v === 'Chatest') v = ''; // legacy placeholder — ไม่มี sprite จริง + if (v) return v; + } catch (e) { /* ignore */ } + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; + } + function getPlayCharacterId() { + if (forceDefaultCharacter) { + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; + } + return getStoredCharacterId(); + } + const MOVE_SPEED = 0.15; + const PATH_ARRIVE_THRESH = 0.15; + + function isMovementKey(code) { + return ['KeyW','KeyA','KeyS','KeyD','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].indexOf(code) !== -1; + } + function isChatFocused() { + return false; + } + let zoom = 1.4; + let froggerScore = 0; + let lastFroggerKey = 0; + let gauntletObstacles = []; + /** อินเทอร์โพเลตการวาด obstacle ระหว่างแพ็กเก็ต sync (~220ms) */ + let gauntletObsRenderPrev = []; + let gauntletObsRenderNext = []; + let gauntletObsBlendT0 = 0; + let meGauntletJumpTicks = 0; + /** ค่าที่ใช้วาดการยกตัว (เลอร์ปจาก meGauntletJumpTicks ให้โค้งกระโดดไม่กระตุก) */ + let meGauntletJumpVis = 0; + /** ซิงก์จากเซิร์ฟเวอร์ (gauntlet-sync / GET /api/game-timing) */ + let gauntletRuntimeTickMs = 220; + let gauntletRuntimeJumpTicks = 16; + /** Stack: รอบสวิงต่อวินาที — จาก GET /api/game-timing · ค่าน้อย = สวิงช้า */ + let playStackSwingHz = 0.55; + /** Stack: ความกว้างบล็อก (tile) — null = คำนวณจากโซนลงบนแผนที่ */ + let playStackBlockWidthTiles = null; + /** กระโดดให้รอด: ความสูงกระโดด = ทวีคูณ × ความสูงตัว (ch×tile) — จาก GET /api/game-timing */ + let playJumpSurviveJumpHeightMult = 1.5; + /** จำกัดเวลารอบภารกิจกระโดดขึ้นแท่น (วินาที) — จาก GET /api/game-timing; 0 = ใช้ค่าเริ่ม 60 ในเกม */ + let playJumpSurviveMissionTimeSec = 0; + /** ยิงยานอวกาศ (space_shooter): เวลารอบจาก game-timing; 0 = ใช้ค่าเริ่ม 90 ในเกมถ้าแมปไม่ทับ */ + let playSpaceShooterMissionTimeSec = 0; + /** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync (พรมแดง Gauntlet เท่านั้น) */ + let gauntletRuntimeTimeLimitSec = 0; + /** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */ + let gauntletEndsAtMs = null; + /** Crown (mno9kb07): เซิร์ฟยังไม่ปล่อยรัน — หยุด tick / เวลา */ + let gauntletCrownRunHeldRemote = false; + /** null | 'howto' | 'countdown' | 'live' — พรีรันก่อน GO */ + let gauntletCrownPregamePhase = null; + let gauntletCrownLobbyReadyMap = {}; + let gauntletCrownCountdownTimer = null; + let lastGauntletJumpKey = 0; + /** รูป lane: หลาย URL สุ่มตาม id คงที่ · laser: แยกบน/ล่าง/เส้น + สี/ความหนา */ + let gauntletLaneImageUrls = []; + let gauntletLaserTopUrl = ''; + let gauntletLaserBottomUrl = ''; + let gauntletLaserLineUrl = ''; + let gauntletLaserFillColor = 'rgba(140,230,255,0.42)'; + let gauntletLaserStrokeColor = 'rgba(255,220,255,0.9)'; + let gauntletLaserLineWidthPx = 2; + const gauntletAssetImageCache = new Map(); + + /** แปลง URL จาก Admin/game-timing ให้โหลดได้ (nginx เสิร์ฟจาก /Game/...) */ + function normalizeGauntletAssetUrlForPlay(u) { + if (typeof u !== 'string') return ''; + const t = u.trim(); + if (!t) return ''; + if (/^https?:\/\//i.test(t)) return t; + const qIdx = t.indexOf('?'); + const base = (qIdx >= 0 ? t.slice(0, qIdx) : t).trim(); + const qs = qIdx >= 0 ? t.slice(qIdx) : ''; + if (!base) return ''; + if (base.startsWith('/')) return base + qs; + if (/^Game\//i.test(base)) return '/' + base.replace(/^\/+/, '') + qs; + return base + qs; + } + + function ensureGauntletAssetImage(url) { + const u = normalizeGauntletAssetUrlForPlay(typeof url === 'string' ? url : ''); + if (!u) return null; + let rec = gauntletAssetImageCache.get(u); + if (rec) return rec; + rec = { img: new Image(), ready: false }; + if (/^https?:\/\//i.test(u)) { + try { rec.img.crossOrigin = 'anonymous'; } catch (e) { /* ignore */ } + } + rec.img.onload = function () { rec.ready = true; }; + rec.img.onerror = function () { rec.ready = false; }; + rec.img.src = u; + gauntletAssetImageCache.set(u, rec); + return rec; + } + + function pickGauntletLaneImageRec(obsId) { + if (!gauntletLaneImageUrls.length) return null; + const n = gauntletLaneImageUrls.length; + const idx = Math.abs(Number(obsId) | 0) % n; + const u = gauntletLaneImageUrls[idx]; + return u ? ensureGauntletAssetImage(u) : null; + } + + function clampClientLaserColor(s, def) { + const t = String(s || '').trim().slice(0, 100); + if (!t || /[<>"'`]/.test(t)) return def; + return t; + } + + let playQuizPhaseLocal = null; + let playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + let playQuizText = ''; + let playQuizPhaseEndsAt = 0; + let playQuizTimerInterval = null; + /** Preview-only: real question pool + phased timer (matches server quiz-settings / map quizQuestions). */ + let previewQuizPool = []; + let previewQuizTiming = { readMs: 10000, answerMs: 5000, betweenMs: 3500 }; + let previewQuizStep = 'read'; + let previewQuizCurrent = null; + let playLiveQuizScores = {}; + /** คะแนนต่อคำตอบที่ถูกเมื่อส่งป้ายที่ฮับ/โซนส่ง — quiz_carry เท่านั้น */ + const QUIZ_CARRY_POINTS_PER_CORRECT = 10; + /** quiz_carry: หยิบตัวเลือกมาวางโซนกลาง — เล่นพร้อมกัน (ไม่สลับตา) */ + let quizCarryPool = []; + let quizCarryCurrent = null; + /** editor embed + preview: นับ 3–2–1 ก่อนโชว์คำถามและให้เดิน/หยิบได้ */ + let quizCarryEmbedPendingQuestion = null; + let quizCarryEmbedCountdownStartAt = 0; + let quizCarryEmbedCountdownEndAt = 0; + /** quiz_carry: epoch ให้หยิบตัวเลือกได้ · epoch ปิดรอบตอบ (ตั้งที่ Admin แท็บหยิบมาวาง) */ + let quizCarryOptionRevealAt = 0; + let quizCarryAnswerCloseAt = 0; + let quizCarryCarryTimingMs = { carryReadMs: 3000, carryAnswerMs: 5000 }; + /** พรีวิว (รวม editor embed): จำนวนข้อที่เล่นก่อนโอเวอร์เลย์จบ — 0 = ไม่จบอัตโนมัติ */ + let quizCarrySessionLength = 0; + let quizCarryRoundsCompleted = 0; + let quizCarrySessionEnded = false; + /** หลัง timeup บนโต๊ะ (embed): รอ 5s แล้วโชว์ result-complete / result-gameover */ + let quizCarryResultEndTimer = null; + /** หลังโชว์ result-end (embed preview): รอ 5s แล้วกลับ room-lobby — ใช้ร่วม quiz_carry + crown */ + let embedPreviewLobbyReturnTimer = null; + /** embed พรีวิว: รอ Ready / START ของโฮสต์ก่อนนับ 3–2–1 */ + let quizCarryPregameActive = false; + let playHostId = null; + /** ซิงก์จากเซิร์ฟเวอร์: socketId → กด Ready แล้ว */ + let quizCarryLobbyReadyMap = {}; + /** จาก /api/quiz-settings carryMapPanelTheme — ใช้กับ #quiz-map-question-panel เฉพาะ quiz_carry */ + let quizCarryMapPanelTheme = null; + /** จาก /api/quiz-settings carryEmbedCountdownTheme — สี/ขนาด overlay นับ 3-2-1 (embed) */ + let quizCarryEmbedCountdownTheme = null; + /** จาก /api/quiz-settings carryChoicePlaqueThemes — ป้าย canvas ต่อช่องตัวเลือก 0..15 (null = ใช้ค่าเริ่มต้นทุกช่อง) */ + let quizCarryChoicePlaqueThemes = null; + /** จาก carryChoicePlaqueMapScale — ขยายป้ายบนแมป + ฟอนต์ (0.85–2.5) */ + let quizCarryPlaqueMapScale = 1.25; + const quizCarryChoiceImageCache = new Map(); + /** สแนปจาก join-space (Node) — ใช้เมื่อ fetch HTTP quiz-settings ไม่ได้หรือไม่มีธีม */ + let quizCarryJoinSettingsSnap = null; + + /** quiz_carry — โซนตัวเลือกบนแมป / ธีมป้ายต่อช่องสูงสุด 16 */ + const QUIZ_CARRY_MAX_OPTION_SLOTS = 16; + + /** jump_survive — กล้องตามตัวในกรอบ + แพลตฟอร์มเลื่อนขึ้น (scroll) ไม่ลากทั้งฉาก */ + let jumpSurviveCamCenterX = 0; + let jumpSurviveCamCenterY = 0; + let jumpSurviveEliminated = false; + /** จับเวลาโหมดกระโดดให้รอด (วินาทีบน HUD) */ + let jumpSurviveSessionStartMs = 0; + let jumpSurvivePlatformScrollPx = 0; + let jumpSurviveVy = 0; + let jumpSurviveLastTickMs = 0; + let jumpSurviveJumpQueued = false; + let jumpSurviveOnGround = false; + let jumpSurviveLastEmitT = 0; + /** mnptfts2: howto → countdown → live → ended (คะแนน 100 ผู้รอดสุดท้าย) */ + let jumpSurviveMissionPhase = null; + let jumpSurviveGameEnded = false; + let jumperMissionCountdownTimer = null; + + /** space_shooter — ยิงหิน (จุดเกิดจากกริด P1–P6) */ + let spaceShooterBullets = []; + let spaceShooterAsteroids = []; + let spaceShooterPopups = []; + let spaceShooterLastTickMs = 0; + let spaceShooterSpawnAccMs = 0; + let spaceShooterFireCd = 0; + let spaceShooterSessionStartMs = 0; + let spaceShooterLastMoveEmit = 0; + let spaceShooterGameEnded = false; + + /** balloon_boss — ลูกโป้งยิงบอส (Mega Virus) */ + let balloonBossPendingShots = []; + let balloonBossPlayerBullets = []; + let balloonBossBossBullets = []; + let balloonBossSessionStartMs = 0; + let balloonBossLastTickMs = 0; + let balloonBossLastMoveEmit = 0; + let balloonBossPlayerFireCd = 0; + let balloonBossGameEnded = false; + let balloonBossHitFx = []; + let balloonBossBossFireAcc = 0; + /** quiz_battle — โดม MCQ กด E (ข้อจาก battleQuizMcq) */ + let quizBattleMcqPool = []; + let quizBattleAnsweredComps = new Set(); + let quizBattleModalCompId = null; + let qbAutoLastComp = null; // โดมล่าสุดที่ trigger อัตโนมัติ (กันเด้งซ้ำขณะยืนค้าง) + const SPACE_SHOOTER_SHIP_COLORS = ['#50fa7b', '#ff79c6', '#ff5555', '#f1fa8c', '#bd93f9', '#8be9fd']; + + document.getElementById('room-id').textContent = spaceId; + + function hidePlayQuizHud() { + const sb = document.getElementById('play-quiz-scoreboard'); + if (sb) sb.classList.add('is-hidden'); + const fb = document.getElementById('play-quiz-feedback'); + if (fb) { fb.classList.add('is-hidden'); fb.textContent = ''; } + } + + function renderPlayQuizScoreboard(scores) { + const wrap = document.getElementById('play-quiz-scoreboard'); + const ul = document.getElementById('play-quiz-scoreboard-list'); + if (!wrap || !ul || !mapData || (!isQuiz() && !isQuizCarry() && !isQuizBattle())) return; + if (!scores || typeof scores !== 'object') return; + wrap.classList.remove('is-hidden'); + ul.textContent = ''; + const merged = { ...scores }; + others.forEach((_, id) => { if (merged[id] == null) merged[id] = 0; }); + if (myId != null && merged[myId] == null) merged[myId] = 0; + const rows = []; + if (myId != null) { + rows.push({ id: myId, nick: me.nickname || 'คุณ', sc: merged[myId] != null ? merged[myId] : 0 }); + } + others.forEach((o, id) => { + rows.push({ id, nick: (o && o.nickname) ? String(o.nickname) : id, sc: merged[id] != null ? merged[id] : 0 }); + }); + rows.sort((a, b) => b.sc - a.sc || a.nick.localeCompare(b.nick, 'th')); + rows.forEach((row) => { + const li = document.createElement('li'); + if (row.id === myId) li.className = 'play-quiz-scoreboard-me'; + const spN = document.createElement('span'); + spN.className = 'play-quiz-scoreboard-name'; + spN.textContent = row.nick; + const spV = document.createElement('span'); + spV.className = 'play-quiz-scoreboard-val'; + spV.textContent = String(row.sc); + li.appendChild(spN); + li.appendChild(spV); + ul.appendChild(li); + }); + if (isQuizCarry() && useCyberPlayHud()) { + wrap.classList.add('is-hidden'); + } + } + + function initPlayLiveQuizScoresZeros() { + if ((!isQuiz() && !isQuizCarry() && !isQuizBattle()) || myId == null) return; + playLiveQuizScores = {}; + playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + renderPlayQuizScoreboard(playLiveQuizScores); + } + + function showPlayQuizFeedback(r) { + const el = document.getElementById('play-quiz-feedback'); + if (!el || !r || !r.results || myId == null) return; + let mine = null; + let botRight = 0, botWrong = 0; + for (let i = 0; i < r.results.length; i++) { + const row = r.results[i]; + if (row.id === myId) mine = row; + else if (isPreviewBotId(row.id)) { + if (row.right) botRight++; + else botWrong++; + } + } + if (!mine) return; + el.classList.remove('is-hidden'); + const botExtra = previewFillBots && (botRight + botWrong > 0) + ? ' · บอท ถูก ' + botRight + ' / ผิด ' + botWrong + ' (Bots: ' + botRight + ' right / ' + botWrong + ' wrong)' + : ''; + if (mine.right) { + el.className = 'play-quiz-feedback play-quiz-feedback-ok'; + el.textContent = 'คุณตอบถูก · คะแนนรวม ' + (typeof mine.score === 'number' ? mine.score : 0) + ' แต้ม' + botExtra; + } else { + el.className = 'play-quiz-feedback play-quiz-feedback-bad'; + el.textContent = (mine.choice == null + ? 'คุณไม่ได้ยืนในโซนตอบ — นับเป็นผิด' + : 'คุณตอบผิด — กลับจุดเกิด และเข้าโซนตอบไม่ได้อีก') + botExtra; + } + if (typeof window.__playQuizFeedbackT === 'number') clearTimeout(window.__playQuizFeedbackT); + window.__playQuizFeedbackT = setTimeout(() => { el.classList.add('is-hidden'); }, 4200); + } + + function getTileBoundsForOnesGrid(grid) { + if (!grid || !grid.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (let yy = 0; yy < grid.length; yy++) { + const row = grid[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (Number(row[xx]) === 1) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + } + } + } + if (minX === Infinity) return null; + return { minX, minY, maxX, maxY }; + } + + function getQuizQuestionAreaTileBounds(md) { + if (!md || md.gameType !== 'quiz') return null; + return getTileBoundsForOnesGrid(md.quizQuestionArea); + } + + /** quiz_carry: โซนทองพิเศษสำหรับข้อความคำถาม (ถ้าไม่วาด ใช้โซนกลางม่วงแทนใน syncPlayQuizMapPanel) */ + function getQuizCarryQuestionAreaTileBounds(md) { + if (!md || md.gameType !== 'quiz_carry') return null; + return getTileBoundsForOnesGrid(md.quizQuestionArea); + } + + function clearQuizMapPanelThemeInline(panel, textEl) { + if (!panel || !textEl) return; + panel.style.removeProperty('--qmap-bg'); + panel.style.removeProperty('--qmap-border'); + panel.style.removeProperty('--qmap-border-w'); + panel.style.removeProperty('--qmap-shadow'); + panel.style.removeProperty('background'); + panel.style.removeProperty('border'); + panel.style.removeProperty('border-color'); + panel.style.removeProperty('border-width'); + panel.style.removeProperty('border-style'); + panel.style.removeProperty('box-shadow'); + textEl.style.removeProperty('--qmap-text'); + textEl.style.removeProperty('--qmap-text-shadow'); + textEl.style.removeProperty('color'); + textEl.style.removeProperty('text-shadow'); + textEl.style.removeProperty('font-size'); + textEl.style.removeProperty('line-height'); + textEl.style.removeProperty('width'); + textEl.style.removeProperty('box-sizing'); + textEl.style.removeProperty('max-height'); + textEl.style.removeProperty('overflow'); + } + + function clearQuizMapQuestionCarryFeedback() { + if (typeof window.__quizMapCarryFeedbackT === 'number') { + clearTimeout(window.__quizMapCarryFeedbackT); + window.__quizMapCarryFeedbackT = null; + } + const icon = document.getElementById('quiz-map-q-feedback-icon'); + const scoreWrap = document.getElementById('quiz-map-q-feedback-score-wrap'); + const scoreEl = document.getElementById('quiz-map-q-feedback-score'); + if (icon) { + icon.classList.add('is-hidden'); + icon.removeAttribute('src'); + } + if (scoreEl) scoreEl.classList.remove('quiz-map-q-feedback-score--pop'); + if (scoreWrap) { + scoreWrap.classList.add('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'true'); + } + } + + /** ไอคอนถูก/ผิดที่ขอบบนแผง + score+10 ตรงกลางใต้ข้อความ (เฉพาะถูก) — ตาม mock quiz_carry */ + function showQuizMapQuestionCarryFeedback(isCorrect) { + if (!isQuizCarry()) return; + const panel = document.getElementById('quiz-map-question-panel'); + const icon = document.getElementById('quiz-map-q-feedback-icon'); + const scoreWrap = document.getElementById('quiz-map-q-feedback-score-wrap'); + const scoreEl = document.getElementById('quiz-map-q-feedback-score'); + if (!panel || !icon || panel.classList.contains('is-hidden')) return; + clearQuizMapQuestionCarryFeedback(); + icon.src = isCorrect ? QUIZ_CARRY_MAP_FEEDBACK_IMG.correct : QUIZ_CARRY_MAP_FEEDBACK_IMG.incorrect; + icon.classList.remove('is-hidden'); + if (isCorrect && scoreWrap && scoreEl) { + scoreEl.src = QUIZ_CARRY_MAP_FEEDBACK_IMG.scorePlus; + scoreWrap.classList.remove('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'false'); + scoreEl.classList.remove('quiz-map-q-feedback-score--pop'); + void scoreEl.offsetWidth; + scoreEl.classList.add('quiz-map-q-feedback-score--pop'); + } else if (scoreWrap) { + scoreWrap.classList.add('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'true'); + } + window.__quizMapCarryFeedbackT = setTimeout(() => { + window.__quizMapCarryFeedbackT = null; + clearQuizMapQuestionCarryFeedback(); + }, QUIZ_MAP_CARRY_FEEDBACK_MS); + } + + /** ค่าเริ่มต้นแผงคำถามบนแผนที่ (ตรงกับ server defaultCarryMapPanelTheme) — ใช้เมื่อยังไม่โหลดจาก API */ + function defaultCarryMapPanelThemePlay() { + return { + panelBg: 'rgba(12, 14, 28, 0.88)', + panelBorder: 'rgba(255, 214, 102, 0.7)', + borderWidthPx: 2, + textColor: '#f1f5f9', + questionFontMinPx: 10, + questionFontMaxPx: 24, + }; + } + + /** ตรงกับ drawQuizCarryChoiceLabels — carryChoicePlaqueMapScale */ + function quizCarryPlaqueMapScaleClampedPlay() { + const sc = Number(quizCarryPlaqueMapScale); + if (!Number.isFinite(sc)) return 1.25; + return Math.max(0.85, Math.min(2.5, sc)); + } + + /** + * ขนาดตัวอักษรแผงคำถาม DOM ให้เท่าป้ายคำตอบบนพื้น: Math.max(10, Math.min(24, tileSize*zoom*0.24*ps)) + * แล้ว clamp ด้วย questionFontMinPx / questionFontMaxPx จากธีม (ถ้าต้องการใหญ่กว่าป้ายได้โดยยกเพดาน max) + */ + function quizMapQuestionFontPxLikeCarryPlaques(th, zoomVal) { + const ts = tileSize * zoomVal; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + const base = Math.max(10, Math.min(24, ts * 0.24 * ps)); + const fb = defaultCarryMapPanelThemePlay(); + let mn = Number(th && th.questionFontMinPx); + let mx = Number(th && th.questionFontMaxPx); + if (!Number.isFinite(mn)) mn = fb.questionFontMinPx; + if (!Number.isFinite(mx)) mx = fb.questionFontMaxPx; + mn = Math.round(Math.max(8, Math.min(40, mn))); + mx = Math.round(Math.max(10, Math.min(72, mx))); + if (mx < mn) { + const s = mn; + mn = mx; + mx = s; + } + return Math.max(mn, Math.min(mx, base)); + } + + /** + * ลด px ทีละ 1 จนเนื้อหาไม่ล้นกล่อง (ไม่ใช้ scrollbar) + * zoom out กล่องเตี้ยมาก: อนุญาตต่ำกว่า questionFontMinPx ของธีมได้ถึง hardMin (≥6) ไม่ให้บรรทัดถูกตัด + * แผงใช้ justify-content:flex-start ใน CSS — ไม่จัดกึ่งกลางแนวตั้งแล้วโดน overflow ตัดบน-ล่าง + */ + function fitQuizMapQuestionFontToPanel(panel, textEl, startPx, minPx) { + const cs = window.getComputedStyle(panel); + const padT = parseFloat(cs.paddingTop) || 0; + const padB = parseFloat(cs.paddingBottom) || 0; + const padL = parseFloat(cs.paddingLeft) || 0; + const padR = parseFloat(cs.paddingRight) || 0; + const availH = Math.max(12, panel.clientHeight - padT - padB); + const availW = Math.max(24, panel.clientWidth - padL - padR); + const themeMin = Math.max(8, Math.floor(Number(minPx) || 8)); + const hardMin = Math.min(themeMin, Math.max(6, Math.floor(availH / 7))); + let px = Math.round(Math.min(80, Math.max(hardMin, Number(startPx) || hardMin))); + textEl.style.setProperty('line-height', '1.28', 'important'); + textEl.style.setProperty('width', '100%', 'important'); + textEl.style.setProperty('box-sizing', 'border-box', 'important'); + textEl.style.removeProperty('max-height'); + textEl.style.setProperty('overflow', 'hidden', 'important'); + for (let i = 0; i < 96 && px > hardMin; i++) { + textEl.style.setProperty('font-size', String(px) + 'px', 'important'); + const sh = textEl.scrollHeight; + const sw = textEl.scrollWidth; + if (sh <= availH + 2 && sw <= availW + 2) break; + px -= 1; + } + textEl.style.setProperty('font-size', String(px) + 'px', 'important'); + textEl.style.setProperty('max-height', availH + 'px', 'important'); + } + + /** ธีมจาก quiz-settings / join API — ถ้า JSON ไม่มีฟอนต์ให้ใช้ค่าเริ่มต้น */ + function parseCarryMapPanelThemeObject(raw) { + let t = raw; + if (typeof t === 'string') { + try { + t = JSON.parse(t); + } catch (e) { + t = null; + } + } + if (!t || typeof t !== 'object') return null; + const bw = Number(t.borderWidthPx); + const borderWidthPx = Number.isFinite(bw) ? Math.max(0, Math.min(12, Math.round(bw))) : 2; + const fb = defaultCarryMapPanelThemePlay(); + let qMin = Number(t.questionFontMinPx); + let qMax = Number(t.questionFontMaxPx); + if (!Number.isFinite(qMin)) qMin = fb.questionFontMinPx; + if (!Number.isFinite(qMax)) qMax = fb.questionFontMaxPx; + qMin = Math.round(Math.max(10, Math.min(40, qMin))); + qMax = Math.round(Math.max(14, Math.min(56, qMax))); + if (qMax < qMin) { + const s = qMin; + qMin = qMax; + qMax = s; + } + return { + panelBg: String(t.panelBg || '').trim().slice(0, 120), + panelBorder: String(t.panelBorder || '').trim().slice(0, 120), + borderWidthPx, + textColor: String(t.textColor || '').trim().slice(0, 120), + questionFontMinPx: qMin, + questionFontMaxPx: qMax, + }; + } + + /** + * ชั้นทับจาก carryMapPanelTheme ในไฟล์แมป — ฟอนต์ใส่เฉพาะเมื่อ JSON มี key จริง + * (กันธีมแมปมีแค่สีแล้วไปทับขนาดฟอนต์จาก Admin เป็นค่าเริ่ม 16/24) + */ + function parseCarryMapPanelThemeMapOverlay(raw) { + let t = raw; + if (typeof t === 'string') { + try { + t = JSON.parse(t); + } catch (e) { + t = null; + } + } + if (!t || typeof t !== 'object') return null; + const bw = Number(t.borderWidthPx); + const borderWidthPx = Number.isFinite(bw) ? Math.max(0, Math.min(12, Math.round(bw))) : 2; + const out = { + panelBg: String(t.panelBg || '').trim().slice(0, 120), + panelBorder: String(t.panelBorder || '').trim().slice(0, 120), + borderWidthPx, + textColor: String(t.textColor || '').trim().slice(0, 120), + }; + const hasMin = Object.prototype.hasOwnProperty.call(t, 'questionFontMinPx'); + const hasMax = Object.prototype.hasOwnProperty.call(t, 'questionFontMaxPx'); + if (hasMin || hasMax) { + const fb = defaultCarryMapPanelThemePlay(); + let qMin = hasMin ? Number(t.questionFontMinPx) : fb.questionFontMinPx; + let qMax = hasMax ? Number(t.questionFontMaxPx) : fb.questionFontMaxPx; + if (!Number.isFinite(qMin)) qMin = fb.questionFontMinPx; + if (!Number.isFinite(qMax)) qMax = fb.questionFontMaxPx; + qMin = Math.round(Math.max(10, Math.min(40, qMin))); + qMax = Math.round(Math.max(14, Math.min(56, qMax))); + if (qMax < qMin) { + const s = qMin; + qMin = qMax; + qMax = s; + } + out.questionFontMinPx = qMin; + out.questionFontMaxPx = qMax; + } + return out; + } + + function setQuizCarryMapPanelThemeFromApi(s) { + quizCarryMapPanelTheme = null; + if (!s || typeof s !== 'object') return; + const parsed = parseCarryMapPanelThemeObject(s.carryMapPanelTheme); + if (!parsed) return; + quizCarryMapPanelTheme = parsed; + } + + function defaultCarryChoicePlaqueThemePlay() { + return { + borderMode: 'neon', + fixedBorder: 'rgba(122, 200, 255, 0.9)', + fillBg: 'rgba(12, 10, 20, 0.88)', + textColor: 'rgba(248, 249, 255, 1)', + borderWidthPx: 2.5, + plaqueImageUrl: '', + }; + } + + function parseCarryChoicePlaqueThemeObject(raw) { + const d = defaultCarryChoicePlaqueThemePlay(); + if (!raw || typeof raw !== 'object') return d; + const safeColor = (x, fb) => { + const t = String(x == null ? '' : x).trim().slice(0, 120); + if (!t || /url\s*\(|expression|@import|\/\*|javascript|<|>|\\0/i.test(t)) return fb; + if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(t)) return t; + if (/^rgba?\(\s*[^)]+\)$/i.test(t)) return t.replace(/\s+/g, ' ').trim(); + return fb; + }; + const mode = String(raw.borderMode || '').toLowerCase() === 'fixed' ? 'fixed' : 'neon'; + let bw = Number(raw.borderWidthPx); + if (!Number.isFinite(bw)) bw = d.borderWidthPx; + bw = Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10; + return { + borderMode: mode, + fixedBorder: safeColor(raw.fixedBorder, d.fixedBorder), + fillBg: safeColor(raw.fillBg, d.fillBg), + textColor: safeColor(raw.textColor, d.textColor), + borderWidthPx: bw, + plaqueImageUrl: sanitizeQuizCarryImageUrlClient(raw.plaqueImageUrl), + }; + } + + function buildCarryChoicePlaqueThemesArrayFromApi(s) { + if (!s || typeof s !== 'object') return null; + if (Array.isArray(s.carryChoicePlaqueThemes) && s.carryChoicePlaqueThemes.length) { + const out = []; + for (let i = 0; i < QUIZ_CARRY_MAX_OPTION_SLOTS; i++) { + out.push(parseCarryChoicePlaqueThemeObject(s.carryChoicePlaqueThemes[i])); + } + return out; + } + if (s.carryChoicePlaqueTheme && typeof s.carryChoicePlaqueTheme === 'object') { + const one = parseCarryChoicePlaqueThemeObject(s.carryChoicePlaqueTheme); + return Array.from({ length: QUIZ_CARRY_MAX_OPTION_SLOTS }, () => ({ ...one })); + } + return null; + } + + function getEffectiveCarryChoicePlaqueThemeForChoice(choiceIndex) { + const i = Math.max(0, Math.min(QUIZ_CARRY_MAX_OPTION_SLOTS - 1, choiceIndex | 0)); + if (quizCarryChoicePlaqueThemes && quizCarryChoicePlaqueThemes[i]) { + return quizCarryChoicePlaqueThemes[i]; + } + return defaultCarryChoicePlaqueThemePlay(); + } + + function setQuizCarryChoicePlaqueThemeFromApi(s) { + quizCarryChoicePlaqueThemes = buildCarryChoicePlaqueThemesArrayFromApi(s); + } + + function sanitizeQuizCarryImageUrlClient(u) { + const s = String(u == null ? '' : u).trim().slice(0, 512); + if (!s || /[\s<>'"`]/.test(s)) return ''; + if (s.startsWith('/')) { + if (!/^\/[\w\-./?#=&%+]+$/i.test(s)) return ''; + return s; + } + const low = s.toLowerCase(); + if (!low.startsWith('https://') && !low.startsWith('http://')) return ''; + try { + const parsed = new URL(s); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return ''; + return s; + } catch (e) { + return ''; + } + } + + /** crossOrigin=anonymous กับ same-origin บางเซิร์ฟ/nginx ทำให้โหลดรูปล้ม — ใช้เฉพาะข้ามโดเมน */ + function quizCarryApplyImageCrossOrigin(img, rawUrl) { + if (!img) return; + try { + img.removeAttribute('crossorigin'); + const u = sanitizeQuizCarryImageUrlClient(rawUrl); + if (!u || u.startsWith('data:')) return; + let imgOrigin = ''; + if (u.startsWith('http://') || u.startsWith('https://')) { + imgOrigin = new URL(u).origin; + } else if (u.startsWith('/')) { + imgOrigin = window.location.origin; + } + if (imgOrigin && imgOrigin !== window.location.origin) { + img.crossOrigin = 'anonymous'; + } + } catch (e) { /* ignore */ } + } + + function preloadQuizCarryChoiceImages(q) { + if (!q) return; + if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) { + for (let i = 0; i < q.choiceImageUrls.length; i++) { + const raw = sanitizeQuizCarryImageUrlClient(q.choiceImageUrls[i]); + if (!raw) continue; + if (quizCarryChoiceImageCache.has(raw)) continue; + const im = new Image(); + quizCarryApplyImageCrossOrigin(im, raw); + im.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(raw, im); + try { + im.src = raw; + } catch (e) { /* ignore */ } + } + } + if (Array.isArray(q.choices)) { + for (let j = 0; j < q.choices.length; j++) { + const raw2 = getQuizCarryPlaqueImageUrlForIndex(q, j); + if (!raw2 || quizCarryChoiceImageCache.has(raw2)) continue; + const im2 = new Image(); + quizCarryApplyImageCrossOrigin(im2, raw2); + im2.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(raw2, im2); + try { + im2.src = raw2; + } catch (e) { /* ignore */ } + } + } + } + + function getQuizCarryChoiceImageCached(url) { + const u = sanitizeQuizCarryImageUrlClient(url); + if (!u) return null; + const im = quizCarryChoiceImageCache.get(u); + if (im && im.complete && im.naturalWidth > 0) return im; + return null; + } + + function getQuizCarryChoiceImageUrlForIndex(q, idx) { + if (!q || !Array.isArray(q.choiceImageUrls)) return ''; + const u = q.choiceImageUrls[idx]; + return sanitizeQuizCarryImageUrlClient(u); + } + + /** รูปป้าย: คำถามต่อช่องก่อน แล้วค่อยรูปจากธีมช่อง (carryChoicePlaqueThemes) */ + function getQuizCarryPlaqueImageUrlForIndex(q, idx) { + const perQ = getQuizCarryChoiceImageUrlForIndex(q, idx); + if (perQ) return perQ; + const th = getEffectiveCarryChoicePlaqueThemeForChoice(idx); + const u = th && th.plaqueImageUrl ? sanitizeQuizCarryImageUrlClient(th.plaqueImageUrl) : ''; + return u || ''; + } + + function defaultCarryEmbedCountdownThemePlay() { + return { + overlayBackdrop: 'rgba(8, 10, 20, 0.42)', + innerBg: 'rgba(12, 14, 28, 0.82)', + innerBorder: 'rgba(122, 162, 247, 0.45)', + innerBorderWpx: 1, + innerRadiusPx: 12, + digitColor: '#ffe066', + mapDigitCqmin: 78, + mapDigitCqh: 82, + mapDigitMaxPx: 200, + screenDigitVw: 28, + screenDigitMaxPx: 132, + }; + } + + function parseCarryEmbedCountdownThemeObject(raw) { + const d = defaultCarryEmbedCountdownThemePlay(); + if (!raw || typeof raw !== 'object') return d; + const clampN = (v, lo, hi, def) => { + const n = Number(v); + if (!Number.isFinite(n)) return def; + return Math.max(lo, Math.min(hi, Math.round(n))); + }; + const clipStr = (x, maxLen) => { + const t = String(x == null ? '' : x).trim().slice(0, maxLen); + return t || null; + }; + const safeColor = (x, fb) => { + const t = clipStr(x, 120); + if (!t || /url\s*\(|expression|@import|\/\*|javascript|<|>|\\0/i.test(t)) return fb; + if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(t)) return t; + if (/^rgba?\(\s*[^)]+\)$/i.test(t)) return t.replace(/\s+/g, ' ').trim(); + return fb; + }; + return { + overlayBackdrop: safeColor(raw.overlayBackdrop, d.overlayBackdrop), + innerBg: safeColor(raw.innerBg, d.innerBg), + innerBorder: safeColor(raw.innerBorder, d.innerBorder), + innerBorderWpx: clampN(raw.innerBorderWpx, 0, 12, d.innerBorderWpx), + innerRadiusPx: clampN(raw.innerRadiusPx, 0, 32, d.innerRadiusPx), + digitColor: safeColor(raw.digitColor, d.digitColor), + mapDigitCqmin: clampN(raw.mapDigitCqmin, 35, 100, d.mapDigitCqmin), + mapDigitCqh: clampN(raw.mapDigitCqh, 35, 100, d.mapDigitCqh), + mapDigitMaxPx: clampN(raw.mapDigitMaxPx, 48, 400, d.mapDigitMaxPx), + screenDigitVw: clampN(raw.screenDigitVw, 6, 44, d.screenDigitVw), + screenDigitMaxPx: clampN(raw.screenDigitMaxPx, 48, 220, d.screenDigitMaxPx), + }; + } + + function setQuizCarryEmbedCountdownThemeFromApi(s) { + quizCarryEmbedCountdownTheme = parseCarryEmbedCountdownThemeObject(s && s.carryEmbedCountdownTheme); + applyQuizCarryEmbedCountdownThemeToDom(); + } + + const CARRY_ECD_VAR_KEYS = [ + '--carry-ecd-overlay', + '--carry-ecd-inner-bg', + '--carry-ecd-inner-border', + '--carry-ecd-inner-border-w', + '--carry-ecd-inner-radius', + '--carry-ecd-digit-color', + '--carry-ecd-map-digit-fs', + '--carry-ecd-screen-vw', + '--carry-ecd-screen-max', + ]; + + function clearQuizCarryEmbedCountdownThemeDomVars() { + const root = document.getElementById('quiz-carry-embed-countdown'); + if (!root) return; + CARRY_ECD_VAR_KEYS.forEach((k) => { try { root.style.removeProperty(k); } catch (e) { /* ignore */ } }); + } + + /** ใส่ CSS variables บน #quiz-carry-embed-countdown — อ่านจาก quizCarryEmbedCountdownTheme */ + function applyQuizCarryEmbedCountdownThemeToDom() { + const root = document.getElementById('quiz-carry-embed-countdown'); + const numEl = document.getElementById('quiz-carry-embed-countdown-num'); + if (!root) return; + clearQuizCarryEmbedCountdownThemeDomVars(); + const th = quizCarryEmbedCountdownTheme || defaultCarryEmbedCountdownThemePlay(); + root.style.setProperty('--carry-ecd-overlay', th.overlayBackdrop); + root.style.setProperty('--carry-ecd-inner-bg', th.innerBg); + root.style.setProperty('--carry-ecd-inner-border', th.innerBorder); + root.style.setProperty('--carry-ecd-inner-border-w', `${Math.max(0, Math.min(12, Math.round(Number(th.innerBorderWpx) || 1)))}px`); + root.style.setProperty('--carry-ecd-inner-radius', `${Math.max(0, Math.min(32, Math.round(Number(th.innerRadiusPx) || 12)))}px`); + root.style.setProperty('--carry-ecd-digit-color', th.digitColor); + const cqmin = Math.max(35, Math.min(100, Math.round(Number(th.mapDigitCqmin) || 78))); + const cqh = Math.max(35, Math.min(100, Math.round(Number(th.mapDigitCqh) || 82))); + const mpx = Math.max(48, Math.min(400, Math.round(Number(th.mapDigitMaxPx) || 200))); + root.style.setProperty('--carry-ecd-map-digit-fs', `clamp(14px, min(${cqmin}cqmin, ${cqh}cqh), ${mpx}px)`); + const vw = Math.max(6, Math.min(44, Math.round(Number(th.screenDigitVw) || 28))); + const smx = Math.max(48, Math.min(220, Math.round(Number(th.screenDigitMaxPx) || 132))); + root.style.setProperty('--carry-ecd-screen-vw', `${vw}vw`); + root.style.setProperty('--carry-ecd-screen-max', `${smx}px`); + if (numEl) { + numEl.style.textShadow = '0 0 0.45em currentColor, 0 4px 14px rgba(0,0,0,0.55)'; + } + } + + /** default ← quiz-settings/API ← แมป (สีจากแมปทับได้; ฟอนต์จากแมปเฉพาะเมื่อแมประบุ key) */ + function getEffectiveCarryMapPanelThemeForApply() { + const d = defaultCarryMapPanelThemePlay(); + const api = quizCarryMapPanelTheme != null && typeof quizCarryMapPanelTheme === 'object' ? quizCarryMapPanelTheme : null; + let merged = api ? { ...d, ...api } : { ...d }; + if (mapData && mapData.carryMapPanelTheme != null) { + const mp = parseCarryMapPanelThemeMapOverlay(mapData.carryMapPanelTheme); + if (mp) merged = { ...merged, ...mp }; + } + return merged; + } + + function applyQuizCarryMapPanelThemeIfNeeded(panel, textEl) { + if (!panel || !textEl) return; + if (!isQuizCarry()) { + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + const th = getEffectiveCarryMapPanelThemeForApply(); + clearQuizMapPanelThemeInline(panel, textEl); + const bgStr = th.panelBg != null ? String(th.panelBg).trim() : ''; + const brStr = th.panelBorder != null ? String(th.panelBorder).trim() : ''; + const txStr = th.textColor != null ? String(th.textColor).trim() : ''; + const wRaw = Number(th.borderWidthPx); + const w = Number.isFinite(wRaw) ? Math.max(0, Math.min(12, Math.round(wRaw))) : 0; + const bgUse = bgStr || 'transparent'; + const brUse = brStr || 'transparent'; + const txUse = txStr || '#f1f5f9'; + panel.style.setProperty('--qmap-bg', bgUse); + panel.style.setProperty('--qmap-border', brUse); + panel.style.setProperty('--qmap-border-w', String(w) + 'px'); + panel.style.setProperty('--qmap-shadow', 'none'); + textEl.style.setProperty('--qmap-text', txUse); + textEl.style.setProperty('--qmap-text-shadow', 'none'); + /* โหลด play.html แบบแคชเก่า (ไม่มี var) ยังต้องทับได้ — ใช้ !important คู่กับตัวแปร */ + panel.style.setProperty('background', bgUse, 'important'); + panel.style.setProperty('border', String(w) + 'px solid ' + brUse, 'important'); + panel.style.setProperty('box-shadow', 'none', 'important'); + textEl.style.setProperty('color', txUse, 'important'); + textEl.style.setProperty('text-shadow', 'none', 'important'); + } + + function syncPlayQuizMapPanel() { + const panel = document.getElementById('quiz-map-question-panel'); + const textEl = document.getElementById('quiz-map-question-text'); + if (!panel || !textEl || !mapData) return; + /* เอดิเตอร์ embed: ซ่อนแผงทองเฉพาะโหมด quiz (ทับกลาง) — quiz_carry ต้องโชว์ตามโซนที่วาดบนแผนที่ */ + if (previewMode && editorEmbedReturn && !isQuizCarry()) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + if (isQuizBattle()) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + if (!isQuiz() && !isQuizCarry()) { + clearQuizMapQuestionCarryFeedback(); + return; + } + const bounds = isQuiz() + ? getQuizQuestionAreaTileBounds(mapData) + : (getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData)); + const text = (playQuizText || '').trim(); + if (!bounds || !text) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + 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; + textEl.textContent = text; + panel.style.left = Math.round(left) + 'px'; + panel.style.top = Math.round(top) + 'px'; + panel.style.width = Math.round(Math.max(48, wPx)) + 'px'; + panel.style.height = Math.round(Math.max(40, hPx)) + 'px'; + panel.classList.remove('is-hidden'); + panel.setAttribute('aria-hidden', 'false'); + applyQuizCarryMapPanelThemeIfNeeded(panel, textEl); + const thFont = getEffectiveCarryMapPanelThemeForApply(); + const startFontPx = quizMapQuestionFontPxLikeCarryPlaques(thFont, zoom); + const mnFont = Math.max(8, Math.round(Number(thFont.questionFontMinPx) || 10)); + fitQuizMapQuestionFontToPanel(panel, textEl, startFontPx, mnFont); + } + + /** โซนเดียวกับแผงคำถามบนแผนที่ — ยึด timeup ให้ตรงกล่อง #quiz-map-question-panel (ห้ามคำนวณซ้ำแล้วคลาด) */ + function syncQuizCarryTimeupDeskLayerPosition() { + const layer = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!layer || layer.classList.contains('is-hidden')) return; + const anchor = layer.querySelector('.qc-timeup-desk-anchor') + || layer.querySelector('.qc-timeup-desk-inner') + || (() => { + const img = document.getElementById('quiz-carry-timeup-txt-img'); + return img && img.parentElement && img.parentElement !== layer ? img.parentElement : null; + })(); + if (!anchor || !canvas) return; + const clearToFullStack = () => { + anchor.style.left = '0'; + anchor.style.top = '0'; + anchor.style.width = '100%'; + anchor.style.height = '100%'; + }; + const stack = document.getElementById('play-canvas-stack'); + const panel = document.getElementById('quiz-map-question-panel'); + if (stack && panel && !panel.classList.contains('is-hidden')) { + try { + const sr = stack.getBoundingClientRect(); + const pr = panel.getBoundingClientRect(); + const rw = pr.width; + const rh = pr.height; + if (rw >= 8 && rh >= 8 && sr.width > 0 && sr.height > 0) { + anchor.style.left = Math.round(pr.left - sr.left) + 'px'; + anchor.style.top = Math.round(pr.top - sr.top) + 'px'; + anchor.style.width = Math.round(rw) + 'px'; + anchor.style.height = Math.round(rh) + 'px'; + return; + } + } catch (e) { /* fallback ด้านล่าง */ } + } + const pl = panel && panel.style && String(panel.style.left || '').trim(); + const pt = panel && panel.style && String(panel.style.top || '').trim(); + const pw = panel && panel.style && String(panel.style.width || '').trim(); + const ph = panel && panel.style && String(panel.style.height || '').trim(); + if (pl && pt && pw && ph) { + anchor.style.left = pl; + anchor.style.top = pt; + anchor.style.width = pw; + anchor.style.height = ph; + return; + } + if (!mapData || mapData.gameType !== 'quiz_carry') { + clearToFullStack(); + return; + } + const bounds = getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData); + if (!bounds) { + clearToFullStack(); + return; + } + 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; + anchor.style.left = Math.round(left) + 'px'; + anchor.style.top = Math.round(top) + 'px'; + anchor.style.width = Math.round(Math.max(48, wPx)) + 'px'; + anchor.style.height = Math.round(Math.max(40, hPx)) + 'px'; + } + + function carryEmbedCountdownAnchorResolved() { + if (!mapData || !isQuizCarry()) return 'screen'; + const v = mapData.carryEmbedCountdownAnchor; + if (v === 'grid' || v === 'map') return v; + return 'screen'; + } + + function resetQuizCarryEmbedCountdownLayoutDom() { + const ov = document.getElementById('quiz-carry-embed-countdown'); + const inner = document.getElementById('quiz-carry-embed-countdown-inner'); + if (ov) ov.classList.remove('quiz-carry-embed-countdown--map-anchor'); + if (inner) { + inner.classList.remove('quiz-carry-embed-countdown-inner--map-rect'); + inner.style.position = ''; + inner.style.left = ''; + inner.style.top = ''; + inner.style.width = ''; + inner.style.height = ''; + inner.style.transform = ''; + inner.style.maxWidth = ''; + inner.style.boxSizing = ''; + inner.style.display = ''; + inner.style.margin = ''; + } + } + + /** พรีวิว embed: วางกล่อง 3–2–1 กลางจอ / ชิดโซนทองหรือโซนกลาง / ชิดกริด carryEmbedCountdownArea — ตาม carryEmbedCountdownAnchor */ + function syncQuizCarryEmbedCountdownLayout() { + if (!previewMode || !editorEmbedReturn || !mapData || !isQuizCarry()) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + const ov = document.getElementById('quiz-carry-embed-countdown'); + const inner = document.getElementById('quiz-carry-embed-countdown-inner'); + const canvasEl = document.getElementById('game-canvas'); + if (!ov || !inner || !canvasEl) return; + if (ov.classList.contains('is-hidden')) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + const anchor = carryEmbedCountdownAnchorResolved(); + if (anchor === 'screen') { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + let bounds = null; + if (anchor === 'grid') { + bounds = getTileBoundsForOnesGrid(mapData.carryEmbedCountdownArea); + if (!bounds) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + } else { + bounds = getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData); + if (!bounds) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + } + 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 r = canvasEl.getBoundingClientRect(); + const sl = r.left + l; + const st = r.top + t; + ov.classList.add('quiz-carry-embed-countdown--map-anchor'); + inner.classList.add('quiz-carry-embed-countdown-inner--map-rect'); + inner.style.position = 'fixed'; + inner.style.left = Math.round(sl) + 'px'; + inner.style.top = Math.round(st) + 'px'; + inner.style.width = Math.round(Math.max(100, wPx)) + 'px'; + inner.style.height = Math.round(Math.max(72, hPx)) + 'px'; + inner.style.transform = 'none'; + inner.style.boxSizing = 'border-box'; + inner.style.display = 'flex'; + inner.style.flexDirection = 'column'; + inner.style.alignItems = 'center'; + inner.style.justifyContent = 'center'; + inner.style.margin = '0'; + inner.style.maxWidth = 'none'; + } + + /** quiz_carry + embed: คำถามไปที่แผงทองบนแผนที่ / ใน overlay นับถอยหลัง — ไม่ใช้แถบบน */ + function syncQuizCarryEmbedQuestionStrip() { + const wrap = document.getElementById('quiz-carry-embed-q-strip'); + const p = document.getElementById('quiz-carry-embed-q-strip-text'); + if (!wrap || !p) return; + wrap.classList.remove('quiz-carry-embed-q-strip--countdown'); + wrap.classList.add('is-hidden'); + wrap.setAttribute('aria-hidden', 'true'); + } + + function updatePlayQuizTimerDisplay() { + const el = document.getElementById('quiz-game-timer'); + if (!el) return; + if (!playQuizPhaseEndsAt) { + el.textContent = ''; + return; + } + const s = Math.max(0, Math.ceil((playQuizPhaseEndsAt - Date.now()) / 1000)); + el.textContent = s + ' วินาที'; + } + + function clampPreviewMs(n, def, minV, maxV) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(minV, Math.min(maxV, Math.floor(v))); + } + + function clampCarrySessionLen(n, def) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(0, Math.min(500, Math.floor(v))); + } + + function buildQuizPoolFromMap(md) { + if (!md || !Array.isArray(md.quizQuestions)) return []; + return md.quizQuestions + .filter((q) => q && String(q.text || '').trim()) + .map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue })); + } + + function pickRandomQuizFromPool(pool) { + if (!pool || !pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function clearPreviewBotAnswerPaths() { + if (!previewFillBots) return; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botPath = []; + o.botAnswerWander = false; + }); + } + + function resetPreviewBotsQuizState() { + if (!previewFillBots) return; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.quizCannotTrue = false; + o.quizCannotFalse = false; + o.botPath = []; + o.botAnswerWander = false; + }); + } + + function applyPreviewReadPhase(q, poolLen) { + clearPreviewBotAnswerPaths(); + previewQuizStep = 'read'; + playQuizPhaseLocal = 'read'; + previewQuizCurrent = q; + playQuizText = q.text; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.readMs; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] อ่านคำถาม · สุ่มจากชุด ' + poolLen + ' ข้อ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'โซนทองบนแผนที่ = ข้อความคำถาม · ฟ้า = จริง · ชมพู = เท็จ'; + } + + function applyPreviewAnswerPhase() { + previewQuizStep = 'answer'; + playQuizPhaseLocal = 'answer'; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.answerMs; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] เดินไปโซน ถูก / ผิด'; + const leg = document.getElementById('quiz-play-legend'); + if (leg && previewQuizCurrent) { + leg.textContent = previewQuizCurrent.answerTrue + ? 'เฉลย (ทดสอบ): คำตอบที่ถูกคือ «จริง» — โซนสีฟ้า' + : 'เฉลย (ทดสอบ): คำตอบที่ถูกคือ «เท็จ» — โซนสีชมพู'; + } + previewBotsPrepareAnswerRound(); + } + + function applyPreviewBetweenPhase() { + clearPreviewBotAnswerPaths(); + previewQuizStep = 'between'; + playQuizPhaseLocal = null; + playQuizText = 'ข้อถัดไปกำลังจะมา…'; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.betweenMs; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] พักระหว่างข้อ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'รอสักครู่แล้วจะสุ่มคำถามใหม่จากชุดเดิม'; + } + + function quizCellOnPlay(grid, x, y) { + return !!(grid && grid[y] && grid[y][x] === 1); + } + + function spawnTileWalkablePlay(md, x, y) { + const w = md.width || 20; + const h = md.height || 15; + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + if (row && row[x] === 1) return false; + return true; + } + + /** เหมือน server pickRandomSpawnFromMap — สุ่มด้วย crypto.getRandomValues */ + function pickRandomSpawnFromMapPlay(md) { + const fallback = md.spawn || { x: 1, y: 1 }; + const fx = Number.isFinite(Number(fallback.x)) ? Number(fallback.x) : 1; + const fy = Number.isFinite(Number(fallback.y)) ? Number(fallback.y) : 1; + const grid = md.spawnArea; + if (!grid || !Array.isArray(grid)) return { x: fx, y: fy }; + const w = md.width || 20; + const h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && spawnTileWalkablePlay(md, x, y)) pool.push({ x, y }); + } + } + if (!pool.length) return { x: fx, y: fy }; + const u = new Uint32Array(1); + (typeof crypto !== 'undefined' && crypto.getRandomValues) + ? crypto.getRandomValues(u) + : (u[0] = (Math.floor(Math.random() * 0xffffffff) >>> 0)); + const pick = pool[u[0] % pool.length]; + return { x: pick.x, y: pick.y }; + } + + function parseLobbyPlayerSpawnsFromMapPlay(md) { + const w = md.width || 20; + const h = md.height || 15; + const out = [null, null, null, null, null, null]; + const raw = md && md.lobbyPlayerSpawns; + if (!Array.isArray(raw)) return out; + for (let i = 0; i < 6 && i < raw.length; i++) { + const cell = raw[i]; + if (!cell || typeof cell !== 'object') continue; + const x = Math.floor(Number(cell.x)); + const y = Math.floor(Number(cell.y)); + if (!Number.isFinite(x) || !Number.isFinite(y)) continue; + if (x < 0 || x >= w || y < 0 || y >= h) continue; + out[i] = { x, y }; + } + return out; + } + + /** สอดคล้องกับ server pickSpawnForJoin — ใช้พรีวิวบอท / ทดสอบ */ + function pickSpawnForJoinPlay(md, joinOrderIndex) { + if (!md) return { x: 1, y: 1 }; + const mode = md.lobbySpawnMode; + const ord = joinOrderIndex | 0; + if (mode === 'slots6' && ord >= 6) return pickRandomSpawnFromMapPlay(md); + const j = Math.min(Math.max(0, ord), 5); + if (mode === 'fixed' && md.spawn) { + const fx = Number.isFinite(Number(md.spawn.x)) ? Math.floor(Number(md.spawn.x)) : 1; + const fy = Number.isFinite(Number(md.spawn.y)) ? Math.floor(Number(md.spawn.y)) : 1; + const w = md.width || 20; + const h = md.height || 15; + const x = Math.max(0, Math.min(w - 1, fx)); + const y = Math.max(0, Math.min(h - 1, fy)); + if (spawnTileWalkablePlay(md, x, y)) return { x, y }; + return pickRandomSpawnFromMapPlay(md); + } + if (mode === 'slots6') { + const slots = parseLobbyPlayerSpawnsFromMapPlay(md); + const pick = slots[j]; + if (pick && spawnTileWalkablePlay(md, pick.x, pick.y)) return { x: pick.x, y: pick.y }; + return pickRandomSpawnFromMapPlay(md); + } + return pickRandomSpawnFromMapPlay(md); + } + + const GAUNTLET_PREVIEW_MAX = 6; + function gauntletSpawnYsPlay(md, playerCount) { + const h = md.height || 15; + const lo = 1; + const hi = Math.max(lo, h - 2); + const n = Math.min(GAUNTLET_PREVIEW_MAX, Math.max(1, playerCount)); + if (hi <= lo) return Array.from({ length: n }, () => lo); + const ys = []; + for (let i = 0; i < n; i++) { + ys.push(Math.round(lo + (i * (hi - lo)) / Math.max(1, n - 1))); + } + return ys; + } + function collectGauntletSpawnSlotsFromSpawnAreaPlay(md) { + const grid = md.spawnArea; + if (!grid || !Array.isArray(grid)) return []; + const w = md.width || 20; + const h = md.height || 15; + const slots = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && spawnTileWalkablePlay(md, x, y)) slots.push({ x, y }); + } + } + slots.sort((a, b) => a.y - b.y || a.x - b.x); + return slots; + } + function collectGauntletSpawnSlotsPlay(md) { + const explicit = md.gauntletPlayerSpawns; + if (Array.isArray(explicit) && explicit.length > 0) { + const w = md.width || 20; + const h = md.height || 15; + const slots = []; + for (const raw of explicit) { + if (slots.length >= GAUNTLET_PREVIEW_MAX) break; + const x = Math.floor(Number(raw && raw.x)); + const y = Math.floor(Number(raw && raw.y)); + if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x >= w || y < 0 || y >= h) continue; + if (!spawnTileWalkablePlay(md, x, y)) continue; + slots.push({ x, y }); + } + if (slots.length > 0) return slots; + } + return collectGauntletSpawnSlotsFromSpawnAreaPlay(md); + } + function gauntletResolveSpawnXForRowPlay(md, spawnColX, y, slotXFallback) { + const w = md.width || 20; + if (spawnTileWalkablePlay(md, spawnColX, y)) return spawnColX; + if (slotXFallback != null && spawnTileWalkablePlay(md, slotXFallback, y)) return slotXFallback; + for (let d = 0; d < w; d++) { + if (spawnColX + d < w && spawnTileWalkablePlay(md, spawnColX + d, y)) return spawnColX + d; + if (spawnColX - d >= 0 && spawnTileWalkablePlay(md, spawnColX - d, y)) return spawnColX - d; + } + return Math.max(0, Math.min(w - 1, spawnColX)); + } + function gauntletSpawnPositionsPlay(md, playerCount) { + const n = Math.min(GAUNTLET_PREVIEW_MAX, Math.max(1, playerCount)); + const slots = collectGauntletSpawnSlotsPlay(md); + const fallbackYs = gauntletSpawnYsPlay(md, n); + const spawnColX = slots.length ? Math.min(...slots.map((s) => s.x)) : 1; + const out = []; + for (let i = 0; i < n; i++) { + const y = i < slots.length + ? slots[i].y + : (fallbackYs[i] != null ? fallbackYs[i] : fallbackYs[fallbackYs.length - 1]); + const slotX = i < slots.length ? slots[i].x : null; + const x = gauntletResolveSpawnXForRowPlay(md, spawnColX, y, slotX); + out.push({ x, y }); + } + return out; + } + /** + * โหมดทดสอบพรมแดง: จัดตำแหน่งเกิดให้ตรง server (spawnArea หรือกระจาย y) + * @param {boolean} [onlyBots=false] ถ้า true = จัดแค่บอท (หลัง game-start; ไม่ทับ me/คนจริงจาก peersSnap) + */ + function applyGauntletPreviewSpawnLayout(onlyBots) { + if (!mapData || mapData.gameType !== 'gauntlet') return; + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + const humanCount = 1 + realIds.length; + const total = Math.min(GAUNTLET_PREVIEW_MAX, humanCount + botIds.length); + const pos = gauntletSpawnPositionsPlay(mapData, total); + if (onlyBots) { + let idx = humanCount; + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + return; + } + let idx = 0; + if (idx < pos.length) { + me.x = pos[idx].x; + me.y = pos[idx].y; + me.tx = me.x; + me.ty = me.y; + idx++; + } + realIds.forEach((rid) => { + const o = others.get(rid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + } + + function isPreviewBotId(id) { + return typeof id === 'string' && id.indexOf(PREVIEW_BOT_PREFIX) === 0; + } + + function countPlayHumans() { + let n = 1; + others.forEach((_, id) => { if (!isPreviewBotId(id)) n++; }); + return n; + } + + function rebalancePreviewBots() { + if (!playBotsEnabled() || !mapData) return; + if (isDetectiveMinigamePlay() && isQuizCarry()) { + /* บอทคดีสืบสวน — ใช้ stepQuizCarryPreviewBots */ + } + const human = countPlayHumans(); + const wantBots = Math.max(0, playBotTargetHeadcount() - human); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o) return; + if (o.botTier == null) { + const tierRoll = Math.random(); + o.botTier = tierRoll < 0.28 ? 'sharp' : tierRoll < 0.62 ? 'avg' : 'weak'; + } + if (o.quizCannotTrue == null) o.quizCannotTrue = false; + if (o.quizCannotFalse == null) o.quizCannotFalse = false; + if (!Array.isArray(o.botPath)) o.botPath = []; + if (o.botAnswerWander == null) o.botAnswerWander = false; + if (o.gauntletJumpTicks == null) o.gauntletJumpTicks = 0; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks; + if (o.gauntletScore == null) { + o.gauntletScore = (mapData && mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) ? 100 : 0; + } + if (o.gauntletEliminated == null) o.gauntletEliminated = false; + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)]; + o.botWanderDx = wd[0]; + o.botWanderDy = wd[1]; + } + if (typeof o.botWanderNextTurn !== 'number') { + o.botWanderNextTurn = Date.now() + 400 + Math.floor(Math.random() * 900); + } + }); + let botIds = [...others.keys()].filter(isPreviewBotId); + while (botIds.length > wantBots) { + const drop = botIds.pop(); + if (drop) others.delete(drop); + } + botIds = [...others.keys()].filter(isPreviewBotId); + while (botIds.length < wantBots) { + const id = PREVIEW_BOT_PREFIX + (++previewBotSeq); + const joinIdx = countPlayHumans() + botIds.length; + const sp = pickSpawnForJoinPlay(mapData, joinIdx); + const jx = (Math.random() - 0.5) * 0.4; + const jy = (Math.random() - 0.5) * 0.4; + let x = sp.x + 0.5 + jx; + let y = sp.y + 0.5 + jy; + if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(x, y); + x = pathSnap.x; + y = pathSnap.y; + } + const tierRoll = Math.random(); + const botTier = tierRoll < 0.28 ? 'sharp' : tierRoll < 0.62 ? 'avg' : 'weak'; + const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)]; + const crownStart = mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay(); + others.set(id, { + x, y, tx: x, ty: y, + direction: ['down', 'up', 'left', 'right'][Math.floor(Math.random() * 4)], + nickname: 'บอท', + characterId: getPlayCharacterId(), + playTint: playTintFromPeerId(id), + botTier, + gauntletJumpTicks: 0, + gauntletJumpVis: 0, + gauntletScore: crownStart ? 100 : 0, + gauntletEliminated: false, + quizCannotTrue: false, + quizCannotFalse: false, + quizCarryHeld: null, + botPath: [], + botAnswerWander: false, + botWanderDx: wd[0], + botWanderDy: wd[1], + botWanderNextTurn: Date.now() + 400 + Math.floor(Math.random() * 1000), + jumpSurviveEliminated: false, + spaceShooterScore: 0, + balloonBossScore: 0, + balloonBossBalloons: 5, + balloonBossEliminated: false, + botQuizCarryPathfindAfter: performance.now() + previewBotSeq * 75 + Math.floor(Math.random() * 160), + }); + botIds.push(id); + } + let k = 0; + [...others.keys()].filter(isPreviewBotId).sort().forEach((bid) => { + const o = others.get(bid); + if (!o) return; + const tag = o.botTier === 'sharp' ? '(ฉลาด)' : o.botTier === 'weak' ? '(พลาดบ่อย)' : '(กลาง)'; + o.nickname = 'บอท ' + (++k) + ' ' + tag; + }); + if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) { + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o) return; + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + o.tx = s.x; + o.ty = s.y; + }); + } + if (mapData.gameType === 'quiz_carry') { + const t0 = performance.now(); + [...others.keys()].filter(isPreviewBotId).forEach((bid, idx) => { + const o = others.get(bid); + if (!o) return; + o.botQuizCarryPathfindAfter = t0 + idx * 90 + Math.floor(Math.random() * 140); + }); + } + if (mapData.gameType === 'gauntlet') { + applyGauntletPreviewSpawnLayout(false); + emitGauntletPreviewRowsToServer(); + } + if (mapData.gameType === 'stack') { + applyStackPreviewSpawnLayout(); + rebuildStackPreviewTurnOrder(); + } + if (mapData.gameType === 'jump_survive') { + applyJumpSurvivePreviewSpawnLayout(false); + } + if (mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + if (quizCarryPregameActive && mapData && mapData.gameType === 'quiz_carry') updateQuizCarryPregameHud(); + } + + function pickRandomWalkableCellInAnswerGrid(grid, o) { + if (!grid || !mapData) return null; + const w = mapData.width || 20, h = mapData.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] !== 1) continue; + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, NaN, NaN, o)) pool.push({ x, y }); + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + /** ช่วงตอบ: บอทสุ่มเดินไปโซนถูก/ผิด/เดินมั่ว ตามระดับ (ฉลาด/กลาง/พลาดบ่อย) */ + function previewBotsPrepareAnswerRound() { + if (!previewFillBots || !mapData || !isQuiz() || !previewQuizCurrent) return; + const correctTrue = !!previewQuizCurrent.answerTrue; + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botPath = []; + o.botAnswerWander = false; + const tier = o.botTier || 'avg'; + let pCorrect = 0.52; + if (tier === 'sharp') pCorrect = 0.84; + else if (tier === 'weak') pCorrect = 0.22; + if (Math.random() < 0.14) { + o.botAnswerWander = true; + return; + } + const wantsCorrect = Math.random() < pCorrect; + const targetTrue = wantsCorrect ? correctTrue : !correctTrue; + const zoneGrid = targetTrue ? qt : qf; + const dest = pickRandomWalkableCellInAnswerGrid(zoneGrid, o); + if (!dest) { + o.botAnswerWander = true; + return; + } + const path = pathfindPlayForBot(o.x, o.y, dest.x + 0.5, dest.y + 0.5, o); + if (!path || path.length <= 1) { + o.botAnswerWander = true; + return; + } + o.botPath = path.slice(1); + }); + } + + /** คัดลอกตรรกะจาก server — ดีดออกนอกโซนจริง/เท็จ (โหมดทดสอบ preview บน play.html) */ + function findNearestOutsideQuizAnswerZonesPlay(md, sx, sy) { + const w = md.width || 20; + const h = md.height || 15; + const tGrid = md.quizTrueArea || []; + const fGrid = md.quizFalseArea || []; + const inAnswer = (x, y) => quizCellOnPlay(tGrid, x, y) || quizCellOnPlay(fGrid, x, y); + const walkable = (x, y) => { + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + return row && row[x] !== 1; + }; + const fx = Math.floor(Number(sx)); + const fy = Math.floor(Number(sy)); + if (!walkable(fx, fy)) { + const sp = md.spawn || { x: 1, y: 1 }; + return { x: (typeof sp.x === 'number' ? sp.x : 1) + 0.5, y: (typeof sp.y === 'number' ? sp.y : 1) + 0.5 }; + } + if (!inAnswer(fx, fy)) return { x: sx, y: sy }; + const q = [[fx, fy]]; + const seen = new Set([`${fx},${fy}`]); + const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; + while (q.length) { + const [cx, cy] = q.shift(); + for (let di = 0; di < dirs.length; di++) { + const nx = cx + dirs[di][0]; + const ny = cy + dirs[di][1]; + const k = `${nx},${ny}`; + if (seen.has(k)) continue; + if (!walkable(nx, ny)) continue; + seen.add(k); + if (!inAnswer(nx, ny)) { + return { x: nx + 0.5, y: ny + 0.5 }; + } + q.push([nx, ny]); + } + } + const sp = md.spawn || { x: 1, y: 1 }; + return { x: (typeof sp.x === 'number' ? sp.x : 1) + 0.5, y: (typeof sp.y === 'number' ? sp.y : 1) + 0.5 }; + } + + function quizResolveChoiceFromStanding(ox, oy, correctTrue) { + const tx = Math.floor(ox); + const ty = Math.floor(oy); + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + const inT = quizCellOnPlay(qt, tx, ty); + const inF = quizCellOnPlay(qf, tx, ty); + let choice = null; + if (inT && !inF) choice = true; + else if (inF && !inT) choice = false; + else if (inT && inF) choice = true; + const right = choice !== null && choice === correctTrue; + return { choice, right }; + } + + /** เฉลยรอบทดสอบบนเครื่อง — ให้พฤติกรรมใกล้เคียง server (ผิด = ล็อกโซน + ดีดออก) */ + function resolvePreviewQuizRound() { + if (!previewMode || !mapData || !isQuiz() || !previewQuizCurrent || myId == null) return; + const correctTrue = !!previewQuizCurrent.answerTrue; + const results = []; + const mine = quizResolveChoiceFromStanding(me.x, me.y, correctTrue); + let right = mine.right; + let choice = mine.choice; + if (mine.right) { + playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + 1; + } else { + playQuizPlayerLocal.cannotTrue = true; + playQuizPlayerLocal.cannotFalse = true; + const sp = pickRandomSpawnFromMapPlay(mapData); + const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5); + me.x = pos.x; + me.y = pos.y; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + results.push({ + id: myId, + right, + choice, + score: playLiveQuizScores[myId] != null ? playLiveQuizScores[myId] : 0, + }); + if (previewFillBots) { + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + const br = quizResolveChoiceFromStanding(o.x, o.y, correctTrue); + o.botPath = []; + o.botAnswerWander = false; + if (br.right) { + playLiveQuizScores[id] = (playLiveQuizScores[id] || 0) + 1; + } else { + o.quizCannotTrue = true; + o.quizCannotFalse = true; + const sp = pickRandomSpawnFromMapPlay(mapData); + const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5); + o.x = pos.x + (Math.random() - 0.5) * 0.35; + o.y = pos.y + (Math.random() - 0.5) * 0.35; + } + results.push({ + id, + right: br.right, + choice: br.choice, + score: playLiveQuizScores[id] != null ? playLiveQuizScores[id] : 0, + }); + }); + } + const r = { results, scores: { ...playLiveQuizScores } }; + renderPlayQuizScoreboard(playLiveQuizScores); + showPlayQuizFeedback(r); + } + + function advancePreviewQuizIfDue() { + if (!previewMode || !isQuiz() || !playQuizPhaseEndsAt || Date.now() < playQuizPhaseEndsAt) return; + const pool = previewQuizPool; + if (!pool || !pool.length) return; + if (previewQuizStep === 'read') { + applyPreviewAnswerPhase(); + return; + } + if (previewQuizStep === 'answer') { + resolvePreviewQuizRound(); + applyPreviewBetweenPhase(); + return; + } + if (previewQuizStep === 'between') { + const q = pickRandomQuizFromPool(pool); + if (q) applyPreviewReadPhase(q, pool.length); + } + } + + async function loadPreviewQuizAndStart() { + resetPreviewBotsQuizState(); + let settings = null; + try { + const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' }); + if (r.ok) settings = await r.json(); + } catch (e) { /* use map fallback */ } + let pool = []; + if (settings && Array.isArray(settings.questions) && settings.questions.length) { + pool = settings.questions + .filter((q) => q && String(q.text || '').trim()) + .map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue })); + } + if (!pool.length && mapData) pool = buildQuizPoolFromMap(mapData); + previewQuizPool = pool; + const dRead = 10000; + const dAns = 5000; + const dBet = 3500; + previewQuizTiming = { + readMs: clampPreviewMs(settings && settings.readMs, dRead, 1000, 300000), + answerMs: clampPreviewMs(settings && settings.answerMs, dAns, 1000, 300000), + betweenMs: clampPreviewMs(settings && settings.betweenMs, dBet, 0, 300000), + }; + previewQuizCurrent = null; + if (!pool.length) { + playQuizPhaseLocal = 'read'; + previewQuizStep = 'read'; + playQuizText = 'ยังไม่มีคำถามในชุด — ตั้งคำถามใน Admin → คำถามเกม หรือในแมป (quizQuestions)'; + playQuizPhaseEndsAt = 0; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] ไม่มีคำถามในระบบ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'โซนสีบนแผนที่ยังใช้ดูเลย์เอาต์ได้ตามปกติ'; + initPlayLiveQuizScoresZeros(); + startPlayQuizTimer(); + return; + } + const q = pickRandomQuizFromPool(pool); + if (q) applyPreviewReadPhase(q, pool.length); + initPlayLiveQuizScoresZeros(); + startPlayQuizTimer(); + } + + function startPlayQuizTimer() { + if (playQuizTimerInterval) clearInterval(playQuizTimerInterval); + playQuizTimerInterval = setInterval(() => { + updatePlayQuizTimerDisplay(); + advancePreviewQuizIfDue(); + }, 200); + updatePlayQuizTimerDisplay(); + } + + function teardownPlayQuizUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) ov.classList.add('is-hidden'); + const panel = document.getElementById('quiz-map-question-panel'); + const mapQText = document.getElementById('quiz-map-question-text'); + if (panel) { + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, mapQText); + } + playQuizPhaseLocal = null; + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + playQuizText = ''; + playQuizPhaseEndsAt = 0; + previewQuizPool = []; + previewQuizCurrent = null; + previewQuizStep = 'read'; + playLiveQuizScores = {}; + playPath = []; + hidePlayQuizHud(); + quizCarryJoinSettingsSnap = null; + quizCarryMapPanelTheme = null; + quizCarryChoicePlaqueThemes = null; + resetQuizCarryPlayState(); + resetQuizBattlePlayState(); + const grabBtnTeardown = document.getElementById('quiz-carry-grab-btn'); + if (grabBtnTeardown) { + grabBtnTeardown.classList.add('is-hidden'); + grabBtnTeardown.classList.remove('quiz-carry-grab-btn--active'); + grabBtnTeardown.classList.remove('quiz-carry-grab-btn--place'); + grabBtnTeardown.style.pointerEvents = ''; + const im = grabBtnTeardown.querySelector('img'); + if (im) im.src = '/Game/img/quiz-carry/btn-grab.png'; + } + } + + function setupPlayQuizUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else ov.classList.remove('is-hidden'); + } + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + const leg = document.getElementById('quiz-play-legend'); + if (previewMode) { + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] กำลังโหลดคำถาม…'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'ดึงชุดคำถามจาก /api/quiz-settings (หรือจากแมป)…'; + if (leg) leg.textContent = ''; + loadPreviewQuizAndStart(); + } else { + playQuizPhaseLocal = null; + playQuizText = 'รอคำถามจากโฮสต์…'; + playQuizPhaseEndsAt = 0; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'แมพตอบคำถาม'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'เข้าร่วมห้องแล้ว — เมื่อโฮสต์เริ่มเกม ข้อความและเวลาจะอัปเดตที่นี่ (เล่นจริงแนะนำผ่าน room-lobby)'; + if (leg) leg.textContent = 'โซนสีบนแผนที่คือจุดตอบเหมือนใน Lobby'; + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + } + } + + function isFrogger() { return mapData && mapData.gameType === 'frogger'; } + function isGauntlet() { return mapData && mapData.gameType === 'gauntlet'; } + function isGauntletFaceRightMapMno9kb07() { + return !!(isGauntlet() && playSessionMapId === GAUNTLET_FACE_RIGHT_MAP_ID); + } + /** Crown heist rules + UI (เดียวกับหันขวา — ฉาก mno9kb07) */ + function isGauntletCrownHeistMapPlay() { + return isGauntletFaceRightMapMno9kb07(); + } + let gauntletCrownHowtoVisible = false; + function isGauntletCrownPregameBlockingPlay() { + if (!isGauntletCrownHeistMapPlay()) return false; + if (gauntletCrownRunHeldRemote) return true; + if (gauntletCrownPregamePhase != null && gauntletCrownPregamePhase !== 'live') return true; + return false; + } + function isStack() { return mapData && mapData.gameType === 'stack'; } + function isJumpSurvive() { return mapData && mapData.gameType === 'jump_survive'; } + + function currentPlayMapId() { + const q = (playSessionMapId || '').trim(); + if (q) return q; + if (mapData && mapData.id != null && String(mapData.id).trim() !== '') return String(mapData.id).trim(); + return (playMapIdFromQuery || '').trim(); + } + + function isJumpSurviveMissionUiMapPlay() { + return isJumpSurvive() && currentPlayMapId() === JUMP_SURVIVE_MISSION_MAP_ID; + } + + function isJumpSurviveMissionPregameBlockingPlay() { + if (!isJumpSurviveMissionUiMapPlay()) return false; + return jumpSurviveMissionPhase != null && jumpSurviveMissionPhase !== 'live' && jumpSurviveMissionPhase !== 'ended'; + } + + function jumperAssetUrl(file) { + return BASE + '/img/Jumper/' + file; + } + + function hideConflictingOverlaysForJumpSurviveMission() { + hideConflictingOverlaysForGauntletCrown(); + } + + function jumpSurviveTimeLimitSecForMission() { + const m = Number(mapData && mapData.jumpSurviveTimeSec); + if (Number.isFinite(m) && m > 0 && m < 7200) return Math.floor(m); + const j = Number(playJumpSurviveMissionTimeSec); + if (Number.isFinite(j) && j > 0 && j < 7200) return Math.floor(j); + return 60; + } + + function jumpSurviveRemainingSecMission() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveSessionStartMs <= 0) return null; + const lim = jumpSurviveTimeLimitSecForMission(); + if (!(lim > 0)) return null; + const elapsed = (performance.now() - jumpSurviveSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function applyJumpSurviveJumperPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = jumperAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = jumperAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + } + + function showJumpSurviveMissionHowtoOverlay() { + if (!isJumpSurviveMissionUiMapPlay()) return; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + jumpSurviveMissionPhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.textContent = ''; + st.classList.remove('gch-status--jumper'); + st.classList.add('is-hidden'); + } + const btn = document.getElementById('btn-gch-ready'); + if (btn) { + const humans = quizCarryPregameHumanIds(); + const soleHuman = humans.length === 1; + const canGo = soleHuman || isMePlayHost(); + btn.classList.remove('is-start-phase'); + btn.classList.toggle('is-read-only', !canGo); + btn.disabled = !canGo; + btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host'; + btn.setAttribute('aria-pressed', 'false'); + } + } + + function beginJumpSurviveMissionCountdownThenRun() { + if (!isJumpSurviveMissionUiMapPlay()) return; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + jumpSurviveMissionPhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.classList.add('is-hidden'); + st.classList.remove('gch-status--jumper'); + } + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = function () { + if (cd) cd.classList.add('is-hidden'); + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + jumpSurviveMissionPhase = 'live'; + jumpSurviveGameEnded = false; + jumpSurviveEliminated = false; + jumpSurviveInitRuntime(); + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + numEl.textContent = String(n); + const step = function () { + n--; + if (n > 0) { + numEl.textContent = String(n); + jumperMissionCountdownTimer = setTimeout(step, 1000); + } else { + jumperMissionCountdownTimer = null; + runFinish(); + } + }; + jumperMissionCountdownTimer = setTimeout(step, 1000); + } + + /** Jumper ภารกิจ mnptfts2: เกรดจากจำนวนผู้รอด — 6=A … 3=D; น้อยกว่า 2 = F; พอดี 2 คน = E */ + function jumpSurviveGradeFromSurvivorCount(survivors) { + const s = Math.max(0, Math.floor(Number(survivors) || 0)); + if (s >= 6) return 'A'; + if (s === 5) return 'B'; + if (s === 4) return 'C'; + if (s === 3) return 'D'; + if (s === 2) return 'E'; + return 'F'; + } + + function jumpSurviveRollRewardCardForGrade(grade) { + if (grade === 'F') return null; + const r = Math.random(); + if (grade === 'A' || grade === 'B') { + if (r < 0.35) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.7) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'C' || grade === 'D') { + if (r < 0.25) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.45) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'E') { + if (r < 0.12) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (r < 0.25) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.45) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + + function jumpSurviveMissionTotalParticipants() { + let n = myId != null ? 1 : 0; + if (others && typeof others.size === 'number') n += others.size; + return n; + } + + function jumpSurviveMissionCountAlive() { + let n = 0; + if (myId != null && !jumpSurviveEliminated) n++; + if (others && typeof others.forEach === 'function') { + others.forEach(function (o) { + if (o && !o.jumpSurviveEliminated) n++; + }); + } + return n; + } + + /** จบทันทีเมื่อไม่มีผู้รอด หรือเหลือคนเดียว (หลายคนในรอบ) — โหมด solo 1 คนยังเล่นจนหมดเวลา */ + function jumpSurviveMissionMaybeEarlyFinish() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) return; + const total = jumpSurviveMissionTotalParticipants(); + const alive = jumpSurviveMissionCountAlive(); + if (alive === 0) { + endJumpSurviveMissionRound(); + return; + } + if (total >= 2 && alive === 1) { + endJumpSurviveMissionRound(); + } + } + + function jumpSurviveBuildMissionPayload() { + const rows = []; + const pushEnt = function (id, nickname, characterId, eliminated) { + const alive = !eliminated; + const baseScore = alive ? 100 : 0; + rows.push({ + id: id, + nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id), + characterId: characterId ?? null, + baseScore: baseScore, + eliminated: !!eliminated, + rankBonus: 0, + finalScore: baseScore, + }); + }; + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), jumpSurviveEliminated); + } + others.forEach(function (o, id) { + if (!o) return; + pushEnt(id, o.nickname, o.characterId, !!o.jumpSurviveEliminated); + }); + rows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + let survivorCount = 0; + for (let si = 0; si < rows.length; si++) { + if (rows[si] && !rows[si].eliminated) survivorCount++; + } + const participantCount = rows.length; + const grade = jumpSurviveGradeFromSurvivorCount(survivorCount); + const top = rows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: row.baseScore, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const rewardCard = jumpSurviveRollRewardCardForGrade(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'jumper', + survivorCount: survivorCount, + participantCount: participantCount, + }; + } + + function jumpSurviveMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'jumper' || !previewFillBots || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + rankBonus: 0, + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: o.jumpSurviveEliminated ? 0 : 100, + eliminated: !!o.jumpSurviveEliminated, + rankBonus: 0, + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + let survivorCount = 0; + for (let si = 0; si < baseRows.length; si++) { + if (baseRows[si] && !baseRows[si].eliminated) survivorCount++; + } + const participantCount = baseRows.length; + const grade = jumpSurviveGradeFromSurvivorCount(survivorCount); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: jumpSurviveRollRewardCardForGrade(grade), + totalParts: totalParts, + uiSkin: 'jumper', + survivorCount: survivorCount, + participantCount: participantCount, + }; + } + + function endJumpSurviveMissionRound() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveGameEnded) return; + jumpSurviveGameEnded = true; + jumpSurviveMissionPhase = 'ended'; + applyJumpSurviveJumperPanelImages(); + const mission = jumpSurviveBuildMissionPayload(); + showGauntletCrownMissionOverlay(mission); + } + function isSpaceShooter() { return mapData && mapData.gameType === 'space_shooter'; } + function isBalloonBoss() { return mapData && mapData.gameType === 'balloon_boss'; } + function isQuizCarry() { return mapData && mapData.gameType === 'quiz_carry'; } + function isQuizBattle() { return mapData && mapData.gameType === 'quiz_battle'; } + + function quizCarryNeonColorForChoice(choiceIndex) { + const i = Math.max(0, choiceIndex | 0); + const h = (i * 47 + 100) % 360; + return 'hsl(' + h + ', 72%, 62%)'; + } + + function quizCarryMinimapOptionFillCss(ov) { + const th = getEffectiveCarryChoicePlaqueThemeForChoice((Number(ov) | 0) - 1); + if (th.borderMode === 'fixed' && th.fixedBorder) { + const m = /^rgba?\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i.exec(String(th.fixedBorder).trim()); + if (m) { + const a0 = m[4] != null && m[4] !== '' ? Number(m[4]) : 1; + const aa = (Number.isFinite(a0) ? Math.max(0, Math.min(1, a0)) : 1) * 0.32; + const t = Math.round(aa * 1000) / 1000; + return 'rgba(' + m[1] + ',' + m[2] + ',' + m[3] + ',' + t + ')'; + } + } + const i = Math.max(0, ov - 1); + const h = (i * 47 + 100) % 360; + return 'hsla(' + h + ', 58%, 52%, 0.32)'; + } + + function normalizeQuizCarryQuestionFromAny(q) { + if (!q) return null; + let text = String(q.text || q.question || q.prompt || q.title || '').trim(); + if (Array.isArray(q.choices) && q.choices.length >= 2) { + const choices = q.choices.map((c) => String(c)); + let ci = Number(q.correctIndex); + if (!Number.isFinite(ci) || ci < 0 || ci >= choices.length) ci = 0; + if (!text) text = 'เลือกคำตอบที่ถูกต้อง — หยิบตัวเลือกไปวางโซนส่งคำตอบ'; + let chSlot = Number(q.countdownHighlightSlot); + if (!Number.isFinite(chSlot) || chSlot < 1 || chSlot > 16) chSlot = null; + else chSlot = Math.floor(chSlot); + let choiceImageUrls = null; + if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) { + const urls = choices.map((_, idx) => sanitizeQuizCarryImageUrlClient(q.choiceImageUrls[idx])); + if (urls.some((u) => u)) choiceImageUrls = urls; + } + return { text, choices, correctIndex: ci, countdownHighlightSlot: chSlot, choiceImageUrls }; + } + if (!text) return null; + const t = !!q.answerTrue; + return { text, choices: ['จริง', 'เท็จ'], correctIndex: t ? 0 : 1, countdownHighlightSlot: null }; + } + + function buildQuizCarryPoolFromMap(md) { + if (!md || !Array.isArray(md.quizQuestions)) return []; + const out = []; + for (let i = 0; i < md.quizQuestions.length; i++) { + const n = normalizeQuizCarryQuestionFromAny(md.quizQuestions[i]); + if (n) out.push(n); + } + return out; + } + + function getQuizCarryHubTileBounds(md) { + if (!md || md.gameType !== 'quiz_carry') return null; + const grid = md.quizCarryHubArea; + if (!grid || !grid.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (let yy = 0; yy < grid.length; yy++) { + const row = grid[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (row[xx] === 1) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + } + } + } + if (minX === Infinity) return null; + return { minX, minY, maxX, maxY }; + } + + /** จุดกลางโซนตัวเลือกหยิบมาวาง (ค่า grid 1..N = ลำดับ choices) เพื่อวาดข้อความบนแผนที่ */ + function getQuizCarryOptionClusterBounds(md, slot1toN) { + const g = md && md.quizCarryOptionArea; + if (!g || !g.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let n = 0, sumX = 0, sumY = 0; + for (let yy = 0; yy < g.length; yy++) { + const row = g[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (Number(row[xx]) === slot1toN) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + sumX += xx + 0.5; + sumY += yy + 0.5; + n++; + } + } + } + if (!n || minX === Infinity) return null; + return { + minX, minY, maxX, maxY, + cx: sumX / n, + cy: sumY / n, + }; + } + + function canvasWordWrapLines(ctx, text, maxWidth) { + const raw = String(text || '').trim(); + if (!raw) return []; + const words = raw.split(/\s+/); + const lines = []; + let cur = words[0]; + for (let i = 1; i < words.length; i++) { + const w = words[i]; + const test = cur + ' ' + w; + if (ctx.measureText(test).width <= maxWidth) cur = test; + else { + lines.push(cur); + cur = w; + } + } + lines.push(cur); + return lines; + } + + function quizCarryOptionHeldByAnyone(optionIdx) { + if (optionIdx == null || optionIdx < 0) return false; + if (me.quizCarryHeld === optionIdx) return true; + for (const o of others.values()) { + if (o && o.quizCarryHeld === optionIdx) return true; + } + return false; + } + + /** สไปรต์ทับช่องโซนกลาง (ม่วง) อย่างน้อย 1 ช่อง */ + function spriteOverlapsQuizCarryHubArea(md, sp) { + if (!md || md.gameType !== 'quiz_carry' || !sp) return false; + const hub = md.quizCarryHubArea; + if (!hub || !hub.length) return false; + const mw = md.width || 20, mh = md.height || 15; + const x0 = sp.x | 0, y0 = sp.y | 0, ww = sp.w | 0, hh = sp.h | 0; + for (let yy = y0; yy < y0 + hh; yy++) { + if (yy < 0 || yy >= mh) continue; + const row = hub[yy]; + if (!row) continue; + for (let xx = x0; xx < x0 + ww; xx++) { + if (xx < 0 || xx >= mw) continue; + if (row[xx] === 1) return true; + } + } + return false; + } + + /** ตัวเลือกหนึ่งข้อ — มีคนถือป้ายอยู่ได้แค่คนเดียว (หยิบซ้ำไม่ได้) */ + function pickQuizCarryTargetOptionIndexAvoidingTaken(preferredIdx) { + const n = quizCarryCurrent && quizCarryCurrent.choices ? quizCarryCurrent.choices.length : 0; + if (n < 1) return null; + let p = preferredIdx | 0; + if (p < 0 || p >= n) p = 0; + if (!quizCarryOptionHeldByAnyone(p)) return p; + const avail = []; + for (let i = 0; i < n; i++) { + if (!quizCarryOptionHeldByAnyone(i)) avail.push(i); + } + if (!avail.length) return null; + return avail[Math.floor(Math.random() * avail.length)]; + } + + /** + * วาดป้ายมุมมน + ขอบเรือง + ข้อความ (รูปแบบเดียวกันทั้งแมปและติดตัว) — สีจาก carryChoicePlaqueThemes[choiceIndex] · รูปจาก plaqueExtra.imageUrl + * @param tether ถ้ามี วาดเส้นขาวจาก (x0,y0) ถึง (x1,y1) ก่อนป้าย + * @param plaqueExtra optional { imageUrl } + */ + function drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, tether, plaqueExtra) { + const th = getEffectiveCarryChoicePlaqueThemeForChoice(choiceIndex); + const neon = th.borderMode === 'fixed' ? th.fixedBorder : quizCarryNeonColorForChoice(choiceIndex); + const fillCol = th.fillBg || 'rgba(12, 10, 20, 0.88)'; + const textCol = th.textColor || '#f8f9ff'; + const baseLw = Math.max(0.5, Math.min(8, Number(th.borderWidthPx))); + const imgUrl = plaqueExtra && plaqueExtra.imageUrl ? String(plaqueExtra.imageUrl) : ''; + const elImg = plaqueExtra && plaqueExtra.imageElement; + const imgFromEl = (elImg && elImg.complete && elImg.naturalWidth > 0) ? elImg : null; + const img = !imgFromEl && imgUrl ? getQuizCarryChoiceImageCached(imgUrl) : null; + const drawPlaqueImg = imgFromEl || ((img && img.complete && img.naturalWidth > 0) ? img : null); + const rr = Math.max(6, Math.min(12, Math.min(w, h) * 0.2)); + const simplePreviewPlaque = previewMode && editorEmbedReturn; + function pathBoard() { + ctx.beginPath(); + if (typeof ctx.roundRect === 'function') ctx.roundRect(signX, signY, w, h, rr); + else ctx.rect(signX, signY, w, h); + } + if (!simplePreviewPlaque) { + for (let g = 4; g >= 1; g--) { + ctx.strokeStyle = neon; + ctx.globalAlpha = 0.07 + g * 0.06; + ctx.lineWidth = baseLw + g * 3.2; + pathBoard(); + ctx.stroke(); + } + } + ctx.globalAlpha = 1; + ctx.fillStyle = fillCol; + pathBoard(); + ctx.fill(); + if (drawPlaqueImg) { + ctx.save(); + pathBoard(); + ctx.clip(); + const padImg = Math.max(2, padY * 0.65); + const ix = signX + padImg; + const iy = signY + padImg; + const iw = Math.max(0, w - padImg * 2); + const ih = Math.max(0, h - padImg * 2); + if (iw > 0 && ih > 0) { + const nw = drawPlaqueImg.naturalWidth; + const nh = drawPlaqueImg.naturalHeight; + const ir = nw / nh; + let dw; + let dh; + if (iw / ih > ir) { + dh = ih; + dw = dh * ir; + } else { + dw = iw; + dh = dw / ir; + } + const dx = ix + (iw - dw) / 2; + const dy = iy + (ih - dh) / 2; + ctx.drawImage(drawPlaqueImg, 0, 0, nw, nh, dx, dy, dw, dh); + } + ctx.restore(); + } + ctx.strokeStyle = neon; + if (simplePreviewPlaque) { + ctx.lineWidth = Math.max(1, baseLw * 0.85); + ctx.shadowBlur = 0; + pathBoard(); + ctx.stroke(); + } else { + ctx.lineWidth = baseLw; + ctx.shadowColor = neon; + ctx.shadowBlur = 14; + pathBoard(); + ctx.stroke(); + ctx.shadowBlur = 8; + pathBoard(); + ctx.stroke(); + ctx.shadowBlur = 0; + } + const attachX = signX + w / 2; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = textCol; + const lh = lineH || 12; + if (!simplePreviewPlaque && lines && lines.length) { + ctx.shadowColor = 'rgba(0,0,0,0.55)'; + ctx.shadowBlur = 3; + } + if (lines && lines.length) { + const n = lines.length; + const textBlockH = n * lh; + const y0 = signY + Math.max(padY, (h - textBlockH) / 2); + const firstLineCenterY = y0 + lh / 2; + for (let li = 0; li < n; li++) { + ctx.fillText(lines[li], attachX, firstLineCenterY + li * lh); + } + } + ctx.shadowBlur = 0; + if (tether && Number.isFinite(tether.x0) && Number.isFinite(tether.y0)) { + ctx.strokeStyle = 'rgba(255,255,255,0.95)'; + ctx.lineWidth = 1.5; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.shadowBlur = 0; + ctx.beginPath(); + ctx.moveTo(tether.x0, tether.y0); + ctx.lineTo(tether.x1, tether.y1); + ctx.stroke(); + } + } + + /** + * กล่องป้ายบนจอ (signX, signY, w, h) ให้ตรงกับ drawQuizCarryChoiceLabels สำหรับช่อง idx 0-based + */ + function computeQuizCarryChoicePlaqueScreenRect(ctx, worldToScreen, zoom, idx0Based) { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return null; + const q = quizCarryCurrent; + const choices = q.choices; + if (!choices || !choices.length || idx0Based < 0 || idx0Based >= choices.length) return null; + const i = idx0Based; + const b = getQuizCarryOptionClusterBounds(mapData, i + 1); + if (!b) return null; + const ts = tileSize * zoom; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + const choiceText = String(choices[i] || '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(q, i); + const gridIdleEl = findQuizCarryGridIdleImgForChoiceIndex(i); + const plaqueExtra = imgUrl ? { imageUrl: imgUrl } + : (gridIdleEl && gridIdleEl.complete && gridIdleEl.naturalWidth ? { imageElement: gridIdleEl } : {}); + if (!choiceText && !plaqueExtra.imageUrl && !plaqueExtra.imageElement) return null; + const tileSpanX = (b.maxX - b.minX + 1); + const tileSpanY = (b.maxY - b.minY + 1); + const minPlaqueW = Math.ceil(Math.max(72, tileSpanX * ts - 16) * ps); + const minPlaqueH = Math.ceil(Math.max(52, tileSpanY * ts - 12) * ps); + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + ctx.save(); + let signX; + let signY; + let w; + let h; + if (!choiceText && (plaqueExtra.imageUrl || plaqueExtra.imageElement)) { + const minW = minPlaqueW; + const minH = minPlaqueH; + signX = sx - minW / 2; + signY = sy - minH / 2; + w = minW; + h = minH; + ctx.restore(); + return { signX, signY, w, h }; + } + const line = choiceText; + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + let maxW = Math.max(56, tileSpanX * ts - 14); + const lineH = fontPx * 1.2; + let textLines = canvasWordWrapLines(ctx, line, maxW); + if (!textLines.length) textLines = [line]; + if (textLines.length * lineH > tileSpanY * ts - 8 && textLines.length > 1) { + const fp2 = Math.max(9, fontPx * 0.85); + ctx.font = '600 ' + fp2 + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + const lh2 = fp2 * 1.2; + maxW = Math.max(48, maxW - 4); + textLines = canvasWordWrapLines(ctx, line, maxW); + if (textLines.length * lh2 > tileSpanY * ts - 8) { + textLines = textLines.slice(0, Math.max(1, Math.floor((tileSpanY * ts - 8) / lh2))); + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + const maxLineW = Math.max(...textLines.map((t) => ctx.measureText(t).width), 40); + const padX = 10; + const padY = 8; + w = Math.ceil(maxLineW + padX * 2); + h = Math.ceil(textLines.length * lh2 + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + signX = sx - w / 2; + signY = sy - h / 2; + ctx.restore(); + return { signX, signY, w, h }; + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + let maxLineW = 0; + for (let ti = 0; ti < textLines.length; ti++) { + const lw = ctx.measureText(textLines[ti]).width; + if (lw > maxLineW) maxLineW = lw; + } + const padX = 10; + const padY = 8; + w = Math.ceil(Math.max(maxLineW, 52) + padX * 2); + h = Math.ceil(textLines.length * lineH + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + signX = sx - w / 2; + signY = sy - h / 2; + ctx.restore(); + return { signX, signY, w, h }; + } + + /** + * ขอบเรืองรอบช่องตัวเลือก (carryEmbedCountdownHighlight + carryEmbedCountdownHighlightColor) + * ขนาดกรอบเท่าป้ายบนแมป (เดียวกับ drawQuizCarryChoiceLabels) ไม่ใช่ทั้ง cluster กริด + */ + function drawQuizCarryEmbedCountdownHighlight(ctx, worldToScreen, zoom, timeMs) { + if (!isQuizCarry() || !mapData) return; + if (mapData.carryEmbedCountdownHighlight === false) return; + const optIdx = getQuizCarryLocalGrabbableOptionIndex(); + if (optIdx == null) return; + const pr = computeQuizCarryChoicePlaqueScreenRect(ctx, worldToScreen, zoom, optIdx); + if (!pr || pr.w < 4 || pr.h < 4) return; + const rx = pr.signX; + const ry = pr.signY; + const rw = pr.w; + const rh = pr.h; + const rr = Math.max(6, Math.min(16, Math.min(rw, rh) * 0.14)); + const colRaw = mapData.carryEmbedCountdownHighlightColor; + const strokeBase = typeof colRaw === 'string' && /^#[0-9a-fA-F]{6}$/.test(colRaw.trim()) ? colRaw.trim() : '#ffb44a'; + const pulse = 0.55 + 0.45 * Math.sin((timeMs || 0) / 180); + ctx.save(); + function pathRr() { + ctx.beginPath(); + if (typeof ctx.roundRect === 'function') ctx.roundRect(rx, ry, rw, rh, rr); + else ctx.rect(rx, ry, rw, rh); + } + pathRr(); + ctx.fillStyle = strokeBase; + ctx.globalAlpha = 0.12 * pulse; + ctx.fill(); + for (let g = 6; g >= 1; g--) { + ctx.strokeStyle = strokeBase; + ctx.globalAlpha = (0.07 + g * 0.055) * pulse; + ctx.lineWidth = 2.5 + g * 4; + pathRr(); + ctx.stroke(); + } + ctx.globalAlpha = 0.98; + ctx.strokeStyle = strokeBase; + ctx.lineWidth = 4; + ctx.shadowColor = strokeBase; + ctx.shadowBlur = 22 * pulse; + pathRr(); + ctx.stroke(); + ctx.shadowBlur = 12 * pulse; + pathRr(); + ctx.stroke(); + ctx.shadowBlur = 0; + ctx.globalAlpha = 1; + ctx.restore(); + } + + function drawQuizCarryChoiceLabels(ctx, worldToScreen, zoom) { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return; + if (!quizCarryOptionsPickableNow()) return; + const choices = quizCarryCurrent.choices; + if (!choices || !choices.length) return; + const maxSlots = choices.length; + const ts = tileSize * zoom; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + ctx.save(); + for (let i = 0; i < Math.min(choices.length, maxSlots); i++) { + if (quizCarryOptionHeldByAnyone(i)) continue; + const b = getQuizCarryOptionClusterBounds(mapData, i + 1); + if (!b) continue; + const choiceText = String(choices[i] || '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(quizCarryCurrent, i); + const gridIdleEl = findQuizCarryGridIdleImgForChoiceIndex(i); + const plaqueExtra = imgUrl ? { imageUrl: imgUrl } + : (gridIdleEl && gridIdleEl.complete && gridIdleEl.naturalWidth ? { imageElement: gridIdleEl } : {}); + if (!choiceText && !plaqueExtra.imageUrl && !plaqueExtra.imageElement) continue; + const tileSpanX = (b.maxX - b.minX + 1); + const tileSpanY = (b.maxY - b.minY + 1); + const minPlaqueW = Math.ceil(Math.max(72, tileSpanX * ts - 16) * ps); + const minPlaqueH = Math.ceil(Math.max(52, tileSpanY * ts - 12) * ps); + if (!choiceText && (plaqueExtra.imageUrl || plaqueExtra.imageElement)) { + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + const lineH = fontPx * 1.2; + const padY = 8; + const minW = minPlaqueW; + const minH = minPlaqueH; + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + drawQuizCarryNeonPlaque(ctx, sx - minW / 2, sy - minH / 2, minW, minH, [], lineH, padY, i, null, plaqueExtra); + continue; + } + const line = choiceText; + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + let maxW = Math.max(56, tileSpanX * ts - 14); + const lineH = fontPx * 1.2; + let textLines = canvasWordWrapLines(ctx, line, maxW); + if (!textLines.length) textLines = [line]; + if (textLines.length * lineH > tileSpanY * ts - 8 && textLines.length > 1) { + const fp2 = Math.max(9, fontPx * 0.85); + ctx.font = '600 ' + fp2 + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + const lh2 = fp2 * 1.2; + maxW = Math.max(48, maxW - 4); + textLines = canvasWordWrapLines(ctx, line, maxW); + if (textLines.length * lh2 > tileSpanY * ts - 8) { + textLines = textLines.slice(0, Math.max(1, Math.floor((tileSpanY * ts - 8) / lh2))); + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + const maxLineW = Math.max(...textLines.map((t) => ctx.measureText(t).width), 40); + const padX = 10; + const padY = 8; + let w = Math.ceil(maxLineW + padX * 2); + let h = Math.ceil(textLines.length * lh2 + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const signX = sx - w / 2; + const signY = sy - h / 2; + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, textLines, lh2, padY, i, null, plaqueExtra); + continue; + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + let maxLineW = 0; + for (let ti = 0; ti < textLines.length; ti++) { + const lw = ctx.measureText(textLines[ti]).width; + if (lw > maxLineW) maxLineW = lw; + } + const padX = 10; + const padY = 8; + let w = Math.ceil(Math.max(maxLineW, 52) + padX * 2); + let h = Math.ceil(textLines.length * lineH + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const signX = sx - w / 2; + const signY = sy - h / 2; + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, textLines, lineH, padY, i, null, plaqueExtra); + } + ctx.restore(); + } + + function quizCarryHubCellPlay(md, tx, ty) { + const g = md && md.quizCarryHubArea; + return !!(g && g[ty] && g[ty][tx] === 1); + } + + function quizCarryInteractiveCellPlay(md, tx, ty) { + const g = md && md.interactive; + return !!(g && g[ty] && g[ty][tx] === 1); + } + + function quizCarryMapHasAnswerInteractive(md) { + if (!md || !md.interactive || !md.interactive.length) return false; + const h = md.height || 15, w = md.width || 20; + for (let y = 0; y < h; y++) { + const row = md.interactive[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1) return true; + } + } + return false; + } + + /** @returns {number|null} index ใน choices จากช่องหมายเลข 1..N บนแมป */ + function quizCarryOptionIndexAtPlay(md, tx, ty) { + const g = md && md.quizCarryOptionArea; + if (!g || !g[ty]) return null; + const v = g[ty][tx]; + if (v < 1 || v > QUIZ_CARRY_MAX_OPTION_SLOTS) return null; + const idx = v - 1; + const n = quizCarryCurrent && quizCarryCurrent.choices ? quizCarryCurrent.choices.length : 0; + if (n < 1 || idx >= n) return null; + return idx; + } + + /** ร่างตัวละครทับช่องโซนกลาง (ม่วง) — ใช้บล็อกการเดิน */ + function quizCarryFootprintOverlapsHub(px, py) { + if (!mapData || !isQuizCarry()) return false; + const md = mapData; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryHubCellPlay(md, tx, ty)) return true; + } + return false; + } + + /** ยืนข้างนอกแต่ชิดโซนกลาง — ใช้ให้กด F ส่งคำตอบได้โดยไม่ต้องย่ำบนม่วง */ + function quizCarryFootprintAdjacentToHub(md, px, py) { + if (!md) return false; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryHubCellPlay(md, tx, ty)) continue; + for (let di = 0; di < dirs.length; di++) { + const nx = tx + dirs[di][0], ny = ty + dirs[di][1]; + if (quizCarryHubCellPlay(md, nx, ny)) return true; + } + } + return false; + } + + function quizCarryCanSubmitAtHub(md, px, py) { + if (!md) return false; + return quizCarryFootprintOverlapsHub(px, py) || quizCarryFootprintAdjacentToHub(md, px, py); + } + + function quizCarryFootprintOverlapsInteractive(md, px, py) { + if (!md) return false; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryInteractiveCellPlay(md, tx, ty)) return true; + } + return false; + } + + function quizCarryFootprintAdjacentToInteractive(md, px, py) { + if (!md) return false; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryInteractiveCellPlay(md, tx, ty)) continue; + for (let di = 0; di < dirs.length; di++) { + const nx = tx + dirs[di][0], ny = ty + dirs[di][1]; + if (quizCarryInteractiveCellPlay(md, nx, ny)) return true; + } + } + return false; + } + + function quizCarryCanSubmitAtInteractiveZone(md, px, py) { + if (!md) return false; + return quizCarryFootprintOverlapsInteractive(md, px, py) || quizCarryFootprintAdjacentToInteractive(md, px, py); + } + + /** ส่งคำตอบ: ถ้ามีโซน interactive (เขียว) ในแมป ใช้โซนนั้น — ไม่มี fallback ชิดโซนกลาง */ + function quizCarryCanSubmitAnswerAt(md, px, py) { + if (!md) return false; + if (quizCarryMapHasAnswerInteractive(md)) { + return quizCarryCanSubmitAtInteractiveZone(md, px, py); + } + return quizCarryCanSubmitAtHub(md, px, py); + } + + /** ข้อความใน HUD / โซนคำถาม — ไม่รวมตัวเลือก (แสดงบนแผนที่ตามจุดสี) */ + function formatQuizCarryQuestionHud(q) { + if (!q) return ''; + return String(q.text || q.question || q.prompt || q.title || '').trim(); + } + + function quizCarryRestoreEmbedPhaseLabel() { + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (!phaseEl || !mapData) return; + phaseEl.textContent = quizCarryMapHasAnswerInteractive(mapData) + ? 'กด F หรือปุ่ม GRAB หยิบบนโซนตัวเลือก · ส่งที่โซน interactive (เขียว) — เล่นพร้อมกันได้' + : 'กด F หรือปุ่ม GRAB หยิบบนโซนตัวเลือก · ส่งเมื่อชิดโซนกลาง (ม่วงเป็นกำแพง) — เล่นพร้อมกันได้'; + } + + function isQuizCarryEmbedCountdownBlockingMovement() { + return !!(previewMode && editorEmbedReturn && isQuizCarry() && quizCarryEmbedCountdownEndAt > Date.now()); + } + + function isQuizCarryEmbedLobbyBlockingMovement() { + return !!(previewMode && editorEmbedReturn && isQuizCarry() && quizCarryPregameActive); + } + + function quizCarryPregameHumanIds() { + const ids = []; + if (myId != null && !isPreviewBotId(myId)) ids.push(String(myId)); + others.forEach((_, id) => { + if (!isPreviewBotId(id)) ids.push(String(id)); + }); + return ids; + } + + function quizCarryPregameBotCount() { + let n = 0; + others.forEach((_, id) => { + if (isPreviewBotId(id)) n++; + }); + return n; + } + + function quizCarryPregameReadyNumerator() { + let n = quizCarryPregameBotCount(); + quizCarryPregameHumanIds().forEach((id) => { + if (quizCarryLobbyReadyMap[id]) n++; + }); + return n; + } + + function quizCarryPregameTotalPlayers() { + return quizCarryPregameHumanIds().length + quizCarryPregameBotCount(); + } + + function isMePlayHost() { + if (myId == null) return false; + if (playHostId == null) return true; + return String(playHostId) === String(myId); + } + + function hideQuizCarryPregameOverlay() { + const ov = document.getElementById('quiz-carry-pregame-overlay'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + } + + function showQuizCarryPregameOverlay() { + const ov = document.getElementById('quiz-carry-pregame-overlay'); + if (ov) { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + updateQuizCarryPregameHud(); + } + + function quizCarrySyncGuestReadyIfNeeded() { + if (!quizCarryPregameActive || myId == null || isMePlayHost()) return; + const sid = String(myId); + if (quizCarryLobbyReadyMap[sid]) return; + quizCarryLobbyReadyMap[sid] = true; + if (socket && socket.connected) socket.emit('quiz-carry-lobby-ready', { ready: true }); + } + + function updateQuizCarryPregameHud() { + if (!quizCarryPregameActive) return; + const st = document.getElementById('quiz-carry-pregame-status'); + const primary = document.getElementById('quiz-carry-pregame-primary'); + const primaryImg = document.getElementById('quiz-carry-pregame-primary-img'); + const num = quizCarryPregameReadyNumerator(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + if (st) { + st.textContent = 'Ready Status : ' + num + '/' + tot; + } + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]); + const meReady = !!(myId && quizCarryLobbyReadyMap[String(myId)]); + if (primaryImg) { + primaryImg.src = humansReady ? '/Game/img/quiz-carry/btn-start.png' : '/Game/img/quiz-carry/btn-ready.png'; + primaryImg.alt = humansReady ? 'START' : 'READY'; + } + if (primary) { + primary.classList.toggle('is-start-phase', humansReady); + primary.classList.toggle('is-read-only', !isMePlayHost()); + primary.classList.toggle('is-pressed', isMePlayHost() && meReady && !humansReady); + primary.disabled = !isMePlayHost(); + primary.setAttribute('aria-pressed', humansReady ? 'false' : (meReady ? 'true' : 'false')); + primary.title = isMePlayHost() + ? (humansReady ? 'START' : (meReady ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + } + + function beginQuizCarryEmbedPregame() { + if (!(previewMode && editorEmbedReturn && isQuizCarry())) return; + quizCarryPregameActive = true; + quizCarryLobbyReadyMap = {}; + quizCarryPregameHumanIds().forEach((id) => { quizCarryLobbyReadyMap[id] = false; }); + showQuizCarryPregameOverlay(); + if (socket && socket.connected) socket.emit('quiz-carry-lobby-sync-request'); + quizCarrySyncGuestReadyIfNeeded(); + updateQuizCarryPregameHud(); + } + + function endQuizCarryEmbedPregameAndStart() { + if (!quizCarryPregameActive) return; + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryPickNextQuestion(); + if (!(previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now())) { + quizCarryRestoreEmbedPhaseLabel(); + } + } + + function quizCarryOptionsPickableNow() { + if (!isQuizCarry() || !quizCarryCurrent) return true; + if (!quizCarryOptionRevealAt || quizCarryOptionRevealAt <= 0) return true; + return Date.now() >= quizCarryOptionRevealAt; + } + + function quizCarryApplyPhaseTimersForCurrentQuestion() { + if (!quizCarryCurrent) { + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + return; + } + const readMs = Math.max(0, Math.floor(Number(quizCarryCarryTimingMs.carryReadMs)) || 0); + const ansMs = Math.max(1000, Math.floor(Number(quizCarryCarryTimingMs.carryAnswerMs)) || 5000); + const t0 = Date.now(); + if (readMs <= 0) { + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = t0 + ansMs; + } else { + quizCarryOptionRevealAt = t0 + readMs; + quizCarryAnswerCloseAt = quizCarryOptionRevealAt + ansMs; + } + } + + function updateQuizCarryCarryPhaseHud() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!isQuizCarry() || !quizCarryCurrent) return; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (!phaseEl) return; + const now = Date.now(); + if (quizCarryOptionRevealAt > 0 && now < quizCarryOptionRevealAt) { + const remain = Math.max(0, Math.ceil((quizCarryOptionRevealAt - now) / 1000)); + phaseEl.textContent = 'อ่านคำถาม — ตัวเลือกขึ้นใน ' + remain + ' วิ'; + return; + } + if (quizCarryAnswerCloseAt > now) { + const remain = Math.max(0, Math.ceil((quizCarryAnswerCloseAt - now) / 1000)); + phaseEl.textContent = 'หยิบ/ส่งคำตอบ — เหลือ ' + remain + ' วิ'; + return; + } + quizCarryRestoreEmbedPhaseLabel(); + } + + function tickQuizCarryRoundTimers() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return; + if (quizCarryEmbedCountdownEndAt > Date.now()) return; + if (quizCarryEmbedPendingQuestion) return; + if (!quizCarryAnswerCloseAt || quizCarryAnswerCloseAt <= 0) return; + if (Date.now() < quizCarryAnswerCloseAt) return; + quizCarryAnswerCloseAt = 0; + quizCarryOptionRevealAt = 0; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + renderPlayQuizScoreboard(playLiveQuizScores); + quizCarryAfterRoundResolved(); + } + + /** นับ 3–2–1: ไม่โชว์ข้อความคำถามใน overlay — มีแค่เลข */ + function hideQuizCarryEmbedCountdownQuestionChrome() { + const cq = document.getElementById('quiz-carry-embed-countdown-q'); + const ck = document.getElementById('quiz-carry-embed-countdown-kicker'); + if (cq) { + cq.textContent = ''; + cq.style.display = 'none'; + } + if (ck) ck.style.display = 'none'; + } + + function hideQuizCarryEmbedCountdownOverlay() { + resetQuizCarryEmbedCountdownLayoutDom(); + const ov = document.getElementById('quiz-carry-embed-countdown'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + hideQuizCarryEmbedCountdownQuestionChrome(); + } + + function tickQuizCarryEmbedCountdown() { + if (!previewMode || !editorEmbedReturn || !isQuizCarry()) return; + if (!quizCarryEmbedPendingQuestion || quizCarryEmbedCountdownEndAt <= 0) return; + const now = Date.now(); + const ov = document.getElementById('quiz-carry-embed-countdown'); + const numEl = document.getElementById('quiz-carry-embed-countdown-num'); + if (now >= quizCarryEmbedCountdownEndAt) { + const pq = quizCarryEmbedPendingQuestion; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + if (!pq) return; + quizCarryCurrent = pq; + preloadQuizCarryChoiceImages(pq); + playQuizText = formatQuizCarryQuestionHud(pq); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + quizCarryRestoreEmbedPhaseLabel(); + others.forEach((o) => { + if (!o) return; + o.botQuizCarryPathfindAfter = 0; + }); + syncQuizCarryEmbedQuestionStrip(); + quizCarryApplyPhaseTimersForCurrentQuestion(); + return; + } + const elapsed = now - quizCarryEmbedCountdownStartAt; + const n = 3 - Math.min(2, Math.floor(elapsed / 1000)); + if (numEl) numEl.textContent = String(Math.max(1, Math.min(3, n))); + if (ov) { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + syncQuizCarryEmbedCountdownLayout(); + hideQuizCarryEmbedCountdownQuestionChrome(); + syncQuizCarryEmbedQuestionStrip(); + } + + function quizCarryPickNextQuestion() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!quizCarryPool.length) { + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + playQuizText = 'ไม่มีคำถาม — ใส่ quizQuestions ในแมป (choices + correctIndex) หรือตั้งใน Admin'; + return; + } + let q; + let guard = 0; + do { + q = quizCarryPool[Math.floor(Math.random() * quizCarryPool.length)]; + guard++; + } while (guard < 12 && quizCarryPool.length > 1 && quizCarryCurrent && q && q.text === quizCarryCurrent.text); + if (previewMode && editorEmbedReturn) { + quizCarryEmbedPendingQuestion = q; + preloadQuizCarryChoiceImages(q); + quizCarryEmbedCountdownStartAt = Date.now(); + quizCarryEmbedCountdownEndAt = quizCarryEmbedCountdownStartAt + 3000; + quizCarryCurrent = null; + playQuizText = ''; + clearQuizMapQuestionCarryFeedback(); + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + o.botPathStuckTicks = 0; + /* ต้องใช้ performance.now() ช่วงกับ stepQuizCarryPreviewBots — ห้ามใช้ Date.now() epoch ไม่งั้นบอทค้าง pathfind ตลอด */ + o.botQuizCarryPathfindAfter = 0; + }); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = '…'; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'รอเริ่มคำถาม — นับถอยหลัง 3 · 2 · 1'; + tickQuizCarryEmbedCountdown(); + syncQuizCarryEmbedQuestionStrip(); + return; + } + quizCarryCurrent = q; + preloadQuizCarryChoiceImages(q); + playQuizText = formatQuizCarryQuestionHud(q); + clearQuizMapQuestionCarryFeedback(); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + }); + quizCarryApplyPhaseTimersForCurrentQuestion(); + } + + /** หลังจบข้อหนึ่งข้อ (ถูกหรือหมดเวลา): พรีวิวนับรอบจนครบ carrySessionLength แล้วจบเซสชัน */ + function quizCarryAfterRoundResolved() { + if (!isQuizCarry()) return; + if (isDetectiveMinigamePlay()) { + finishDetectiveMinigameAndReturnLobby(); + return; + } + if (!previewMode) { + quizCarryPickNextQuestion(); + return; + } + quizCarryRoundsCompleted++; + const lim = quizCarrySessionLength; + if (lim > 0 && quizCarryRoundsCompleted >= lim) { + showQuizCarrySessionCompleteOverlay(); + return; + } + quizCarryPickNextQuestion(); + } + + function playQuizCarryAvatarUrlForMission(characterId) { + if (!characterId) return ''; + return BASE + '/img/characters/' + encodeURIComponent(String(characterId)) + '_down.png'; + } + + /** อวาตาร์สรุปภารกิจ / DOM — ใช้สี gen เดียวกับในเกม (เลเยอร์ + tint) */ + function applyQuizCarryRankAvatarTint(imgEl, characterId, peerId) { + if (!imgEl || !characterId) return; + let attempts = 0; + const fallbackUrl = playQuizCarryAvatarUrlForMission(characterId); + const tick = () => { + attempts++; + const nowT = Date.now(); + const rawImg = getCharacterFrame(characterId, 'down', nowT, false) || getCharacterImg(characterId, 'down'); + if (!rawImg) { + if (fallbackUrl) imgEl.src = fallbackUrl; + return; + } + if ((!rawImg.complete || !rawImg.naturalWidth) && attempts < 28) { + if (fallbackUrl) imgEl.src = fallbackUrl + '?p=' + String(attempts); + setTimeout(tick, 90); + return; + } + if (!rawImg.naturalWidth) { + if (fallbackUrl) imgEl.src = fallbackUrl; + return; + } + const tint = playTintFromPeerId(peerId != null ? String(peerId) : String(characterId)); + const out = getPlayTintedAvatarSource(rawImg, characterId, 'down', nowT, false, tint); + if (out && out.tagName === 'CANVAS' && out.width > 0) { + try { + imgEl.src = out.toDataURL('image/png'); + return; + } catch (e) { /* ต่อไปลอง walk หรือ URL */ } + } + if (out && out.src && out !== rawImg) { + imgEl.src = out.src; + return; + } + if (attempts < 28 && playCharLayerDiscoveryPending(characterId)) { + setTimeout(tick, 120); + return; + } + imgEl.src = rawImg.src || fallbackUrl; + }; + tick(); + } + + /** เกรดจากค่าเฉลี่ยคะแนนทีม (0–100 หลังปัด) — A 80–100, B 60–79, C 0–59 */ + function quizCarryGradeFromTeamAverage(avgRounded) { + if (avgRounded >= 80) return 'A'; + if (avgRounded >= 60) return 'B'; + return 'C'; + } + + function quizCarryRollCardRewardForGrade(grade) { + const u = Math.random() * 100; + if (grade === 'A') { + if (u < 30) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' }; + if (u < 80) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'B') { + if (u < 20) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' }; + if (u < 50) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'C') { + if (u < 20) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + return null; + } + + function quizCarryBuildMissionRankList() { + const ranks = []; + if (myId != null) { + ranks.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + score: Math.max(0, Number(playLiveQuizScores[myId]) || 0), + characterId: me.characterId || getPlayCharacterId(), + }); + } + others.forEach((o, id) => { + if (!o) return; + ranks.push({ + id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id), + score: Math.max(0, Number(playLiveQuizScores[id]) || 0), + characterId: o.characterId || null, + }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + return ranks.slice(0, 5); + } + + function cancelEmbedPreviewLobbyReturnTimer() { + if (embedPreviewLobbyReturnTimer != null) { + clearTimeout(embedPreviewLobbyReturnTimer); + embedPreviewLobbyReturnTimer = null; + } + } + + function cancelQuizCarryResultEndAfterTimeup() { + if (quizCarryResultEndTimer != null) { + clearTimeout(quizCarryResultEndTimer); + quizCarryResultEndTimer = null; + } + cancelEmbedPreviewLobbyReturnTimer(); + } + + /** หลังภาพ result-complete / gameover (editor embed + preview): รอ 5s แล้วไปล็อบบี้ห้องพรีวิว */ + function scheduleEmbedPreviewReturnToLobbyAfterResultEnd() { + cancelEmbedPreviewLobbyReturnTimer(); + if (!(previewMode && editorEmbedReturn)) return; + embedPreviewLobbyReturnTimer = setTimeout(function () { + embedPreviewLobbyReturnTimer = null; + window.location.href = buildRoomLobbyReturnHref(); + }, 5000); + } + + /** มีผู้เล่นอย่างน้อยหนึ่งคนได้คะแนน > 0 (= ส่งคำตอบถูกที่ฮับอย่างน้อย 1 ครั้ง; คะแนนต่อข้อคงที่ QUIZ_CARRY_POINTS_PER_CORRECT) */ + function quizCarryAnyPlayerHasCorrectAnswer() { + if (!playLiveQuizScores || typeof playLiveQuizScores !== 'object') return false; + const keys = Object.keys(playLiveQuizScores); + for (let i = 0; i < keys.length; i++) { + if (Math.max(0, Number(playLiveQuizScores[keys[i]]) || 0) > 0) return true; + } + return false; + } + + function hideQuizCarryResultEndLayer() { + cancelEmbedPreviewLobbyReturnTimer(); + const el = document.getElementById('quiz-carry-result-end-layer'); + if (!el) return; + el.classList.add('is-hidden'); + el.setAttribute('aria-hidden', 'true'); + } + + function showQuizCarryResultEndLayer(useComplete) { + const el = document.getElementById('quiz-carry-result-end-layer'); + const img = document.getElementById('quiz-carry-result-end-img'); + if (!el || !img) return; + const fname = useComplete ? 'result-complete.png' : 'result-gameover.png'; + img.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now()); + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + } + + /** Embed editor: หลัง timeup บนโต๊ะ — รอ 5 วิ แล้วโชว์ result-complete / result-gameover (ใช้ร่วม quiz_carry + crown) */ + function scheduleEmbedDeskResultAfterDelay(okResolver) { + cancelQuizCarryResultEndAfterTimeup(); + hideQuizCarryResultEndLayer(); + const resolve = typeof okResolver === 'function' ? okResolver : function () { return !!okResolver; }; + quizCarryResultEndTimer = setTimeout(function () { + quizCarryResultEndTimer = null; + let anyOk = false; + try { + anyOk = !!resolve(); + } catch (e) { + anyOk = false; + } + hideQuizCarryTimeupOnDeskLayer(); + showQuizCarryResultEndLayer(anyOk); + scheduleEmbedPreviewReturnToLobbyAfterResultEnd(); + }, 5000); + } + + function hideQuizCarryTimeupOnDeskLayer() { + cancelQuizCarryResultEndAfterTimeup(); + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) return; + const anchor = el.querySelector('.qc-timeup-desk-anchor') || el.querySelector('.qc-timeup-desk-inner'); + if (anchor) { + anchor.style.left = ''; + anchor.style.top = ''; + anchor.style.width = ''; + anchor.style.height = ''; + } + el.classList.add('is-hidden'); + el.setAttribute('aria-hidden', 'true'); + } + + /** หลังกดรับหลักฐาน (embed preview) — แสดง timeup กลางแคนวาส (Jumper mnptfts2 = img/Jumper/result-timeup.png) แยกจากป๊อปสรุปคะแนน + * @param {function():boolean} [embedResultOkFn] — ถ้าไม่ส่ง = ใช้คะแนน quiz_carry (คนตอบถูก); ส่งเมื่อ crown เพื่อเลือก complete vs gameover */ + function showQuizCarryTimeupOnDeskLayer(embedResultOkFn) { + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) return; + const img = document.getElementById('quiz-carry-timeup-txt-img'); + if (img) { + if (isJumpSurviveMissionUiMapPlay()) { + img.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + }; + img.src = jumperAssetUrl('result-timeup.png') + '?v=' + String(Date.now()); + } else { + img.onerror = null; + img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + } + } + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + try { + syncQuizCarryTimeupDeskLayerPosition(); + } catch (e) { /* ignore */ } + if (previewMode && editorEmbedReturn) { + scheduleEmbedDeskResultAfterDelay(embedResultOkFn || quizCarryAnyPlayerHasCorrectAnswer); + } + } + + function showQuizCarryMissionSummaryOverlay(opts) { + opts = opts || {}; + const forceF = !!opts.forceGradeF; + const overlay = document.getElementById('quiz-carry-mission-overlay'); + if (!overlay) return; + hideQuizCarryTimeupOnDeskLayer(); + const ranks = quizCarryBuildMissionRankList(); + const parts = ranks.map((r) => Math.max(0, Number(r.score) || 0)); + const total = parts.reduce((a, b) => a + b, 0); + const n = ranks.length; + const avgForGrade = n ? Math.min(100, Math.max(0, Math.round(total / n))) : 0; + const grade = forceF ? 'F' : quizCarryGradeFromTeamAverage(avgForGrade); + const reward = grade === 'F' ? null : quizCarryRollCardRewardForGrade(grade); + + const rowEl = document.getElementById('qc-mission-rank-row'); + const totalEl = document.getElementById('qc-mission-total-line'); + const gradeEl = document.getElementById('qc-mission-grade'); + const cardTextEl = document.getElementById('qc-mission-card-text'); + const checkEl = document.getElementById('qc-mission-check'); + const successTxt = document.getElementById('qc-mission-success-txt'); + const rankTagLabels = ['[1st]', '[2nd]', '[3rd]', '[4th]', '[5th]']; + if (rowEl) { + rowEl.innerHTML = ''; + ranks.forEach((r, idx) => { + const rank = idx + 1; + const cell = document.createElement('div'); + cell.className = 'qc-mission-rank-cell'; + const tag = document.createElement('div'); + tag.className = 'qc-mission-rank-tag'; + tag.textContent = rankTagLabels[rank - 1] || ('[' + rank + ']'); + const frame = document.createElement('div'); + frame.className = 'qc-mission-rank-frame'; + const img = document.createElement('img'); + img.className = 'qc-mission-rank-avatar'; + img.alt = ''; + applyQuizCarryRankAvatarTint(img, r.characterId, r.id); + img.onerror = function () { + this.onerror = null; + this.src = 'data:image/svg+xml,' + encodeURIComponent('?'); + }; + frame.appendChild(img); + const nick = document.createElement('div'); + nick.className = 'qc-mission-rank-nick'; + nick.textContent = r.nickname; + nick.title = r.nickname; + const sc = document.createElement('div'); + sc.className = 'qc-mission-rank-score'; + sc.textContent = String(r.score); + cell.appendChild(tag); + cell.appendChild(frame); + cell.appendChild(nick); + cell.appendChild(sc); + rowEl.appendChild(cell); + }); + } + if (totalEl) { + totalEl.textContent = ''; + if (!n) { + totalEl.textContent = '—'; + } else { + const lab = document.createElement('span'); + lab.textContent = 'คะแนนรวม '; + const nums = document.createElement('span'); + nums.className = 'qc-mission-total-nums'; + nums.textContent = '(' + parts.join('+') + ') = ' + total; + const tail = document.createElement('span'); + tail.textContent = ' · เฉลี่ยทีม ' + avgForGrade + '/100'; + totalEl.appendChild(lab); + totalEl.appendChild(nums); + totalEl.appendChild(tail); + } + } + if (gradeEl) { + gradeEl.textContent = grade; + gradeEl.className = 'qc-mission-grade qc-mission-grade--' + String(grade).toLowerCase(); + } + const okMission = grade !== 'F'; + if (checkEl) checkEl.style.display = okMission ? '' : 'none'; + if (successTxt) { + successTxt.style.display = okMission ? '' : 'none'; + if (okMission) successTxt.textContent = 'ภารกิจสำเร็จ'; + } + if (cardTextEl) { + cardTextEl.textContent = ''; + if (grade === 'F') { + const pl = document.createElement('div'); + pl.className = 'qc-mission-reward-plaque qc-mission-reward-plaque--muted'; + const t = document.createElement('div'); + t.className = 'qc-mission-reward-plaque-title'; + t.textContent = 'ไม่ได้รับการ์ด'; + const s = document.createElement('div'); + s.className = 'qc-mission-reward-plaque-sub'; + s.textContent = 'เกรด F'; + pl.appendChild(t); + pl.appendChild(s); + cardTextEl.appendChild(pl); + } else if (reward) { + const pl = document.createElement('div'); + pl.className = 'qc-mission-reward-plaque'; + const t = document.createElement('div'); + t.className = 'qc-mission-reward-plaque-title'; + t.textContent = reward.th; + const s = document.createElement('div'); + s.className = 'qc-mission-reward-plaque-sub'; + s.textContent = reward.en; + pl.appendChild(t); + pl.appendChild(s); + cardTextEl.appendChild(pl); + } + } + overlay.classList.remove('is-hidden'); + const btn = document.getElementById('btn-quiz-carry-mission-done'); + if (btn) { + btn.onclick = function () { + overlay.classList.add('is-hidden'); + if (previewMode && editorEmbedReturn) { + showQuizCarryTimeupOnDeskLayer(); + } else { + window.location.href = buildRoomLobbyReturnHref(); + } + }; + } + } + + function showQuizCarrySessionCompleteOverlay() { + if (quizCarrySessionEnded) return; + quizCarrySessionEnded = true; + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + playQuizText = 'ครบชุดคำถามแล้ว'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'ครบ ' + String(quizCarryRoundsCompleted) + ' ข้อ'; + const missionOv = document.getElementById('quiz-carry-mission-overlay'); + if (missionOv && quizCarryUseMissionSummaryOverlay) { + showQuizCarryMissionSummaryOverlay({ + forceGradeF: !!(playQuizPlayerLocal && playQuizPlayerLocal.eliminated), + }); + renderPlayQuizScoreboard(playLiveQuizScores); + return; + } + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) titleEl.textContent = 'เกมจบ · Game over'; + msgEl.textContent = 'ครบจำนวนคำถามที่ตั้งไว้ (' + String(quizCarryRoundsCompleted) + ' ข้อ) · Quiz carry session finished'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + score: Math.max(0, Number(playLiveQuizScores[myId]) || 0), + }); + } + others.forEach((o, id) => { + if (!o) return; + ranks.push({ + id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id), + score: Math.max(0, Number(playLiveQuizScores[id]) || 0), + }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + renderPlayQuizScoreboard(playLiveQuizScores); + } + + /** Toast กลางบน (หยิบ / ถูก / ผิด / คำใบ้) — ปิดการแสดงตามคำขอ UX */ + function showQuizCarryToast(msg, ok) { + if (typeof window.__quizCarryToastT === 'number') clearTimeout(window.__quizCarryToastT); + window.__quizCarryToastT = null; + const el = document.getElementById('play-quiz-feedback'); + if (!el) return; + el.classList.add('is-hidden'); + el.className = 'play-quiz-feedback'; + el.textContent = ''; + } + + /** + * หยิบ/ส่งคำตอบ — ต้องกด F (fromKey) สำหรับผู้เล่น · บอท preview เรียกแบบ silent เมื่อหยุดเดินแล้ว + * @returns {boolean} ทำ action สำเร็จหรือไม่ + */ + function tryQuizCarryInteractionForPlayer(actorId, ox, oy, opts) { + opts = opts || {}; + if (quizCarryPregameActive) return false; + if (quizCarrySessionEnded) return false; + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return false; + const md = mapData; + const footprint = quizTilesFootprintPlay(ox, oy); + const ent = actorId === myId ? me : others.get(actorId); + if (!ent) return false; + let pickupIdx = null; + for (const k of footprint) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + const opt = quizCarryOptionIndexAtPlay(md, tx, ty); + if (opt != null) pickupIdx = pickupIdx === null ? opt : pickupIdx; + } + const canSubmitAnswer = quizCarryCanSubmitAnswerAt(md, ox, oy); + const correct = quizCarryCurrent.correctIndex; + let held = ent.quizCarryHeld; + if (held != null && canSubmitAnswer) { + if (!quizCarryOptionsPickableNow()) return false; + ent.quizCarryHeld = null; + const ok = held === correct; + if (ok) { + playLiveQuizScores[actorId] = (playLiveQuizScores[actorId] || 0) + QUIZ_CARRY_POINTS_PER_CORRECT; + const lim = previewMode && isQuizCarry() ? quizCarrySessionLength : 0; + const willEnd = lim > 0 && (quizCarryRoundsCompleted + 1) >= lim; + const showMapFb = !opts.silent && actorId === myId; + if (showMapFb) showQuizMapQuestionCarryFeedback(true); + if (!opts.silent) { + const pts = QUIZ_CARRY_POINTS_PER_CORRECT; + showQuizCarryToast(willEnd ? ('ถูกต้อง +' + pts + ' แต้ม — ครบชุดคำถาม') : ('ถูกต้อง +' + pts + ' แต้ม — สุ่มคำถามใหม่'), true); + } + me.quizCarryHeld = null; + others.forEach((o) => { if (o) o.quizCarryHeld = null; }); + const afterOk = () => { quizCarryAfterRoundResolved(); }; + if (showMapFb) { + window.setTimeout(afterOk, 560); + } else { + afterOk(); + } + } else { + if (!opts.silent && actorId === myId) { + showQuizMapQuestionCarryFeedback(false); + } + if (!opts.silent) { + showQuizCarryToast('ผิด — คืนป้ายที่โซนตัวเลือกแล้ว กด F หยิบใหม่ได้', false); + } + } + renderPlayQuizScoreboard(playLiveQuizScores); + return true; + } + if (held == null && pickupIdx != null) { + if (!quizCarryOptionsPickableNow()) return false; + if (quizCarryOptionHeldByAnyone(pickupIdx)) { + if (opts.fromKey && actorId === myId && !opts.silent) { + showQuizCarryToast('ตัวเลือกนี้มีคนถืออยู่แล้ว — เลือกข้ออื่น', false); + } + return false; + } + ent.quizCarryHeld = pickupIdx; + const label = (quizCarryCurrent.choices && quizCarryCurrent.choices[pickupIdx]) || String(pickupIdx); + if (!opts.silent) { + showQuizCarryToast('หยิบ: ' + label, true); + } + return true; + } + if (opts.fromKey && actorId === myId && !opts.silent) { + const t = Date.now(); + if (t - (me.quizCarryHintT || 0) > 2200) { + me.quizCarryHintT = t; + if (held != null && !canSubmitAnswer) { + showQuizCarryToast(quizCarryMapHasAnswerInteractive(md) + ? 'ถือป้ายอยู่ — ยืนที่โซน interactive (เขียว) หรือชิดขอบแล้วกด F เพื่อส่ง' + : 'ถือป้ายอยู่ — ยืนชิดโซนกลาง (ม่วง) แล้วกด F เพื่อส่ง', false); + } else { + showQuizCarryToast(quizCarryMapHasAnswerInteractive(md) + ? 'ยืนโซนตัวเลือกแล้วกด F หยิบ · ยืนโซนเขียว (interactive) แล้วกด F ส่งเมื่อถือป้าย' + : 'ยืนโซนตัวเลือกแล้วกด F หยิบ · ยืนชิดโซนกลางแล้วกด F ส่งเมื่อถือป้าย', false); + } + } + } + return false; + } + + function quizCarryGrabCoreGatesOk() { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return false; + if (quizCarryPregameActive || quizCarrySessionEnded) return false; + if (myId == null) return false; + if (isQuizCarryEmbedCountdownBlockingMovement()) return false; + if (!quizCarryOptionsPickableNow()) return false; + return true; + } + + /** ดัชนีตัวเลือก 0-based ที่ผู้เล่นท้องถิ่นยืนอยู่และหยิบได้ — ไม่มีคืน null */ + function getQuizCarryLocalGrabbableOptionIndex() { + if (!quizCarryGrabCoreGatesOk()) return null; + if (me.quizCarryHeld != null) return null; + const md = mapData; + const footprint = quizTilesFootprintPlay(me.x, me.y); + let pickupIdx = null; + for (const k of footprint) { + const p = k.split(','); + const tx = +p[0]; + const ty = +p[1]; + const opt = quizCarryOptionIndexAtPlay(md, tx, ty); + if (opt != null) pickupIdx = pickupIdx === null ? opt : pickupIdx; + } + if (pickupIdx == null || quizCarryOptionHeldByAnyone(pickupIdx)) return null; + return pickupIdx; + } + + /** ยืนบนช่องตัวเลือกและหยิบได้ */ + function quizCarryGrabPickupAvailable() { + return getQuizCarryLocalGrabbableOptionIndex() != null; + } + + /** ถือป้ายแล้วยืนโซนส่งได้ */ + function quizCarryGrabSubmitAvailable() { + if (!quizCarryGrabCoreGatesOk()) return false; + if (me.quizCarryHeld == null) return false; + return quizCarryCanSubmitAnswerAt(mapData, me.x, me.y); + } + + /** true = กด F / ปุ่ม จะหยิบหรือส่งได้ทันที */ + function quizCarryGrabInteractionAvailable() { + return quizCarryGrabPickupAvailable() || quizCarryGrabSubmitAvailable(); + } + + function syncQuizCarryGrabButton() { + const btn = document.getElementById('quiz-carry-grab-btn'); + if (!btn) return; + if (!isQuizCarry() || !mapData) { + btn.classList.add('is-hidden'); + btn.classList.remove('quiz-carry-grab-btn--active'); + btn.classList.remove('quiz-carry-grab-btn--place'); + btn.setAttribute('aria-disabled', 'true'); + return; + } + const grabSrc = BASE + '/img/quiz-carry/btn-grab.png'; + const placeSrc = BASE + '/img/quiz-carry/btn-drop.png'; + const img = btn.querySelector('img'); + btn.classList.remove('is-hidden'); + const active = quizCarryGrabInteractionAvailable(); + btn.classList.toggle('quiz-carry-grab-btn--active', active); + btn.setAttribute('aria-disabled', active ? 'false' : 'true'); + btn.style.pointerEvents = active ? '' : 'none'; + if (img) { + if (active && quizCarryGrabSubmitAvailable()) { + img.src = placeSrc; + btn.classList.add('quiz-carry-grab-btn--place'); + btn.title = 'ส่ง / วางคำตอบ — เหมือนกด F'; + btn.setAttribute('aria-label', 'ส่งหรือวางคำตอบ'); + } else { + img.src = grabSrc; + btn.classList.remove('quiz-carry-grab-btn--place'); + btn.title = 'หยิบตัวเลือก — เหมือนกด F'; + btn.setAttribute('aria-label', 'หยิบตัวเลือก (Grab)'); + } + } + } + + function resetQuizCarryPlayState() { + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryPool = []; + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownStartAt = 0; + quizCarryEmbedCountdownEndAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + quizCarryRoundsCompleted = 0; + quizCarrySessionEnded = false; + hideQuizCarryEmbedCountdownOverlay(); + hideQuizCarryTimeupOnDeskLayer(); + hideQuizCarryResultEndLayer(); + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + } + + /** รวม carry จากดิสก์ — ใช้เฉพาะ GET ที่ Node รองรับ (path เดียวกับ server.js) ไม่เรียก .php เพื่อกัน 404 บน nginx/php-fpm */ + async function mergeQuizCarrySettingsFromDisk(s) { + const out = s && typeof s === 'object' ? s : {}; + try { + const rd = await fetch(BASE + '/api/quiz-carry-from-disk?_=' + Date.now(), { cache: 'no-store' }); + if (rd.ok) { + const disk = await rd.json(); + if (disk && typeof disk === 'object') { + if (disk.carryMapPanelTheme && typeof disk.carryMapPanelTheme === 'object') { + out.carryMapPanelTheme = disk.carryMapPanelTheme; + } + if (disk.carryEmbedCountdownTheme && typeof disk.carryEmbedCountdownTheme === 'object') { + out.carryEmbedCountdownTheme = disk.carryEmbedCountdownTheme; + } + if (Array.isArray(disk.carryChoicePlaqueThemes) && disk.carryChoicePlaqueThemes.length) { + out.carryChoicePlaqueThemes = disk.carryChoicePlaqueThemes; + } else if (disk.carryChoicePlaqueTheme && typeof disk.carryChoicePlaqueTheme === 'object') { + out.carryChoicePlaqueTheme = disk.carryChoicePlaqueTheme; + } + if (disk.carryReadMs != null) out.carryReadMs = disk.carryReadMs; + if (disk.carryAnswerMs != null) out.carryAnswerMs = disk.carryAnswerMs; + if (disk.carrySessionLength != null) out.carrySessionLength = disk.carrySessionLength; + const diskPlaqueScale = Number(disk.carryChoicePlaqueMapScale); + if (Number.isFinite(diskPlaqueScale)) { + out.carryChoicePlaqueMapScale = Math.max(0.85, Math.min(2.5, diskPlaqueScale)); + } + if (Array.isArray(disk.carryQuestions) && disk.carryQuestions.length > 0) { + out.carryQuestions = disk.carryQuestions; + } + } + } + } catch (e) { /* ignore */ } + return out; + } + + async function loadQuizCarryPoolAndStart() { + quizCarryRoundsCompleted = 0; + quizCarrySessionEnded = false; + hideQuizCarryTimeupOnDeskLayer(); + hideQuizCarryResultEndLayer(); + quizCarrySessionLength = 0; + let pool = []; + let s = {}; + const snap = quizCarryJoinSettingsSnap && typeof quizCarryJoinSettingsSnap === 'object' ? quizCarryJoinSettingsSnap : null; + try { + const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' }); + if (r.ok) { + const raw = await r.text(); + const trimmed = (raw || '').trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + const parsed = JSON.parse(trimmed); + if (parsed && typeof parsed === 'object') s = parsed; + } + } + } catch (e) { /* Node/HTML error — ต่อจาก merge ดิสก์ */ } + try { + s = await mergeQuizCarrySettingsFromDisk(s); + } catch (e) { /* ignore */ } + if (snap) { + if (s.carryReadMs == null && snap.carryReadMs != null) s.carryReadMs = snap.carryReadMs; + if (s.carryAnswerMs == null && snap.carryAnswerMs != null) s.carryAnswerMs = snap.carryAnswerMs; + if (s.carrySessionLength == null && snap.carrySessionLength != null) s.carrySessionLength = snap.carrySessionLength; + } + if (snap && snap.carryMapPanelTheme && typeof snap.carryMapPanelTheme === 'object') { + s.carryMapPanelTheme = snap.carryMapPanelTheme; + } + if (snap && snap.carryEmbedCountdownTheme && typeof snap.carryEmbedCountdownTheme === 'object') { + s.carryEmbedCountdownTheme = snap.carryEmbedCountdownTheme; + } + /* ไม่ทับ carryChoicePlaqueThemes จาก snap ถ้ามีแล้ว — snapshot ตอน join อาจเก่ากว่าไฟล์ที่เพิ่งบันทึกใน Admin (รูปป้ายหาย) */ + if (snap && (!Array.isArray(s.carryChoicePlaqueThemes) || !s.carryChoicePlaqueThemes.length)) { + if (Array.isArray(snap.carryChoicePlaqueThemes) && snap.carryChoicePlaqueThemes.length) { + s.carryChoicePlaqueThemes = snap.carryChoicePlaqueThemes; + } else if (snap.carryChoicePlaqueTheme && typeof snap.carryChoicePlaqueTheme === 'object') { + s.carryChoicePlaqueTheme = snap.carryChoicePlaqueTheme; + } + } + if (snap && (s.carryChoicePlaqueMapScale == null || !Number.isFinite(Number(s.carryChoicePlaqueMapScale)))) { + const sm = Number(snap.carryChoicePlaqueMapScale); + if (Number.isFinite(sm)) s.carryChoicePlaqueMapScale = sm; + } + { + const sc = Number(s.carryChoicePlaqueMapScale); + quizCarryPlaqueMapScale = Number.isFinite(sc) ? Math.max(0.85, Math.min(2.5, sc)) : 1.25; + } + try { + quizCarryCarryTimingMs = { + carryReadMs: clampPreviewMs(s.carryReadMs, 3000, 0, 120000), + carryAnswerMs: clampPreviewMs(s.carryAnswerMs, 5000, 1000, 300000), + }; + quizCarrySessionLength = clampCarrySessionLen(s.carrySessionLength, 0); + setQuizCarryMapPanelThemeFromApi(s); + setQuizCarryEmbedCountdownThemeFromApi(s); + setQuizCarryChoicePlaqueThemeFromApi(s); + for (let ti = 0; ti < QUIZ_CARRY_MAX_OPTION_SLOTS; ti++) { + const tUrl = sanitizeQuizCarryImageUrlClient(getEffectiveCarryChoicePlaqueThemeForChoice(ti).plaqueImageUrl); + if (!tUrl || quizCarryChoiceImageCache.has(tUrl)) continue; + const pim = new Image(); + quizCarryApplyImageCrossOrigin(pim, tUrl); + pim.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(tUrl, pim); + try { + pim.src = tUrl; + } catch (e) { /* ignore */ } + } + const carryOnly = s && Array.isArray(s.carryQuestions) && s.carryQuestions.length > 0; + const source = carryOnly ? s.carryQuestions : (s && Array.isArray(s.questions) ? s.questions : []); + for (let i = 0; i < source.length; i++) { + const n = normalizeQuizCarryQuestionFromAny(source[i]); + if (n) pool.push(n); + } + } catch (e) { /* map only */ } + if (!pool.length && mapData) pool = buildQuizCarryPoolFromMap(mapData); + quizCarryPool = pool; + for (let pi = 0; pi < pool.length; pi++) preloadQuizCarryChoiceImages(pool[pi]); + playLiveQuizScores = {}; + if (myId != null) playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + if (!pool.length) { + playQuizText = 'ไม่มีคำถาม — ตั้งใน Admin → คำถามหลายตัวเลือก หรือใส่ในแมป (quizQuestions)'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + try { + syncPlayQuizMapPanel(); + } catch (e) { /* ignore */ } + quizCarryJoinSettingsSnap = null; + return; + } + if (previewMode && editorEmbedReturn) { + beginQuizCarryEmbedPregame(); + } else if (isDetectiveMinigamePlay()) { + quizCarryPickNextQuestion(); + } else { + quizCarryPickNextQuestion(); + } + if (!(previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now())) { + quizCarryRestoreEmbedPhaseLabel(); + } + try { + syncPlayQuizMapPanel(); + } catch (e) { /* ignore */ } + quizCarryJoinSettingsSnap = null; + } + + function setupPlayQuizCarryUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) { + if (isDetectiveMinigamePlay() || (previewMode && editorEmbedReturn)) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } else { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + } + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + const leg = document.getElementById('quiz-play-legend'); + if (leg) { + leg.textContent = isDetectiveMinigamePlay() + ? '' + : (quizCarryMapHasAnswerInteractive(mapData) + ? 'แต่ละตัวเลือกมีคนถือได้คนเดียว — ส่งคำตอบที่โซน interactive (เขียว) · โซนกลางม่วงเป็นกำแพง · กด F หรือปุ่ม GRAB มุมขวาล่าง' + : 'แต่ละตัวเลือกมีคนถือได้คนเดียว — โซนกลางเป็นกำแพง · ยืนชิดขอบแล้วกด F หรือปุ่ม GRAB ส่งเมื่อถือป้าย'); + } + resetQuizCarryPlayState(); + if (previewMode) { + loadQuizCarryPoolAndStart(); + } else { + playQuizText = 'เข้าห้องแล้ว — คำถามจากแมป (เล่นจริงแนะนำซิงก์ผ่านโฮสต์ในอนาคต)'; + loadQuizCarryPoolAndStart(); + } + renderPlayQuizScoreboard(playLiveQuizScores); + syncQuizCarryGrabButton(); + } + + function pickRandomTileForQuizCarryOption(md, optionIndex, o) { + const w = md.width || 20, h = md.height || 15; + const want = optionIndex + 1; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.quizCarryOptionArea && md.quizCarryOptionArea[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === want && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileInQuizCarryHub(md, o) { + const w = md.width || 20, h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.quizCarryHubArea && md.quizCarryHubArea[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1 && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileInQuizCarryInteractive(md, o) { + const w = md.width || 20, h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.interactive && md.interactive[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1 && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileForQuizCarrySubmit(md, o) { + if (quizCarryMapHasAnswerInteractive(md)) { + const p = pickRandomTileInQuizCarryInteractive(md, o); + if (p) return p; + } + return pickRandomTileInQuizCarryHub(md, o); + } + + /** ช่วงไม่มีข้อ / รอตัวเลือก — ให้บอทเดินเล่นในแมปเหมือน preview ทั่วไป (กันยืนนิ่งทั้งเกม) */ + function quizCarryPreviewBotLobbyWanderStep(o, w, h) { + const now = Date.now(); + o.botIsWalking = false; + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600; + if (now >= o.botWanderNextTurn) { + o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200); + if (Math.random() < 0.55) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + } + const accX = o.botWanderDx; + const accY = o.botWanderDy; + if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up'; + else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left'; + const step = MOVE_SPEED; + const nx = o.x + accX * step; + const ny = o.y + accY * step; + const ox = o.x; + const oy = o.y; + if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + } + if (!Number.isFinite(o.x)) o.x = 0.5; + if (!Number.isFinite(o.y)) o.y = 0.5; + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) o.botIsWalking = true; + } + + function stepQuizCarryPreviewBots() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!playBotsEnabled() || !isQuizCarry() || !mapData) return; + const w = mapData.width || 20, h = mapData.height || 15; + const nowPf = performance.now(); + const pathfindMinGap = editorEmbedReturn ? 280 : 140; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + if (quizCarryFootprintOverlapsHub(o.x, o.y)) { + const sn = snapPositionOutOfQuizCarryHubIfNeeded(o.x, o.y); + if (Math.abs(sn.x - o.x) > 0.02 || Math.abs(sn.y - o.y) > 0.02) { + o.x = sn.x; + o.y = sn.y; + o.botPath = []; + o.botPathStuckTicks = 0; + o.botQuizCarryPathfindAfter = 0; + } + } + if (!o.botPath || o.botPath.length === 0) { + tryQuizCarryInteractionForPlayer(id, o.x, o.y, { silent: true }); + } + /* ช่วงนับถอยหลัง / เปลี่ยนข้อ — ยังให้เดินได้ */ + if (!quizCarryCurrent) { + quizCarryPreviewBotLobbyWanderStep(o, w, h); + return; + } + const tier = o.botTier || 'avg'; + const pCorrect = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.35 : 0.65; + const rawWant = Math.random() < pCorrect ? quizCarryCurrent.correctIndex + : Math.floor(Math.random() * Math.max(2, (quizCarryCurrent.choices || []).length)); + const wantIdx = pickQuizCarryTargetOptionIndexAvoidingTaken(rawWant); + if (!o.botPath || !o.botPath.length) { + if (typeof o.botQuizCarryPathfindAfter !== 'number') o.botQuizCarryPathfindAfter = 0; + if (nowPf < o.botQuizCarryPathfindAfter) { + quizCarryPreviewBotLobbyWanderStep(o, w, h); + return; + } + const jitter = (String(id).length * 31 + ((o.x | 0) ^ (o.y | 0)) * 7) % 160; + if (quizCarryOptionsPickableNow()) { + if (o.quizCarryHeld == null && wantIdx != null) { + const dest = pickRandomTileForQuizCarryOption(mapData, wantIdx, o); + if (dest) { + const path = pathfindPlayForBot(o.x, o.y, dest.x + 0.5, dest.y + 0.5, o); + if (path && path.length > 1) o.botPath = path.slice(1); + } + } else { + const sub = pickRandomTileForQuizCarrySubmit(mapData, o); + if (sub) { + const path = pathfindPlayForBot(o.x, o.y, sub.x + 0.5, sub.y + 0.5, o); + if (path && path.length > 1) o.botPath = path.slice(1); + } + } + o.botQuizCarryPathfindAfter = nowPf + pathfindMinGap + jitter; + } else { + o.botQuizCarryPathfindAfter = nowPf + 120; + quizCarryPreviewBotLobbyWanderStep(o, w, h); + } + } else { + stepPreviewBotAlongPath(o, w, h); + } + clampPlayEntityFootprintToMap(o, mapData); + }); + separateClumpedPreviewBots(); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const ob = others.get(bid); + if (ob) clampPlayEntityFootprintToMap(ob, mapData); + }); + } + + function getStackAreaBoundsPlay(area, w, h) { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let any = false; + for (let y = 0; y < h; y++) { + const row = area && area[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1) { + any = true; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + } + if (!any) return null; + return { + minX, maxX, minY, maxY, + cx: (minX + maxX + 1) / 2, + cy: (minY + maxY + 1) / 2, + }; + } + + function getStackCameraCentersPx() { + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const rel = getStackAreaBoundsPlay(mapData.stackReleaseArea, w, h); + let cx = (w / 2) * ts; + let cy = (h / 2) * ts; + if (land && rel) { + cx = land.cx * ts; + cy = ((land.cy + rel.cy) / 2) * ts; + } else if (land) { + cx = land.cx * ts; + cy = land.cy * ts; + } else if (rel) { + cx = rel.cx * ts; + cy = rel.cy * ts; + } + return { px: cx, py: cy }; + } + + /** ใช้ playStackBlockWidthTiles + แผนที่ — เรียกหลังโหลด /api/game-timing เมื่อหอว่าง */ + function reapplyStackMiniSizingFromGlobals() { + if (!mapData || !isStack() || !stackMini) return; + if (stackMini.layers && stackMini.layers.length > 0) return; + if (stackFall || stackMini.settling) return; + const wMap = mapData.width || 20; + const hMap = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap); + const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2)); + const iw = playStackBlockWidthTiles != null && Number.isFinite(playStackBlockWidthTiles) + ? Math.max(0.85, Math.min(3.2, playStackBlockWidthTiles)) + : autoW; + stackMini.initialWidthTiles = iw; + stackMini.widthTiles = iw; + } + + function resetStackMinigameState() { + stackMini = null; + if (!mapData || !isStack()) return; + const w = mapData.width || 20, h = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const rel = getStackAreaBoundsPlay(mapData.stackReleaseArea, w, h); + let towerCX = w * 0.5; + let swingAmp = 1.85; + let floorWorldY = (h - 1) * tileSize; + let swingWorldY = floorWorldY - tileSize * 1.35; + if (land) { + towerCX = land.cx; + floorWorldY = (land.maxY + 1) * tileSize; + const span = Math.max(1, land.maxX - land.minX + 1); + swingAmp = Math.min(span * 0.44, Math.max(0.6, span * 0.36)); + } + if (rel) { + swingWorldY = (rel.cy + 0.5) * tileSize; + const spanR = Math.max(1, rel.maxX - rel.minX + 1); + swingAmp = Math.max(swingAmp, Math.min(spanR * 0.42, 4.2)); + } + const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2)); + const initW = playStackBlockWidthTiles != null && Number.isFinite(playStackBlockWidthTiles) + ? Math.max(0.85, Math.min(3.2, playStackBlockWidthTiles)) + : autoW; + stackFall = null; + stackMini = { + topCenterX: towerCX, + widthTiles: initW, + initialWidthTiles: initW, + craneWorldX: towerCX * tileSize, + swingAmp, + phase: Math.random() * Math.PI * 2, + phaseSpeed: playStackSwingHz, + floorWorldY, + swingWorldY, + layerWorldH: Math.max(14, tileSize * 0.3), + blockStripH: Math.max(16, tileSize * 0.32), + layers: [], + lives: 3, + score: 0, + combo: 0, + lastDropAt: 0, + stackTiltRad: 0, + stackTiltVel: 0, + }; + if (previewFillBots) { + me.stackPreviewHumanPts = 0; + stackPreviewHudLog = []; + } + } + + function stackEaseDrop(t) { + t = Math.max(0, Math.min(1, t)); + return t * t; + } + + /** ตกช้า/นุ่มขึ้นเมื่อปล่อยเพี้ยนกลาง (คล้าย Tower of Babel — มีน้ำหนักก่อนนิ่ง) */ + function stackEaseDropPhys(t, sloppyN) { + t = Math.max(0, Math.min(1, t)); + if (sloppyN < 0.14) return t * t; + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; + } + + /** + * @param {object} hit + * @param {{ human?: boolean, botId?: string } | undefined} previewAttribution — โหมด preview: แยกคะแนนแสดงในแถบ (ตึกยังเป็นของร่วม) + */ + function applyStackHitFromDrop(hit, previewAttribution) { + if (!stackMini || !hit) return; + if (hit.miss) { + stackMini.lives = Math.max(0, stackMini.lives - 1); + stackMini.combo = 0; + if (stackMini.lives <= 0) { + stackMini.score = Math.max(0, stackMini.score - 2); + stackMini.lives = 3; + stackMini.layers = []; + stackMini.widthTiles = stackMini.initialWidthTiles; + const lb = getStackAreaBoundsPlay(mapData.stackLandArea, mapData.width || 20, mapData.height || 15); + stackMini.topCenterX = lb ? lb.cx : (mapData.width || 20) * 0.5; + stackMini.combo = 0; + stackMini.stackTiltRad = 0; + stackMini.stackTiltVel = 0; + } + } else { + const W = stackMini.initialWidthTiles; + stackMini.layers.push({ w: W, cx: hit.placedCx }); + stackMini.score += hit.pts; + if (previewFillBots && previewAttribution) { + if (previewAttribution.human) { + me.stackPreviewHumanPts = (me.stackPreviewHumanPts || 0) + hit.pts; + } else if (previewAttribution.botId) { + const ob = others.get(previewAttribution.botId); + if (ob) ob.stackBotScore = (ob.stackBotScore || 0) + hit.pts; + } + } + stackMini.combo = hit.perfect ? stackMini.combo + 1 : 0; + stackMini.widthTiles = W; + stackMini.topCenterX = hit.placedCx; + } + } + + /** + * บล็อกทุกชั้นหน้ากว้างเท่ากัน (initialWidthTiles) · พลาดเมื่อทับแท่นไม่พอ + * วาดชั้นเป็นสี่เหลี่ยมแนวนอน (ไม่หมุนต่อชั้น) เพื่อกองตรง ไม่บิดเป็นกองไม้ + */ + function computeStackDropResult(m) { + const raw = m.swingAmp * Math.sin(m.phase); + const wMap = mapData.width || 20; + const hMap = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap); + const fallW = m.initialWidthTiles; + let supportCx; + let supportW; + if (m.layers.length === 0) { + supportCx = land ? land.cx : wMap * 0.5; + supportW = land ? (land.maxX - land.minX + 1) : Math.max(8, fallW * 2.2); + } else { + const sup = m.layers[m.layers.length - 1]; + supportCx = sup.cx; + supportW = fallW; + } + const c1 = m.topCenterX + raw; + const half1 = fallW * 0.5; + const half0 = supportW * 0.5; + const left = Math.max(supportCx - half0, c1 - half1); + const right = Math.min(supportCx + half0, c1 + half1); + const overlap = right - left; + const missThreshold = + m.layers.length === 0 + ? Math.max(0.32, fallW * 0.11) + : Math.max(0.26, fallW * 0.17); + const miss = overlap < missThreshold; + const placedCx = c1; + const delta = c1 - supportCx; + const span = supportW * 0.5 + fallW * 0.5; + const offN = Math.abs(delta) / Math.max(0.01, span); + const perfect = !miss && offN < 0.075; + const bonus = perfect ? Math.min(4, m.combo || 0) : 0; + const pts = perfect ? (2 + bonus) : (miss ? 0 : 1); + const supportRatio = fallW > 0 ? overlap / fallW : 0; + return { + miss, + perfect, + pts, + landOffsetTiles: raw, + placedW: fallW, + placedCx, + overlap, + supportRatio, + delta, + }; + } + + /** + * เริ่มแอนิเมชันบล็อกร่วง (ผู้เล่น/บอทใช้ path เดียวกัน) + * @param {object} hit จาก computeStackDropResult + * @param {{ human?: boolean, botId?: string } | null} previewActor โหมด preview: ใครเป็นคนปล่อย (สำหรับคะแนน + เลื่อนตา) + * @returns {boolean} + */ + function startStackFallAnimation(hit, previewActor) { + if (!stackMini || !hit || stackFall || stackMini.settling) return false; + const m = stackMini; + const tw = m.widthTiles * tileSize; + const swingXW = (m.topCenterX + m.swingAmp * Math.sin(m.phase)) * tileSize; + const lh = m.layerWorldH || Math.max(14, tileSize * 0.3); + let y1 = m.floorWorldY - (m.layers.length + 1) * lh; + if (y1 <= m.swingWorldY) y1 = m.swingWorldY + lh * 0.55; + const landOff = hit.landOffsetTiles || 0; + const landCxWorld = (m.topCenterX + landOff) * tileSize; + const xLeft0 = swingXW - tw / 2; + const xLeft1 = landCxWorld - tw / 2; + const offN = Math.min(1, Math.abs(landOff) / Math.max(0.01, m.swingAmp)); + const dur = Math.max(400, Math.min(1280, 240 + (y1 - m.swingWorldY) * 0.92 + offN * 560)); + const tiltMax = hit.miss + ? 0.28 * (0.25 + offN) + : 0.16 * offN * (hit.perfect ? 0.1 : 1); + const isHumanPreview = !!(previewActor && previewActor.human); + const botIdPreview = previewActor && previewActor.botId ? previewActor.botId : null; + stackFall = { + t0: performance.now(), + dur, + xLeft0, + xLeft1, + tw, + y0: m.swingWorldY, + y1, + hit, + tiltMax, + sloppyN: offN, + previewHumanDrop: isHumanPreview, + previewBotId: botIdPreview, + }; + return true; + } + + /** @returns {boolean} เริ่มแอนิเมชันร่วงแล้วหรือไม่ */ + function tryHumanStackDrop() { + if (!stackMini || !isStack() || stackFall || stackMini.settling) return false; + if (playBotsEnabled() && mapData && mapData.gameType === 'stack' && + (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount)) { + rebuildStackPreviewTurnOrder(); + } + if (playBotsEnabled() && !isStackPreviewHumanTurn()) return false; + const now = Date.now(); + if (now - stackMini.lastDropAt < 480) return false; + const hit = computeStackDropResult(stackMini); + if (!startStackFallAnimation(hit, playBotsEnabled() ? { human: true } : null)) return false; + if (playBotsEnabled()) { + lastStackPreviewActorId = '__human__'; + lastStackPreviewActorUntil = Date.now() + 900; + } + return true; + } + + /** บอท preview: ปล่อยแบบมีแอนิเมชันร่วงเหมือนผู้เล่น — apply หลังแอนิเมชันจบ */ + function simulatePreviewBotStackDrop(botId, o) { + if (!stackMini || stackFall || stackMini.settling) return; + const hit = computeStackDropResult(stackMini); + if (!startStackFallAnimation(hit, playBotsEnabled() ? { botId } : null)) return; + o.stackBotGlowUntil = Date.now() + Math.max(900, 400 + (stackFall && stackFall.dur ? stackFall.dur : 800)); + lastStackPreviewActorId = botId; + lastStackPreviewActorUntil = Date.now() + Math.max(1000, 500 + (stackFall && stackFall.dur ? stackFall.dur : 800)); + } + + /** Stack preview: สลับตา — เฉพาะบอทที่ถึงตาถึงจะจำลองปล่อยหนึ่งครั้งแล้วเลื่อนตา */ + function stepPreviewStackTurnBased(nowMs) { + if (!playBotsEnabled() || !isStack() || !stackMini || stackFall || stackMini.settling) return; + if (mapData && mapData.gameType === 'stack' && + (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount)) { + rebuildStackPreviewTurnOrder(); + } + const order = stackPreviewTurnOrder; + if (!order || order.length !== stackTurnCount) return; + const cur = order[stackPreviewTurnIndex]; + if (!cur || cur.kind !== 'bot') return; + if (!cur.botId) { + advanceStackPreviewTurn(); + return; + } + const o = others.get(cur.botId); + if (!o) { + advanceStackPreviewTurn(); + return; + } + if (stackPreviewBotThinkUntil === 0) { + stackPreviewBotThinkUntil = nowMs + 320 + Math.random() * 480; + return; + } + if (nowMs < stackPreviewBotThinkUntil) return; + stackPreviewBotThinkUntil = 0; + simulatePreviewBotStackDrop(cur.botId, o); + } + + function stackTickFrame() { + const now = performance.now(); + const dt = Math.min(0.06, (now - lastStackTickMs) / 1000); + lastStackTickMs = now; + if (stackMini && stackMini.settling) { + const s = stackMini.settling; + const u = (now - s.t0) / s.dur; + if (u >= 1) { + const wasHuman = !!s.previewHumanDrop; + const settleBotId = s.previewBotId || null; + applyStackHitFromDrop({ miss: true, perfect: false, pts: 0, landOffsetTiles: 0 }, undefined); + if (playBotsEnabled() && (wasHuman || settleBotId)) { + stackPreviewLogStackDrop({ miss: true, perfect: false, pts: 0 }, { human: wasHuman, botId: settleBotId }); + } + stackMini.settling = null; + stackMini.lastDropAt = Date.now(); + if (playBotsEnabled() && (wasHuman || settleBotId)) advanceStackPreviewTurn(); + } + } else if (stackFall) { + const t = (now - stackFall.t0) / stackFall.dur; + if (t >= 1) { + const wasHuman = !!stackFall.previewHumanDrop; + const fallBotId = stackFall.previewBotId || null; + const hit = stackFall.hit; + const stableMin = 0.52; + if (hit.miss) { + applyStackHitFromDrop(hit); + if (playBotsEnabled() && (wasHuman || fallBotId)) { + stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId }); + } + stackMini.lastDropAt = Date.now(); + stackFall = null; + if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn(); + } else if (hit.supportRatio != null && hit.supportRatio < stableMin) { + const stripH = stackMini.blockStripH || Math.max(16, tileSize * 0.32); + stackMini.settling = { + t0: performance.now(), + dur: Math.max(820, Math.min(2600, 720 + (1 - hit.supportRatio) * 3400)), + hit, + xLeft0: stackFall.xLeft1, + yTop0: stackFall.y1, + twWorld: stackFall.tw, + stripH, + delta: hit.delta || 0, + previewHumanDrop: wasHuman, + previewBotId: fallBotId, + }; + stackFall = null; + } else { + let previewAttr; + if (playBotsEnabled()) { + if (wasHuman) previewAttr = { human: true }; + else if (fallBotId) previewAttr = { botId: fallBotId }; + } + applyStackHitFromDrop(hit, previewAttr); + if (playBotsEnabled() && (wasHuman || fallBotId)) { + stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId }); + } + stackMini.lastDropAt = Date.now(); + stackFall = null; + if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn(); + } + } + } else if (stackMini) { + stackMini.phase += stackMini.phaseSpeed * dt * Math.PI * 2; + } + stepPreviewStackTurnBased(Date.now()); + } + + function applyStackPreviewSpawnLayout() { + if (!mapData || mapData.gameType !== 'stack') return; + const w = mapData.width || 20, h = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const cx = land ? land.cx : w / 2 - 0.5; + const cy = land ? land.cy : h / 2 - 0.5; + me.x = cx; + me.y = cy; + me.tx = cx; + me.ty = cy; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.x = cx + (Math.random() - 0.5) * 0.15; + o.y = cy + (Math.random() - 0.5) * 0.15; + o.tx = o.x; + o.ty = o.y; + o.stackBotScore = 0; + o.stackBotNextTry = Date.now() + 300 + Math.random() * 700; + }); + } + + function drawStackMinigame(ctx, worldToScreen, zoom) { + if (!stackMini) return; + const m = stackMini; + const layerWorldH = m.layerWorldH || Math.max(14, tileSize * 0.3); + const stripH = m.blockStripH || Math.max(16, tileSize * 0.32); + const floorY = m.floorWorldY; + const guideTopY = Math.max(0, m.swingWorldY - tileSize * 6.5); + const [gtx, gty] = worldToScreen(m.topCenterX * tileSize, guideTopY); + const [gbx, gby] = worldToScreen(m.topCenterX * tileSize, floorY); + ctx.strokeStyle = 'rgba(255,255,255,0.22)'; + ctx.lineWidth = Math.max(1, zoom * 0.85); + ctx.setLineDash([5, 7]); + ctx.beginPath(); + ctx.moveTo(gtx, gty); + ctx.lineTo(gbx, gby); + ctx.stroke(); + ctx.setLineDash([]); + ctx.lineWidth = 1; + + const nLay = m.layers.length; + for (let i = 0; i < nLay; i++) { + const L = m.layers[i]; + const lw = (L.w != null ? L.w : m.initialWidthTiles) * tileSize; + const cxW = L.cx * tileSize; + const yb = floorY - (i + 1) * layerWorldH; + const xl = cxW - lw / 2; + const [sx, sy] = worldToScreen(xl, yb); + const drawW = lw * zoom; + const drawH = layerWorldH * zoom; + const hue = (i * 47) % 360; + ctx.fillStyle = 'hsla(' + hue + ', 68%, 56%, 0.9)'; + ctx.fillRect(sx, sy, drawW, drawH); + ctx.strokeStyle = 'rgba(255,255,255,0.38)'; + ctx.lineWidth = 1; + ctx.strokeRect(sx, sy, drawW, drawH); + } + if (m.settling) { + const s = m.settling; + const u = Math.min(1, (performance.now() - s.t0) / s.dur); + const ue = u * u; + const rotSign = s.delta >= 0 ? 1 : -1; + const rotRad = ue * 1.22 * rotSign; + const yFall = ue * ue * tileSize * 6.5; + const xSlide = rotSign * ue * tileSize * 0.62; + const cxW = s.xLeft0 + s.twWorld / 2 + xSlide; + const cyW = s.yTop0 + s.stripH * 0.5 + yFall; + const [cxs, cys] = worldToScreen(cxW, cyW); + const drawW = s.twWorld * zoom; + const drawH = s.stripH * zoom; + const hue = (m.layers.length * 47) % 360; + ctx.save(); + ctx.translate(cxs, cys); + ctx.rotate(rotRad); + ctx.fillStyle = 'hsla(' + hue + ', 70%, 52%, 0.93)'; + ctx.fillRect(-drawW / 2, -drawH / 2, drawW, drawH); + ctx.strokeStyle = 'rgba(255, 200, 140, 0.9)'; + ctx.lineWidth = 2; + ctx.strokeRect(-drawW / 2, -drawH / 2, drawW, drawH); + ctx.restore(); + } else { + const twWorld = m.widthTiles * tileSize; + const drawStripPx = stripH * zoom; + let blockLeftWorld; + let blockTopWorld; + let fallTiltRad = 0; + if (stackFall) { + const tRaw = (performance.now() - stackFall.t0) / stackFall.dur; + const t = Math.min(1, tRaw); + const u = stackEaseDropPhys(t, stackFall.sloppyN); + blockLeftWorld = stackFall.xLeft0 + (stackFall.xLeft1 - stackFall.xLeft0) * u; + blockTopWorld = stackFall.y0 + (stackFall.y1 - stackFall.y0) * u; + fallTiltRad = stackFall.tiltMax * Math.sin(Math.PI * u); + } else { + const swingXW = (m.topCenterX + m.swingAmp * Math.sin(m.phase)) * tileSize; + blockLeftWorld = swingXW - twWorld / 2; + blockTopWorld = m.swingWorldY; + const cableTopY = Math.max(0, m.swingWorldY - tileSize * 6); + const craneX = m.craneWorldX != null ? m.craneWorldX : m.topCenterX * tileSize; + const cxMid = blockLeftWorld + twWorld / 2; + const [tcx, tcy] = worldToScreen(craneX, cableTopY); + const [bcx, bcy] = worldToScreen(cxMid, blockTopWorld); + ctx.strokeStyle = 'rgba(90, 88, 86, 0.75)'; + ctx.lineWidth = Math.max(2.2, zoom * 1.4); + ctx.setLineDash([]); + ctx.beginPath(); + ctx.moveTo(tcx, tcy); + ctx.lineTo(bcx, bcy); + ctx.stroke(); + ctx.strokeStyle = 'rgba(125, 207, 255, 0.35)'; + ctx.lineWidth = Math.max(1, zoom * 0.65); + ctx.setLineDash([3, 5]); + ctx.beginPath(); + ctx.moveTo(tcx, tcy); + ctx.lineTo(bcx, bcy); + ctx.stroke(); + ctx.setLineDash([]); + ctx.lineWidth = 1; + } + const hitMissTint = stackFall && stackFall.hit && stackFall.hit.miss; + ctx.fillStyle = hitMissTint ? 'rgba(247, 118, 190, 0.88)' : 'rgba(125, 207, 255, 0.95)'; + ctx.strokeStyle = hitMissTint ? 'rgba(255, 180, 210, 0.98)' : 'rgba(192, 202, 245, 0.98)'; + const cxW = blockLeftWorld + twWorld / 2; + const cyW = blockTopWorld + stripH * 0.5; + const [cxs, cys] = worldToScreen(cxW, cyW); + ctx.save(); + ctx.translate(cxs, cys); + ctx.rotate(fallTiltRad); + ctx.fillRect(-twWorld * zoom / 2, -drawStripPx / 2, twWorld * zoom, drawStripPx); + ctx.lineWidth = 2; + ctx.strokeRect(-twWorld * zoom / 2, -drawStripPx / 2, twWorld * zoom, drawStripPx); + ctx.lineWidth = 1; + ctx.restore(); + } + const [fsx, fsy] = worldToScreen(0, floorY); + const [fex, fey] = worldToScreen(mapData.width * tileSize, floorY); + ctx.strokeStyle = 'rgba(247, 118, 190, 0.65)'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(fsx, fsy); + ctx.lineTo(fex, fey); + ctx.stroke(); + ctx.lineWidth = 1; + } + + function applyGauntletTimingFromServer(payload) { + if (!payload || typeof payload !== 'object') return; + const tm = Number(payload.gauntletTickMs); + if (Number.isFinite(tm)) gauntletRuntimeTickMs = Math.max(80, Math.min(800, tm)); + const jt = Number(payload.gauntletJumpTicks); + if (Number.isFinite(jt)) gauntletRuntimeJumpTicks = Math.max(4, Math.min(40, jt)); + const tl = Number(payload.gauntletTimeLimitSec); + if (Number.isFinite(tl)) gauntletRuntimeTimeLimitSec = Math.max(0, Math.min(7200, tl)); + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletEndsAt')) { + if (payload.gauntletEndsAt == null) gauntletEndsAtMs = null; + else { + const ge = Number(payload.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(ge) ? ge : null; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletCrownRunHeld')) { + gauntletCrownRunHeldRemote = !!payload.gauntletCrownRunHeld; + if (!gauntletCrownRunHeldRemote && isGauntletCrownHeistMapPlay()) { + gauntletCrownPregamePhase = 'live'; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaneImageUrls') && Array.isArray(payload.gauntletLaneImageUrls)) { + gauntletLaneImageUrls = payload.gauntletLaneImageUrls + .filter((x) => typeof x === 'string') + .map((x) => normalizeGauntletAssetUrlForPlay(x)) + .filter((x) => x) + .slice(0, 24); + gauntletLaneImageUrls.forEach((x) => ensureGauntletAssetImage(x)); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserTopUrl')) { + gauntletLaserTopUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserTopUrl === 'string' ? payload.gauntletLaserTopUrl : ''); + ensureGauntletAssetImage(gauntletLaserTopUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserBottomUrl')) { + gauntletLaserBottomUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserBottomUrl === 'string' ? payload.gauntletLaserBottomUrl : ''); + ensureGauntletAssetImage(gauntletLaserBottomUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserLineUrl')) { + gauntletLaserLineUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserLineUrl === 'string' ? payload.gauntletLaserLineUrl : ''); + ensureGauntletAssetImage(gauntletLaserLineUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserFillColor')) { + gauntletLaserFillColor = clampClientLaserColor(payload.gauntletLaserFillColor, gauntletLaserFillColor); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserStrokeColor')) { + gauntletLaserStrokeColor = clampClientLaserColor(payload.gauntletLaserStrokeColor, gauntletLaserStrokeColor); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserLineWidthPx')) { + const lw = Number(payload.gauntletLaserLineWidthPx); + if (Number.isFinite(lw)) gauntletLaserLineWidthPx = Math.max(0, Math.min(24, Math.round(lw))); + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackSwingHz')) { + const sh = Number(payload.stackSwingHz); + if (Number.isFinite(sh)) playStackSwingHz = Math.max(0.08, Math.min(2.8, sh)); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackBlockWidthTiles')) { + const raw = payload.stackBlockWidthTiles; + if (raw == null || raw === '') playStackBlockWidthTiles = null; + else { + const nw = Number(raw); + playStackBlockWidthTiles = Number.isFinite(nw) && nw >= 0.85 + ? Math.max(0.85, Math.min(3.2, Math.round(nw * 100) / 100)) + : null; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurviveJumpHeightMult')) { + const jm = Number(payload.jumpSurviveJumpHeightMult); + if (Number.isFinite(jm)) { + playJumpSurviveJumpHeightMult = Math.round(Math.max(0.5, Math.min(4, jm)) * 100) / 100; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurviveMissionTimeSec')) { + const jt = Number(payload.jumpSurviveMissionTimeSec); + if (Number.isFinite(jt) && jt > 0) { + playJumpSurviveMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(jt))); + } else { + playJumpSurviveMissionTimeSec = 0; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterMissionTimeSec')) { + const st = Number(payload.spaceShooterMissionTimeSec); + if (Number.isFinite(st) && st > 0) { + playSpaceShooterMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st))); + } else { + playSpaceShooterMissionTimeSec = 0; + } + } + if (isStack() && stackMini) reapplyStackMiniSizingFromGlobals(); + } + + /** เลอร์ปตำแหน่งตัวละครต่อเฟรม (ยิ่งสูงยิ่งตามเป้าเร็ว) */ + const GAUNTLET_VIS_LERP = 0.42; + + function gauntletObsSmoothstep(t) { + t = Math.max(0, Math.min(1, t)); + return t * t * (3 - 2 * t); + } + + function cloneGauntletObsSnap(arr) { + if (!Array.isArray(arr)) return []; + return arr.map((o) => { + if (!o || o.id == null) return null; + const row = { id: o.id, kind: o.kind, x: o.x, y: o.y }; + if (o.kind === 'laser') { + if (o.y0 != null) row.y0 = o.y0; + if (o.y1 != null) row.y1 = o.y1; + } + return row; + }).filter(Boolean); + } + + function gauntletLaserRowHitRange(ob, mapH) { + const h = Math.max(1, Math.floor(Number(mapH)) || 15); + let y0 = ob && ob.y0 != null && Number.isFinite(Number(ob.y0)) ? Math.floor(Number(ob.y0)) : 0; + let y1 = ob && ob.y1 != null && Number.isFinite(Number(ob.y1)) ? Math.floor(Number(ob.y1)) : h - 1; + y0 = Math.max(0, Math.min(h - 1, y0)); + y1 = Math.max(0, Math.min(h - 1, y1)); + if (y1 < y0) { + const t = y0; + y0 = y1; + y1 = t; + } + return { y0, y1 }; + } + + function gauntletLaserOverlapsPlayerRow(ob, py, mapH) { + const { y0, y1 } = gauntletLaserRowHitRange(ob, mapH); + return py >= y0 && py <= y1; + } + + /** โค้งยกตัว: ขึ้นถึงจุดสูงสุดเร็ว ลงชัน (รู้สึกลงพื้นเร็วกว่าแบบสมมาตร) */ + function gauntletLiftHeightNorm(air, jumpTicksMax) { + const jm = Math.max(4, jumpTicksMax || 16); + const a = Math.max(0, Math.min(jm, Number(air) || 0)); + const t = a / jm; + const peakAt = 0.28; + if (t >= peakAt) { + const span = 1 - peakAt; + const u = span > 0 ? (t - peakAt) / span : 0; + return Math.max(0, 1 - u * u * 1.2); + } + return Math.min(1, t / peakAt); + } + + function getGauntletObsDrawPositionsAt(nowMs) { + if (!gauntletObsRenderNext.length) return []; + const elapsed = nowMs - gauntletObsBlendT0; + const alpha = gauntletObsSmoothstep(elapsed / gauntletRuntimeTickMs); + const prevMap = new Map(gauntletObsRenderPrev.map((o) => [o.id, o])); + const out = []; + for (let i = 0; i < gauntletObsRenderNext.length; i++) { + const n = gauntletObsRenderNext[i]; + if (!n) continue; + const p = prevMap.get(n.id); + let drawX = n.x; + if (p && Number.isFinite(p.x) && Number.isFinite(n.x)) drawX = p.x + (n.x - p.x) * alpha; + const row = { id: n.id, kind: n.kind, drawX, y: n.y }; + if (n.kind === 'laser') { + if (n.y0 != null) row.y0 = n.y0; + if (n.y1 != null) row.y1 = n.y1; + } + out.push(row); + } + return out; + } + + function pushGauntletObsRenderFrame(newObs) { + const now = performance.now(); + const next = cloneGauntletObsSnap(newObs); + if (!gauntletObsRenderNext.length) { + gauntletObsRenderPrev = next.slice(); + gauntletObsRenderNext = next.slice(); + } else { + const curDraw = getGauntletObsDrawPositionsAt(now); + const curMap = new Map(curDraw.map((o) => [o.id, o.drawX])); + gauntletObsRenderPrev = next.map((n) => { + const row = { + id: n.id, + kind: n.kind, + x: curMap.has(n.id) ? curMap.get(n.id) : n.x, + y: n.y, + }; + if (n.kind === 'laser') { + if (n.y0 != null) row.y0 = n.y0; + if (n.y1 != null) row.y1 = n.y1; + } + return row; + }); + gauntletObsRenderNext = next; + } + gauntletObsBlendT0 = now; + } + + function lerpGauntletEntityPos(x, y, tx, ty) { + let nx = x; + let ny = y; + if (tx != null && Number.isFinite(tx)) { + nx += (tx - nx) * GAUNTLET_VIS_LERP; + if (Math.abs(tx - nx) < 0.02) nx = tx; + } + if (ty != null && Number.isFinite(ty)) { + ny += (ty - ny) * GAUNTLET_VIS_LERP; + if (Math.abs(ty - ny) < 0.02) ny = ty; + } + return { nx, ny }; + } + + /** + * บอท preview ใน Gauntlet: ตัดสินใจกระโดด (client-only; เซิร์ฟเวอร์ไม่รู้จักบอท) + * — ตอบสนองสิ่งกีดขวางที่ชนเซลล์เดียวกัน + กระโดดล่วงหน้าเมื่อ threat อยู่คอลัมน์ถัดไป + */ + function maybePreviewBotGauntletJump(o, obstacles, w, h) { + if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return; + let px = Math.floor(Number(o.x)) || 0; + let py = Math.floor(Number(o.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + if ((o.gauntletJumpTicks || 0) > 0) return; + const { ch: gauntletCh } = getCharacterFootprintWH(mapData); + let sameCell = false; + let incomingCol = false; + for (let i = 0; i < obstacles.length; i++) { + const obs = obstacles[i]; + if (!obs) continue; + if (obs.kind === 'lane' && typeof obs.y === 'number') { + if (obs.x === px && obs.y >= py && obs.y < py + gauntletCh) sameCell = true; + if (obs.x === px + 1 && obs.y >= py && obs.y < py + gauntletCh) incomingCol = true; + } + if (obs.kind === 'laser' && typeof obs.x === 'number') { + if (obs.x === px && gauntletLaserOverlapsPlayerRow(obs, py, h)) sameCell = true; + if (obs.x === px + 1 && gauntletLaserOverlapsPlayerRow(obs, py, h)) incomingCol = true; + } + } + const tier = o.botTier || 'avg'; + const roll = Math.random(); + const pSame = tier === 'sharp' ? 0.96 : tier === 'weak' ? 0.62 : 0.86; + const pAhead = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.48 : 0.72; + if (sameCell && roll < pSame) { + o.gauntletJumpTicks = gauntletRuntimeJumpTicks; + return; + } + if (incomingCol && roll < pAhead) o.gauntletJumpTicks = gauntletRuntimeJumpTicks; + } + + /** หนึ่ง tick เดียวกับ runGauntletTick ฝั่งเซิร์ฟเวอร์ (เฉพาะ collision + เลื่อน x) */ + function applyGauntletPhysicsToPreviewBot(o, obstacles, w, h) { + if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return; + let px = Math.floor(Number(o.x)) || 0; + let py = Math.floor(Number(o.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + const air = (o.gauntletJumpTicks || 0) > 0; + const { ch: gauntletCh2 } = getCharacterFootprintWH(mapData); + const crown = isGauntletCrownHeistMapPlay(); + let advanceX = false; + let hitBack = false; + for (let i = 0; i < obstacles.length; i++) { + const ob = obstacles[i]; + if (!ob) continue; + if (ob.kind === 'lane' && typeof ob.y === 'number' && ob.x === px && ob.y >= py && ob.y < py + gauntletCh2) { + if (air) advanceX = true; + else hitBack = true; + } + if (ob.kind === 'laser' && typeof ob.x === 'number' && ob.x === px) { + if (!gauntletLaserOverlapsPlayerRow(ob, py, h)) { + /* เลเซอร์ไม่ครอบแถวนี้ */ + } else if (air) { + advanceX = true; + } else { + hitBack = true; + } + } + } + if (advanceX) { + px = Math.min(w - 2, px + 1); + o.gauntletJumpTicks = 0; + if (!crown) { + o.gauntletScore = (o.gauntletScore || 0) + 1; + } + } else if (hitBack) { + if (crown) { + if (px <= 0) { + o.gauntletEliminated = true; + o.gauntletScore = 0; + } else { + o.gauntletScore = Math.max(0, (o.gauntletScore || 0) - 10); + px = Math.max(0, px - 1); + } + } else { + px = Math.max(0, px - 1); + } + } + if ((o.gauntletJumpTicks || 0) > 0) o.gauntletJumpTicks--; + o.tx = px; + o.ty = py; + } + + function applyGauntletPreviewBotsAfterSync() { + if (!previewFillBots || !mapData || mapData.gameType !== 'gauntlet') return; + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return; + const w = mapData.width || 20; + const h = mapData.height || 15; + others.forEach((o, id) => { + if (!isPreviewBotId(id) || !o) return; + maybePreviewBotGauntletJump(o, gauntletObstacles, w, h); + applyGauntletPhysicsToPreviewBot(o, gauntletObstacles, w, h); + }); + } + + /** แจ้งเซิร์ฟเวอร์แถว y ที่มีคุณ+บอททดสอบ — lane spawn ใช้เฉพาะแถวที่มี peer; บอทไม่ใช่ peer */ + function emitGauntletPreviewRowsToServer() { + if (!previewFillBots || !mapData || mapData.gameType !== 'gauntlet') return; + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return; + const h = mapData.height || 15; + const ysSet = new Set(); + const addY = (v) => { + const fy = Math.floor(Number(v)); + if (Number.isFinite(fy) && fy >= 0 && fy < h) ysSet.add(fy); + }; + addY(me.y); + others.forEach((o, id) => { + if (isPreviewBotId(id)) addY(o.y); + }); + socket.emit('gauntlet-preview-rows', { ys: [...ysSet] }); + } + + /** ซ่อน overlay โหมดอื่นที่ค้างจาก embed / session ก่อน — กัน UI ซ้อนไม่ตรง mock */ + function hideConflictingOverlaysForGauntletCrown() { + hideQuizCarryPregameOverlay(); + quizCarryPregameActive = false; + const hideIds = [ + 'quiz-game-overlay', + 'quiz-carry-mission-overlay', + 'gauntlet-ended-overlay', + 'quiz-battle-mcq-overlay', + 'quiz-carry-embed-countdown', + 'gauntlet-crown-countdown', + 'quiz-carry-embed-q-strip', + 'play-quiz-scoreboard', + 'play-quiz-feedback', + ]; + for (let i = 0; i < hideIds.length; i++) { + const el = document.getElementById(hideIds[i]); + if (el) el.classList.add('is-hidden'); + } + const gcm = document.getElementById('gauntlet-crown-mission-overlay'); + if (gcm) gcm.classList.add('is-hidden'); + } + + function gcmRankBadgeUrl(rank) { + const r = Math.floor(Number(rank)) || 99; + if (r === 1) return BASE + '/img/gauntlet-assets/result-1st.png'; + if (r === 2) return BASE + '/img/gauntlet-assets/result-2nd.png'; + if (r === 3) return BASE + '/img/gauntlet-assets/result-3rd.png'; + return BASE + '/img/gauntlet-assets/result-txt-2.png'; + } + + function gauntletCrownPregameReadyNumerator() { + let n = quizCarryPregameBotCount(); + quizCarryPregameHumanIds().forEach((id) => { + if (gauntletCrownLobbyReadyMap[id]) n++; + }); + return n; + } + + function gauntletCrownSyncGuestReadyIfNeeded() { + if (gauntletCrownPregamePhase !== 'howto') return; + if (myId == null || isMePlayHost()) return; + const sid = String(myId); + if (gauntletCrownLobbyReadyMap[sid]) return; + gauntletCrownLobbyReadyMap[sid] = true; + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: true }); + } + + function updateGauntletCrownHowtoHud() { + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!btn || !isGauntletCrownHeistMapPlay() || gauntletCrownPregamePhase !== 'howto') return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + if (st) { + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + } + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + function beginGauntletCrownCountdownThenRun() { + if (!isGauntletCrownHeistMapPlay()) return; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + gauntletCrownPregamePhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = () => { + if (cd) cd.classList.add('is-hidden'); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + if (socket && socket.connected) { + socket.emit('gauntlet-crown-begin-run', (res) => { + if (res && res.ok) gauntletCrownPregamePhase = 'live'; + }); + } else { + gauntletCrownPregamePhase = 'live'; + } + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + numEl.textContent = String(n); + const step = () => { + n--; + if (n > 0) { + numEl.textContent = String(n); + gauntletCrownCountdownTimer = setTimeout(step, 1000); + } else { + gauntletCrownCountdownTimer = null; + runFinish(); + } + }; + gauntletCrownCountdownTimer = setTimeout(step, 1000); + } + + function showGauntletCrownHowtoOverlay() { + if (!isGauntletCrownHeistMapPlay()) return; + hideConflictingOverlaysForGauntletCrown(); + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + gauntletCrownPregamePhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateGauntletCrownHowtoHud(); + } + + /** Editor embed: quiz_carry / crown ใช้เกรด F = gameover — Jumper: ภาพจบใช้เฉพาะเมื่อไม่มีผู้รอด (0 คน) */ + function gauntletCrownEmbedMissionAnyOk(mission) { + if (!mission) return false; + if (mission.uiSkin === 'jumper') { + const sc = Number(mission.survivorCount); + return Number.isFinite(sc) ? sc > 0 : (String(mission.grade || '').trim().toUpperCase().charAt(0) !== 'F'); + } + const g = String(mission.grade || '').trim().toUpperCase().charAt(0); + return g !== 'F'; + } + + function gauntletCrownRankBonusLocal(rankOrdinal) { + if (rankOrdinal === 1) return 100; + if (rankOrdinal === 2) return 80; + if (rankOrdinal === 3) return 60; + return 0; + } + + function gauntletCrownRollRewardCardLocal(grade) { + const r = Math.random(); + if (grade === 'A') { + if (r < 0.3) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' }; + if (r < 0.8) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'B') { + if (r < 0.2) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' }; + if (r < 0.5) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (r < 0.2) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + + /** + * Editor embed + preview bots: รวมแถวอันดับกับบอท (สูงสุด 5 ช่อง) และคำนวณรวม/เฉลี่ย/เกรดให้สอดคล้องกับแถวที่โชว์ + * — อันดับในช่องใช้ลำดับ 1..5 หลังเรียงคะแนน (เหมือนแนว quiz_carry mission row) + */ + function gauntletCrownBuildDisplayMission(mission) { + if (!mission || !Array.isArray(mission.ranked)) return mission; + if (mission.uiSkin === 'jumper') return mission; + if (!(previewFillBots && isGauntletCrownHeistMapPlay() && others && typeof others.forEach === 'function')) return mission; + + const seen = new Set(); + const baseRows = []; + mission.ranked.forEach((r) => { + if (!r) return; + const sid = String(r.id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: r.id, + nickname: (r.nickname && String(r.nickname).trim()) ? String(r.nickname).trim() : 'ผู้เล่น', + characterId: r.characterId ?? null, + baseScore: r.eliminated ? 0 : Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + }); + }); + others.forEach((o, id) => { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: o.gauntletEliminated ? 0 : Math.max(0, Number(o.gauntletScore) | 0), + eliminated: !!o.gauntletEliminated, + }); + }); + + baseRows.sort((a, b) => { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + const nick = String(a.nickname).localeCompare(String(b.nickname), 'th'); + if (nick !== 0) return nick; + return String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map((row, idx) => { + const pos = idx + 1; + const rankBonus = gauntletCrownRankBonusLocal(pos); + const rankLabel = pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos); + const finalScore = row.baseScore + rankBonus; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: row.eliminated, + rank: pos, + rankLabel: rankLabel, + rankBonus: rankBonus, + finalScore: finalScore, + }; + }); + + const totalSum = ranked.reduce(function (s, r) { return s + r.finalScore; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + const rewardCard = gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: ranked.map(function (r) { return r.finalScore; }), + }; + } + + function showGauntletCrownMissionOverlay(mission) { + const ov = document.getElementById('gauntlet-crown-mission-overlay'); + const rowEl = document.getElementById('gcm-rank-row'); + const totalEl = document.getElementById('gcm-total'); + const gradeEl = document.getElementById('gcm-grade'); + const bonusEl = document.getElementById('gcm-bonus'); + const btn = document.getElementById('btn-gcm-done'); + if (!ov || !rowEl || !mission || !Array.isArray(mission.ranked)) return; + let disp = gauntletCrownBuildDisplayMission(mission); + if (mission.uiSkin === 'jumper') { + disp = jumpSurviveMergePreviewBotsMission(disp) || disp; + } + if (!disp || !Array.isArray(disp.ranked)) return; + rowEl.innerHTML = ''; + disp.ranked.forEach((r) => { + const cell = document.createElement('div'); + cell.className = 'gcm-cell'; + const tag = document.createElement('div'); + tag.className = 'gcm-rank-tag'; + const rk = Math.floor(Number(r.rank)) || 0; + if (rk >= 1 && rk <= 3) { + const tagImg = document.createElement('img'); + tagImg.src = gcmRankBadgeUrl(rk); + tagImg.alt = '[' + String(r.rankLabel || rk) + ']'; + tagImg.style.maxWidth = '100%'; + tagImg.style.height = 'auto'; + tag.appendChild(tagImg); + } else { + tag.textContent = '[' + String(r.rankLabel || rk) + ']'; + } + const frame = document.createElement('div'); + frame.className = 'gcm-frame'; + const img = document.createElement('img'); + img.className = 'gcm-av'; + img.alt = ''; + const cid = r.characterId ? String(r.characterId) : ''; + if (cid) { + const im = getCharacterImg(cid, 'down'); + if (im && im.src) img.src = im.src; + } else { + img.src = defaultAvatarImg.src; + } + img.onerror = function () { + this.onerror = null; + this.src = defaultAvatarImg.src; + }; + frame.appendChild(img); + if (r.eliminated) { + const st = document.createElement('img'); + st.className = 'gcm-stamp'; + const jSkin = mission && mission.uiSkin === 'jumper'; + st.src = jSkin ? jumperAssetUrl('stamp-sacrifice.png') : (BASE + '/img/gauntlet-assets/result-Lose_stamp.png'); + st.alt = jSkin ? 'ผู้เสียสละ' : 'เสียชีวิต'; + st.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/result-Lose_stamp.png'; + this.alt = 'เสียชีวิต'; + }; + frame.appendChild(st); + } + const nick = document.createElement('div'); + nick.className = 'gcm-nick'; + nick.textContent = r.nickname || '—'; + const sc = document.createElement('div'); + sc.className = 'gcm-sc'; + const jumpPlain = mission && mission.uiSkin === 'jumper'; + const botScorePlain = previewFillBots && isGauntletCrownHeistMapPlay() && isPreviewBotId(r.id); + sc.textContent = (jumpPlain || botScorePlain) + ? String(Math.max(0, Number(r.baseScore) || 0)) + : String(r.baseScore || 0) + '(+' + String(r.rankBonus || 0) + ')'; + cell.appendChild(tag); + cell.appendChild(frame); + cell.appendChild(nick); + cell.appendChild(sc); + rowEl.appendChild(cell); + }); + if (totalEl) { + const parts = (disp.totalParts || []).map((n) => String(Math.max(0, Number(n) || 0))); + const sum = Math.max(0, Number(disp.totalSum) || 0); + const avg = Math.max(0, Math.floor(Number(disp.averageScore) || 0)); + const jumperSkin = mission && mission.uiSkin === 'jumper'; + if (jumperSkin) { + const sc = Math.max(0, Math.floor(Number(disp.survivorCount) || 0)); + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้รอด ' + sc + '/' + pc + ' → เกรด'; + } else { + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · เฉลี่ย ' + avg + ' → เกรด'; + } + } + if (gradeEl) { + const g = String(disp.grade || 'C').toUpperCase().charAt(0); + gradeEl.textContent = g; + gradeEl.className = 'gcm-grade gcm-grade--' + g.toLowerCase(); + } + if (bonusEl) { + bonusEl.innerHTML = ''; + const gLetter = String(disp.grade || 'C').toUpperCase().charAt(0); + const missionOk = gLetter !== 'F'; + if (missionOk) { + const row = document.createElement('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + row.style.gap = '8px'; + const chk = document.createElement('img'); + chk.src = BASE + '/img/gauntlet-assets/result-check.png'; + chk.alt = ''; + chk.width = 26; + chk.height = 26; + const sp = document.createElement('span'); + sp.style.color = '#9ece6a'; + sp.style.fontWeight = '700'; + sp.textContent = 'ภารกิจสำเร็จ'; + row.appendChild(chk); + row.appendChild(sp); + bonusEl.appendChild(row); + } + const rc = disp.rewardCard; + if (missionOk && rc && rc.th) { + const pl = document.createElement('div'); + pl.className = 'gcm-plaque'; + pl.innerHTML = '' + String(rc.th) + '' + String(rc.en || '') + ''; + bonusEl.appendChild(pl); + } else if (!missionOk) { + const pl = document.createElement('div'); + pl.className = 'gcm-plaque'; + pl.style.borderColor = 'rgba(148, 153, 168, 0.45)'; + pl.style.opacity = '0.95'; + pl.innerHTML = 'ไม่ได้รับการ์ดเกรด ' + gLetter + ' · ภารกิจไม่ผ่านเกณฑ์'; + bonusEl.appendChild(pl); + } + } + ov.classList.remove('is-hidden'); + function goLobby() { + ov.classList.add('is-hidden'); + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = function () { + if (previewMode && editorEmbedReturn) { + ov.classList.add('is-hidden'); + showQuizCarryTimeupOnDeskLayer(function () { return gauntletCrownEmbedMissionAnyOk(disp); }); + } else { + goLobby(); + } + }; + } + } + + function isLobby() { return mapData && mapData.gameType === 'lobby'; } + function isQuiz() { return mapData && mapData.gameType === 'quiz'; } + function getLane(y) { + if (!mapData || !mapData.lanes) return null; + const lane = mapData.lanes.find(l => l.y === y); + return lane || (mapData.lanes[y] != null ? mapData.lanes[y] : null); + } + function getVehiclePositions(lane, width, timeMs) { + if (!lane || (lane.type !== 'road' && lane.type !== 'water')) return []; + const speed = (lane.speed != null ? lane.speed : 1) * 0.05; + const dir = lane.dir === -1 ? -1 : 1; + const spacing = lane.spacing != null ? lane.spacing : (lane.type === 'road' ? 3 : 2.5); + const period = width + 2; + const count = Math.max(2, Math.floor(period / spacing)); + const positions = []; + for (let i = 0; i < count; i++) { + const p = i * spacing + (timeMs * speed * dir) / 60; + const pNorm = ((p % period) + period) % period; + const vx = pNorm - 1; + positions.push(Math.max(-1, Math.min(width, vx))); + } + return positions; + } + function checkFroggerCollision() { + const fy = Math.floor(me.y); + const lane = getLane(fy); + if (!lane) return false; + if (lane.type === 'road') { + const positions = getVehiclePositions(lane, mapData.width, Date.now()); + for (let i = 0; i < positions.length; i++) { + if (Math.abs(positions[i] - me.x) < 1.1) return true; + } + } + if (lane.type === 'water') { + const positions = getVehiclePositions(lane, mapData.width, Date.now()); + let onLog = false; + for (let i = 0; i < positions.length; i++) { + if (Math.abs(positions[i] - me.x) < 1.2) { onLog = true; break; } + } + if (!onLog) return true; + } + return false; + } + function respawnFrogger() { + const sp = mapData.spawn || { x: 1, y: mapData.height - 1 }; + me.x = sp.x; me.y = sp.y; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + + /** รองรับ x/y เป็น string จาก Socket — ถ้าเช็คแค่ typeof number จะร่วงไป mapData.spawn ทุกครั้ง (จุดเกิดไม่สุ่ม) */ + function peerXYFromJoin(peer, spawnFb) { + const sx = Number(spawnFb && spawnFb.x); + const sy = Number(spawnFb && spawnFb.y); + const defX = Number.isFinite(sx) ? sx : 1; + const defY = Number.isFinite(sy) ? sy : 1; + if (!peer) return { x: defX, y: defY }; + const x = Number(peer.x); + const y = Number(peer.y); + if (Number.isFinite(x) && Number.isFinite(y)) return { x, y }; + return { x: defX, y: defY }; + } + + /** Hub = 0/1 · กริดตัวเลือก = 0 หรือเลข 1..16 (ห้ามบังคับ === 1 เหมือน hub — เลข 2–16จะหาย) */ + function normalizeQuizCarryLayersInPlay(md) { + if (!md || md.gameType !== 'quiz_carry') return; + const w = md.width || 20, h = md.height || 15; + const maxOpt = QUIZ_CARRY_MAX_OPTION_SLOTS; + const srcHub = md.quizCarryHubArea || []; + const hubRows = []; + for (let y = 0; y < h; y++) { + const r = srcHub[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + hubRows.push(row); + } + md.quizCarryHubArea = hubRows; + const srcOpt = md.quizCarryOptionArea || []; + const optRows = []; + for (let y = 0; y < h; y++) { + const r = srcOpt[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= maxOpt ? Math.floor(n) : 0); + } + optRows.push(row); + } + md.quizCarryOptionArea = optRows; + const srcCd = md.carryEmbedCountdownArea || []; + const cdRows = []; + for (let y = 0; y < h; y++) { + const r = srcCd[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + row.push(Number(v) === 1 ? 1 : 0); + } + cdRows.push(row); + } + md.carryEmbedCountdownArea = cdRows; + } + + /** โดม = ช่องติดกัน (4 ทิศ) ต่อ 1 ข้อ · comp id เรียงตามการค้นพบบนแมป */ + function normalizeQuizBattleDomeInPlay(md) { + if (!md || md.gameType !== 'quiz_battle') return; + const w = md.width || 20, h = md.height || 15; + const src = md.quizBattleDomeArea || []; + const grid = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + grid.push(row); + } + md.quizBattleDomeArea = grid; + const comp = Array(h).fill(0).map(() => Array(w).fill(0)); + let nextId = 0; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (grid[y][x] !== 1 || comp[y][x]) continue; + nextId++; + const stack = [[x, y]]; + comp[y][x] = nextId; + while (stack.length) { + const c = stack.pop(); + const cx = c[0], cy = c[1]; + for (let i = 0; i < 4; i++) { + const dx = (i === 0 ? 1 : i === 1 ? -1 : 0); + const dy = (i === 2 ? 1 : i === 3 ? -1 : 0); + const nx = cx + dx, ny = cy + dy; + if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue; + if (grid[ny][nx] !== 1 || comp[ny][nx]) continue; + comp[ny][nx] = nextId; + stack.push([nx, ny]); + } + } + } + } + md.quizBattleDomeComp = comp; + } + + /** เส้นทาง Quiz Battle — วาดอย่างน้อย 1 ช่องแล้วจะเดินได้เฉพาะบนเส้นทาง (เปลี่ยนแค่รูปพื้นหลังได้) */ + function normalizeQuizBattlePathInPlay(md) { + if (!md || md.gameType !== 'quiz_battle') return; + const w = md.width || 20, h = md.height || 15; + const src = md.quizBattlePathArea || []; + const grid = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + grid.push(row); + } + md.quizBattlePathArea = grid; + } + + function quizBattlePathModeActive(md) { + if (!md || md.gameType !== 'quiz_battle' || !md.quizBattlePathArea) return false; + const g = md.quizBattlePathArea; + for (let y = 0; y < g.length; y++) { + const row = g[y]; + if (!row) continue; + for (let x = 0; x < row.length; x++) if (row[x] === 1) return true; + } + return false; + } + + function quizBattleFootprintFullyOnPath(md, px, py) { + if (!md || !quizBattlePathModeActive(md)) return true; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return false; + const tiles = quizTilesFootprintPlay(px, py); + if (tiles.size === 0) return false; + const g = md.quizBattlePathArea; + for (const k of tiles) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!g[ty] || g[ty][tx] !== 1) return false; + } + /* กันเดินลง/ข้ามขอบเลน: เท้า (≈ py+0.5 / px+0.5) ต้องอยู่บนเส้นทาง — หยุดก่อนล้ำช่องถัดไปที่ไม่ใช่ path */ + const h = md.height || 15, w = md.width || 20; + const { cw, ch } = getCharacterFootprintWH(md); + const fy = Math.floor(py + 0.5); + if (fy >= 0 && fy < h) { + const minTx = Math.floor(px), maxTx = Math.min(w - 1, minTx + cw - 1); + for (let tx = Math.max(0, minTx); tx <= maxTx; tx++) { + if (!g[fy] || g[fy][tx] !== 1) return false; + } + } + const fx = Math.floor(px + 0.5); + if (fx >= 0 && fx < w) { + const minTy = Math.floor(py), maxTy = Math.min(h - 1, minTy + ch - 1); + for (let ty = Math.max(0, minTy); ty <= maxTy; ty++) { + if (!g[ty] || g[ty][fx] !== 1) return false; + } + } + return true; + } + + /** สแน็ปบนเลน — เช็คแค่กำแพง + อยู่บน path (ให้ตรงกับ server) ไม่ใช้ blockPlayer/ผู้เล่นคนอื่น เพราะจะทำให้หาจุดสแน็ปไม่ได้แล้วค้างนอกเลน */ + function quizBattleSnapTargetValidPlay(x, y) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + for (const k of quizTilesWallCollisionFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (quizBattlePathModeActive(mapData) && !quizBattleFootprintFullyOnPath(mapData, x, y)) return false; + return true; + } + + function snapPositionOntoQuizBattlePathIfNeeded(px, py) { + if (!mapData || !isQuizBattle() || !quizBattlePathModeActive(mapData)) return { x: px, y: py }; + if (quizBattleSnapTargetValidPlay(px, py)) return { x: px, y: py }; + const w = mapData.width || 20, h = mapData.height || 15; + const g = mapData.quizBattlePathArea; + let bestX = null, bestY = null, bestD = Infinity; + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + const nx = tx + 0.01; + const ny = ty + 0.01; + if (!quizBattleSnapTargetValidPlay(nx, ny)) continue; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + /* footprint ใหญ่กว่าเลน (เช่น 2×2 บนทางแคบ 1 ช่อง): หาช่อง path ใกล้สุดที่เดินได้อย่างน้อยที่เซ็นเตอร์ — ดีกว่าปล่อยค้างนอกเลน */ + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + if (!spawnTileWalkablePlay(mapData, tx, ty)) continue; + const nx = tx + 0.5; + const ny = ty + 0.5; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + return { x: px, y: py }; + } + + /** บังคับให้ผู้เล่นอยู่บนเลน — เรียกก่อน return กลาง tick (แชท / path หมด) เพราะเดิมข้ามบล็อกสแน็ปท้าย tick */ + function enforceQuizBattleLaneOnMePlay() { + if (!mapData || !isQuizBattle() || !quizBattlePathModeActive(mapData)) return; + if (quizBattleSnapTargetValidPlay(me.x, me.y)) return; + const snapped = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = snapped.x; + me.y = snapped.y; + } + + function hideQuizBattleMcqModal() { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + } + + function flashQuizBattleFeedback(text, ok) { + const el = document.getElementById('play-quiz-feedback'); + if (!el) return; + el.textContent = text || ''; + el.classList.remove('is-hidden', 'play-quiz-feedback-ok', 'play-quiz-feedback-bad'); + el.classList.add(ok ? 'play-quiz-feedback-ok' : 'play-quiz-feedback-bad'); + if (typeof window.__quizBattleFbT === 'number') clearTimeout(window.__quizBattleFbT); + window.__quizBattleFbT = setTimeout(() => { el.classList.add('is-hidden'); }, 1800); + } + + function getQuizBattleDomeCompIdsUnderFootprint(px, py) { + if (!mapData || !isQuizBattle()) return []; + const comp = mapData.quizBattleDomeComp; + const grid = mapData.quizBattleDomeArea; + if (!comp || !grid) return []; + const ids = []; + const seen = Object.create(null); + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + const cid = comp[ty] && comp[ty][tx]; + if (grid[ty] && grid[ty][tx] === 1 && cid > 0 && !seen[cid]) { + seen[cid] = 1; + ids.push(cid); + } + } + return ids; + } + + function getPrimaryQuizBattleDomeCompAt(px, py) { + const arr = getQuizBattleDomeCompIdsUnderFootprint(px, py); + if (!arr.length) return null; + return Math.min.apply(null, arr); + } + + function showQuizBattleMcqModal(q) { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + const textEl = document.getElementById('quiz-battle-mcq-text'); + if (!ov || !textEl || !q) return; + textEl.textContent = q.text || ''; + const labels = ['A', 'B', 'C']; + for (let i = 0; i < 3; i++) { + const btn = ov.querySelector('.quiz-battle-choice[data-idx="' + i + '"]'); + if (btn) { + const ch = (q.choices && q.choices[i]) ? String(q.choices[i]) : ''; + btn.textContent = labels[i] + (ch ? (': ' + ch.slice(0, 96)) : ''); + } + } + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + + function trySubmitQuizBattleChoice(idx) { + if (quizBattleModalCompId == null || !mapData || !isQuizBattle()) return; + const compId = quizBattleModalCompId; + const pool = quizBattleMcqPool; + if (!pool.length) { + flashQuizBattleFeedback('ยังไม่มีคำถาม — ตั้ง battleQuizMcq ใน Admin → Quiz Battle', false); + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + return; + } + const q = pool[(compId - 1) % pool.length]; + if (!q) return; + const ok = Number(q.correctIndex) === Number(idx); + if (ok) { + quizBattleAnsweredComps.add(compId); + if (myId != null) { + if (!playLiveQuizScores) playLiveQuizScores = {}; + playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + 1; + renderPlayQuizScoreboard(playLiveQuizScores); + } + /* ส่งให้ server เก็บคะแนน (authoritative) แล้ว broadcast อันดับให้ทุกคน — แบบ ZEP */ + try { if (typeof socket !== 'undefined' && socket) socket.emit('quizbattle-solve', { compId }); } catch (e) { /* ignore */ } + updateQuizBattleProgressHud(); + flashQuizBattleFeedback('ถูกต้อง · +1', true); + } else { + flashQuizBattleFeedback('ยังไม่ถูก — ลองใหม่ (กด E อีกครั้ง)', false); + } + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + } + + /** ZEP-style: เดินถึงสถานี (โดม) ที่ยังไม่ตอบ → เปิดคำถามอัตโนมัติ (ครั้งเดียวต่อการเข้า · เดินออกแล้วกลับมาใหม่ถึงเด้งอีก) */ + function maybeAutoOpenQuizBattle() { + if (!isQuizBattle() || myId == null) return; + const compNow = getPrimaryQuizBattleDomeCompAt(me.x, me.y); + if (compNow == null) { qbAutoLastComp = null; return; } + const ov = document.getElementById('quiz-battle-mcq-overlay'); + const modalOpen = quizBattleModalCompId != null || (ov && !ov.classList.contains('is-hidden')); + if (modalOpen) return; + if (compNow === qbAutoLastComp) return; // เข้าโดมนี้รอบนี้ไปแล้ว + qbAutoLastComp = compNow; + if (quizBattleAnsweredComps.has(compNow)) return; // ตอบถูกแล้ว + const pool = quizBattleMcqPool; + if (!pool.length) return; + const q = pool[(compNow - 1) % pool.length]; + if (!q) return; + quizBattleModalCompId = compNow; + showQuizBattleMcqModal(q); + } + + function tryOpenQuizBattleFromKey() { + if (!isQuizBattle() || !mapData || myId == null) return; + const compId = getPrimaryQuizBattleDomeCompAt(me.x, me.y); + if (compId == null) { + flashQuizBattleFeedback('ยังไม่ยืนบนโดมคำถาม', false); + return; + } + if (quizBattleAnsweredComps.has(compId)) { + flashQuizBattleFeedback('ตอบโดมนี้ถูกแล้ว', false); + return; + } + const pool = quizBattleMcqPool; + if (!pool.length) { + flashQuizBattleFeedback('ยังไม่มีคำถามในระบบ (Admin → Quiz Battle)', false); + return; + } + const q = pool[(compId - 1) % pool.length]; + if (!q) return; + quizBattleModalCompId = compId; + showQuizBattleMcqModal(q); + } + + function pushSanitizedBattleMcqFromPayload(s) { + if (!s || !Array.isArray(s.battleQuizMcq)) return 0; + /* กรองเฉพาะคลังคำถามของหัวข้อห้องนี้ (mapData.quizBattleCategoryId เช่น topic1..topic10) — ถ้าไม่ระบุ ใช้ทั้งหมด */ + const roomCat = (mapData && mapData.quizBattleCategoryId) ? String(mapData.quizBattleCategoryId).trim() : ''; + let n = 0; + for (let i = 0; i < s.battleQuizMcq.length; i++) { + const raw = s.battleQuizMcq[i]; + if (!raw || !String(raw.text || '').trim()) continue; + if (roomCat && String(raw.categoryId || '').trim() !== roomCat) continue; + const ch = Array.isArray(raw.choices) ? raw.choices : []; + if (ch.length !== 3) continue; + const a = String(ch[0] || '').trim(); + const b = String(ch[1] || '').trim(); + const c = String(ch[2] || '').trim(); + if (!a || !b || !c) continue; + let ci = Number(raw.correctIndex); + if (!Number.isFinite(ci)) ci = 0; + ci = Math.max(0, Math.min(2, Math.floor(ci))); + quizBattleMcqPool.push({ + text: String(raw.text).trim(), + choices: [a, b, c], + correctIndex: ci, + }); + n++; + } + return n; + } + + /** + * โหลดชุดข้อ A/B/C — ลอง Node ก่อน แล้ว fallback PHP อ่านจากดิสก์ (กรณี nginx ไม่ส่ง /api/quiz-settings ถึง Node) + */ + async function loadQuizBattleMcqPool() { + quizBattleMcqPool = []; + const bust = '_=' + Date.now(); + const tryUrls = [ + BASE + '/api/quiz-settings?' + bust, + BASE + '/api-quiz-battle-mcq.php?' + bust, + ]; + for (let u = 0; u < tryUrls.length; u++) { + try { + const r = await fetch(tryUrls[u], { cache: 'no-store' }); + if (!r.ok) continue; + const s = await r.json(); + if (pushSanitizedBattleMcqFromPayload(s) > 0) break; + } catch (e) { /* next url */ } + } + } + + function resetQuizBattlePlayState() { + quizBattleMcqPool = []; + quizBattleAnsweredComps = new Set(); + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + hideQuizBattleProgressHud(); + } + + /* ===== Quiz Battle ฉากเดิน (ZEP) — Progress + Ranking sync จาก server ===== */ + let quizBattleScoresBound = false; + function ensureQuizBattleScoresListener() { + if (quizBattleScoresBound || typeof socket === 'undefined' || !socket) return; + quizBattleScoresBound = true; + socket.on('quizbattle-scores', (data) => { + if (!data || typeof data.scores !== 'object' || !isQuizBattle()) return; + const sc = data.scores || {}; + if (!playLiveQuizScores) playLiveQuizScores = {}; + /* server เป็นเจ้าของคะแนน (จำนวนโดมที่ปลดได้) — sync ทับของทุกคน เพื่ออันดับตรงกันแบบ ZEP */ + Object.keys(sc).forEach((id) => { playLiveQuizScores[id] = sc[id]; }); + renderPlayQuizScoreboard(playLiveQuizScores); + updateQuizBattleProgressHud(); + }); + } + + function quizBattleTotalDomes() { + if (!mapData || !isQuizBattle()) return 0; + const comp = mapData.quizBattleDomeComp; + if (!comp) return 0; + let mx = 0; + for (let y = 0; y < comp.length; y++) { + const row = comp[y] || []; + for (let x = 0; x < row.length; x++) if (row[x] > mx) mx = row[x]; + } + return mx; + } + + function updateQuizBattleProgressHud() { + if (!isQuizBattle()) return; + let el = document.getElementById('quiz-battle-progress-hud'); + if (!el) { + el = document.createElement('div'); + el.id = 'quiz-battle-progress-hud'; + el.style.cssText = 'position:fixed;top:12px;left:12px;z-index:60;background:rgba(8,12,28,0.82);color:#fff;font-family:Kanit,Sarabun,system-ui,sans-serif;font-weight:700;padding:8px 18px;border-radius:999px;border:1px solid rgba(255,255,255,0.2);font-size:18px;box-shadow:0 4px 14px rgba(0,0,0,0.35);pointer-events:none'; + document.body.appendChild(el); + } + const total = quizBattleTotalDomes(); + const mine = (myId != null && playLiveQuizScores && playLiveQuizScores[myId] != null) + ? playLiveQuizScores[myId] + : (quizBattleAnsweredComps ? quizBattleAnsweredComps.size : 0); + el.textContent = 'Progress ' + mine + '/' + total + (total > 0 && mine >= total ? ' ✓' : ''); + el.style.color = (total > 0 && mine >= total) ? '#34d399' : '#fff'; + } + + function hideQuizBattleProgressHud() { + const el = document.getElementById('quiz-battle-progress-hud'); + if (el) el.remove(); + } + + function setupPlayQuizBattleUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + /* ทดสอบจากเอดิเตอร์: อย่าเปิดแผงคำถามเต็มจอล่าง — บังมองเห็นบอท/แผนที่ (overlay ยังอัปเดตข้อความไว้ถ้าเปิด preview แบบไม่ embed) */ + if (ov) { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else ov.classList.remove('is-hidden'); + } + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'Quiz Battle'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'เดินทับโดม (สีฟ้า–ขอบแดง) แล้วกด E เพื่อตอบ A / B / C'; + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + const leg = document.getElementById('quiz-play-legend'); + if (leg) { + leg.textContent = quizBattlePathModeActive(mapData) + ? 'โหมดเส้นทาง: เดินได้เฉพาะช่องเส้นทาง (ม่วงบนแผนที่) — วางโดมบนเส้นทาง · ไม่วาดเส้นทางใน Editor = เดินอิสระ · เปลี่ยนรูปพื้นหลังได้ทีหลัง' + : 'คำถามจาก Admin → Quiz Battle (battleQuizMcq) · ช่องโดมติดกัน = 1 ข้อ · ถูกแล้วได้ +1 ต่อโดม'; + } + quizBattleAnsweredComps = new Set(); + quizBattleModalCompId = null; + quizBattleMcqPool = []; + hideQuizBattleMcqModal(); + playLiveQuizScores = {}; + if (myId != null) playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + renderPlayQuizScoreboard(playLiveQuizScores); + /* sync คะแนน/อันดับข้ามผู้เล่นจาก server (ZEP) + โชว์ Progress N/total */ + ensureQuizBattleScoresListener(); + updateQuizBattleProgressHud(); + try { if (typeof socket !== 'undefined' && socket) socket.emit('quizbattle-hello', (res) => { + if (res && res.scores && typeof res.scores === 'object') { + Object.keys(res.scores).forEach((id) => { playLiveQuizScores[id] = res.scores[id]; }); + renderPlayQuizScoreboard(playLiveQuizScores); + updateQuizBattleProgressHud(); + } + }); } catch (e) { /* ignore */ } + loadQuizBattleMcqPool().then(() => { + const q2 = document.getElementById('quiz-game-question'); + if (!q2) return; + if (!quizBattleMcqPool.length) { + q2.textContent = 'ยังไม่มีคำถาม — ไปที่ Admin แท็บ Quiz Battle แล้วบันทึกข้อ A/B/C (ครบคำถาม + A + B + C) · หรือเช็คว่าเซิร์ฟเวอร์เกม (Node) รันอยู่ · ลองรีเฟรชแบบ hard refresh (Ctrl+F5)'; + } else { + const q0 = quizBattleMcqPool[0]; + const snippet = q0 && q0.text ? String(q0.text).trim().slice(0, 100) : ''; + const more = q0 && q0.text && String(q0.text).trim().length > 100 ? '…' : ''; + q2.textContent = snippet + ? ('โหลดคำถามแล้ว ' + quizBattleMcqPool.length + ' ข้อ · ตัวอย่าง: 「' + snippet + more + '」\nเข้าโดมสีฟ้า (ขอบแดง) แล้วกด E เพื่อเลือก A / B / C') + : ('โหลดคำถามแล้ว ' + quizBattleMcqPool.length + ' ข้อ — เดินเข้าโดม (ฟ้า–ขอบแดง) แล้วกด E เพื่อตอบ A / B / C'); + } + const ov2 = document.getElementById('quiz-game-overlay'); + if (ov2 && previewMode && editorEmbedReturn) ov2.classList.add('is-hidden'); + }); + } + + function normalizeJumpSurvivePlatformAreaInPlay(md) { + if (!md || md.gameType !== 'jump_survive') return; + const w = md.width || 20, h = md.height || 15; + const src = md.jumpSurvivePlatformArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + md.jumpSurvivePlatformArea = rows; + } + + function normalizeShooterSpawnSlotsInPlay(md) { + if (!md || md.gameType !== 'space_shooter') return; + const w = md.width || 20, h = md.height || 15; + const src = md.shooterSpawnSlots || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= 6 ? Math.floor(n) : 0); + } + rows.push(row); + } + md.shooterSpawnSlots = rows; + } + + function getShooterSpawnWorldCenterFromMap(md, slot1to6, ts) { + if (!md || !md.shooterSpawnSlots) return null; + const w = md.width || 20, h = md.height || 15; + const g = md.shooterSpawnSlots; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (g[y] && g[y][x] === slot1to6) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; + } + } + return null; + } + + function buildSpaceShooterParticipantRefsPlay() { + const refs = []; + if (me) refs.push({ id: myId, ref: me }); + const humanIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + humanIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + botIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + return refs.filter((r) => r.ref); + } + + function applySpaceShooterSpawnLayoutPlay() { + if (!mapData || mapData.gameType !== 'space_shooter') return; + normalizeShooterSpawnSlotsInPlay(mapData); + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const { cw, ch } = getCharacterFootprintWH(mapData); + const mw = w * ts, mh = h * ts; + const refs = buildSpaceShooterParticipantRefsPlay(); + const n = refs.length || 1; + refs.forEach((entry, idx) => { + const ent = entry.ref; + const slot = (idx % 6) + 1; + const cen = getShooterSpawnWorldCenterFromMap(mapData, slot, ts); + let cx, cy; + if (cen) { + cx = cen.cx; + cy = cen.cy; + } else { + cx = ((idx + 1) / (n + 1)) * mw; + cy = Math.min(mh - ts * 1.2, (h - 1.5) * ts); + } + cx = Math.max(ts * 0.5, Math.min(mw - ts * 0.5, cx)); + cy = clampSpaceShooterWorldCy(cy, mh); + ent.spaceShooterCx = cx; + ent.spaceShooterCy = cy; + if (typeof ent.spaceShooterScore !== 'number' || !Number.isFinite(ent.spaceShooterScore)) ent.spaceShooterScore = 0; + ent.x = cx / ts - cw * 0.5; + ent.y = cy / ts - ch * 0.92; + ent.tx = ent.x; + ent.ty = ent.y; + ent.direction = 'up'; + }); + } + + function spaceShooterTimeLimitSecPlay() { + const t = Number(mapData && mapData.spaceShooterTimeSec); + if (Number.isFinite(t) && t === 0) return 0; + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + const g = Number(playSpaceShooterMissionTimeSec); + if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g); + return 90; + } + + function spaceShooterRemainingSecPlay() { + const lim = spaceShooterTimeLimitSecPlay(); + if (lim <= 0) return null; + const elapsed = (performance.now() - spaceShooterSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function endSpaceShooterTimeUp() { + if (spaceShooterGameEnded || !mapData || mapData.gameType !== 'space_shooter') return; + spaceShooterGameEnded = true; + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterSpawnAccMs = 0; + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) titleEl.textContent = 'หมดเวลา · Time up'; + msgEl.textContent = 'ยิงยานอวกาศจบแล้ว · SPACE SHOOTER finished — อันดับจากคะแนนยิงหิน'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ id: myId, nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', score: Math.max(0, me.spaceShooterScore | 0) }); + } + others.forEach((o, id) => { + ranks.push({ id, nickname: (o && o.nickname) ? String(o.nickname).trim() : id, score: Math.max(0, o.spaceShooterScore | 0) }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + } + + function syncSpaceShooterFootFromShipCenter(ent) { + if (!ent || !mapData || ent.spaceShooterCx == null || ent.spaceShooterCy == null) return; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + ent.x = ent.spaceShooterCx / ts - cw * 0.5; + ent.y = ent.spaceShooterCy / ts - ch * 0.92; + } + + /** จำกัดกลางยานแนวตั้งให้อยู่แค่ครึ่งล่างของแมพ (ไม่ขึ้นเกินกึ่งกลางความสูง) */ + function clampSpaceShooterWorldCy(cy, mhPx) { + if (!Number.isFinite(cy) || !Number.isFinite(mhPx) || mhPx <= 0) return cy; + const edge = 20; + const lo = mhPx * 0.5; + const hi = Math.max(lo + 4, mhPx - edge); + return Math.max(lo, Math.min(hi, cy)); + } + + function stepSpaceShooterPreviewBots(dt) { + if (spaceShooterGameEnded || !previewFillBots || !mapData || mapData.gameType !== 'space_shooter') return; + const ts = tileSize; + const mw = (mapData.width || 20) * ts; + const mh = (mapData.height || 15) * ts; + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o || o.spaceShooterCx == null) return; + let nearest = null, nd = 1e9; + for (let i = 0; i < spaceShooterAsteroids.length; i++) { + const a = spaceShooterAsteroids[i]; + if (!a) continue; + const d = Math.abs(a.x - o.spaceShooterCx) + Math.abs(a.y - o.spaceShooterCy) * 0.15; + if (d < nd) { nd = d; nearest = a; } + } + let vx = 0; + if (nearest) vx = nearest.x > o.spaceShooterCx ? 1 : nearest.x < o.spaceShooterCx ? -1 : 0; + else vx = Math.sin(performance.now() / 900 + bid.length) > 0 ? 1 : -1; + let vy = 0; + if (nearest && Math.abs(nearest.y - o.spaceShooterCy) > ts * 0.25) { + vy = nearest.y > o.spaceShooterCy ? 1 : -1; + } else if (!nearest) { + vy = Math.sin(performance.now() / 700 + bid.charCodeAt(0)) > 0 ? 1 : -1; + } + const spd = (o.botTier === 'sharp' ? 195 : o.botTier === 'weak' ? 115 : 155); + o.spaceShooterCx = Math.max(18, Math.min(mw - 18, o.spaceShooterCx + vx * spd * dt)); + o.spaceShooterCy = clampSpaceShooterWorldCy(o.spaceShooterCy + vy * spd * 0.7 * dt, mh); + syncSpaceShooterFootFromShipCenter(o); + o.tx = o.x; + o.ty = o.y; + if (typeof o.spaceShooterBotFireCd !== 'number') o.spaceShooterBotFireCd = 0; + o.spaceShooterBotFireCd -= dt; + if (o.spaceShooterBotFireCd <= 0 && nearest && nd < ts * 5) { + o.spaceShooterBotFireCd = o.botTier === 'sharp' ? 0.22 : o.botTier === 'weak' ? 0.48 : 0.34; + spaceShooterBullets.push({ x: o.spaceShooterCx, y: o.spaceShooterCy - 16, vy: -500, ownerId: bid }); + } + }); + } + + function spaceShooterTickFrame() { + if (!mapData || mapData.gameType !== 'space_shooter') return; + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const mw = w * ts, mh = h * ts; + const { cw, ch } = getCharacterFootprintWH(mapData); + const now = performance.now(); + const dt = Math.min(0.055, spaceShooterLastTickMs ? (now - spaceShooterLastTickMs) / 1000 : 0.016); + spaceShooterLastTickMs = now; + + if (spaceShooterGameEnded) { + const wallClock = Date.now(); + for (let pi = spaceShooterPopups.length - 1; pi >= 0; pi--) { + if (wallClock >= spaceShooterPopups[pi].until) spaceShooterPopups.splice(pi, 1); + } + return; + } + const remSec = spaceShooterRemainingSecPlay(); + if (remSec != null && remSec <= 0) { + endSpaceShooterTimeUp(); + return; + } + + const interval = Math.max(320, Number(mapData.spaceShooterAsteroidIntervalMs) || 1040); + spaceShooterSpawnAccMs += dt * 1000; + while (spaceShooterSpawnAccMs >= interval) { + spaceShooterSpawnAccMs -= interval; + spaceShooterAsteroids.push({ + x: 36 + Math.random() * (mw - 72), + y: -45 - Math.random() * 90, + r: 13 + Math.random() * 17, + vy: 58 + Math.random() * 62, + hp: 1, + }); + } + + others.forEach((o, id) => { + if (isPreviewBotId(id)) return; + if (o.tx != null) o.x += (o.tx - o.x) * 0.28; + if (o.ty != null) o.y += (o.ty - o.y) * 0.28; + o.spaceShooterCx = (o.x + cw * 0.5) * ts; + o.spaceShooterCy = clampSpaceShooterWorldCy((o.y + ch * 0.92) * ts, mh); + }); + + let vx = 0; + let vy = 0; + if (!isChatFocused()) { + if (keys['ArrowLeft'] || keys['KeyA']) vx -= 1; + if (keys['ArrowRight'] || keys['KeyD']) vx += 1; + if (keys['ArrowUp'] || keys['KeyW']) vy -= 1; + if (keys['ArrowDown'] || keys['KeyS']) vy += 1; + } + const moveSpd = 280; + if (me.spaceShooterCx != null) { + me.spaceShooterCx = Math.max(18, Math.min(mw - 18, me.spaceShooterCx + vx * moveSpd * dt)); + } + if (me.spaceShooterCy != null) { + me.spaceShooterCy = clampSpaceShooterWorldCy(me.spaceShooterCy + vy * moveSpd * dt, mh); + } + syncSpaceShooterFootFromShipCenter(me); + + spaceShooterFireCd -= dt; + const fireHeld = !isChatFocused() && !!keys['Space']; + if (fireHeld && spaceShooterFireCd <= 0 && me.spaceShooterCy != null) { + spaceShooterFireCd = 0.21; + spaceShooterBullets.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 20, vy: -580, ownerId: myId }); + } + + stepSpaceShooterPreviewBots(dt); + + for (let i = spaceShooterBullets.length - 1; i >= 0; i--) { + const b = spaceShooterBullets[i]; + b.y += b.vy * dt; + if (b.y < -50 || b.x < -30 || b.x > mw + 30) spaceShooterBullets.splice(i, 1); + } + for (let ai = spaceShooterAsteroids.length - 1; ai >= 0; ai--) { + const a = spaceShooterAsteroids[ai]; + a.y += a.vy * dt; + if (a.y > mh + 100) spaceShooterAsteroids.splice(ai, 1); + } + + for (let bi = spaceShooterBullets.length - 1; bi >= 0; bi--) { + const b = spaceShooterBullets[bi]; + let hit = -1; + for (let ai = 0; ai < spaceShooterAsteroids.length; ai++) { + const a = spaceShooterAsteroids[ai]; + const dx = b.x - a.x, dy = b.y - a.y; + const rr = (a.r + 7) * (a.r + 7); + if (dx * dx + dy * dy <= rr) { hit = ai; break; } + } + if (hit >= 0) { + const a = spaceShooterAsteroids[hit]; + a.hp = (a.hp || 1) - 1; + spaceShooterBullets.splice(bi, 1); + if (a.hp <= 0) { + spaceShooterAsteroids.splice(hit, 1); + const add = 5; + if (b.ownerId === myId) { + me.spaceShooterScore = Math.max(0, (me.spaceShooterScore || 0) + add); + spaceShooterPopups.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 }); + } else { + const o = others.get(b.ownerId); + if (o) { + o.spaceShooterScore = Math.max(0, (o.spaceShooterScore || 0) + add); + spaceShooterPopups.push({ x: o.spaceShooterCx, y: o.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 }); + } + } + } + } + } + + const wallClock = Date.now(); + for (let pi = spaceShooterPopups.length - 1; pi >= 0; pi--) { + /* until ใช้ Date.now() — ห้ามเทียบกับ performance.now() (สเกลคนละระบบ → ลบไม่ออก) */ + if (wallClock >= spaceShooterPopups[pi].until) spaceShooterPopups.splice(pi, 1); + } + + me.direction = 'up'; + me.isWalking = vx !== 0 || vy !== 0; + const tEmit = Date.now(); + if (tEmit - spaceShooterLastMoveEmit > 90 && socket && myId != null) { + spaceShooterLastMoveEmit = tEmit; + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + spaceShooterScore: Math.max(0, me.spaceShooterScore | 0), + }); + } + } + + function drawSpaceShooterCombatLayer(ctx, worldToScreen, zDraw, timeMs) { + if (!mapData || !isSpaceShooter()) return; + const refs = buildSpaceShooterParticipantRefsPlay(); + const wallNow = typeof timeMs === 'number' ? timeMs : Date.now(); + + for (let i = 0; i < spaceShooterAsteroids.length; i++) { + const a = spaceShooterAsteroids[i]; + const [sx, sy] = worldToScreen(a.x, a.y); + const sr = Math.max(6, a.r * zDraw); + const grd = ctx.createRadialGradient(sx - sr * 0.25, sy - sr * 0.25, sr * 0.1, sx, sy, sr); + grd.addColorStop(0, 'rgba(90, 85, 95, 0.95)'); + grd.addColorStop(0.55, 'rgba(35, 32, 42, 0.98)'); + grd.addColorStop(1, 'rgba(18, 16, 24, 1)'); + ctx.fillStyle = grd; + ctx.beginPath(); + ctx.arc(sx, sy, sr, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 90, 60, 0.75)'; + ctx.lineWidth = Math.max(1.5, zDraw * 1.2); + ctx.beginPath(); + ctx.arc(sx, sy, sr * 0.88, 0.6, 1.9); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(sx + sr * 0.15, sy + sr * 0.1, sr * 0.35, 0, Math.PI * 2); + ctx.stroke(); + ctx.shadowColor = 'rgba(255, 60, 40, 0.55)'; + ctx.shadowBlur = sr * 0.45; + ctx.strokeStyle = 'rgba(255, 120, 80, 0.35)'; + ctx.beginPath(); + ctx.arc(sx, sy, sr + 3 * zDraw, 0, Math.PI * 2); + ctx.stroke(); + ctx.shadowBlur = 0; + } + + for (let i = 0; i < spaceShooterBullets.length; i++) { + const b = spaceShooterBullets[i]; + for (let k = 0; k < 4; k++) { + const t = k / 4; + const yy = b.y + t * 22; + const [bx, by] = worldToScreen(b.x, yy); + ctx.fillStyle = `rgba(255, ${180 - k * 35}, ${80 - k * 15}, ${0.95 - k * 0.18})`; + ctx.beginPath(); + ctx.arc(bx, by, Math.max(2, 3.2 * zDraw - k * 0.4), 0, Math.PI * 2); + ctx.fill(); + } + } + + refs.forEach((entry, idx) => { + const ent = entry.ref; + if (ent.spaceShooterCx == null || ent.spaceShooterCy == null) return; + const [sx, sy] = worldToScreen(ent.spaceShooterCx, ent.spaceShooterCy); + const col = SPACE_SHOOTER_SHIP_COLORS[idx % SPACE_SHOOTER_SHIP_COLORS.length]; + const bodyW = 14 * zDraw; + const bodyH = 22 * zDraw; + ctx.save(); + ctx.translate(sx, sy); + ctx.fillStyle = col; + ctx.strokeStyle = 'rgba(180, 255, 255, 0.85)'; + ctx.lineWidth = Math.max(1.2, zDraw); + ctx.beginPath(); + ctx.moveTo(0, -bodyH * 0.55); + ctx.lineTo(-bodyW * 0.55, bodyH * 0.42); + ctx.lineTo(0, bodyH * 0.22); + ctx.lineTo(bodyW * 0.55, bodyH * 0.42); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = 'rgba(255, 220, 120, 0.95)'; + ctx.beginPath(); + ctx.moveTo(-bodyW * 0.22, bodyH * 0.38); + ctx.lineTo(0, bodyH * 0.72); + ctx.lineTo(bodyW * 0.22, bodyH * 0.38); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + const name = (ent.nickname || '').slice(0, 10); + if (name) { + ctx.font = `${Math.max(9, 10 * zDraw)}px system-ui, "Kanit", sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(224, 242, 255, 0.92)'; + ctx.strokeStyle = 'rgba(0, 20, 40, 0.85)'; + ctx.lineWidth = 3; + ctx.strokeText(name, sx, sy - bodyH * 0.62); + ctx.fillText(name, sx, sy - bodyH * 0.62); + ctx.textAlign = 'left'; + } + }); + + for (let i = 0; i < spaceShooterPopups.length; i++) { + const p = spaceShooterPopups[i]; + if (wallNow >= p.until) continue; + const [px, py] = worldToScreen(p.x, p.y); + const dur = 700; + const age = 1 - Math.max(0, Math.min(1, (p.until - wallNow) / dur)); + ctx.save(); + ctx.globalAlpha = Math.max(0, 1 - age * 0.92); + ctx.font = `bold ${Math.max(12, 16 * zDraw)}px Orbitron, ui-sans-serif, sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = '#ffe066'; + ctx.strokeStyle = 'rgba(120, 60, 0, 0.6)'; + ctx.lineWidth = 3; + ctx.strokeText(p.text || '+5', px, py - age * 18); + ctx.fillText(p.text || '+5', px, py - age * 18); + ctx.restore(); + } + } + + const BALLOON_BOSS_PLAYER_COLORS = ['#ff79c6', '#7aa2f7', '#f9e2af', '#f7768e', '#9ece6a', '#bb9af7']; + + function normalizeBalloonBossPlayerSlotsInPlay(md) { + if (!md || md.gameType !== 'balloon_boss') return; + const w = md.width || 20, h = md.height || 15; + const src = md.balloonBossPlayerSlots || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= 6 ? Math.floor(n) : 0); + } + rows.push(row); + } + md.balloonBossPlayerSlots = rows; + } + + function getBalloonBossPlayerSpawnWorldCenterFromMap(md, slot1to6, ts) { + if (!md || !md.balloonBossPlayerSlots) return null; + const w = md.width || 20, h = md.height || 15; + const g = md.balloonBossPlayerSlots; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (g[y] && g[y][x] === slot1to6) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; + } + } + return null; + } + + function getBalloonBossBossWorldCenterPlay(md, ts) { + const bx = md && md.balloonBossBossSpawn; + if (bx && Number.isFinite(Number(bx.x)) && Number.isFinite(Number(bx.y))) { + return { cx: (Number(bx.x) + 0.5) * ts, cy: (Number(bx.y) + 0.5) * ts }; + } + const w = md.width || 20, h = md.height || 15; + return { cx: (w * 0.5) * ts, cy: (h * 0.38) * ts }; + } + + function buildBalloonBossParticipantRefsPlay() { + const refs = []; + if (me) refs.push({ id: myId, ref: me }); + const humanIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + humanIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + botIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + return refs.filter((r) => r.ref); + } + + function balloonBossBalloonsStartPlay() { + const n = Number(mapData && mapData.balloonBossBalloonsPerPlayer); + return Math.max(1, Math.min(12, Number.isFinite(n) ? Math.floor(n) : 5)); + } + + function balloonBossMaxHpPlay() { + const n = Number(mapData && mapData.balloonBossMaxHp); + return Math.max(20, Math.min(9999, Number.isFinite(n) ? Math.floor(n) : 100)); + } + + function balloonBossFireDelayMsPlay() { + const n = Number(mapData && mapData.balloonBossFireDelayMs); + return Math.max(120, Math.min(2000, Number.isFinite(n) ? Math.floor(n) : 380)); + } + + /** ความเร่งยาน (px/s²) — ต่ำ = คุมยาก ลื่น */ + function balloonBossShipAccelPlay() { + const n = Number(mapData && mapData.balloonBossShipAccel); + return Math.max(80, Math.min(520, Number.isFinite(n) ? n : 210)); + } + + /** ความเร็วสูงสุดยาน (px/s) */ + function balloonBossShipMaxSpeedPlay() { + const n = Number(mapData && mapData.balloonBossShipMaxSpeed); + return Math.max(60, Math.min(320, Number.isFinite(n) ? n : 158)); + } + + /** แรงหน่วงต่อวินาที (velocity *= 1 - min(1, drag*dt)) — สูง = หยุดเร็วขึ้น */ + function balloonBossShipDragPlay() { + const n = Number(mapData && mapData.balloonBossShipDrag); + return Math.max(0.4, Math.min(6, Number.isFinite(n) ? n : 1.65)); + } + + function balloonBossTimeLimitSecPlay() { + const t = Number(mapData && mapData.balloonBossTimeSec); + if (Number.isFinite(t) && t === 0) return 0; + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + return 120; + } + + function balloonBossRemainingSecPlay() { + const lim = balloonBossTimeLimitSecPlay(); + if (lim <= 0) return null; + const elapsed = (performance.now() - balloonBossSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function balloonBossTeamDamagePlay() { + let s = Math.max(0, me.balloonBossScore | 0); + others.forEach((o) => { s += Math.max(0, o.balloonBossScore | 0); }); + return s; + } + + function syncBalloonBossFootFromCenter(ent) { + if (!ent || !mapData || ent.balloonBossCx == null || ent.balloonBossCy == null) return; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + ent.x = ent.balloonBossCx / ts - cw * 0.5; + ent.y = ent.balloonBossCy / ts - ch * 0.92; + } + + function clampBalloonBossWorld(cx, cy, mw, mh) { + const e = 22; + return { + cx: Math.max(e, Math.min(mw - e, cx)), + cy: Math.max(e, Math.min(mh - e, cy)), + }; + } + + function applyBalloonBossSpawnLayoutPlay() { + if (!mapData || mapData.gameType !== 'balloon_boss') return; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const { cw, ch } = getCharacterFootprintWH(mapData); + const mw = w * ts, mh = h * ts; + const refs = buildBalloonBossParticipantRefsPlay(); + const n = refs.length || 1; + const bStart = balloonBossBalloonsStartPlay(); + refs.forEach((entry, idx) => { + const ent = entry.ref; + const slot = (idx % 6) + 1; + const cen = getBalloonBossPlayerSpawnWorldCenterFromMap(mapData, slot, ts); + let cx, cy; + if (cen) { + cx = cen.cx; + cy = cen.cy; + } else { + const t = (idx + 1) / (n + 1); + cx = t * mw; + cy = Math.min(mh - ts * 1.2, (h - 1.2) * ts); + } + const cl = clampBalloonBossWorld(cx, cy, mw, mh); + ent.balloonBossCx = cl.cx; + ent.balloonBossCy = cl.cy; + if (typeof ent.balloonBossScore !== 'number' || !Number.isFinite(ent.balloonBossScore)) ent.balloonBossScore = 0; + if (typeof ent.balloonBossBalloons !== 'number' || !Number.isFinite(ent.balloonBossBalloons)) ent.balloonBossBalloons = bStart; + if (typeof ent.balloonBossHitIframe !== 'number') ent.balloonBossHitIframe = 0; + ent.balloonBossVelX = 0; + ent.balloonBossVelY = 0; + ent.x = ent.balloonBossCx / ts - cw * 0.5; + ent.y = ent.balloonBossCy / ts - ch * 0.92; + ent.tx = ent.x; + ent.ty = ent.y; + ent.direction = 'up'; + }); + } + + function endBalloonBossGame(reason) { + if (balloonBossGameEnded || !mapData || mapData.gameType !== 'balloon_boss') return; + balloonBossGameEnded = true; + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) { + titleEl.textContent = reason === 'victory' ? 'ชนะบอส! · Boss defeated' : 'หมดเวลา · Time up'; + } + msgEl.textContent = reason === 'victory' + ? 'MEGA VIRUS ถูกกำจัดแล้ว — อันดับจากความเสียหายที่ทำกับบอส' + : 'หมดเวลา — อันดับจากความเสียหายที่ทำกับบอส'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ id: myId, nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', score: Math.max(0, me.balloonBossScore | 0) }); + } + others.forEach((o, id) => { + ranks.push({ id, nickname: (o && o.nickname) ? String(o.nickname).trim() : id, score: Math.max(0, o.balloonBossScore | 0) }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — DMG ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + } + + function stepBalloonBossPreviewBots(dt, mw, mh, bossCx, bossCy, ts) { + if (!previewFillBots || !mapData || mapData.gameType !== 'balloon_boss') return; + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o || o.balloonBossEliminated || o.balloonBossCx == null) return; + const wob = Math.sin(performance.now() / 800 + bid.length) * 0.55; + const wob2 = Math.cos(performance.now() / 1100 + bid.charCodeAt(0)) * 0.45; + const spd = 48; + let nx = o.balloonBossCx + wob * spd * dt; + let ny = o.balloonBossCy + wob2 * spd * 0.72 * dt; + const cl = clampBalloonBossWorld(nx, ny, mw, mh); + o.balloonBossCx = cl.cx; + o.balloonBossCy = cl.cy; + syncBalloonBossFootFromCenter(o); + o.tx = o.x; + o.ty = o.y; + if (typeof o.balloonBossBotNextFire === 'number') o.balloonBossBotNextFire -= dt; + else o.balloonBossBotNextFire = 0.8 + Math.random() * 0.9; + if (o.balloonBossBotNextFire <= 0) { + o.balloonBossBotNextFire = 0.9 + Math.random() * 1.1; + const delayMs = balloonBossFireDelayMsPlay(); + const dx = bossCx - o.balloonBossCx, dy = bossCy - o.balloonBossCy; + const d = Math.sqrt(dx * dx + dy * dy) || 1; + balloonBossPendingShots.push({ + releaseAt: performance.now() + delayMs, + sx: o.balloonBossCx, sy: o.balloonBossCy, + vx: (dx / d) * 520, vy: (dy / d) * 520, + ownerId: bid, + }); + } + }); + } + + function balloonBossTickFrame() { + if (!mapData || mapData.gameType !== 'balloon_boss') return; + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const mw = w * ts, mh = h * ts; + const { cw, ch } = getCharacterFootprintWH(mapData); + others.forEach((o, id) => { + if (isPreviewBotId(id)) return; + if (!o || o.balloonBossEliminated) return; + if (o.tx != null) o.x += (o.tx - o.x) * 0.24; + if (o.ty != null) o.y += (o.ty - o.y) * 0.24; + const cx = (o.x + cw * 0.5) * ts; + const cy = (o.y + ch * 0.92) * ts; + const cl = clampBalloonBossWorld(cx, cy, mw, mh); + o.balloonBossCx = cl.cx; + o.balloonBossCy = cl.cy; + }); + const now = performance.now(); + const dt = Math.min(0.055, balloonBossLastTickMs ? (now - balloonBossLastTickMs) / 1000 : 0.016); + balloonBossLastTickMs = now; + const bossC = getBalloonBossBossWorldCenterPlay(mapData, ts); + const bossR = Math.max(36, ts * 1.15); + const maxHp = balloonBossMaxHpPlay(); + const teamDmg = balloonBossTeamDamagePlay(); + + if (me.balloonBossEliminated) { + me.balloonBossVelX = 0; + me.balloonBossVelY = 0; + } + if (balloonBossGameEnded) { + for (let hi = balloonBossHitFx.length - 1; hi >= 0; hi--) { + balloonBossHitFx[hi].t -= dt; + if (balloonBossHitFx[hi].t <= 0) balloonBossHitFx.splice(hi, 1); + } + return; + } + const remSec = balloonBossRemainingSecPlay(); + if (remSec != null && remSec <= 0) { + endBalloonBossGame('time'); + return; + } + if (teamDmg >= maxHp) { + endBalloonBossGame('victory'); + return; + } + + for (let hi = balloonBossHitFx.length - 1; hi >= 0; hi--) { + balloonBossHitFx[hi].t -= dt; + if (balloonBossHitFx[hi].t <= 0) balloonBossHitFx.splice(hi, 1); + } + + const aliveRefs = buildBalloonBossParticipantRefsPlay().filter((e) => e.ref && !e.ref.balloonBossEliminated); + + for (let pi = balloonBossPendingShots.length - 1; pi >= 0; pi--) { + const pnd = balloonBossPendingShots[pi]; + if (now >= pnd.releaseAt) { + balloonBossPlayerBullets.push({ x: pnd.sx, y: pnd.sy, vx: pnd.vx, vy: pnd.vy, ownerId: pnd.ownerId }); + balloonBossPendingShots.splice(pi, 1); + } + } + + const bossFireEvery = 0.92; + balloonBossBossFireAcc += dt; + while (balloonBossBossFireAcc >= bossFireEvery) { + balloonBossBossFireAcc -= bossFireEvery; + if (aliveRefs.length) { + const wave = Math.floor(now / 1000); + const tgt = aliveRefs[wave % aliveRefs.length].ref; + if (tgt && tgt.balloonBossCx != null) { + const dx = tgt.balloonBossCx - bossC.cx, dy = tgt.balloonBossCy - bossC.cy; + const d = Math.sqrt(dx * dx + dy * dy) || 1; + const spd = 290; + balloonBossBossBullets.push({ x: bossC.cx, y: bossC.cy, vx: (dx / d) * spd, vy: (dy / d) * spd }); + } + } + } + + function applyBalloonBossHitToEntity(ent) { + if (!ent || ent.balloonBossEliminated) return; + if (ent.balloonBossHitIframe > now) return; + ent.balloonBossHitIframe = now + 520; + ent.balloonBossBalloons = Math.max(0, (ent.balloonBossBalloons | 0) - 1); + if (ent.balloonBossBalloons <= 0) { + ent.balloonBossEliminated = true; + ent.balloonBossBalloons = 0; + } + if (ent === me && socket) { + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + balloonBossBalloons: me.balloonBossBalloons, + balloonBossEliminated: !!me.balloonBossEliminated, + balloonBossScore: Math.max(0, me.balloonBossScore | 0), + }); + } + } + + for (let bi = balloonBossBossBullets.length - 1; bi >= 0; bi--) { + const b = balloonBossBossBullets[bi]; + b.x += b.vx * dt; + b.y += b.vy * dt; + if (b.x < -80 || b.x > mw + 80 || b.y < -80 || b.y > mh + 80) { + balloonBossBossBullets.splice(bi, 1); + continue; + } + let hit = false; + aliveRefs.forEach((entry) => { + if (hit) return; + const ent = entry.ref; + if (!ent || ent.balloonBossCx == null) return; + const dx = b.x - ent.balloonBossCx, dy = b.y - ent.balloonBossCy; + if (dx * dx + dy * dy <= (26 * 26)) { + hit = true; + applyBalloonBossHitToEntity(ent); + } + }); + if (hit) balloonBossBossBullets.splice(bi, 1); + } + + for (let i = balloonBossPlayerBullets.length - 1; i >= 0; i--) { + const b = balloonBossPlayerBullets[i]; + b.x += b.vx * dt; + b.y += b.vy * dt; + if (b.x < -60 || b.x > mw + 60 || b.y < -60 || b.y > mh + 60) { + balloonBossPlayerBullets.splice(i, 1); + continue; + } + const dx = b.x - bossC.cx, dy = b.y - bossC.cy; + if (dx * dx + dy * dy <= bossR * bossR) { + balloonBossHitFx.push({ x: bossC.cx, y: bossC.cy, t: 0.22 }); + const add = 1; + if (b.ownerId === myId) { + me.balloonBossScore = Math.max(0, (me.balloonBossScore | 0) + add); + } else { + const o = others.get(b.ownerId); + if (o) o.balloonBossScore = Math.max(0, (o.balloonBossScore | 0) + add); + } + balloonBossPlayerBullets.splice(i, 1); + } + } + + let inX = 0, inY = 0; + if (!me.balloonBossEliminated && !isChatFocused()) { + if (keys['ArrowLeft'] || keys['KeyA']) inX -= 1; + if (keys['ArrowRight'] || keys['KeyD']) inX += 1; + if (keys['ArrowUp'] || keys['KeyW']) inY -= 1; + if (keys['ArrowDown'] || keys['KeyS']) inY += 1; + } + const accel = balloonBossShipAccelPlay(); + const maxSpd = balloonBossShipMaxSpeedPlay(); + const dragPerSec = balloonBossShipDragPlay(); + if (typeof me.balloonBossVelX !== 'number' || !Number.isFinite(me.balloonBossVelX)) me.balloonBossVelX = 0; + if (typeof me.balloonBossVelY !== 'number' || !Number.isFinite(me.balloonBossVelY)) me.balloonBossVelY = 0; + let nvx = me.balloonBossVelX; + let nvy = me.balloonBossVelY; + if (inX !== 0 || inY !== 0) { + const il = Math.sqrt(inX * inX + inY * inY) || 1; + nvx += (inX / il) * accel * dt; + nvy += (inY / il) * accel * dt; + } + const drag = Math.min(1, dragPerSec * dt); + nvx *= (1 - drag); + nvy *= (1 - drag); + const curSpd = Math.sqrt(nvx * nvx + nvy * nvy); + if (curSpd > maxSpd) { + const s = maxSpd / curSpd; + nvx *= s; + nvy *= s; + } + me.balloonBossVelX = nvx; + me.balloonBossVelY = nvy; + if (!me.balloonBossEliminated && me.balloonBossCx != null) { + const ncx = me.balloonBossCx + nvx * dt; + const ncy = me.balloonBossCy + nvy * dt; + const cl = clampBalloonBossWorld(ncx, ncy, mw, mh); + if (Math.abs(cl.cx - ncx) > 0.5) me.balloonBossVelX = 0; + if (Math.abs(cl.cy - ncy) > 0.5) me.balloonBossVelY = 0; + me.balloonBossCx = cl.cx; + me.balloonBossCy = cl.cy; + } + syncBalloonBossFootFromCenter(me); + me.direction = 'up'; + me.isWalking = Math.abs(me.balloonBossVelX) > 8 || Math.abs(me.balloonBossVelY) > 8 || inX !== 0 || inY !== 0; + + balloonBossPlayerFireCd -= dt; + const wantFire = !isChatFocused() && !!keys['Space'] && !me.balloonBossEliminated; + if (wantFire && balloonBossPlayerFireCd <= 0 && me.balloonBossCy != null) { + balloonBossPlayerFireCd = 0.35; + const delayMs = balloonBossFireDelayMsPlay(); + const dx = bossC.cx - me.balloonBossCx, dy = bossC.cy - me.balloonBossCy; + const d = Math.sqrt(dx * dx + dy * dy) || 1; + balloonBossPendingShots.push({ + releaseAt: now + delayMs, + sx: me.balloonBossCx, sy: me.balloonBossCy, + vx: (dx / d) * 480, vy: (dy / d) * 480, + ownerId: myId, + }); + } + + stepBalloonBossPreviewBots(dt, mw, mh, bossC.cx, bossC.cy, ts); + + const tEmit = Date.now(); + if (tEmit - balloonBossLastMoveEmit > 95 && socket && myId != null) { + balloonBossLastMoveEmit = tEmit; + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + balloonBossScore: Math.max(0, me.balloonBossScore | 0), + balloonBossBalloons: Math.max(0, me.balloonBossBalloons | 0), + balloonBossEliminated: !!me.balloonBossEliminated, + }); + } + } + + function drawBalloonBossCombatLayer(ctx, worldToScreen, zDraw, timeMs) { + if (!mapData || !isBalloonBoss()) return; + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const mw = w * ts, mh = h * ts; + const bossC = getBalloonBossBossWorldCenterPlay(mapData, ts); + const maxHp = balloonBossMaxHpPlay(); + const dmg = balloonBossTeamDamagePlay(); + const hpLeft = Math.max(0, maxHp - dmg); + const bossR = Math.max(36, ts * 1.15); + const refs = buildBalloonBossParticipantRefsPlay(); + const leaderId = (() => { + let best = null, bs = -1; + refs.forEach((e) => { + const sc = Math.max(0, e.ref.balloonBossScore | 0); + if (sc > bs) { bs = sc; best = e.id; } + }); + return best; + })(); + + for (let hi = 0; hi < balloonBossHitFx.length; hi++) { + const fx = balloonBossHitFx[hi]; + const [hx, hy] = worldToScreen(fx.x, fx.y); + const pulse = 1 + (fx.t || 0) * 3; + ctx.save(); + ctx.globalAlpha = Math.min(1, (fx.t || 0) * 4); + ctx.strokeStyle = 'rgba(255, 220, 120, 0.9)'; + ctx.lineWidth = 3 * zDraw; + ctx.beginPath(); + ctx.arc(hx, hy, bossR * zDraw * 0.5 * pulse, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + } + + const [bsx, bsy] = worldToScreen(bossC.cx, bossC.cy); + ctx.save(); + ctx.translate(bsx, bsy); + ctx.strokeStyle = 'rgba(0, 255, 255, 0.75)'; + ctx.lineWidth = Math.max(2, 2.5 * zDraw); + ctx.beginPath(); + for (let k = 0; k < 6; k++) { + const ang = (k / 6) * Math.PI * 2 - Math.PI / 2; + const px = Math.cos(ang) * bossR * zDraw * 1.05; + const py = Math.sin(ang) * bossR * zDraw * 1.05; + if (k === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + } + ctx.closePath(); + ctx.stroke(); + ctx.fillStyle = 'rgba(0, 40, 60, 0.35)'; + ctx.fill(); + + const skullR = bossR * zDraw * 0.62; + const grd = ctx.createRadialGradient(-skullR * 0.2, -skullR * 0.25, skullR * 0.1, 0, 0, skullR); + grd.addColorStop(0, 'rgba(55, 50, 62, 0.98)'); + grd.addColorStop(1, 'rgba(22, 18, 30, 1)'); + ctx.fillStyle = grd; + ctx.beginPath(); + ctx.arc(0, 0, skullR, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = 'rgba(255, 40, 60, 0.95)'; + ctx.shadowColor = 'rgba(255, 0, 0, 0.8)'; + ctx.shadowBlur = 12 * zDraw; + ctx.beginPath(); + ctx.arc(-skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2); + ctx.arc(skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + + ctx.fillStyle = 'rgba(255, 100, 200, 0.9)'; + ctx.font = `bold ${Math.max(10, 11 * zDraw)}px ui-monospace, monospace`; + ctx.textAlign = 'center'; + ctx.fillText('MEGA VIRUS : [ ' + hpLeft + ' / ' + maxHp + ' ]', 0, -bossR * zDraw - 14); + const barW = bossR * zDraw * 2.2; + const barH = Math.max(5, 6 * zDraw); + ctx.fillStyle = 'rgba(0,0,0,0.55)'; + ctx.fillRect(-barW / 2, -bossR * zDraw - 8, barW, barH); + ctx.fillStyle = 'rgba(255, 80, 200, 0.85)'; + ctx.fillRect(-barW / 2, -bossR * zDraw - 8, barW * (hpLeft / maxHp), barH); + ctx.strokeStyle = 'rgba(180, 255, 255, 0.5)'; + ctx.strokeRect(-barW / 2, -bossR * zDraw - 8, barW, barH); + ctx.restore(); + + function drawBalloonsCluster(sx, sy, count, col, z) { + const n = Math.max(0, Math.min(8, count | 0)); + for (let b = 0; b < n; b++) { + const ox = (b - (n - 1) / 2) * 7 * z; + const oy = -18 * z - Math.abs(b - (n - 1) / 2) * 2 * z; + ctx.fillStyle = col; + ctx.beginPath(); + ctx.ellipse(sx + ox, sy + oy, 6.5 * z, 8 * z, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255,255,255,0.35)'; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + + refs.forEach((entry, idx) => { + const ent = entry.ref; + if (ent.balloonBossCx == null || ent.balloonBossCy == null) return; + const [sx, sy] = worldToScreen(ent.balloonBossCx, ent.balloonBossCy); + const col = BALLOON_BOSS_PLAYER_COLORS[idx % BALLOON_BOSS_PLAYER_COLORS.length]; + const z = zDraw; + ctx.save(); + if (ent.balloonBossEliminated) ctx.globalAlpha = 0.38; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.65)'; + ctx.lineWidth = Math.max(1.2, 1.5 * z); + ctx.beginPath(); + ctx.arc(sx, sy, 22 * z, 0, Math.PI * 2); + ctx.stroke(); + drawBalloonsCluster(sx, sy, ent.balloonBossBalloons | 0, col, z); + const nm = String(ent.nickname || '').slice(0, 12); + ctx.font = `${Math.max(9, 10 * z)}px system-ui, "Kanit", sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(248, 250, 252, 0.95)'; + ctx.strokeStyle = 'rgba(0, 10, 30, 0.85)'; + ctx.lineWidth = 3; + let label = nm; + if (entry.id === leaderId && leaderId != null) label = '♔ ' + nm; + ctx.strokeText(label, sx, sy + 28 * z); + ctx.fillText(label, sx, sy + 28 * z); + const charImg = getPlayTintedAvatarSource( + getAvatarImg(ent.characterId, 'up', timeMs, !!ent.isWalking), + ent.characterId, 'up', timeMs, !!ent.isWalking, + ent.playTint || playTintFromPeerId(entry.id) + ); + const size = 26 * z; + if (charImg && ((charImg.tagName === 'CANVAS' && charImg.width > 0) || (charImg.complete && charImg.naturalWidth))) { + const iw = charImg.tagName === 'CANVAS' ? charImg.width : charImg.naturalWidth; + const ih = charImg.tagName === 'CANVAS' ? charImg.height : charImg.naturalHeight; + const sc = Math.min(size / iw, size / ih, 1); + const dw = iw * sc, dh = ih * sc; + ctx.drawImage(charImg, 0, 0, iw, ih, sx - dw / 2, sy - dh + 4 * z, dw, dh); + } else { + ctx.fillStyle = col; + ctx.fillRect(sx - 10 * z, sy - 18 * z, 20 * z, 22 * z); + } + ctx.restore(); + }); + + for (let i = 0; i < balloonBossPlayerBullets.length; i++) { + const b = balloonBossPlayerBullets[i]; + const [bx, by] = worldToScreen(b.x, b.y); + ctx.save(); + ctx.translate(bx, by); + ctx.rotate(Math.atan2(b.vy, b.vx) + Math.PI / 2); + ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.beginPath(); + ctx.moveTo(0, -8 * zDraw); + ctx.lineTo(-5 * zDraw, 6 * zDraw); + ctx.lineTo(5 * zDraw, 6 * zDraw); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = 'rgba(0, 255, 200, 0.5)'; + ctx.font = `${Math.max(7, 8 * zDraw)}px monospace`; + ctx.textAlign = 'center'; + ctx.fillText('01001', 0, 12 * zDraw); + ctx.restore(); + } + for (let i = 0; i < balloonBossBossBullets.length; i++) { + const b = balloonBossBossBullets[i]; + const [bx, by] = worldToScreen(b.x, b.y); + ctx.fillStyle = 'rgba(255, 60, 120, 0.92)'; + ctx.beginPath(); + ctx.arc(bx, by, Math.max(3, 4.5 * zDraw), 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 200, 255, 0.6)'; + ctx.stroke(); + } + } + + function jumpSurviveCollectPlatformCells(md) { + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + const out = []; + if (!pa) return out; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (pa[y] && pa[y][x] === 1) out.push({ x, y }); + } + } + return out; + } + + /** ความสูงแมปเป็นคาบวนของแพลตฟอร์ม — แถวที่เลื่อนออกบนจะวนมาโผล่ล่าง */ + function jumpSurvivePlatformPeriodPx(md) { + return (md.height || 15) * tileSize; + } + + /** + * ยืนบนหน้าแพลตฟอร์มช่อง (tx,ty) — เลือกชั้น k ที่ใกล้ hintY (กล้อง / กลางแมป) + */ + function jumpSurviveGridPosStandingOnPlatform(md, tx, ty, scrollPx) { + const ts = tileSize; + const h = md.height || 15; + const period = jumpSurvivePlatformPeriodPx(md); + const { cw, ch } = getCharacterFootprintWH(md); + const rawTop = ty * ts - scrollPx; + const hintY = (jumpSurviveCamCenterY > 1) + ? jumpSurviveCamCenterY + : h * ts * 0.72; + const k = Math.round((hintY - rawTop - ts * 0.35) / period); + const platTop = rawTop + k * period; + const feetY = platTop; + const pxc = (tx + 0.5) * ts; + return { + x: pxc / ts - cw * 0.5, + y: feetY / ts - ch, + }; + } + + function jumpSurviveGridPosFromTileCenter(md, tx, ty) { + return jumpSurviveGridPosStandingOnPlatform(md, tx, ty, 0); + } + + function jumpSurviveRandomStandGridPos(md) { + const cells = jumpSurviveCollectPlatformCells(md); + const sc = jumpSurvivePlatformScrollPx; + if (cells.length) { + const c = cells[Math.floor(Math.random() * cells.length)]; + return jumpSurviveGridPosStandingOnPlatform(md, c.x, c.y, sc); + } + const sp = md.spawn || { x: 1, y: 1 }; + return jumpSurviveGridPosStandingOnPlatform(md, Number(sp.x) || 1, Number(sp.y) || 1, sc); + } + + /** ทดสอบจากเอดิเตอร์: วางคน+บอทบนแพลตฟอร์ม (ไม่ให้เกิดบน spawnArea บนฟ้าแล้วลอย) */ + function applyJumpSurvivePreviewSpawnLayout(onlyBots) { + if (!mapData || mapData.gameType !== 'jump_survive') return; + const cells = jumpSurviveCollectPlatformCells(mapData); + cells.sort((a, b) => b.y - a.y || a.x - b.x); + const sc = jumpSurvivePlatformScrollPx; + const pool = cells.length + ? cells.map((c) => jumpSurviveGridPosStandingOnPlatform(mapData, c.x, c.y, sc)) + : [jumpSurviveGridPosStandingOnPlatform(mapData, Number(mapData.spawn && mapData.spawn.x) || 1, Number(mapData.spawn && mapData.spawn.y) || 1, sc)]; + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + + function stampJumpBotState(o) { + if (!o) return; + o.jumpSurviveVy = 0; + o.jumpSurviveOnGround = true; + o.jumpSurviveEliminated = false; + o.tx = o.x; + o.ty = o.y; + } + + if (onlyBots) { + let idx = 1 + realIds.length; + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || !pool.length) return; + const p = pool[idx % pool.length]; + idx++; + o.x = p.x + (Math.random() - 0.5) * 0.08; + o.y = p.y + (Math.random() - 0.5) * 0.08; + stampJumpBotState(o); + }); + return; + } + + let idx = 0; + function assignEnt(ent) { + if (!ent || !pool.length) return; + const p = pool[idx % pool.length]; + idx++; + ent.x = p.x + (Math.random() - 0.5) * 0.08; + ent.y = p.y + (Math.random() - 0.5) * 0.08; + stampJumpBotState(ent); + } + assignEnt(me); + realIds.forEach((rid) => assignEnt(others.get(rid))); + botIds.forEach((bid) => assignEnt(others.get(bid))); + jumpSurviveInitRuntime(); + } + + /** + * ฟิสิกส์กระโดดร่วม (ผู้เล่น + บอท preview) + * @returns {{ pxc: number, feetY: number, vy: number, onGround: boolean, died: boolean }} + */ + function jumpSurvivePhysicsIntegrate(md, opts) { + const { + dt, + cw, ch, + pxc0, feetY0, vy0, wasOnGround, + jumpQueued, + moveLeft, moveRight, + camCenterY, + halfViewH, + } = opts; + const w = md.width || 20, h = md.height || 15; + const ts = tileSize; + const bw = ts * 0.34; + const bh = ts * 0.9; + let pxc = pxc0; + let feetY = feetY0; + let vy = vy0; + let onGround = wasOnGround; + + const gFull = Number(md.jumpSurviveGravity) > 0 ? Number(md.jumpSurviveGravity) : 3200; + const g = gFull * dt; + let jumpV0; + if (Number(md.jumpSurviveJumpImpulse) > 0) { + jumpV0 = -Number(md.jumpSurviveJumpImpulse); + } else { + const mult = (typeof playJumpSurviveJumpHeightMult === 'number' && playJumpSurviveJumpHeightMult > 0) + ? playJumpSurviveJumpHeightMult + : 1.5; + const charHpixels = ch * ts; + const impulse = Math.sqrt(Math.max(1e-6, 2 * gFull * mult * charHpixels)); + jumpV0 = -Math.min(impulse, ts * 36); + } + let moveX = (Number(md.jumpSurviveMovePxPerSec) > 0 ? Number(md.jumpSurviveMovePxPerSec) : 200) * dt; + if (isJumpSurviveMissionUiMapPlay()) moveX *= 1.35; + + if (jumpQueued && onGround) { + vy = jumpV0; + onGround = false; + } + vy += g; + + let nx = pxc; + if (moveLeft) nx -= moveX; + if (moveRight) nx += moveX; + nx = Math.max(bw + 1, Math.min(w * ts - bw - 1, nx)); + + const headY0 = feetY - bh; + const tryL = nx - bw; + const tryR = nx + bw; + if (!jumpSurviveOverlapsSolidForHorzMove(md, tryL, headY0, tryR, feetY)) { + pxc = nx; + } + + feetY += vy * dt; + let headTop = feetY - bh; + if (jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + if (vy > 0) { + for (let s = 0; s < 22; s++) { + feetY -= ts * 0.035; + headTop = feetY - bh; + if (!jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + vy = 0; + onGround = true; + break; + } + } + } else if (vy < 0) { + for (let s = 0; s < 22; s++) { + feetY += ts * 0.035; + headTop = feetY - bh; + if (!jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + vy = 0; + break; + } + } + } + } else { + onGround = false; + } + + headTop = feetY - bh; + const headBandBottom = headTop + bh * 0.42; + if (jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY) && + jumpSurviveOverlapsObjectWall(md, pxc - bw, headTop, pxc + bw, headBandBottom)) { + return { pxc, feetY, vy, onGround, died: true }; + } + + headTop = feetY - bh; + const topKill = camCenterY - halfViewH - ts * 0.35; + const botKill = camCenterY + halfViewH + ts * 0.85; + if (feetY < topKill || feetY > botKill || headTop < -ts * 2) { + return { pxc, feetY, vy, onGround, died: true }; + } + + return { pxc, feetY, vy, onGround, died: false }; + } + + function jumpSurviveEntityFromHitbox(o, md, pxc, feetYpx) { + const { cw, ch } = getCharacterFootprintWH(md); + o.x = pxc / tileSize - cw * 0.5; + o.y = feetYpx / tileSize - ch; + } + + function jumpSurviveInitRuntime() { + jumpSurviveLastTickMs = performance.now(); + jumpSurviveSessionStartMs = performance.now(); + jumpSurviveVy = 0; + jumpSurviveJumpQueued = false; + jumpSurviveOnGround = false; + jumpSurvivePlatformScrollPx = 0; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + if (isJumpSurviveMissionUiMapPlay()) { + const w = mapData.width || 20, h = mapData.height || 15; + jumpSurviveCamCenterX = (w * ts) * 0.5; + jumpSurviveCamCenterY = (h * ts) * 0.5; + } else { + jumpSurviveCamCenterX = (me.x + cw * 0.5) * ts; + jumpSurviveCamCenterY = (me.y + ch * 0.5) * ts; + } + } + + function jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const x0 = Math.floor(left / ts); + const x1 = Math.floor((right - 1e-6) / ts); + const y0 = Math.floor(top / ts); + const y1 = Math.floor((bottom - 1e-6) / ts); + for (let ty = y0; ty <= y1; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (tx < 0 || tx >= w || ty >= h) return true; + if (ty < 0) continue; + const ob = md.objects && md.objects[ty] && md.objects[ty][tx]; + if (ob === 1) return true; + } + } + return false; + } + + function jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, scrollPx) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + if (!pa) return false; + const period = jumpSurvivePlatformPeriodPx(md); + if (period < ts) return false; + const x0 = Math.max(0, Math.floor(left / ts)); + const x1 = Math.min(w - 1, Math.floor((right - 1e-6) / ts)); + const vm = ts * 2; + for (let ty = 0; ty < h; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (!pa[ty] || pa[ty][tx] !== 1) continue; + const rawTop = ty * ts - scrollPx; + const kMin = Math.ceil((top - rawTop - ts - vm) / period); + const kMax = Math.floor((bottom - rawTop + vm) / period); + for (let k = kMin; k <= kMax; k++) { + const platTop = rawTop + k * period; + const platBot = platTop + ts; + const tileLeft = tx * ts; + const tileRight = (tx + 1) * ts; + if (platBot > top && platTop < bottom && right > tileLeft && left < tileRight) return true; + } + } + } + return false; + } + + function jumpSurviveOverlapsSolid(md, left, top, right, bottom) { + if (jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom)) return true; + return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, jumpSurvivePlatformScrollPx); + } + + /** + * ชนแนวนอน: objects เต็มกล่อง; แพลตฟอร์มไม่นับช่วงปลายเท้าเล็กน้อย + * (ถ้าใช้กล่องเต็มกับแพลตฟอร์ม ขณะยืนบนแพลตฟอร์มมักทับช่องแนวตั้ง — เดินซ้ายขวาถูกปฏิเสธทั้งก้อน) + */ + function jumpSurviveOverlapsSolidForHorzMove(md, left, top, right, bottom) { + if (jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom)) return true; + const ts = tileSize; + const footSlop = ts * 0.16; + const platformBottom = bottom - footSlop; + if (platformBottom <= top + ts * 0.05) return false; + return jumpSurviveOverlapsPlatforms(md, left, top, right, platformBottom, jumpSurvivePlatformScrollPx); + } + + /** Head/body overlaps กำแพง (objects=1) เท่านั้น — ใช้ตรวจชนฝาบนแล้วตาย */ + function jumpSurviveOverlapsObjectWall(md, left, top, right, bottom) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const x0 = Math.floor(left / ts); + const x1 = Math.floor((right - 1e-6) / ts); + const y0 = Math.floor(top / ts); + const y1 = Math.floor((bottom - 1e-6) / ts); + for (let ty = y0; ty <= y1; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (tx < 0 || tx >= w || ty < 0 || ty >= h) continue; + const ob = md.objects && md.objects[ty] && md.objects[ty][tx]; + if (ob === 1) return true; + } + } + return false; + } + + function jumpSurviveSyncMeFromHitbox(pxc, feetYpx) { + jumpSurviveEntityFromHitbox(me, mapData, pxc, feetYpx); + } + + function jumpSurviveCameraTargetPx(md) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + if (isJumpSurviveMissionUiMapPlay()) { + return { px: (w * ts) * 0.5, py: (h * ts) * 0.5 }; + } + const { cw, ch } = getCharacterFootprintWH(md); + const cx = (e) => (e.x + cw * 0.5) * ts; + const cy = (e) => (e.y + ch * 0.5) * ts; + if (!jumpSurviveEliminated) return { px: cx(me), py: cy(me) }; + const aliveHumanIds = [...others.keys()].filter((id) => { + if (isPreviewBotId(id)) return false; + const o = others.get(id); + return o && !o.jumpSurviveEliminated; + }); + const aliveBotIds = [...others.keys()].filter((id) => { + if (!isPreviewBotId(id)) return false; + const o = others.get(id); + return o && !o.jumpSurviveEliminated; + }); + const useIds = aliveHumanIds.length ? aliveHumanIds : aliveBotIds; + if (!useIds.length) return { px: cx(me), py: cy(me) }; + let sx = 0, sy = 0; + useIds.forEach((id) => { + const o = others.get(id); + if (!o) return; + sx += cx(o); + sy += cy(o); + }); + const n = useIds.length; + return { px: sx / n, py: sy / n }; + } + + /** มีแพลตฟอร์มใต้จุดเล็กน้อยข้างหน้าแนว wishDir หรือไม่ — กันบอทเดินหลุมแล้วตกจอ */ + function jumpSurviveBotHasFloorAhead(md, pxc, feetY, wishDir, scrollPx) { + if (!wishDir) return true; + const ts = tileSize; + const bw = ts * 0.34; + const probeX = pxc + wishDir * (bw + ts * 0.82); + const pad = ts * 0.24; + const left = probeX - pad; + const right = probeX + pad; + const top = feetY - ts * 0.45; + const bottom = feetY + ts * 8; + return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, scrollPx); + } + + function stepJumpSurvivePreviewBots(dt, halfViewH) { + if (!previewFillBots || !mapData || !isJumpSurvive()) return; + const md = mapData; + const { cw, ch } = getCharacterFootprintWH(md); + const ts = tileSize; + const wpx = (md.width || 20) * ts; + const centerPx = wpx * 0.5; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + if (o.jumpSurviveEliminated) return; + if (typeof o.jumpSurviveVy !== 'number') o.jumpSurviveVy = 0; + if (o.jumpSurviveOnGround == null) o.jumpSurviveOnGround = false; + if (typeof o.jumpSurviveNextJumpAi !== 'number') o.jumpSurviveNextJumpAi = Date.now() + 300 + Math.floor(Math.random() * 500); + if (typeof o.jumpSurviveWishNext !== 'number') o.jumpSurviveWishNext = Date.now() + 200; + if (o.jumpSurviveWishX == null) o.jumpSurviveWishX = 0; + + let pxc = (o.x + cw * 0.5) * ts; + let feetY = (o.y + ch) * ts; + + const camCy = jumpSurviveCamCenterY; + const mission = isJumpSurviveMissionUiMapPlay(); + const lowDanger = feetY > camCy + halfViewH - ts * 8.5; + + if (Date.now() >= o.jumpSurviveWishNext) { + const wMin = mission && lowDanger ? 180 : 380; + const wSpan = mission && lowDanger ? 480 : 700; + o.jumpSurviveWishNext = Date.now() + wMin + Math.floor(Math.random() * wSpan); + const tier = o.botTier || 'avg'; + if (mission && lowDanger && Math.random() < 0.55) { + o.jumpSurviveWishX = 0; + } else if (pxc < centerPx - ts * 1.8) o.jumpSurviveWishX = 1; + else if (pxc > centerPx + ts * 1.8) o.jumpSurviveWishX = -1; + else if (tier === 'sharp') { + const roam = mission ? 0.42 : 0.55; + o.jumpSurviveWishX = Math.random() < roam ? (Math.random() < 0.5 ? -1 : 1) : 0; + } else { + const roam = mission ? 0.28 : 0.4; + o.jumpSurviveWishX = Math.random() < roam ? (Math.random() < 0.5 ? -1 : 1) : 0; + } + } + + let moveLeft = o.jumpSurviveWishX < 0; + let moveRight = o.jumpSurviveWishX > 0; + let hazardJump = false; + if (o.jumpSurviveOnGround) { + const sc = jumpSurvivePlatformScrollPx; + if (moveRight && !jumpSurviveBotHasFloorAhead(md, pxc, feetY, 1, sc)) { + o.jumpSurviveWishX = Math.random() < 0.65 ? -1 : 0; + hazardJump = true; + } else if (moveLeft && !jumpSurviveBotHasFloorAhead(md, pxc, feetY, -1, sc)) { + o.jumpSurviveWishX = Math.random() < 0.65 ? 1 : 0; + hazardJump = true; + } + moveLeft = o.jumpSurviveWishX < 0; + moveRight = o.jumpSurviveWishX > 0; + } + + let jumpQ = false; + if (hazardJump) o.jumpSurviveNextJumpAi = Math.min(o.jumpSurviveNextJumpAi, Date.now()); + if (o.jumpSurviveOnGround && Date.now() >= o.jumpSurviveNextJumpAi) { + o.jumpSurviveNextJumpAi = Date.now() + 300 + Math.floor(Math.random() * (hazardJump ? 520 : 1100)); + const tier = o.botTier || 'avg'; + let jp = tier === 'sharp' ? 0.5 : tier === 'weak' ? 0.25 : 0.36; + if (mission) jp += 0.12; + if (lowDanger) jp += 0.22; + jp = Math.min(0.9, jp); + if (hazardJump || Math.random() < jp) jumpQ = true; + } + + const r = jumpSurvivePhysicsIntegrate(md, { + dt, + cw, + ch, + pxc0: pxc, + feetY0: feetY, + vy0: o.jumpSurviveVy, + wasOnGround: o.jumpSurviveOnGround, + jumpQueued: jumpQ, + moveLeft, + moveRight, + camCenterY: jumpSurviveCamCenterY, + halfViewH, + }); + + if (r.died) { + o.jumpSurviveEliminated = true; + o.jumpSurviveVy = 0; + jumpSurviveEntityFromHitbox(o, md, r.pxc, r.feetY); + o.tx = o.x; + o.ty = o.y; + o.botIsWalking = false; + return; + } + o.jumpSurviveVy = r.vy; + o.jumpSurviveOnGround = r.onGround; + jumpSurviveEntityFromHitbox(o, md, r.pxc, r.feetY); + o.tx = o.x; + o.ty = o.y; + if (moveRight) o.direction = 'right'; + else if (moveLeft) o.direction = 'left'; + o.botIsWalking = Math.abs(r.vy) > 40 || moveLeft || moveRight; + }); + } + + function jumpSurviveTickFrame() { + if (!mapData || !isJumpSurvive()) return; + if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) return; + const remEnd = jumpSurviveRemainingSecMission(); + if (remEnd !== null && remEnd <= 0) { + endJumpSurviveMissionRound(); + return; + } + } + const md = mapData; + const ts = tileSize; + const now = performance.now(); + const dt = Math.min(0.05, Math.max(0, (now - jumpSurviveLastTickMs) / 1000)); + jumpSurviveLastTickMs = now; + + const halfViewH = canvas.height / (2 * zoom); + const rise = (Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42) * dt; + jumpSurvivePlatformScrollPx += rise; + + const camTgt = jumpSurviveCameraTargetPx(md); + const ck = Math.min(1, dt * 7); + jumpSurviveCamCenterX += (camTgt.px - jumpSurviveCamCenterX) * ck; + jumpSurviveCamCenterY += (camTgt.py - jumpSurviveCamCenterY) * ck; + + if (jumpSurviveEliminated) { + stepJumpSurvivePreviewBots(dt, halfViewH); + jumpSurviveMissionMaybeEarlyFinish(); + return; + } + + const { cw, ch } = getCharacterFootprintWH(md); + let pxc = (me.x + cw * 0.5) * ts; + let feetY = (me.y + ch) * ts; + + const jq = jumpSurviveJumpQueued; + jumpSurviveJumpQueued = false; + + const r = jumpSurvivePhysicsIntegrate(md, { + dt, + cw, + ch, + pxc0: pxc, + feetY0: feetY, + vy0: jumpSurviveVy, + wasOnGround: jumpSurviveOnGround, + jumpQueued: jq, + moveLeft: !!(keys['ArrowLeft'] || keys['KeyA']), + moveRight: !!(keys['ArrowRight'] || keys['KeyD']), + camCenterY: jumpSurviveCamCenterY, + halfViewH, + }); + + if (r.died) { + jumpSurviveEliminated = true; + jumpSurviveVy = 0; + jumpSurviveSyncMeFromHitbox(r.pxc, r.feetY); + ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'KeyA', 'KeyW', 'KeyS', 'KeyD'].forEach((c) => { keys[c] = false; }); + } else { + jumpSurviveVy = r.vy; + jumpSurviveOnGround = r.onGround; + jumpSurviveSyncMeFromHitbox(r.pxc, r.feetY); + } + + stepJumpSurvivePreviewBots(dt, halfViewH); + + if (!jumpSurviveEliminated) { + me.isWalking = Math.abs(jumpSurviveVy) > 40 || !!(keys['ArrowLeft'] || keys['KeyA'] || keys['ArrowRight'] || keys['KeyD']); + if (keys['ArrowRight'] || keys['KeyD']) me.direction = 'right'; + else if (keys['ArrowLeft'] || keys['KeyA']) me.direction = 'left'; + + if (Date.now() - jumpSurviveLastEmitT > 80) { + jumpSurviveLastEmitT = Date.now(); + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + } + + jumpSurviveMissionMaybeEarlyFinish(); + } + + function gridImageLibIdleUrlPlay(entry) { + if (entry == null) return ''; + if (typeof entry === 'string') return entry; + if (typeof entry === 'object' && entry.idle) return String(entry.idle); + return ''; + } + function gridImageLibHeldUrlPlay(entry) { + if (entry == null || typeof entry === 'string') return ''; + if (typeof entry === 'object' && entry.held && String(entry.held).length) return String(entry.held); + return ''; + } + + function normalizeGridImageCellsOnMap(md) { + if (!md) return; + const w = md.width || 20, h = md.height || 15; + if (!Array.isArray(md.gridImageLibrary)) md.gridImageLibrary = []; + md.gridImageLibrary = md.gridImageLibrary + .map((raw) => { + if (typeof raw === 'string' && raw.length > 0 && raw.length < 30000000) return { idle: raw, held: null }; + if (raw && typeof raw === 'object' && typeof raw.idle === 'string' && raw.idle.length > 0 && raw.idle.length < 30000000) { + let held = typeof raw.held === 'string' && raw.held.length > 0 && raw.held.length < 30000000 ? raw.held : null; + if (held && held === raw.idle) held = null; + return { idle: raw.idle, held }; + } + return null; + }) + .filter(Boolean); + const libLen = md.gridImageLibrary.length; + let placements = []; + if (Array.isArray(md.gridImageSprites) && md.gridImageSprites.length) { + placements = md.gridImageSprites.map((raw) => { + const i = Math.max(0, Math.floor(Number(raw.i))); + const x = Math.floor(Number(raw.x)); + const y = Math.floor(Number(raw.y)); + const ww = Math.max(1, Math.floor(Number(raw.w)) || 1); + const hh = Math.max(1, Math.floor(Number(raw.h)) || 1); + const rawB = raw.bindCarryOption; + const bn = typeof rawB === 'number' ? rawB : parseInt(String(rawB), 10); + const base = { i, x, y, w: ww, h: hh }; + if (Number.isFinite(bn) && bn >= 1 && bn <= QUIZ_CARRY_MAX_OPTION_SLOTS) base.bindCarryOption = bn; + return base; + }).filter((s) => Number.isFinite(s.i) && s.i >= 0 && s.i < libLen && + Number.isFinite(s.x) && Number.isFinite(s.y) && s.w >= 1 && s.h >= 1 && + s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0); + placements = placements.map((s) => { + let x = s.x, y = s.y, ww = s.w, hh = s.h; + if (x < 0) { ww += x; x = 0; } + if (y < 0) { hh += y; y = 0; } + if (x + ww > w) ww = w - x; + if (y + hh > h) hh = h - y; + const out = { i: s.i, x, y, w: Math.max(1, ww), h: Math.max(1, hh) }; + if (s.bindCarryOption != null) out.bindCarryOption = s.bindCarryOption; + return out; + }).filter((s) => s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0); + } else { + const cells = md.gridImageCells; + if (Array.isArray(cells) && cells.length === h && libLen > 0) { + for (let yy = 0; yy < h; yy++) { + const row = cells[yy]; + if (!row) continue; + for (let xx = 0; xx < w; xx++) { + if (row[xx] == null) continue; + const iv = parseInt(row[xx], 10); + if (!Number.isFinite(iv) || iv < 0 || iv >= libLen) continue; + placements.push({ i: iv, x: xx, y: yy, w: 1, h: 1 }); + } + } + } + } + md.gridImagePlacements = placements; + const outCells = Array(h).fill(0).map(() => Array(w).fill(null)); + placements.forEach((sp) => { + for (let yy = sp.y; yy < sp.y + sp.h; yy++) { + for (let xx = sp.x; xx < sp.x + sp.w; xx++) { + if (yy >= 0 && yy < h && xx >= 0 && xx < w) outCells[yy][xx] = sp.i; + } + } + }); + md.gridImageCells = outCells; + } + + function loadMapGridImages() { + mapGridImageImgs = []; + mapGridImageHeldImgs = []; + if (!mapData || !Array.isArray(mapData.gridImageLibrary) || !mapData.gridImageLibrary.length) return; + mapData.gridImageLibrary.forEach((entry, i) => { + mapGridImageImgs[i] = null; + mapGridImageHeldImgs[i] = null; + const idle = gridImageLibIdleUrlPlay(entry); + if (!idle) return; + const im = new Image(); + im.onload = () => { try { draw(); } catch (e) {} }; + im.onerror = () => {}; + im.src = idle; + mapGridImageImgs[i] = im; + const held = gridImageLibHeldUrlPlay(entry); + if (held && held !== idle) { + const hm = new Image(); + hm.onload = () => { try { draw(); } catch (e) {} }; + hm.onerror = () => {}; + hm.src = held; + mapGridImageHeldImgs[i] = hm; + } + }); + } + + /** โซนตัวเลือกที่ทับสไปรต์มากที่สุด — quiz_carry สลับรูป idle/held · ถ้าไม่ทับเลยลองขยายขอบ 1 ช่อง (มาร์กเกอร์ชิดขอบสไปรต์) */ + function dominantQuizCarryOptionInSpriteRect(md, sp) { + if (!md || md.gameType !== 'quiz_carry' || !md.quizCarryOptionArea) return null; + const g = md.quizCarryOptionArea; + const mw = md.width || 20, mh = md.height || 15; + const bx = sp.x | 0, by = sp.y | 0, bw = sp.w | 0, bh = sp.h | 0; + const counts = new Map(); + function addOverlapRect(x0, y0, ww, hh) { + for (let yy = y0; yy < y0 + hh; yy++) { + if (yy < 0 || yy >= mh) continue; + const row = g[yy]; + if (!row) continue; + for (let xx = x0; xx < x0 + ww; xx++) { + if (xx < 0 || xx >= mw) continue; + const v = row[xx]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + if (!Number.isFinite(n) || n < 1 || n > QUIZ_CARRY_MAX_OPTION_SLOTS) continue; + const idx = n - 1; + counts.set(idx, (counts.get(idx) || 0) + 1); + } + } + } + addOverlapRect(bx, by, bw, bh); + if (!counts.size) { + const pad = 1; + const x0 = Math.max(0, bx - pad); + const y0 = Math.max(0, by - pad); + const x1 = Math.min(mw, bx + bw + pad); + const y1 = Math.min(mh, by + bh + pad); + addOverlapRect(x0, y0, x1 - x0, y1 - y0); + } + if (!counts.size) return null; + let best = null; + let bestN = -1; + counts.forEach((n, k) => { + if (best == null || n > bestN || (n === bestN && k < best)) { + bestN = n; + best = k; + } + }); + return best; + } + + /** 0-based choice index สำหรับสลับ idle/held — ใช้ bindCarryOption จากแมปก่อน แล้วค่อยนับทับโซน */ + function quizCarryOptionIndexForGridSprite(md, sp) { + if (!sp) return null; + const rawB = sp.bindCarryOption; + if (rawB != null && rawB !== '') { + const n = typeof rawB === 'number' ? rawB : parseInt(String(rawB), 10); + if (Number.isFinite(n) && n >= 1 && n <= QUIZ_CARRY_MAX_OPTION_SLOTS) return n - 1; + } + return dominantQuizCarryOptionInSpriteRect(md, sp); + } + + /** รูป held จากคลังกริดสำหรับข้อนี้ — ใช้วาดป้ายติดตัว (ไม่ผ่าน sanitize URL) */ + function findQuizCarryGridHeldImgForChoiceIndex(choiceIdx) { + if (choiceIdx == null || choiceIdx < 0 || !isQuizCarry() || !mapData) return null; + const pl = mapData.gridImagePlacements; + if (!Array.isArray(pl) || !pl.length) return null; + for (let gi = 0; gi < pl.length; gi++) { + const sp = pl[gi]; + const opt = quizCarryOptionIndexForGridSprite(mapData, sp); + if (opt !== choiceIdx) continue; + const gh = mapGridImageHeldImgs[sp.i]; + if (gh && gh.complete && gh.naturalWidth) return gh; + } + return null; + } + + /** รูป idle จากคลังกริดสำหรับข้อนี้ — วาดบนป้ายตัวเลือกบนพื้นเมื่อไม่มีรูปจากคำถาม/ธีม */ + function findQuizCarryGridIdleImgForChoiceIndex(choiceIdx) { + if (choiceIdx == null || choiceIdx < 0 || !isQuizCarry() || !mapData) return null; + const pl = mapData.gridImagePlacements; + if (!Array.isArray(pl) || !pl.length) return null; + for (let gi = 0; gi < pl.length; gi++) { + const sp = pl[gi]; + const opt = quizCarryOptionIndexForGridSprite(mapData, sp); + if (opt !== choiceIdx) continue; + const gid = mapGridImageImgs[sp.i]; + if (gid && gid.complete && gid.naturalWidth) return gid; + } + return null; + } + + function applyMapAndStart(gameMapData, res) { + const prevWasSpaceShooter = mapData && mapData.gameType === 'space_shooter'; + const prevWasBalloonBoss = mapData && mapData.gameType === 'balloon_boss'; + mapData = gameMapData; + if (res && res.mapId != null && String(res.mapId).trim() !== '') { + playSessionMapId = String(res.mapId).trim(); + } + if (res && res.hostId != null) playHostId = res.hostId; + if (mapData.gameType !== 'space_shooter') { + spaceShooterGameEnded = false; + if (prevWasSpaceShooter) { + const gend = document.getElementById('gauntlet-ended-overlay'); + if (gend) gend.classList.add('is-hidden'); + } + } + if (mapData.gameType !== 'balloon_boss') { + balloonBossGameEnded = false; + if (prevWasBalloonBoss) { + const gend = document.getElementById('gauntlet-ended-overlay'); + if (gend) gend.classList.add('is-hidden'); + } + } + if (!mapData.lanes && mapData.gameType === 'frogger') mapData.lanes = []; + if (!mapData.interactive) mapData.interactive = []; + if (mapData.gameType === 'quiz') { + if (!mapData.quizTrueArea) mapData.quizTrueArea = []; + if (!mapData.quizFalseArea) mapData.quizFalseArea = []; + if (!mapData.quizQuestionArea) mapData.quizQuestionArea = []; + } + if (mapData.gameType === 'stack') { + if (!mapData.stackReleaseArea) mapData.stackReleaseArea = []; + if (!mapData.stackLandArea) mapData.stackLandArea = []; + } + if (mapData.gameType === 'quiz_carry') { + if (!mapData.quizCarryHubArea) mapData.quizCarryHubArea = []; + if (!mapData.quizCarryOptionArea) mapData.quizCarryOptionArea = []; + if (!mapData.carryEmbedCountdownArea) mapData.carryEmbedCountdownArea = []; + if (!mapData.quizQuestions) mapData.quizQuestions = []; + if (!mapData.quizQuestionArea) mapData.quizQuestionArea = []; + normalizeQuizCarryLayersInPlay(mapData); + } + if (mapData.gameType === 'quiz_battle') { + if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = []; + if (!mapData.quizBattlePathArea) mapData.quizBattlePathArea = []; + normalizeQuizBattlePathInPlay(mapData); + normalizeQuizBattleDomeInPlay(mapData); + } + tileSize = mapData.tileSize || 32; + mapBackgroundImg = null; + if (mapData.backgroundImage) { + mapBackgroundImg = new Image(); + mapBackgroundImg.src = mapData.backgroundImage; + } + normalizeGridImageCellsOnMap(mapData); + loadMapGridImages(); + var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน' + : (isGauntlet() ? ' | พรมแดงสุดท้าย (Last Light)' + : (isLobby() ? ' | โถงรอ' : (isQuiz() ? ' | ตอบคำถาม' : (isQuizCarry() ? ' | หยิบคำตอบมาวางกลาง' : (isQuizBattle() ? ' | Quiz Battle · โดม E' : (isStack() ? ' | Stack ซ้อนตึก' : (isJumpSurvive() ? ' | กระโดดให้รอด' : (isSpaceShooter() ? ' | ยิงยานอวกาศ' : (isBalloonBoss() ? ' | ลูกโป้งยิงบอส' : ''))))))))); + var prevTag = previewMode ? '[ทดสอบ] ' : ''; + document.getElementById('room-id').textContent = prevTag + spaceId + modeLabel; + const plist = Array.isArray(res.peers) ? res.peers : []; + const myPeer = plist.find(p => p.id === myId); + const myPos = peerXYFromJoin(myPeer, mapData.spawn); + me.x = myPos.x; + me.y = myPos.y; + if (isQuizCarry()) { + const hubSnap = snapPositionOutOfQuizCarryHubIfNeeded(me.x, me.y); + me.x = hubSnap.x; + me.y = hubSnap.y; + } + if (isQuizBattle()) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = pathSnap.x; + me.y = pathSnap.y; + } + me.direction = myPeer?.direction ?? 'down'; + me.characterId = myPeer?.characterId || null; + meGauntletJumpTicks = typeof myPeer?.gauntletJumpTicks === 'number' ? myPeer.gauntletJumpTicks : 0; + meGauntletJumpVis = meGauntletJumpTicks; + { + const sc0 = Number(myPeer && myPeer.gauntletScore); + me.gauntletScore = Number.isFinite(sc0) ? Math.max(0, sc0) : 0; + me.gauntletEliminated = !!(myPeer && myPeer.gauntletEliminated); + } + { + const sh0 = Number(myPeer && myPeer.spaceShooterScore); + me.spaceShooterScore = Number.isFinite(sh0) ? Math.max(0, sh0) : 0; + } + { + const bSt = balloonBossBalloonsStartPlay(); + const bs0 = Number(myPeer && myPeer.balloonBossScore); + me.balloonBossScore = Number.isFinite(bs0) ? Math.max(0, bs0) : 0; + const bb0 = Number(myPeer && myPeer.balloonBossBalloons); + me.balloonBossBalloons = Number.isFinite(bb0) ? Math.max(0, Math.floor(bb0)) : bSt; + me.balloonBossEliminated = !!(myPeer && myPeer.balloonBossEliminated); + } + gauntletObstacles = []; + gauntletObsRenderPrev = []; + gauntletObsRenderNext = []; + gauntletObsBlendT0 = 0; + me.playTint = playTintFromPeerId(String((myId != null && myId !== '') ? myId : (nick || 'local'))); + others.clear(); + plist.forEach(p => { + if (p.id !== myId) { + const pos = peerXYFromJoin(p, mapData.spawn); + let px = pos.x, py = pos.y; + if (isQuizCarry()) { + const hubSnap = snapPositionOutOfQuizCarryHubIfNeeded(px, py); + px = hubSnap.x; + py = hubSnap.y; + } + if (isQuizBattle()) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(px, py); + px = pathSnap.x; + py = pathSnap.y; + } + others.set(p.id, { + x: px, y: py, tx: px, ty: py, direction: p.direction, nickname: p.nickname, characterId: p.characterId, + playTint: playTintFromPeerId(p.id), + gauntletJumpTicks: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0, + gauntletJumpVis: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0, + gauntletScore: (() => { const s = Number(p.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + gauntletEliminated: !!p.gauntletEliminated, + jumpSurviveEliminated: false, + spaceShooterScore: (() => { const s = Number(p.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossScore: (() => { const s = Number(p.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBalloons: (() => { + const s = Number(p.balloonBossBalloons); + return Number.isFinite(s) ? Math.max(0, Math.floor(s)) : balloonBossBalloonsStartPlay(); + })(), + balloonBossEliminated: !!p.balloonBossEliminated, + quizCarryHeld: null, + }); + } + }); + if (mapData.gameType === 'gauntlet') { + me.tx = me.x; + me.ty = me.y; + if (res.gauntletEndsAt != null) { + const geJoin = Number(res.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(geJoin) ? geJoin : null; + } else { + gauntletEndsAtMs = null; + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => {}); + } else { + me.tx = null; + me.ty = null; + gauntletEndsAtMs = null; + } + rebalancePreviewBots(); + if (mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + if (isStack()) { + lastStackTickMs = performance.now(); + resetStackMinigameState(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + else reapplyStackMiniSizingFromGlobals(); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => { + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }); + } else { + stackMini = null; + } + playPath = []; + me.isWalking = false; + if (mapData.gameType === 'jump_survive') { + if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = []; + if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = []; + normalizeJumpSurvivePlatformAreaInPlay(mapData); + jumpSurviveEliminated = false; + jumpSurviveGameEnded = false; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + if (isJumpSurviveMissionUiMapPlay()) { + jumpSurviveMissionPhase = 'howto'; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + setTimeout(function () { showJumpSurviveMissionHowtoOverlay(); }, 50); + } else { + jumpSurviveMissionPhase = null; + jumpSurviveInitRuntime(); + } + } + if (mapData.gameType === 'space_shooter') { + spaceShooterGameEnded = false; + normalizeShooterSpawnSlotsInPlay(mapData); + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterPopups = []; + spaceShooterLastTickMs = performance.now(); + spaceShooterSpawnAccMs = 0; + spaceShooterFireCd = 0; + spaceShooterSessionStartMs = performance.now(); + spaceShooterLastMoveEmit = 0; + } + if (mapData.gameType === 'balloon_boss') { + balloonBossGameEnded = false; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossHitFx = []; + balloonBossLastTickMs = performance.now(); + balloonBossPlayerFireCd = 0; + balloonBossSessionStartMs = performance.now(); + balloonBossLastMoveEmit = 0; + balloonBossBossFireAcc = 0; + } + if (isQuiz()) setupPlayQuizUi(); + else if (isQuizCarry()) setupPlayQuizCarryUi(); + else if (isQuizBattle()) setupPlayQuizBattleUi(); + else teardownPlayQuizUi(); + if (mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) { + hideConflictingOverlaysForGauntletCrown(); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccR = document.getElementById('gauntlet-crown-countdown'); + if (gccR) gccR.classList.add('is-hidden'); + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50); + } else if (mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay()) { + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccRj = document.getElementById('gauntlet-crown-countdown'); + if (gccRj) gccRj.classList.add('is-hidden'); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + } else { + const hov = document.getElementById('gauntlet-crown-howto-overlay'); + if (hov) { + hov.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + } + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccR2 = document.getElementById('gauntlet-crown-countdown'); + if (gccR2) gccR2.classList.add('is-hidden'); + } + resizeCanvas(); draw(); tick(); + } + + socket.on('connect', () => { + firstCharacterDefaultPromise.then(() => { + socket.emit('join-space', { spaceId, nickname: nick, characterId: getPlayCharacterId() }, (res) => { + if (!res || !res.ok) { + const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้'; + alert(errMsg); + const caseLocked = /เริ่มคดี|ไม่รับผู้เล่น/.test(errMsg); + const detectiveReturn = params.get('detectiveReturn') === '1' + || (function () { try { return sessionStorage.getItem('detectiveMinigameReturn') === '1'; } catch (e) { return false; } })(); + if (caseLocked || detectiveReturn) { + window.location.replace(buildRoomLobbyReturnHref()); + } else { + window.location.replace(BASE + '/lobby.html'); + } + return; + } + myId = socket.id; + let botSlots = parseInt(res.botSlotCount, 10); + if ((!botSlots || botSlots < 1) && res.maxPlayers > 0 && res.maxPlayers < 6) { + botSlots = 6 - res.maxPlayers; + } + if (botSlots > 0) { + detectiveCaseFillBots = true; + detectiveBotSlotCount = Math.min(6, Math.max(0, botSlots)); + } + if (isDetectiveMinigamePlay()) { + try { document.documentElement.classList.add('play-detective-minigame'); } catch (eD) { /* ignore */ } + } + if (res.quizCarrySettingsSnap && typeof res.quizCarrySettingsSnap === 'object') { + quizCarryJoinSettingsSnap = res.quizCarrySettingsSnap; + if (res.quizCarrySettingsSnap.carryMapPanelTheme && typeof res.quizCarrySettingsSnap.carryMapPanelTheme === 'object') { + setQuizCarryMapPanelThemeFromApi({ carryMapPanelTheme: res.quizCarrySettingsSnap.carryMapPanelTheme }); + } + if (res.quizCarrySettingsSnap.carryEmbedCountdownTheme && typeof res.quizCarrySettingsSnap.carryEmbedCountdownTheme === 'object') { + setQuizCarryEmbedCountdownThemeFromApi({ carryEmbedCountdownTheme: res.quizCarrySettingsSnap.carryEmbedCountdownTheme }); + } + if (Array.isArray(res.quizCarrySettingsSnap.carryChoicePlaqueThemes) && res.quizCarrySettingsSnap.carryChoicePlaqueThemes.length) { + setQuizCarryChoicePlaqueThemeFromApi({ carryChoicePlaqueThemes: res.quizCarrySettingsSnap.carryChoicePlaqueThemes }); + } else if (res.quizCarrySettingsSnap.carryChoicePlaqueTheme && typeof res.quizCarrySettingsSnap.carryChoicePlaqueTheme === 'object') { + setQuizCarryChoicePlaqueThemeFromApi({ carryChoicePlaqueTheme: res.quizCarrySettingsSnap.carryChoicePlaqueTheme }); + } + const joinSc = Number(res.quizCarrySettingsSnap.carryChoicePlaqueMapScale); + if (Number.isFinite(joinSc)) { + quizCarryPlaqueMapScale = Math.max(0.85, Math.min(2.5, joinSc)); + } + } + if (previewMode && editorEmbedReturn && res.mapData && res.mapData.gameType === 'lobby') { + const q = []; + q.push('space=' + encodeURIComponent(spaceId)); + q.push('nick=' + encodeURIComponent(nick)); + const mid = params.get('map'); + if (mid) q.push('map=' + encodeURIComponent(mid)); + q.push('preview=1'); + q.push('editorEmbed=1'); + if (params.get('defaultChar') != null) q.push('defaultChar=' + encodeURIComponent(params.get('defaultChar'))); + window.location.replace(BASE + '/room-lobby.html?' + q.join('&')); + return; + } + const playMapId = params.get('map'); + if (playMapId) { + fetch(BASE + '/api/maps/' + encodeURIComponent(playMapId)) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (data) applyMapAndStart(data, res); + else applyMapAndStart(res.mapData, res); + }) + .catch(() => applyMapAndStart(res.mapData, res)); + } else { + applyMapAndStart(res.mapData, res); + } + }); + }); + }); + + const LERP = 0.2; + socket.on('user-joined', (data) => { + if (!data || isPreviewBotId(data.id)) return; + const x = Number(data.x); + const y = Number(data.y); + const px = Number.isFinite(x) ? x : 1; + const py = Number.isFinite(y) ? y : 1; + others.set(data.id, { + x: px, y: py, tx: px, ty: py, + direction: data.direction || 'down', nickname: data.nickname, + characterId: data.characterId ?? null, + playTint: playTintFromPeerId(data.id), + gauntletJumpTicks: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0, + gauntletJumpVis: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0, + gauntletScore: (() => { const s = Number(data.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + jumpSurviveEliminated: false, + spaceShooterScore: (() => { const s = Number(data.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossScore: (() => { const s = Number(data.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBalloons: (() => { + const s = Number(data.balloonBossBalloons); + return Number.isFinite(s) ? Math.max(0, Math.floor(s)) : balloonBossBalloonsStartPlay(); + })(), + balloonBossEliminated: !!data.balloonBossEliminated, + quizCarryHeld: null, + }); + if (previewFillBots) rebalancePreviewBots(); + if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud(); + }); + socket.on('host-changed', (d) => { + if (d && d.hostId != null) playHostId = d.hostId; + if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud(); + }); + socket.on('quiz-carry-lobby-sync', (d) => { + if (!d || typeof d.readyMap !== 'object') return; + quizCarryLobbyReadyMap = { ...d.readyMap }; + if (quizCarryPregameActive && isQuizCarry()) { + quizCarrySyncGuestReadyIfNeeded(); + updateQuizCarryPregameHud(); + } + }); + socket.on('quiz-carry-lobby-started', () => { + if (!(previewMode && editorEmbedReturn && isQuizCarry())) return; + endQuizCarryEmbedPregameAndStart(); + }); + socket.on('gauntlet-crown-lobby-sync', (d) => { + if (!d || typeof d.readyMap !== 'object') return; + gauntletCrownLobbyReadyMap = { ...d.readyMap }; + if (gauntletCrownPregamePhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateGauntletCrownHowtoHud(); + } + }); + socket.on('gauntlet-crown-lobby-started', () => { + if (!isGauntletCrownHeistMapPlay()) return; + beginGauntletCrownCountdownThenRun(); + }); + socket.on('user-move', (data) => { + if (data && myId != null && data.id === myId) { + /* Gauntlet: ตำแหน่งอ้างอิงจาก gauntlet-sync + เลอร์ปเท่านั้น — ไม่งั้น echo จาก move จะสแนป me.x/y โดยไม่อัปเดต tx/ty ทำให้เลอร์ปผิดและชนเซิร์ฟเวอร์ไม่ตรง */ + if (mapData && mapData.gameType === 'gauntlet') { + return; + } + if (mapData && mapData.gameType === 'stack') { + return; + } + if (mapData && mapData.gameType === 'jump_survive') { + return; + } + if (mapData && mapData.gameType === 'space_shooter') { + if (data.spaceShooterScore != null) { + const s = Number(data.spaceShooterScore); + if (Number.isFinite(s)) me.spaceShooterScore = Math.max(0, s); + } + return; + } + if (mapData && mapData.gameType === 'balloon_boss') { + if (data.balloonBossScore != null) { + const s = Number(data.balloonBossScore); + if (Number.isFinite(s)) me.balloonBossScore = Math.max(0, s); + } + if (data.balloonBossBalloons != null) { + const b = Number(data.balloonBossBalloons); + if (Number.isFinite(b)) me.balloonBossBalloons = Math.max(0, Math.floor(b)); + } + if (data.balloonBossEliminated != null) me.balloonBossEliminated = !!data.balloonBossEliminated; + return; + } + if (mapData && mapData.gameType === 'quiz_battle') { + if (data.x != null) { + const x = Number(data.x); + if (Number.isFinite(x)) me.x = x; + } + if (data.y != null) { + const y = Number(data.y); + if (Number.isFinite(y)) me.y = y; + } + const s = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = s.x; + me.y = s.y; + if (data.direction) me.direction = data.direction; + if (data.characterId != null) me.characterId = data.characterId; + playPath = []; + return; + } + if (data.x != null) { + const x = Number(data.x); + if (Number.isFinite(x)) me.x = x; + } + if (data.y != null) { + const y = Number(data.y); + if (Number.isFinite(y)) me.y = y; + } + if (data.direction) me.direction = data.direction; + if (data.characterId != null) me.characterId = data.characterId; + playPath = []; + return; + } + const o = others.get(data.id); + if (o) { + if (mapData && mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData) && !isPreviewBotId(data.id)) { + const px = Number(data.x); + const py = Number(data.y); + if (Number.isFinite(px) && Number.isFinite(py)) { + const sn = snapPositionOntoQuizBattlePathIfNeeded(px, py); + o.tx = sn.x; + o.ty = sn.y; + o.x = sn.x; + o.y = sn.y; + } else { + o.tx = data.x; + o.ty = data.y; + } + } else { + o.tx = data.x; + o.ty = data.y; + } + o.direction = data.direction || o.direction; + if (data.characterId != null) o.characterId = data.characterId; + if (mapData && mapData.gameType === 'space_shooter' && data.spaceShooterScore != null) { + const s = Number(data.spaceShooterScore); + if (Number.isFinite(s)) o.spaceShooterScore = Math.max(0, s); + } + if (mapData && mapData.gameType === 'balloon_boss') { + if (data.balloonBossScore != null) { + const s = Number(data.balloonBossScore); + if (Number.isFinite(s)) o.balloonBossScore = Math.max(0, s); + } + if (data.balloonBossBalloons != null) { + const b = Number(data.balloonBossBalloons); + if (Number.isFinite(b)) o.balloonBossBalloons = Math.max(0, Math.floor(b)); + } + if (data.balloonBossEliminated != null) o.balloonBossEliminated = !!data.balloonBossEliminated; + } + } + }); + socket.on('user-left', (data) => { + if (!data || isPreviewBotId(data.id)) return; + others.delete(data.id); + if (previewFillBots) rebalancePreviewBots(); + }); + socket.on('chat', (data) => { + const box = document.getElementById('chat-messages'); + if (!box) return; + const div = document.createElement('div'); + div.className = 'chat-msg'; + div.textContent = (data.nickname || '') + ': ' + (data.text || ''); + box.appendChild(div); + box.scrollTop = 1e9; + }); + + socket.on('quiz-phase', (p) => { + if (previewMode) return; + if (!p || !mapData || !isQuiz()) return; + if (p.text) playQuizText = p.text; + playQuizPhaseLocal = p.phase; + playQuizPhaseEndsAt = p.endsAt || 0; + const phaseEl = document.getElementById('quiz-game-phase-label'); + const qEl = document.getElementById('quiz-game-question'); + if (phaseEl) { + phaseEl.textContent = p.phase === 'read' + ? ('อ่านคำถาม ข้อ ' + (p.questionIndex || '') + '/' + (p.questionTotal || '')) + : ('เดินไปโซน ถูก / ผิด — ข้อ ' + (p.questionIndex || '') + '/' + (p.questionTotal || '')); + } + if (qEl) qEl.textContent = playQuizText || ''; + const ov = document.getElementById('quiz-game-overlay'); + if (ov) ov.classList.remove('is-hidden'); + if (playQuizTimerInterval) clearInterval(playQuizTimerInterval); + playQuizTimerInterval = setInterval(updatePlayQuizTimerDisplay, 200); + updatePlayQuizTimerDisplay(); + if (!previewMode && isQuiz() && !Object.keys(playLiveQuizScores).length) initPlayLiveQuizScoresZeros(); + }); + + socket.on('quiz-result', (r) => { + if (previewMode || !r) return; + if (r.scores) { + playLiveQuizScores = { ...r.scores }; + renderPlayQuizScoreboard(playLiveQuizScores); + } + showPlayQuizFeedback(r); + }); + + socket.on('quiz-player-state', (st) => { + if (previewMode) return; + if (!st) return; + playQuizPlayerLocal = { + cannotTrue: !!st.cannotTrue, + cannotFalse: !!st.cannotFalse, + eliminated: !!st.eliminated, + score: typeof st.score === 'number' ? st.score : (playQuizPlayerLocal.score || 0), + }; + }); + + socket.on('quiz-ended', () => { + if (previewMode && isQuiz()) return; + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + playQuizPhaseLocal = null; + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) ov.classList.add('is-hidden'); + const panel = document.getElementById('quiz-map-question-panel'); + if (panel) { + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + } + playQuizText = ''; + playQuizPhaseEndsAt = 0; + playLiveQuizScores = {}; + hidePlayQuizHud(); + }); + + socket.on('gauntlet-sync', (data) => { + if (!mapData || mapData.gameType !== 'gauntlet' || !data) return; + applyGauntletTimingFromServer(data); + if (Array.isArray(data.obstacles)) { + gauntletObstacles = data.obstacles; + pushGauntletObsRenderFrame(data.obstacles); + } + if (!Array.isArray(data.players)) return; + data.players.forEach((p) => { + if (!p || p.id == null || myId == null) return; + const pid = String(p.id); + const mid = String(myId); + if (pid === mid) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) me.tx = px; + if (Number.isFinite(py)) me.ty = py; + if (p.direction) me.direction = p.direction; + const jt = Number(p.gauntletJumpTicks); + meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + meGauntletJumpVis = meGauntletJumpTicks; + const sc = Number(p.gauntletScore); + me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + me.gauntletEliminated = !!p.gauntletEliminated; + } else { + const o = others.get(p.id) ?? others.get(pid); + if (o) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) o.tx = px; + if (Number.isFinite(py)) o.ty = py; + if (p.direction) o.direction = p.direction; + const jt = Number(p.gauntletJumpTicks); + o.gauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks; + const sc = Number(p.gauntletScore); + o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + o.gauntletEliminated = !!p.gauntletEliminated; + } + } + }); + if (previewFillBots) { + applyGauntletPreviewBotsAfterSync(); + emitGauntletPreviewRowsToServer(); + } + }); + + socket.on('gauntlet-ended', (data) => { + cancelQuizCarryResultEndAfterTimeup(); + gauntletEndsAtMs = null; + gauntletCrownRunHeldRemote = false; + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc = document.getElementById('gauntlet-crown-countdown'); + if (gcc) gcc.classList.add('is-hidden'); + const hto = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto) hto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + if (data && data.crownMission) { + showGauntletCrownMissionOverlay(data.crownMission); + return; + } + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) { + titleEl.textContent = data && data.reason === 'time' ? 'หมดเวลา · Time up' : 'เกมจบ · Game over'; + } + msgEl.textContent = (data && data.message) || 'เกมพรมแดงจบแล้ว'; + listEl.innerHTML = ''; + const ranks = (data && data.rankings) || []; + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + }); + + socket.on('game-start', (ev) => { + if (!ev || !ev.mapId) return; + const applySnap = (md) => { + mapData = md; + if (ev.mapId != null && String(ev.mapId).trim() !== '') { + playSessionMapId = String(ev.mapId).trim(); + } + if (mapData.gameType !== 'space_shooter') { + spaceShooterGameEnded = false; + } + if (mapData.gameType !== 'balloon_boss') { + balloonBossGameEnded = false; + } + if (!mapData.lanes && mapData.gameType === 'frogger') mapData.lanes = []; + tileSize = mapData.tileSize || 32; + gauntletObstacles = []; + gauntletObsRenderPrev = []; + gauntletObsRenderNext = []; + gauntletObsBlendT0 = 0; + mapBackgroundImg = null; + if (mapData.backgroundImage) { + mapBackgroundImg = new Image(); + mapBackgroundImg.src = mapData.backgroundImage; + } + normalizeGridImageCellsOnMap(mapData); + loadMapGridImages(); + if (mapData.gameType === 'gauntlet' && ev.peersSnap && Array.isArray(ev.peersSnap)) { + ev.peersSnap.forEach((p) => { + if (p.id != null && myId != null && String(p.id) === String(myId)) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) { me.x = px; me.tx = px; } + if (Number.isFinite(py)) { me.y = py; me.ty = py; } + me.direction = p.direction || me.direction; + const jt = Number(p.gauntletJumpTicks); + meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + meGauntletJumpVis = meGauntletJumpTicks; + const sc = Number(p.gauntletScore); + me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + me.gauntletEliminated = !!p.gauntletEliminated; + } else { + const o = others.get(p.id) ?? others.get(String(p.id)); + if (o) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) { o.x = px; o.tx = px; } + if (Number.isFinite(py)) { o.y = py; o.ty = py; } + o.direction = p.direction || o.direction; + const jt = Number(p.gauntletJumpTicks); + o.gauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + o.gauntletJumpVis = o.gauntletJumpTicks; + const sc = Number(p.gauntletScore); + o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + o.gauntletEliminated = !!p.gauntletEliminated; + } + } + }); + } else { + meGauntletJumpTicks = 0; + meGauntletJumpVis = 0; + me.gauntletScore = 0; + me.gauntletEliminated = false; + others.forEach((o) => { + o.gauntletJumpTicks = 0; + o.gauntletJumpVis = 0; + o.gauntletScore = 0; + o.gauntletEliminated = false; + }); + } + if (mapData.gameType === 'gauntlet') { + if (ev.gauntletEndsAt != null) { + const ge = Number(ev.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(ge) ? ge : null; + } else { + gauntletEndsAtMs = null; + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => {}); + if (String(ev.mapId || '') === GAUNTLET_FACE_RIGHT_MAP_ID) { + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc0 = document.getElementById('gauntlet-crown-countdown'); + if (gcc0) gcc0.classList.add('is-hidden'); + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60); + } else { + const hto2 = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto2) hto2.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc1 = document.getElementById('gauntlet-crown-countdown'); + if (gcc1) gcc1.classList.add('is-hidden'); + } + } else { + gauntletEndsAtMs = null; + const skipHideCrownShell = mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay(); + if (!skipHideCrownShell) { + const hto3 = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto3) hto3.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + } + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc2 = document.getElementById('gauntlet-crown-countdown'); + if (gcc2) gcc2.classList.add('is-hidden'); + } + if (mapData.gameType !== 'gauntlet') { + me.tx = null; + me.ty = null; + } + const gauntletOv = document.getElementById('gauntlet-ended-overlay'); + if (gauntletOv) gauntletOv.classList.add('is-hidden'); + const gcmOv = document.getElementById('gauntlet-crown-mission-overlay'); + if (gcmOv) gcmOv.classList.add('is-hidden'); + if (mapData.gameType === 'stack') { + if (!mapData.stackReleaseArea) mapData.stackReleaseArea = []; + if (!mapData.stackLandArea) mapData.stackLandArea = []; + rebalancePreviewBots(); + lastStackTickMs = performance.now(); + resetStackMinigameState(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + else reapplyStackMiniSizingFromGlobals(); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => { + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }); + } else { + stackMini = null; + } + if (mapData.gameType === 'jump_survive') { + if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = []; + if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = []; + normalizeJumpSurvivePlatformAreaInPlay(mapData); + jumpSurviveEliminated = false; + jumpSurviveGameEnded = false; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + if (isJumpSurviveMissionUiMapPlay()) { + jumpSurviveMissionPhase = 'howto'; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + setTimeout(function () { showJumpSurviveMissionHowtoOverlay(); }, 60); + } else { + jumpSurviveMissionPhase = null; + jumpSurviveInitRuntime(); + } + } + if (mapData.gameType === 'space_shooter') { + spaceShooterGameEnded = false; + normalizeShooterSpawnSlotsInPlay(mapData); + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterPopups = []; + spaceShooterLastTickMs = performance.now(); + spaceShooterSpawnAccMs = 0; + spaceShooterFireCd = 0; + spaceShooterSessionStartMs = performance.now(); + spaceShooterLastMoveEmit = 0; + } + if (mapData.gameType === 'balloon_boss') { + balloonBossGameEnded = false; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossHitFx = []; + balloonBossLastTickMs = performance.now(); + balloonBossPlayerFireCd = 0; + balloonBossSessionStartMs = performance.now(); + balloonBossLastMoveEmit = 0; + balloonBossBossFireAcc = 0; + me.balloonBossScore = 0; + me.balloonBossBalloons = balloonBossBalloonsStartPlay(); + me.balloonBossEliminated = false; + others.forEach((o) => { + o.balloonBossScore = 0; + o.balloonBossBalloons = balloonBossBalloonsStartPlay(); + o.balloonBossEliminated = false; + }); + } + if (mapData.gameType === 'quiz_battle') { + if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = []; + if (!mapData.quizBattlePathArea) mapData.quizBattlePathArea = []; + normalizeQuizBattlePathInPlay(mapData); + normalizeQuizBattleDomeInPlay(mapData); + if (quizBattlePathModeActive(mapData)) { + const ms = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = ms.x; + me.y = ms.y; + others.forEach((o) => { + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + }); + } + } + var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน' + : (isGauntlet() ? ' | พรมแดงสุดท้าย (Last Light)' + : (isLobby() ? ' | โถงรอ' : (isQuiz() ? ' | ตอบคำถาม' : (isQuizCarry() ? ' | หยิบคำตอบมาวางกลาง' : (isQuizBattle() ? ' | Quiz Battle · โดม E' : (isStack() ? ' | Stack ซ้อนตึก' : (isJumpSurvive() ? ' | กระโดดให้รอด' : (isSpaceShooter() ? ' | ยิงยานอวกาศ' : (isBalloonBoss() ? ' | ลูกโป้งยิงบอส' : ''))))))))); + var prevTag = previewMode ? '[ทดสอบ] ' : ''; + document.getElementById('room-id').textContent = prevTag + spaceId + modeLabel; + if (isQuiz()) setupPlayQuizUi(); + else if (isQuizCarry()) setupPlayQuizCarryUi(); + else if (isQuizBattle()) setupPlayQuizBattleUi(); + else teardownPlayQuizUi(); + resizeCanvas(); + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + if (previewFillBots && mapData.gameType === 'gauntlet') { + applyGauntletPreviewSpawnLayout(true); + emitGauntletPreviewRowsToServer(); + } + if (previewFillBots && mapData.gameType === 'jump_survive') { + applyJumpSurvivePreviewSpawnLayout(true); + } + if (previewFillBots && mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (previewFillBots && mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + }; + fetch(BASE + '/api/maps/' + encodeURIComponent(ev.mapId)) + .then((r) => (r.ok ? r.json() : null)) + .then((md) => { if (md) applySnap(md); }); + }); + + function resizeCanvas() { + const vw = window.innerWidth || document.documentElement.clientWidth || 800; + 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; + 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); + if (ch < 120) ch = Math.max(240, vh - headerH - 6); + if (cw < 120) cw = Math.max(320, vw); + canvas.width = Math.max(320, cw); + canvas.height = Math.max(240, ch); + syncQuizCarryEmbedCountdownLayout(); + } + + // ให้ scroll wheel ใช้ซูมเข้า/ออกแบบง่าย ๆ + canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + const factor = e.deltaY < 0 ? 1.1 : 0.9; + zoom = Math.max(0.7, Math.min(2.5, zoom * factor)); + }, { passive: false }); + canvas.addEventListener('click', (e) => { + if (!mapData || !isStack() || isChatFocused()) return; + tryHumanStackDrop(); + }); + canvas.addEventListener('dblclick', (e) => { + if (!mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss() || isChatFocused() || isQuizCarryEmbedCountdownBlockingMovement()) return; + const r = canvas.getBoundingClientRect(); + const sx = e.clientX - r.left; + 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 tx = Math.floor(gx / tileSize); + const ty = Math.floor(gy / tileSize); + const mw = mapData.width || 20, mh = mapData.height || 15; + if (tx < 0 || tx >= mw || ty < 0 || ty >= mh) return; + if (!canWalkLikeLobby(tx + 0.5, ty + 0.5)) return; + const path = pathfindPlay(me.x, me.y, tx + 0.5, ty + 0.5); + if (path.length <= 1) return; + playPath = path.slice(1); + }); + + function getCharacterFootprintWH(md) { + if (!md) return { cw: 1, ch: 1 }; + let cw = Math.floor(Number(md.characterCellsW)); + let ch = Math.floor(Number(md.characterCellsH)); + const hasW = Number.isFinite(cw) && cw >= 1; + const hasH = Number.isFinite(ch) && ch >= 1; + if (hasW && hasH) { + return { cw: Math.max(1, Math.min(4, cw)), ch: Math.max(1, Math.min(4, ch)) }; + } + const leg = Math.max(1, Math.min(4, Math.floor(Number(md.characterCells)) || 1)); + return { cw: leg, ch: leg }; + } + + /** + * ขนาดช่องที่ใช้ชนกำแพง (objects=1) เท่านั้น — ชิดเท้า (ล่าง) + กลางแนวนอน · เล็กกว่า footprint = ส่วนบนไม่ติดกำแพง + */ + function getCharacterCollisionFootprintWH(md) { + const { cw, ch } = getCharacterFootprintWH(md); + let colW = Math.floor(Number(md.characterCollisionW)); + let colH = Math.floor(Number(md.characterCollisionH)); + if (!Number.isFinite(colW) || colW < 1) colW = cw; + if (!Number.isFinite(colH) || colH < 1) colH = ch; + colW = Math.max(1, Math.min(cw, colW)); + colH = Math.max(1, Math.min(ch, colH)); + return { cw, ch, colW, colH }; + } + + /** กันตัวละคร footprint ล้นออกนอกแมป — เดิมใช้ w-0.01 ทำให้ cw/ch>1 ชนขอบแล้วตรรกะเดินค้าง */ + function clampPlayEntityFootprintToMap(oEnt, md) { + if (!oEnt || !md) return; + const w = md.width || 20, h = md.height || 15; + const { cw, ch } = getCharacterFootprintWH(md); + const maxX = Math.max(0, w - cw + 0.999); + const maxY = Math.max(0, h - ch + 0.999); + if (!Number.isFinite(oEnt.x)) oEnt.x = 0.5; + if (!Number.isFinite(oEnt.y)) oEnt.y = 0.5; + oEnt.x = Math.max(0, Math.min(maxX, oEnt.x)); + oEnt.y = Math.max(0, Math.min(maxY, oEnt.y)); + } + + /** แยกบอท preview ที่ทับกันบนจุดเดียว (มักชนกันที่ขอบแมป) */ + function separateClumpedPreviewBots() { + if (!previewFillBots || !mapData) return; + const ids = [...others.keys()].filter(isPreviewBotId); + const minD = 0.52; + const pushAmt = MOVE_SPEED * 0.5; + for (let i = 0; i < ids.length; i++) { + for (let j = i + 1; j < ids.length; j++) { + const a = others.get(ids[i]); + const b = others.get(ids[j]); + if (!a || !b) continue; + let ddx = b.x - a.x, ddy = b.y - a.y; + let d = Math.sqrt(ddx * ddx + ddy * ddy); + if (d >= minD) continue; + if (d < 1e-5) { + ddx = (i + j * 2) % 2 === 0 ? 1 : -1; + ddy = ((i + j) % 3) - 1; + d = Math.sqrt(ddx * ddx + ddy * ddy) || 1; + } + const ux = -ddx / d, uy = -ddy / d; + const vx = ddx / d, vy = ddy / d; + const ax2 = a.x + ux * pushAmt, ay2 = a.y + uy * pushAmt; + const bx2 = b.x + vx * pushAmt, by2 = b.y + vy * pushAmt; + if (canWalkLikeLobbyForBot(ax2, ay2, a.x, a.y, a)) { a.x = ax2; a.y = ay2; } + if (canWalkLikeLobbyForBot(bx2, by2, b.x, b.y, b)) { b.x = bx2; b.y = by2; } + clampPlayEntityFootprintToMap(a, mapData); + clampPlayEntityFootprintToMap(b, mapData); + } + } + } + + function quizTilesFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch } = getCharacterFootprintWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const maxTx = Math.min(w - 1, minTx + cw - 1); + const maxTy = Math.min(h - 1, minTy + ch - 1); + for (let ty = minTy; ty <= maxTy; ty++) { + for (let tx = minTx; tx <= maxTx; tx++) { + if (tx >= 0 && ty >= 0) s.add(tx + ',' + ty); + } + } + return s; + } + + /** ร่าง (cw×ch) ของผู้เล่นที่ (px,py) ครอบคลุมช่อง (tx,ty) หรือไม่ — ใช้กับ blockPlayer แทนการเช็คแค่มุมซ้ายบน */ + function playEntityFootprintContainsTile(px, py, tx, ty) { + return quizTilesFootprintPlay(px, py).has(tx + ',' + ty); + } + + /** + * กล่องตรวจโซนห้ามซ้อน (blockPlayer) — วางเหมือนชนกำแพง: กลางแนวนอน + ชิดเท้า + * blockPlayerSeparationW/H: 0 = ใช้เต็มความกว้าง/สูงตัว · ค่าอื่น = จำกัด 1..cw / 1..ch + * รองรับ legacy blockPlayerSeparation เมื่อยังไม่มี W/H ใน JSON + */ + function resolveBlockPlayerNoOverlapBoxWH(md) { + const m = md || mapData; + if (!m) return { cw: 1, ch: 1, boxW: 1, boxH: 1 }; + const { cw, ch } = getCharacterFootprintWH(m); + let w = m.blockPlayerSeparationW != null && m.blockPlayerSeparationW !== '' ? Math.floor(Number(m.blockPlayerSeparationW)) : NaN; + let h = m.blockPlayerSeparationH != null && m.blockPlayerSeparationH !== '' ? Math.floor(Number(m.blockPlayerSeparationH)) : NaN; + const leg = m.blockPlayerSeparation != null && m.blockPlayerSeparation !== '' ? Math.floor(Number(m.blockPlayerSeparation)) : NaN; + const hasW = Number.isFinite(w) && w >= 0; + const hasH = Number.isFinite(h) && h >= 0; + if (!hasW && !hasH && Number.isFinite(leg) && leg >= 1) { + w = Math.min(leg, cw); + h = Math.min(leg, ch); + } else { + if (!hasW) w = 0; + if (!hasH) h = 0; + } + const boxW = !w || w <= 0 ? cw : Math.max(1, Math.min(cw, w)); + const boxH = !h || h <= 0 ? ch : Math.max(1, Math.min(ch, h)); + return { cw, ch, boxW, boxH }; + } + + /** ช่องกริดที่ถือว่า “ผู้เล่นที่ (px,py) กินพื้นที่” สำหรับตรวจโซน blockPlayer — กล่องเดียวกับแนวชนกำแพง */ + function quizTilesBlockPlayerPeerFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch, boxW, boxH } = resolveBlockPlayerNoOverlapBoxWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const offX = minTx + Math.floor((cw - boxW) / 2); + const offY = minTy + (ch - boxH); + for (let ty = offY; ty < offY + boxH; ty++) { + for (let tx = offX; tx < offX + boxW; tx++) { + if (tx >= 0 && ty >= 0 && tx < w && ty < h) s.add(tx + ',' + ty); + } + } + return s; + } + + function playPeerBlockPlayerOccupiesTile(px, py, tx, ty) { + return quizTilesBlockPlayerPeerFootprintPlay(px, py).has(tx + ',' + ty); + } + + /** Footprint ชนกำแพง (objects=1) เท่านั้น — hub / interactive / blockPlayer / quiz ใช้ quizTilesFootprintPlay = เต็ม characterCells */ + function quizTilesWallCollisionFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch, colW, colH } = getCharacterCollisionFootprintWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const offX = minTx + Math.floor((cw - colW) / 2); + const offY = minTy + (ch - colH); + for (let ty = offY; ty < offY + colH; ty++) { + for (let tx = offX; tx < offX + colW; tx++) { + if (tx >= 0 && ty >= 0 && tx < w && ty < h) s.add(tx + ',' + ty); + } + } + return s; + } + + function quizAnswerTileForbiddenForLock(lock, tx, ty) { + if (!lock || lock.eliminated) return false; + if (!mapData) return false; + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + if (lock.cannotTrue && qt && qt[ty] && qt[ty][tx] === 1) return true; + if (lock.cannotFalse && qf && qf[ty] && qf[ty][tx] === 1) return true; + return false; + } + + function quizAnswerTileForbiddenPlay(tx, ty) { + if (!playQuizPlayerLocal) return false; + return quizAnswerTileForbiddenForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, tx, ty); + } + + /** ยืนทับโซนต้องห้ามเมื่อโดนล็อก — ใช้กับ pathfind ปลายทาง / คลิก */ + function quizLockFootprintBlocksForLock(lock, px, py) { + if (!mapData || !isQuiz() || !lock || lock.eliminated) return false; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const txi = +p[0], tyi = +p[1]; + if (quizAnswerTileForbiddenForLock(lock, txi, tyi)) return true; + } + return false; + } + + function quizLockFootprintBlocksPlay(px, py) { + if (!playQuizPlayerLocal) return false; + return quizLockFootprintBlocksForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, px, py); + } + + /** บล็อกเฉพาะการ «เข้า» ช่องตอบใหม่ — ให้เดินออกจากโซนได้ถ้าเคยยืนผิดแล้ว */ + function quizLockWouldEnterForbiddenForLock(lock, ox, oy, nx, ny) { + if (!mapData || !isQuiz() || !lock || lock.eliminated) return false; + const fromS = quizTilesFootprintPlay(ox, oy); + const toS = quizTilesFootprintPlay(nx, ny); + for (const k of toS) { + if (fromS.has(k)) continue; + const p = k.split(','); + const txi = +p[0], tyi = +p[1]; + if (quizAnswerTileForbiddenForLock(lock, txi, tyi)) return true; + } + return false; + } + + function quizLockWouldEnterForbiddenPlay(ox, oy, nx, ny) { + if (!playQuizPlayerLocal) return false; + return quizLockWouldEnterForbiddenForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, ox, oy, nx, ny); + } + + function botQuizLock(o) { + return { + cannotTrue: !!(o && o.quizCannotTrue), + cannotFalse: !!(o && o.quizCannotFalse), + eliminated: false, + }; + } + + /** Same walkability as room-lobby `canWalkLobby` (LobbyA / hall). */ + function canWalkLikeLobby(x, y, fromX, fromY) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + const wallTiles = quizTilesWallCollisionFootprintPlay(x, y); + for (const k of wallTiles) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (wallTiles.size === 0) return false; + const bp = mapData.blockPlayer; + if (bp) { + for (const k of quizTilesFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!bp[ty] || bp[ty][tx] !== 1) continue; + for (const [, o] of others) { + if (playPeerBlockPlayerOccupiesTile(o.x, o.y, tx, ty)) return false; + } + } + } + if (isQuiz() && playQuizPlayerLocal && !playQuizPlayerLocal.eliminated) { + const hasFrom = typeof fromX === 'number' && typeof fromY === 'number' && !Number.isNaN(fromX) && !Number.isNaN(fromY); + if (hasFrom) { + if (quizLockWouldEnterForbiddenPlay(fromX, fromY, x, y)) return false; + } else if (quizLockFootprintBlocksPlay(x, y)) { + return false; + } + } + if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false; + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + if (!quizBattleFootprintFullyOnPath(mapData, x, y)) return false; + } + return true; + } + + function canWalkLikeLobbyForBot(x, y, fromX, fromY, o) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + const wallTilesB = quizTilesWallCollisionFootprintPlay(x, y); + for (const k of wallTilesB) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (wallTilesB.size === 0) return false; + const bp = mapData.blockPlayer; + if (bp) { + for (const k of quizTilesFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!bp[ty] || bp[ty][tx] !== 1) continue; + for (const [, peer] of others) { + if (o && peer === o) continue; + if (playPeerBlockPlayerOccupiesTile(peer.x, peer.y, tx, ty)) return false; + } + if (o && playPeerBlockPlayerOccupiesTile(me.x, me.y, tx, ty)) return false; + } + } + if (isQuiz()) { + const lock = botQuizLock(o); + if (lock.cannotTrue || lock.cannotFalse) { + const hasFrom = typeof fromX === 'number' && typeof fromY === 'number' && !Number.isNaN(fromX) && !Number.isNaN(fromY); + if (hasFrom) { + if (quizLockWouldEnterForbiddenForLock(lock, fromX, fromY, x, y)) return false; + } else if (quizLockFootprintBlocksForLock(lock, x, y)) { + return false; + } + } + } + if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false; + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + if (!quizBattleFootprintFullyOnPath(mapData, x, y)) return false; + } + return true; + } + + /** ถ้าสปอว์น/โหลดมาทับโซน hub ให้หาจุดใกล้ ๆ ที่ footprint ไม่ทับ hub */ + function snapPositionOutOfQuizCarryHubIfNeeded(x, y) { + if (!mapData || !isQuizCarry()) return { x, y }; + if (!quizCarryFootprintOverlapsHub(x, y)) return { x, y }; + const w = mapData.width || 20, h = mapData.height || 15; + const maxR = Math.max(w, h) + 6; + for (let r = 1; r <= maxR; r++) { + for (let dy = -r; dy <= r; dy++) { + for (let dx = -r; dx <= r; dx++) { + if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue; + const nx = x + dx * 0.45; + const ny = y + dy * 0.45; + if (nx < 0 || ny < 0 || nx > w - 0.02 || ny > h - 0.02) continue; + if (quizCarryFootprintOverlapsHub(nx, ny)) continue; + if (!canWalkLikeLobby(nx, ny, NaN, NaN)) continue; + return { x: nx, y: ny }; + } + } + } + return { x, y }; + } + + function stepPreviewBotAlongPath(o, w, h) { + const path = o.botPath; + if (!path || !path.length) return; + const way = path[0]; + const dx = way.x - o.x, dy = way.y - o.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= PATH_ARRIVE_THRESH) { + path.shift(); + while (path.length > 0) { + const w2 = path[0]; + const ux = w2.x - o.x, uy = w2.y - o.y; + if (Math.sqrt(ux * ux + uy * uy) > PATH_ARRIVE_THRESH) break; + path.shift(); + } + clampPlayEntityFootprintToMap(o, mapData); + return; + } + const len = dist || 1; + const pathStepMul = (previewFillBots && editorEmbedReturn) ? 0.56 : 1; + const step = Math.min(MOVE_SPEED * 1.28 * pathStepMul, len); + const nx = o.x + (dx / len) * step; + const ny = o.y + (dy / len) * step; + if (Math.abs(dy) > Math.abs(dx)) o.direction = dy > 0 ? 'down' : 'up'; + else if (Math.abs(dx) > 1e-6) o.direction = dx > 0 ? 'right' : 'left'; + const ox = o.x, oy = o.y; + const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData); + if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else if (!pathStrict) { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + /* เส้นตรงไป waypoint อาจตัดมุม hub / block — ลองก้าวแนวแกนตามทิศหลักอย่างใดอย่างหนึ่ง */ + const cstep = Math.min(MOVE_SPEED * 1.2 * pathStepMul, Math.max(Math.abs(dx), Math.abs(dy), 1e-4)); + const sx = dx > 1e-4 ? 1 : dx < -1e-4 ? -1 : 0; + const sy = dy > 1e-4 ? 1 : dy < -1e-4 ? -1 : 0; + const tryAxisX = () => { + if (sx === 0) return false; + if (!canWalkLikeLobbyForBot(o.x + sx * cstep, o.y, o.x, o.y, o)) return false; + o.x += sx * cstep; + return true; + }; + const tryAxisY = () => { + if (sy === 0) return false; + if (!canWalkLikeLobbyForBot(o.x, o.y + sy * cstep, o.x, o.y, o)) return false; + o.y += sy * cstep; + return true; + }; + if (Math.abs(dx) >= Math.abs(dy)) { + if (!tryAxisX()) tryAxisY(); + } else { + if (!tryAxisY()) tryAxisX(); + } + } + } + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) { + o.botIsWalking = true; + o.botPathStuckTicks = 0; + } else { + o.botPathStuckTicks = (o.botPathStuckTicks | 0) + 1; + if ((o.botPathStuckTicks | 0) >= 20) { + o.botPathStuckTicks = 0; + if (path.length) path.shift(); + if (isQuizCarry() && previewFillBots) o.botQuizCarryPathfindAfter = 0; + } + } + } + + function pickRandomPreviewBotWanderDir() { + const dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]]; + return dirs[Math.floor(Math.random() * dirs.length)]; + } + + function stepPreviewBots() { + if (!playBotsEnabled() || !mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss()) return; + if (isQuizCarry()) { + stepQuizCarryPreviewBots(); + return; + } + const w = mapData.width || 20, h = mapData.height || 15; + const now = Date.now(); + const inAnswerPhase = previewMode && isQuiz() && previewQuizStep === 'answer'; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botIsWalking = false; + if (inAnswerPhase && o.botPath && o.botPath.length > 0 && !o.botAnswerWander) { + stepPreviewBotAlongPath(o, w, h); + return; + } + if (inAnswerPhase && o.botPath && o.botPath.length === 0 && !o.botAnswerWander) { + return; + } + /* ก่อนตอบ / พัก / บอทสับสน: เดินทุกเฟรมตามทิศ (เหมือนผู้เล่นกดค้าง) — เดิมรอเป็นจังหวะแล้วก้าวทีละครั้งเลยดูกระตุก */ + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600; + if (now >= o.botWanderNextTurn) { + o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200); + if (Math.random() < 0.55) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + } + const accX = o.botWanderDx; + const accY = o.botWanderDy; + if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up'; + else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left'; + const step = MOVE_SPEED; + const nx = o.x + accX * step; + const ny = o.y + accY * step; + const ox = o.x, oy = o.y; + const pathStrictW = isQuizBattle() && quizBattlePathModeActive(mapData); + if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else if (!pathStrictW) { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + if (!Number.isFinite(o.x)) o.x = 0.5; + if (!Number.isFinite(o.y)) o.y = 0.5; + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) o.botIsWalking = true; + }); + separateClumpedPreviewBots(); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const ob = others.get(bid); + if (ob) clampPlayEntityFootprintToMap(ob, mapData); + }); + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + o.tx = s.x; + o.ty = s.y; + }); + } + } + + /** A* เหมือน room-lobby — double-click ไปจุดบนแผนที่ */ + function pathfindPlay(fromX, fromY, toX, toY) { + if (!mapData) return []; + const w = mapData.width || 20, h = mapData.height || 15; + const fx = Math.floor(fromX), fy = Math.floor(fromY); + const tx = Math.floor(toX), ty = Math.floor(toY); + if (tx < 0 || tx >= w || ty < 0 || ty >= h || !canWalkLikeLobby(tx + 0.5, ty + 0.5)) return []; + if (fx === tx && fy === ty) return [{ x: tx + 0.5, y: ty + 0.5 }]; + const key = (gx, gy) => gx + ',' + gy; + const open = [{ gx: fx, gy: fy, f: 0, g: 0 }]; + const closed = new Set(); + const cameFrom = {}; + const gScore = { [key(fx, fy)]: 0 }; + const heuristic = (ax, ay) => Math.abs(ax - tx) + Math.abs(ay - ty); + const dirs = [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }]; + while (open.length) { + open.sort((a, b) => a.f - b.f); + const cur = open.shift(); + const ck = key(cur.gx, cur.gy); + if (closed.has(ck)) continue; + closed.add(ck); + if (cur.gx === tx && cur.gy === ty) { + const path = []; + let u = cur; + while (u) { + path.unshift({ x: u.gx + 0.5, y: u.gy + 0.5 }); + u = cameFrom[key(u.gx, u.gy)]; + } + return path; + } + for (const d of dirs) { + const nx = cur.gx + d.dx, ny = cur.gy + d.dy; + if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + if (!canWalkLikeLobby(nx + 0.5, ny + 0.5, cur.gx + 0.5, cur.gy + 0.5)) continue; + const nk = key(nx, ny); + if (closed.has(nk)) continue; + const g = (gScore[ck] ?? Infinity) + 1; + if (g >= (gScore[nk] ?? Infinity)) continue; + gScore[nk] = g; + cameFrom[nk] = cur; + open.push({ gx: nx, gy: ny, f: g + heuristic(nx, ny), g }); + } + } + return []; + } + + function pathfindPlayForBot(fromX, fromY, toX, toY, o) { + if (!mapData) return []; + const w = mapData.width || 20, h = mapData.height || 15; + const fx = Math.floor(fromX), fy = Math.floor(fromY); + const tx = Math.floor(toX), ty = Math.floor(toY); + if (tx < 0 || tx >= w || ty < 0 || ty >= h || !canWalkLikeLobbyForBot(tx + 0.5, ty + 0.5, NaN, NaN, o)) return []; + if (fx === tx && fy === ty) return [{ x: tx + 0.5, y: ty + 0.5 }]; + const key = (gx, gy) => gx + ',' + gy; + const open = [{ gx: fx, gy: fy, f: 0, g: 0 }]; + const closed = new Set(); + const cameFrom = {}; + const gScore = { [key(fx, fy)]: 0 }; + const heuristic = (ax, ay) => Math.abs(ax - tx) + Math.abs(ay - ty); + const dirs = [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }]; + while (open.length) { + open.sort((a, b) => a.f - b.f); + const cur = open.shift(); + const ck = key(cur.gx, cur.gy); + if (closed.has(ck)) continue; + closed.add(ck); + if (cur.gx === tx && cur.gy === ty) { + const path = []; + let u = cur; + while (u) { + path.unshift({ x: u.gx + 0.5, y: u.gy + 0.5 }); + u = cameFrom[key(u.gx, u.gy)]; + } + return path; + } + for (const d of dirs) { + const nx = cur.gx + d.dx, ny = cur.gy + d.dy; + if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + if (!canWalkLikeLobbyForBot(nx + 0.5, ny + 0.5, cur.gx + 0.5, cur.gy + 0.5, o)) continue; + const nk = key(nx, ny); + if (closed.has(nk)) continue; + const g = (gScore[ck] ?? Infinity) + 1; + if (g >= (gScore[nk] ?? Infinity)) continue; + gScore[nk] = g; + cameFrom[nk] = cur; + open.push({ gx: nx, gy: ny, f: g + heuristic(nx, ny), g }); + } + } + return []; + } + + let playPath = []; + + function drawGauntletLaserColumnScreen(rx, ry, rw, rh) { + ctx.save(); + ctx.fillStyle = gauntletLaserFillColor; + ctx.fillRect(rx, ry, rw, rh); + const topRec = gauntletLaserTopUrl ? ensureGauntletAssetImage(gauntletLaserTopUrl) : null; + const botRec = gauntletLaserBottomUrl ? ensureGauntletAssetImage(gauntletLaserBottomUrl) : null; + const lineRec = gauntletLaserLineUrl ? ensureGauntletAssetImage(gauntletLaserLineUrl) : null; + let topH = 0; + let botH = 0; + if (topRec && topRec.img.complete && topRec.img.naturalWidth > 0) { + topH = Math.min(rh * 0.4, rw * topRec.img.naturalHeight / topRec.img.naturalWidth); + } + if (botRec && botRec.img.complete && botRec.img.naturalWidth > 0) { + botH = Math.min(rh * 0.4, rw * botRec.img.naturalHeight / botRec.img.naturalWidth); + } + /* เส้นกลาง tile ทั้งความสูงคอลัมน์ แล้วค่อยวาดหัว/ท้ายทับ — ให้ลำแสงต่อเนื่องแบบสินทรัพย์รวม (รูปอ้างอิง) */ + if (lineRec && lineRec.img.complete && lineRec.img.naturalWidth > 0 && rh > 1) { + const iw = lineRec.img.naturalWidth; + const ih = lineRec.img.naturalHeight; + const scale = rw / iw; + const step = Math.max(1, ih * scale); + let y = ry; + while (y < ry + rh) { + const piece = Math.min(step, ry + rh - y); + const srcH = piece / scale; + try { + ctx.drawImage(lineRec.img, 0, 0, iw, srcH, rx, y, rw, piece); + } catch (e) { /* ignore */ } + y += piece; + } + } + if (topH > 0 && topRec) { + try { ctx.drawImage(topRec.img, rx, ry, rw, topH); } catch (e) { /* ignore */ } + } + if (botH > 0 && botRec) { + try { ctx.drawImage(botRec.img, rx, ry + rh - botH, rw, botH); } catch (e) { /* ignore */ } + } + const lw = Number(gauntletLaserLineWidthPx) || 0; + if (lw > 0) { + ctx.strokeStyle = gauntletLaserStrokeColor; + ctx.lineWidth = lw; + ctx.strokeRect(rx + lw / 2, ry + lw / 2, rw - lw, rh - lw); + } + ctx.restore(); + } + + /** Stack preview: HUD แนว cyberdeck — SCORE ซ้าย, ตาปัจจุบันขวา, เทอร์มินัลล่างซ้าย */ + function drawStackPreviewCyberHud(ctx, cw, ch) { + if (!playBotsEnabled() || !stackMini || !mapData || !isStack()) return; + const now = Date.now(); + const pad = 12; + const currentSeat = getStackPreviewCurrentSeat(); + const order = stackPreviewTurnOrder; + const mono = '11px ui-monospace, "Cascadia Mono", Consolas, monospace'; + const monoSm = '9px ui-monospace, Consolas, monospace'; + const lives = stackMini.lives != null ? stackMini.lives : 3; + const lifeSlots = 3; + const teamScore = stackMini.score || 0; + const combo = stackMini.combo || 0; + + function roundRectPath(x, y, w, h, r) { + const rr = Math.min(r, w / 2, h / 2); + ctx.beginPath(); + ctx.moveTo(x + rr, y); + ctx.arcTo(x + w, y, x + w, y + h, rr); + ctx.arcTo(x + w, y + h, x, y + h, rr); + ctx.arcTo(x, y + h, x, y, rr); + ctx.arcTo(x, y, x + w, y, rr); + ctx.closePath(); + } + + function drawHudAvatar(cx, baselineY, maxSize, characterId, playTint) { + const timeMs = now; + const dir = 'down'; + const rawImg = getAvatarImg(characterId, dir, timeMs, false); + const charImg = playTint ? getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, false, playTint) : rawImg; + let iw = 0; let ih = 0; + if (charImg && charImg.tagName === 'CANVAS' && charImg.width > 0 && charImg.height > 0) { + iw = charImg.width; + ih = charImg.height; + } else if (charImg && charImg.complete && charImg.naturalWidth) { + iw = charImg.naturalWidth; + ih = charImg.naturalHeight; + } + const ax = cx - maxSize / 2; + const ay = baselineY - maxSize; + ctx.save(); + roundRectPath(ax, ay, maxSize, maxSize, 5); + ctx.clip(); + if (iw > 0 && ih > 0) { + const scale = Math.min(maxSize / iw, maxSize / ih, 1); + const dw = iw * scale; + const dh = ih * scale; + ctx.drawImage(charImg, 0, 0, iw, ih, cx - dw / 2, baselineY - dh, dw, dh); + } else { + ctx.fillStyle = 'rgba(80, 200, 255, 0.35)'; + ctx.fillRect(ax, ay, maxSize, maxSize); + } + ctx.restore(); + ctx.strokeStyle = 'rgba(0, 234, 255, 0.5)'; + ctx.lineWidth = 1.5; + roundRectPath(ax, ay, maxSize, maxSize, 5); + ctx.stroke(); + } + + function resolveEntryVisual(entry) { + if (entry.kind === 'human') { + return { + name: String(me.nickname || 'YOU').toUpperCase(), + score: me.stackPreviewHumanPts || 0, + characterId: me.characterId, + playTint: me.playTint || playTintFromPeerId(String(myId || 'me')), + }; + } + const o = entry.botId ? others.get(entry.botId) : null; + return { + name: o ? String(o.nickname || 'BOT').replace(/\s+/g, ' ').toUpperCase().slice(0, 12) : '—', + score: o ? (o.stackBotScore || 0) : 0, + characterId: o ? o.characterId : null, + playTint: o ? (o.playTint || playTintFromPeerId(entry.botId)) : null, + }; + } + + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + + const boardW = Math.min(248, Math.max(200, cw * 0.36)); + const rowH = 42; + const headH = 38; + const boardH = headH + stackTurnCount * rowH + 10; + + ctx.fillStyle = 'rgba(8, 12, 28, 0.9)'; + roundRectPath(pad, 8, boardW, boardH, 8); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 230, 255, 0.5)'; + ctx.lineWidth = 1.5; + roundRectPath(pad, 8, boardW, boardH, 8); + ctx.stroke(); + + ctx.fillStyle = '#5cefff'; + ctx.font = 'bold 12px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText('SCORE', pad + 14, 16); + ctx.fillStyle = 'rgba(94, 239, 255, 0.55)'; + ctx.font = monoSm; + ctx.fillText('shared stack · P1–P6', pad + 62, 18); + + let rowY = 8 + headH; + const drawRows = (entries) => { + entries.forEach((entry) => { + const isCurrent = entry.seat === currentSeat; + const v = resolveEntryVisual(entry); + let actionFlash = false; + if (entry.kind === 'human') { + actionFlash = lastStackPreviewActorId === '__human__' && now < lastStackPreviewActorUntil; + } else if (entry.botId) { + const ob = others.get(entry.botId); + actionFlash = !!(ob && ((now < (ob.stackBotGlowUntil || 0)) || (entry.botId === lastStackPreviewActorId && now < lastStackPreviewActorUntil))); + } + if (isCurrent) { + ctx.fillStyle = 'rgba(255, 0, 180, 0.1)'; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 255, 220, 0.9)'; + ctx.lineWidth = 2; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.stroke(); + } else if (actionFlash) { + ctx.strokeStyle = 'rgba(255, 214, 102, 0.65)'; + ctx.lineWidth = 1.5; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.stroke(); + } + const avS = 34; + drawHudAvatar(pad + 10 + avS / 2, rowY + avS - 3, avS, v.characterId, v.playTint); + ctx.fillStyle = isCurrent ? '#e0f7ff' : '#89b4fa'; + ctx.font = '600 10px ui-sans-serif, system-ui, sans-serif'; + ctx.textBaseline = 'middle'; + ctx.fillText('P' + entry.seat, pad + 14 + avS + 6, rowY + 12); + ctx.fillStyle = '#c0caf5'; + ctx.font = 'bold 10px ' + mono; + ctx.fillText(v.name.slice(0, 11), pad + 14 + avS + 6, rowY + 28); + ctx.fillStyle = actionFlash ? '#ffe066' : '#7dfcff'; + ctx.font = 'bold 13px ' + mono; + ctx.textAlign = 'right'; + ctx.fillText(String(v.score), pad + boardW - 12, rowY + rowH / 2); + ctx.textAlign = 'left'; + rowY += rowH; + }); + }; + + if (order && order.length === stackTurnCount) { + drawRows(order); + } else { + const bots = [...others.keys()].filter(isPreviewBotId).sort(); + const rows = [{ kind: 'human', seat: 1 }]; + for (let i = 0; i < stackTurnCount - 1; i++) { + rows.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null }); + } + drawRows(rows); + } + + const rightW = 136; + let rightX = cw - pad - rightW; + const topY = 8; + if (rightX < pad + boardW + 14) { + rightX = Math.max(pad, cw - pad - rightW); + } + const bigAv = Math.min(78, Math.max(56, ch * 0.11)); + const turnPanelH = bigAv + 118; + ctx.fillStyle = 'rgba(8, 12, 28, 0.9)'; + roundRectPath(rightX, topY, rightW, turnPanelH, 8); + ctx.fill(); + ctx.strokeStyle = 'rgba(187, 154, 247, 0.55)'; + ctx.lineWidth = 1.5; + roundRectPath(rightX, topY, rightW, turnPanelH, 8); + ctx.stroke(); + + let curEntry = null; + if (order && order.length === stackTurnCount) { + curEntry = order.find((e) => e.seat === currentSeat) || null; + } + const curV = curEntry ? resolveEntryVisual(curEntry) : resolveEntryVisual({ kind: 'human', seat: 1 }); + const framePad = 8; + const avCx = rightX + rightW / 2; + const bigFrameTop = topY + framePad - 2; + const bigFrameH = bigAv + 8; + ctx.strokeStyle = 'rgba(255, 120, 200, 0.75)'; + ctx.lineWidth = 2; + roundRectPath(rightX + framePad - 2, topY + framePad - 2, rightW - (framePad - 2) * 2, bigAv + 8, 6); + ctx.stroke(); + drawHudAvatar(avCx, bigFrameTop + bigFrameH - 4, bigAv, curV.characterId, curV.playTint); + + ctx.fillStyle = '#f5c2e7'; + ctx.font = 'bold 10px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.fillText('ACTIVE NODE', rightX + rightW / 2, topY + bigAv + 22); + ctx.fillStyle = '#7dfcff'; + ctx.font = 'bold 11px ' + mono; + ctx.fillText('P' + currentSeat + ' · ' + curV.name.slice(0, 10), rightX + rightW / 2, topY + bigAv + 38); + + ctx.fillStyle = '#bb9af7'; + ctx.font = '600 9px ui-sans-serif, system-ui, sans-serif'; + ctx.fillText('SYSTEM INTEGRITY', rightX + rightW / 2, topY + bigAv + 58); + ctx.font = '14px sans-serif'; + let heartStr = ''; + for (let i = 0; i < lifeSlots; i++) { + heartStr += i < lives ? '♥ ' : '♡ '; + } + ctx.fillStyle = lives <= 1 ? '#f7768e' : '#9ece6a'; + ctx.fillText(heartStr.trim(), rightX + rightW / 2, topY + bigAv + 74); + + ctx.fillStyle = 'rgba(125, 252, 255, 0.85)'; + ctx.font = monoSm; + ctx.fillText('TEAM ' + teamScore + ' · COMBO x' + combo, rightX + rightW / 2, topY + bigAv + 96); + + const termW = Math.min(380, Math.max(220, cw * 0.42)); + const termH = 86; + const termY = Math.max(boardH + 24, ch - termH - 52); + ctx.fillStyle = 'rgba(18, 8, 32, 0.88)'; + roundRectPath(pad, termY, termW, termH, 6); + ctx.fill(); + ctx.strokeStyle = 'rgba(187, 100, 255, 0.45)'; + ctx.lineWidth = 1.5; + roundRectPath(pad, termY, termW, termH, 6); + ctx.stroke(); + + ctx.fillStyle = '#c099ff'; + ctx.font = '600 10px ' + mono; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText('// NODE_LOG', pad + 10, termY + 8); + ctx.fillStyle = '#7dcfff'; + ctx.font = monoSm; + let ly = termY + 26; + const logLines = stackPreviewHudLog.length + ? stackPreviewHudLog.slice(-5) + : ['>>> Awaiting stack sync…', '>>> Space / click = release (your turn only)']; + logLines.forEach((ln) => { + ctx.fillText(ln, pad + 10, ly); + ly += 12; + }); + + const decryptPct = Math.min(100, Math.floor((stackMini.layers.length || 0) * 11 + Math.min(40, teamScore * 3))); + const barY = termY + termH - 18; + const barW = termW - 20; + ctx.fillStyle = 'rgba(0,0,0,0.35)'; + ctx.fillRect(pad + 10, barY, barW, 8); + ctx.fillStyle = 'rgba(0, 255, 200, 0.65)'; + ctx.fillRect(pad + 10, barY, Math.max(4, barW * decryptPct / 100), 8); + ctx.strokeStyle = 'rgba(0, 234, 255, 0.4)'; + ctx.strokeRect(pad + 10, barY, barW, 8); + ctx.fillStyle = '#94e2d5'; + ctx.font = monoSm; + ctx.fillText('TOWER_LINK ' + decryptPct + '%', pad + 10, barY - 14); + + ctx.restore(); + } + + function useCyberPlayHud() { + return !!(mapData && (isJumpSurvive() || isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry())); + } + + /** Cyber HUD: quiz_carry ใช้ playLiveQuizScores (ถูก +QUIZ_CARRY_POINTS_PER_CORRECT ตอนส่งป้ายถูกที่ฮับ) */ + function cyberHudQuizCarryPeerScore(peerId) { + if (peerId == null) return 0; + return Math.max(0, Number(playLiveQuizScores[peerId]) || 0); + } + + function syncPlayCyberHud() { + const root = document.getElementById('play-cyber-hud'); + const stackEl = document.getElementById('play-canvas-stack'); + const on = useCyberPlayHud(); + if (root) { + root.classList.toggle('is-hidden', !on); + root.setAttribute('aria-hidden', on ? 'false' : 'true'); + } + if (stackEl) stackEl.classList.toggle('play-cyber-vignette', !!on); + const gw = document.querySelector('.game-wrap'); + if (gw) gw.classList.toggle('play-cyber-active', !!on); + if (!on || !root) return; + const timeLabelEl = root.querySelector('.play-cyber-time-label'); + if (timeLabelEl) timeLabelEl.style.display = isQuizCarry() ? 'none' : ''; + const timeVal = document.getElementById('play-cyber-time-val'); + const timeSub = document.getElementById('play-cyber-time-sub'); + const portrait = document.getElementById('play-cyber-portrait-img'); + const statusEl = document.getElementById('play-cyber-self-status'); + const hintEl = document.getElementById('play-cyber-hint'); + const previewEl = document.getElementById('play-cyber-preview-line'); + + if (timeSub) { + timeSub.textContent = isQuizCarry() + ? 'QUIZ CARRY · COURT — หยิบป้ายถูกแล้วส่งที่ฮับ (F / Grab)' + : (isGauntlet() ? 'GAUNTLET · SURVIVAL RUN' + : (isSpaceShooter() ? 'SPACE SHOOTER · ARCADE' + : (isBalloonBoss() ? 'BALLOON BOSS · MEGA VIRUS' + : (isJumpSurviveMissionUiMapPlay() ? 'JUMPER · SURVIVE — เหลือเวลา (วินาที) · TIME (sec)' : 'JUMP SURVIVE · NODE UPLINK')))); + } + if (timeVal) { + if (isQuizCarry()) { + if (quizCarrySessionEnded) { + timeVal.textContent = '0'; + } else if (previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now()) { + const rem = Math.max(0, Math.ceil((quizCarryEmbedCountdownEndAt - Date.now()) / 1000)); + timeVal.textContent = String(rem); + } else if (quizCarryPregameActive) { + timeVal.textContent = '···'; + } else if (!quizCarryCurrent) { + timeVal.textContent = '—'; + } else if (quizCarryOptionRevealAt > 0 && Date.now() < quizCarryOptionRevealAt) { + timeVal.textContent = String(Math.max(0, Math.ceil((quizCarryOptionRevealAt - Date.now()) / 1000))); + } else if (quizCarryAnswerCloseAt > Date.now()) { + timeVal.textContent = String(Math.max(0, Math.ceil((quizCarryAnswerCloseAt - Date.now()) / 1000))); + } else { + timeVal.textContent = '0'; + } + } else if (isGauntlet()) { + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) { + timeVal.textContent = '···'; + } else if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) { + const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000)); + const mm = Math.floor(rem / 60); + const ss = rem % 60; + timeVal.textContent = `${mm}:${String(ss).padStart(2, '0')}`; + } else if (gauntletRuntimeTimeLimitSec > 0) { + timeVal.textContent = '···'; + } else { + timeVal.textContent = '∞'; + } + } else if (isSpaceShooter()) { + if (spaceShooterGameEnded) { + timeVal.textContent = '0'; + } else { + const rem = spaceShooterRemainingSecPlay(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else if (isBalloonBoss()) { + if (balloonBossGameEnded) { + timeVal.textContent = '0'; + } else { + const rem = balloonBossRemainingSecPlay(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) { + timeVal.textContent = jumpSurviveMissionPhase === 'howto' ? '···' : (jumpSurviveMissionPhase === 'countdown' ? '···' : '0'); + } else { + const rem = jumpSurviveRemainingSecMission(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else { + const t = jumpSurviveSessionStartMs > 0 + ? Math.max(0, Math.floor((performance.now() - jumpSurviveSessionStartMs) / 1000)) + : 0; + timeVal.textContent = String(t); + } + } + + if (portrait) { + const cid = me.characterId || getPlayCharacterId(); + const dir = (isGauntletFaceRightMapMno9kb07() ? 'right' : (me.direction || 'down')); + const nowT = Date.now(); + const walk = !!me.isWalking; + const rawImg = getAvatarImg(cid, dir, nowT, walk); + const tint = me.playTint || playTintFromPeerId(String(myId != null ? myId : 'me')); + const comp = rawImg && cid && tint + ? getPlayTintedAvatarSource(rawImg, cid, dir, nowT, walk, tint) + : rawImg; + if (comp && comp.tagName === 'CANVAS' && comp.width > 0) { + try { + portrait.src = comp.toDataURL('image/png'); + } catch (e) { + if (rawImg && rawImg.src) portrait.src = rawImg.src; + } + } else if (comp && comp.src) { + portrait.src = comp.src; + } else if (rawImg && rawImg.src) { + portrait.src = rawImg.src; + } + } + if (statusEl) { + if (isQuizCarry() && quizCarrySessionEnded) { + statusEl.textContent = 'SESSION COMPLETE · จบชุดคำถาม'; + } else if (isJumpSurvive() && jumpSurviveEliminated) { + statusEl.textContent = 'SPECTATOR · LINK SEVERED'; + } else if (isBalloonBoss() && me.balloonBossEliminated) { + statusEl.textContent = 'SPECTATOR · BALLOONS GONE'; + } else { + statusEl.textContent = 'OPERATOR ONLINE'; + } + } + if (hintEl) { + if (isQuizCarry()) { + hintEl.textContent = quizCarrySessionEnded + ? 'ชุดคำถามจบแล้ว · Session ended — ดูคะแนนด้าน SCORE' + : ('ยืนโซนตัวเลือกแล้วกด F/Grab หยิบ · ถือป้ายถูกแล้วไปฮับส่ง — ถูก +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' แต้ม (correct +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' on hub)'); + } else if (isJumpSurvive()) { + if (isJumpSurviveMissionUiMapPlay()) { + hintEl.textContent = jumpSurviveEliminated + ? 'คุณตกน้ำแล้ว — ดูเพื่อนต่อ — รอจบเพื่อรับคะแนน (ผู้รอดได้ 100 คะแนน)' + : 'กระโดดตามแท่น — หลีกหนาม/กับดักด้านล่าง · หมดเวลาแล้วสรุปคะแนน (ผู้รอด 100) · Space / W / ↑ กระโดด · A D / ← → เดิน'; + } else { + hintEl.textContent = jumpSurviveEliminated + ? 'Spectate remaining nodes · Exit room to reset session' + : 'Space / W / ↑ jump · A D / arrows move · Hit ceiling/floor = out (watch others)'; + } + } else if (isSpaceShooter()) { + hintEl.textContent = spaceShooterGameEnded + ? 'จบแล้ว · Session ended — ดูอันดับบน overlay / see rankings on overlay' + : 'A D / ← → = move · W S / ↑ ↓ = up/down (lower half of map only) · Space = fire · +5 per hit'; + } else if (isBalloonBoss()) { + hintEl.textContent = balloonBossGameEnded + ? 'จบแล้ว · Session ended — see overlay' + : (me.balloonBossEliminated + ? 'คุณตกรอบแล้ว — ดูเพื่อนเล่น · You are out — spectate' + : 'ยานมีแรงเฉื่อย — A D / arrows / W S เร่งทิศทาง · ปล่อยปุ่มแล้วยังไหล (แรงหน่วง) · Space = ยิง (ดีเลย์) · ลูกโป้งหมด = ตกรอบ'); + } else if (isGauntletCrownHeistMapPlay()) { + hintEl.textContent = 'Space / W / ↑ jump · Start 100 pts · Hit -10 · Fall off left edge = out · Rank bonus at end'; + } else { + hintEl.textContent = 'Space / W / ↑ jump · Lanes + lasers · Clear = right + score'; + } + } + if (previewEl) { + if (previewFillBots && mapData) { + const human = countPlayHumans(); + const bots = [...others.keys()].filter(isPreviewBotId).length; + previewEl.textContent = `SIM · humans ${human} + bots ${bots} · target ${previewTargetHeadcount}`; + previewEl.classList.remove('is-hidden'); + } else { + previewEl.textContent = ''; + previewEl.classList.add('is-hidden'); + } + } + + const ul = document.getElementById('play-cyber-score-list'); + if (!ul) return; + const safeYv = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + const jumpMissionHud = isJumpSurvive() && isJumpSurviveMissionUiMapPlay(); + const rows = []; + rows.push({ + id: myId, + nickname: me.nickname || nick, + characterId: me.characterId || getPlayCharacterId(), + score: isQuizCarry() ? cyberHudQuizCarryPeerScore(myId) + : (isGauntlet() ? (me.gauntletScore || 0) + : (isSpaceShooter() ? (me.spaceShooterScore || 0) + : (isBalloonBoss() ? (me.balloonBossScore || 0) + : (jumpMissionHud ? (jumpSurviveEliminated ? 0 : 100) : 0)))), + y: safeYv(me.y), + eliminated: !!(isJumpSurvive() && jumpSurviveEliminated) || !!(isBalloonBoss() && me.balloonBossEliminated) + || !!(isGauntletCrownHeistMapPlay() && me.gauntletEliminated), + isMe: true, + }); + others.forEach((o, id) => { + rows.push({ + id, + nickname: o.nickname || id.slice(0, 8), + characterId: o.characterId, + score: isQuizCarry() ? cyberHudQuizCarryPeerScore(id) + : (isGauntlet() ? (o.gauntletScore || 0) + : (isSpaceShooter() ? (o.spaceShooterScore || 0) + : (isBalloonBoss() ? (o.balloonBossScore || 0) + : (jumpMissionHud ? (o.jumpSurviveEliminated ? 0 : 100) : 0)))), + y: safeYv(o.y), + eliminated: !!(isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated) + || !!(isBalloonBoss() && o.balloonBossEliminated) + || !!(isGauntletCrownHeistMapPlay() && o.gauntletEliminated), + isMe: false, + }); + }); + let leaderId = null; + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud) { + let mx = -1; + for (let i = 0; i < rows.length; i++) { + if (rows[i].score > mx) { + mx = rows[i].score; + leaderId = rows[i].id; + } + } + } else { + let best = Infinity; + for (let i = 0; i < rows.length; i++) { + if (!rows[i].eliminated && rows[i].y < best) { + best = rows[i].y; + leaderId = rows[i].id; + } + } + } + rows.sort((a, b) => { + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud) { + if (b.score !== a.score) return b.score - a.score; + if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1; + return String(a.nickname || '').localeCompare(String(b.nickname || ''), 'th'); + } + if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1; + return a.y - b.y; + }); + + ul.innerHTML = ''; + const cap = 8; + for (let r = 0; r < Math.min(cap, rows.length); r++) { + const row = rows[r]; + const li = document.createElement('li'); + li.className = 'play-cyber-score-row' + (row.isMe ? ' is-me' : '') + (row.eliminated ? ' is-out' : ''); + const av = document.createElement('img'); + av.className = 'play-cyber-score-av'; + av.alt = ''; + const cid = row.characterId ? String(row.characterId) : ''; + if (!cid) { + av.src = defaultAvatarImg.src; + } else { + const im = getCharacterImg(cid, 'down'); + if (im && im.src) av.src = im.src; + } + const mid = document.createElement('div'); + mid.className = 'play-cyber-score-mid'; + const name = document.createElement('span'); + name.className = 'play-cyber-score-name'; + if (row.id === leaderId && leaderId != null) { + const crown = document.createElement('span'); + crown.className = 'play-cyber-lead-crown'; + crown.textContent = '♔'; + crown.title = 'Leader'; + name.appendChild(crown); + } + name.appendChild(document.createTextNode(String(row.nickname || '').slice(0, 14))); + const st = document.createElement('span'); + st.className = 'play-cyber-score-state'; + st.textContent = isQuizCarry() + ? '' + : (row.eliminated + ? (isBalloonBoss() ? 'OUT' : (isGauntletCrownHeistMapPlay() ? 'เสียชีวิต' : 'OFFLINE')) + : (isBalloonBoss() + ? ('♥' + Math.max(0, row.isMe ? (me.balloonBossBalloons | 0) : ((() => { + const ox = others.get(row.id); + return ox ? (ox.balloonBossBalloons | 0) : 0; + })()))) + : 'LINK_OK')); + mid.appendChild(name); + mid.appendChild(st); + const sc = document.createElement('span'); + sc.className = 'play-cyber-score-val'; + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud) { + sc.textContent = String(row.score); + } else { + sc.textContent = row.eliminated ? '—' : String(r + 1); + } + li.appendChild(av); + li.appendChild(mid); + li.appendChild(sc); + ul.appendChild(li); + } + } + + function draw() { + if (!mapData) return; + const w = mapData.width, h = mapData.height; + + const stackCam = isStack() ? getStackCameraCentersPx() : null; + let camX = stackCam ? stackCam.px : me.x * tileSize; + let camY = stackCam ? stackCam.py : me.y * tileSize; + if (isJumpSurvive()) { + camX = jumpSurviveCamCenterX; + camY = jumpSurviveCamCenterY; + } + let zDraw = zoom; + if (isSpaceShooter() || isBalloonBoss()) { + const mwPx = w * tileSize, mhPx = h * tileSize; + camX = mwPx * 0.5; + camY = mhPx * 0.5; + zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96; + } + const halfW = canvas.width / (2 * zDraw); + const halfH = canvas.height / (2 * zDraw); + + // world bounds ที่กล้องมองเห็น (เป็นพิกัดพิกเซลของ map) + const worldMinX = camX - halfW; + const worldMaxX = camX + halfW; + const worldMinY = camY - halfH; + const worldMaxY = camY + halfH; + + const mapWpx = w * tileSize, mapHpx = h * tileSize; + const visibleW = worldMaxX - worldMinX, visibleH = worldMaxY - worldMinY; + const showGrid = mapData.showMapInGame !== false && mapData.showMapInGame !== 'false'; + const timeMs = Date.now(); + + if (mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) { + /* กล้องเห็นพื้นที่นอก [0,map] ได้ — ห้ามสุ่มต้นทาง drawImage เกินขอบภาพ (จะเกิดซ้ำ/เส้นตัด/ซ้อนกันที่ขอบจอ) */ + ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + const natW = mapBackgroundImg.naturalWidth; + const natH = mapBackgroundImg.naturalHeight; + const bgMinX = Math.max(0, worldMinX); + const bgMaxX = Math.min(mapWpx, worldMaxX); + const bgMinY = Math.max(0, worldMinY); + const bgMaxY = Math.min(mapHpx, worldMaxY); + if (bgMaxX > bgMinX && bgMaxY > bgMinY) { + const srcX = (bgMinX / mapWpx) * natW; + const srcY = (bgMinY / mapHpx) * natH; + const srcW = ((bgMaxX - bgMinX) / mapWpx) * natW; + const srcH = ((bgMaxY - bgMinY) / mapHpx) * natH; + const destX = (bgMinX - camX) * zDraw + canvas.width / 2; + const destY = (bgMinY - camY) * zDraw + canvas.height / 2; + const destW = (bgMaxX - bgMinX) * zDraw; + const destH = (bgMaxY - bgMinY) * zDraw; + if (srcW > 0.25 && srcH > 0.25 && destW > 0.25 && destH > 0.25) { + ctx.drawImage(mapBackgroundImg, srcX, srcY, srcW, srcH, destX, destY, destW, destH); + } + } + } else { + ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + if (isSpaceShooter()) { + ctx.save(); + for (let s = 0; s < 120; s++) { + const sx = (Math.sin(s * 12.9898 + w) * 0.5 + 0.5) * canvas.width; + const sy = (Math.cos(s * 4.1415 + h) * 0.5 + 0.5) * canvas.height; + const br = (s % 4 === 0) ? 1.6 : 0.9; + ctx.fillStyle = s % 7 === 0 ? 'rgba(200, 240, 255, 0.35)' : 'rgba(255, 255, 255, 0.12)'; + ctx.beginPath(); + ctx.arc(sx, sy, br, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); + } else if (isBalloonBoss()) { + ctx.save(); + ctx.fillStyle = 'rgba(30, 45, 120, 0.25)'; + for (let col = 0; col < 24; col++) { + const x = (col + 0.5) * (canvas.width / 24); + for (let r = 0; r < 18; r++) { + const bit = (col * 17 + r * 31 + Math.floor(timeMs / 80)) % 2; + ctx.fillStyle = bit ? 'rgba(0, 255, 200, 0.14)' : 'rgba(180, 200, 255, 0.08)'; + ctx.fillRect(x - 1, (r / 18) * canvas.height + ((timeMs / 40 + col * 3) % 40), 2.2, 10); + } + } + ctx.fillStyle = 'rgba(240, 248, 255, 0.55)'; + ctx.beginPath(); + ctx.moveTo(0, canvas.height); + const baseY = canvas.height * 0.88; + for (let k = 0; k <= 40; k++) { + const bx = (k / 40) * canvas.width; + const by = baseY - Math.abs(Math.sin(k * 1.7 + w)) * canvas.height * 0.08 - (k % 3) * 4; + ctx.lineTo(bx, by); + } + ctx.lineTo(canvas.width, canvas.height); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + } + function worldToScreen(wx, wy) { + const sx = (wx - camX) * zDraw + canvas.width / 2; + const sy = (wy - camY) * zDraw + canvas.height / 2; + return [sx, sy]; + } + + function drawGridImagePlacementsLayer() { + if (!mapData.gridImagePlacements || !mapData.gridImagePlacements.length || !mapData.gridImageLibrary || !mapData.gridImageLibrary.length) return; + for (let gi = 0; gi < mapData.gridImagePlacements.length; gi++) { + const sp = mapData.gridImagePlacements[gi]; + const wx0 = sp.x * tileSize; + const wy0 = sp.y * tileSize; + const ww = sp.w * tileSize; + const wh = sp.h * tileSize; + if (wx0 + ww <= worldMinX || wx0 >= worldMaxX || wy0 + wh <= worldMinY || wy0 >= worldMaxY) continue; + let gim = mapGridImageImgs[sp.i]; + let carrySpriteAlpha = 1; + if (isQuizCarry()) { + const domOpt = quizCarryOptionIndexForGridSprite(mapData, sp); + const onHub = spriteOverlapsQuizCarryHubArea(mapData, sp); + const meHolding = me && me.quizCarryHeld != null; + let carryVisualActive = false; + if (domOpt != null && quizCarryOptionHeldByAnyone(domOpt)) carryVisualActive = true; + else if (domOpt == null && onHub && meHolding) carryVisualActive = true; + if (carryVisualActive) { + const gh = mapGridImageHeldImgs[sp.i]; + if (gh && gh.complete && gh.naturalWidth) gim = gh; + else carrySpriteAlpha = 0.32; + } + } + if (!gim || !gim.complete || !gim.naturalWidth) continue; + const sx0 = (wx0 - camX) * zDraw + canvas.width / 2; + const sy0 = (wy0 - camY) * zDraw + canvas.height / 2; + const sw = ww * zDraw; + const sh = wh * zDraw; + ctx.save(); + if (carrySpriteAlpha < 1) ctx.globalAlpha = carrySpriteAlpha; + ctx.beginPath(); + ctx.rect(sx0, sy0, sw, sh); + ctx.clip(); + ctx.drawImage(gim, sx0, sy0, sw, sh); + ctx.restore(); + ctx.strokeStyle = 'rgba(255,255,255,0.18)'; + ctx.lineWidth = 1; + ctx.strokeRect(sx0 + 0.5, sy0 + 0.5, sw - 1, sh - 1); + } + } + + const startTileX = Math.max(0, Math.floor(worldMinX / tileSize)); + const endTileX = Math.min(w - 1, Math.ceil(worldMaxX / tileSize)); + const startTileY = Math.max(0, Math.floor(worldMinY / tileSize)); + const endTileY = Math.min(h - 1, Math.ceil(worldMaxY / tileSize)); + if (showGrid) { + for (let y = startTileY; y <= endTileY; y++) { + const lane = isFrogger() ? getLane(y) : null; + let rowFill = null; + if (lane) { + if (lane.type === 'goal') rowFill = 'rgba(158,206,106,0.4)'; + else if (lane.type === 'spawn') rowFill = 'rgba(187,154,247,0.35)'; + else if (lane.type === 'road') rowFill = 'rgba(80,70,60,0.6)'; + else if (lane.type === 'water') rowFill = 'rgba(125,207,255,0.5)'; + } else if (isGauntlet()) { + rowFill = (y % 2 === 0) ? 'rgba(247,118,190,0.08)' : 'rgba(180,90,140,0.06)'; + } else if (isStack()) { + rowFill = (y % 2 === 0) ? 'rgba(122, 162, 247, 0.06)' : 'rgba(187, 154, 247, 0.05)'; + } else if (isJumpSurvive()) { + rowFill = (y % 2 === 0) ? 'rgba(90, 200, 255, 0.07)' : 'rgba(60, 140, 200, 0.06)'; + } else if (isSpaceShooter()) { + rowFill = (y % 2 === 0) ? 'rgba(30, 45, 110, 0.22)' : 'rgba(20, 32, 78, 0.28)'; + } else if (isBalloonBoss()) { + rowFill = (y % 2 === 0) ? 'rgba(40, 25, 80, 0.2)' : 'rgba(25, 18, 60, 0.24)'; + } else if (isQuizBattle()) { + rowFill = (y % 2 === 0) ? 'rgba(110, 175, 255, 0.06)' : 'rgba(230, 110, 140, 0.055)'; + } + for (let x = startTileX; x <= endTileX; x++) { + const wx = x * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw; + const ob = mapData.objects?.[y]?.[x] ?? 0; + if (ob === 1) { + ctx.fillStyle = 'rgba(65,72,104,0.92)'; + ctx.fillRect(sx, sy, size, size); + ctx.strokeStyle = '#565f89'; + ctx.strokeRect(sx, sy, size, size); + } else { + const cellColor = showGrid && mapData.cellColors && mapData.cellColors[y] && mapData.cellColors[y][x]; + if (cellColor) { ctx.fillStyle = cellColor; ctx.fillRect(sx, sy, size, size); } + else if (rowFill) { ctx.fillStyle = rowFill; ctx.fillRect(sx, sy, size, size); } + else if (!mapBackgroundImg || !mapBackgroundImg.complete) { + ctx.fillStyle = (x + y) % 2 === 0 ? '#24283b' : '#1f2335'; + ctx.fillRect(sx, sy, size, size); + } + } + } + } + } + drawGridImagePlacementsLayer(); + if (showGrid) { + for (let y = startTileY; y <= endTileY; y++) { + for (let x = startTileX; x <= endTileX; x++) { + const wx = x * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw; + const isInter = mapData.interactive && mapData.interactive[y] && mapData.interactive[y][x] === 1; + if (isInter) { + ctx.fillStyle = 'rgba(158,206,106,0.35)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(158,206,106,0.8)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + if (isQuiz()) { + const isQuizQ = mapData.quizQuestionArea && mapData.quizQuestionArea[y] && mapData.quizQuestionArea[y][x] === 1; + if (isQuizQ) { + ctx.fillStyle = 'rgba(255, 214, 102, 0.32)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(224, 185, 70, 0.78)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isQuizT = mapData.quizTrueArea && mapData.quizTrueArea[y] && mapData.quizTrueArea[y][x] === 1; + if (isQuizT) { + ctx.fillStyle = 'rgba(86, 202, 255, 0.38)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(122, 220, 255, 0.85)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isQuizF = mapData.quizFalseArea && mapData.quizFalseArea[y] && mapData.quizFalseArea[y][x] === 1; + if (isQuizF) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.38)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(255, 130, 200, 0.85)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + } + if (isQuizCarry()) { + const hub = mapData.quizCarryHubArea && mapData.quizCarryHubArea[y] && mapData.quizCarryHubArea[y][x] === 1; + if (hub) { + ctx.fillStyle = 'rgba(187, 154, 247, 0.35)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(200, 170, 255, 0.75)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isCarryQ = mapData.quizQuestionArea && mapData.quizQuestionArea[y] && mapData.quizQuestionArea[y][x] === 1; + if (isCarryQ) { + ctx.fillStyle = 'rgba(255, 214, 102, 0.3)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(224, 185, 70, 0.82)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const ov = mapData.quizCarryOptionArea && mapData.quizCarryOptionArea[y] && mapData.quizCarryOptionArea[y][x]; + if (ov >= 1 && ov <= QUIZ_CARRY_MAX_OPTION_SLOTS) { + ctx.fillStyle = quizCarryMinimapOptionFillCss(ov); + ctx.fillRect(sx + 3, sy + 3, size - 6, size - 6); + } + } + if (isQuizBattle() && showGrid && mapData.quizBattlePathArea && mapData.quizBattlePathArea[y] && mapData.quizBattlePathArea[y][x] === 1) { + ctx.fillStyle = 'rgba(186, 130, 255, 0.4)'; + ctx.fillRect(sx + 1, sy + 1, size - 2, size - 2); + ctx.strokeStyle = 'rgba(220, 170, 255, 0.55)'; + ctx.strokeRect(sx + 1, sy + 1, size - 2, size - 2); + } + if (isQuizBattle() && showGrid && mapData.quizBattleDomeArea && mapData.quizBattleDomeArea[y] && mapData.quizBattleDomeArea[y][x] === 1) { + const cid = mapData.quizBattleDomeComp && mapData.quizBattleDomeComp[y] && mapData.quizBattleDomeComp[y][x]; + const done = cid > 0 && quizBattleAnsweredComps.has(cid); + ctx.fillStyle = done ? 'rgba(90, 90, 110, 0.4)' : 'rgba(100, 200, 255, 0.36)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = done ? 'rgba(140, 140, 160, 0.65)' : 'rgba(255, 100, 130, 0.82)'; + ctx.lineWidth = 2; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.lineWidth = 1; + } + if (isStack()) { + const sr = mapData.stackReleaseArea, sl = mapData.stackLandArea; + if (sr && sr[y] && sr[y][x] === 1) { + ctx.fillStyle = 'rgba(125, 207, 255, 0.28)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(122, 220, 255, 0.55)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + if (sl && sl[y] && sl[y][x] === 1) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.26)'; + ctx.fillRect(sx + 3, sy + 3, size - 6, size - 6); + ctx.strokeStyle = 'rgba(255, 130, 200, 0.5)'; + ctx.strokeRect(sx + 3, sy + 3, size - 6, size - 6); + } + } + if (isBalloonBoss() && showGrid && mapData.balloonBossPlayerSlots) { + const sv = mapData.balloonBossPlayerSlots[y] && mapData.balloonBossPlayerSlots[y][x]; + if (sv >= 1 && sv <= 6) { + ctx.fillStyle = 'rgba(255, 120, 200, 0.12)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(255, 160, 220, 0.45)'; + ctx.lineWidth = 1.5; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.lineWidth = 1; + ctx.fillStyle = 'rgba(255, 228, 250, 0.92)'; + ctx.font = `bold ${Math.max(8, 10 * zDraw)}px sans-serif`; + ctx.textAlign = 'center'; + ctx.fillText('P' + sv, sx + size / 2, sy + size / 2 + 4); + ctx.textAlign = 'left'; + } + } + if (isSpaceShooter() && showGrid && mapData.shooterSpawnSlots) { + const sv = mapData.shooterSpawnSlots[y] && mapData.shooterSpawnSlots[y][x]; + if (sv >= 1 && sv <= 6) { + ctx.fillStyle = 'rgba(0, 255, 240, 0.14)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(120, 255, 255, 0.45)'; + ctx.lineWidth = 1.5; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.lineWidth = 1; + ctx.fillStyle = 'rgba(220, 250, 255, 0.9)'; + ctx.font = `bold ${Math.max(8, 10 * zDraw)}px sans-serif`; + ctx.textAlign = 'center'; + ctx.fillText('P' + sv, sx + size / 2, sy + size / 2 + 4); + ctx.textAlign = 'left'; + } + } + } + } + if (isJumpSurvive() && showGrid && mapData.jumpSurvivePlatformArea) { + const pa = mapData.jumpSurvivePlatformArea; + const sc = jumpSurvivePlatformScrollPx; + const tsz = tileSize; + const period = h * tsz; + const visTop = worldMinY; + const visBot = worldMaxY; + const eps = 1e-6; + for (let py = 0; py < h; py++) { + for (let px = 0; px < w; px++) { + if (!pa[py] || pa[py][px] !== 1) continue; + const tileWorldLeft = px * tsz; + const tileWorldRight = tileWorldLeft + tsz; + if (tileWorldRight <= worldMinX || tileWorldLeft >= worldMaxX) continue; + const rawTop = py * tsz - sc; + const kMin = Math.ceil((visTop - rawTop - tsz + eps) / period); + const kMax = Math.floor((visBot - rawTop - eps) / period); + if (kMax < kMin) continue; + for (let kk = kMin; kk <= kMax; kk++) { + const platTop = rawTop + kk * period; + if (platTop + tsz <= visTop || platTop >= visBot) continue; + const [psx, psy] = worldToScreen(tileWorldLeft, platTop); + const psz = tsz * zDraw; + ctx.fillStyle = 'rgba(0, 40, 52, 0.72)'; + ctx.fillRect(psx + 3, psy + 3, psz - 6, psz - 6); + ctx.strokeStyle = 'rgba(0, 255, 240, 0.55)'; + ctx.lineWidth = 2; + ctx.strokeRect(psx + 3, psy + 3, psz - 6, psz - 6); + ctx.strokeStyle = 'rgba(120, 255, 255, 0.35)'; + ctx.lineWidth = 1; + ctx.strokeRect(psx + 5, psy + 5, psz - 10, psz - 10); + const grad = ctx.createLinearGradient(psx, psy, psx, psy + psz); + grad.addColorStop(0, 'rgba(0, 255, 240, 0.12)'); + grad.addColorStop(0.5, 'rgba(0, 0, 0, 0)'); + grad.addColorStop(1, 'rgba(0, 180, 200, 0.08)'); + ctx.fillStyle = grad; + ctx.fillRect(psx + 4, psy + 4, psz - 8, psz - 8); + ctx.lineWidth = 1; + if (zDraw >= 1.15 && psz > 22) { + ctx.save(); + ctx.font = `600 ${Math.max(7, Math.min(11, psz * 0.14))}px "Share Tech Mono", ui-monospace, monospace`; + ctx.fillStyle = 'rgba(180, 255, 255, 0.5)'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('DATA', psx + psz * 0.5, psy + psz * 0.38); + ctx.font = `500 ${Math.max(5, Math.min(8, psz * 0.1))}px "Share Tech Mono", ui-monospace, monospace`; + ctx.fillStyle = 'rgba(120, 230, 255, 0.38)'; + ctx.fillText('/SEC/BLOCK', psx + psz * 0.5, psy + psz * 0.62); + ctx.restore(); + } + } + } + } + } + if (isFrogger() && mapData.lanes) { + for (let y = startTileY; y <= endTileY; y++) { + const lane = getLane(y); + if (!lane || (lane.type !== 'road' && lane.type !== 'water')) continue; + const positions = getVehiclePositions(lane, mapData.width, timeMs); + const isRoad = lane.type === 'road'; + for (let i = 0; i < positions.length; i++) { + const vx = positions[i]; + const wx = vx * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw * (isRoad ? 1.2 : 1.5); + ctx.fillStyle = isRoad ? '#e0a060' : '#8b7355'; + ctx.fillRect(sx, sy, size, size * 0.7); + ctx.strokeStyle = isRoad ? '#c0caf5' : '#9ece6a'; + ctx.strokeRect(sx, sy, size, size * 0.7); + } + } + } + } + + if (isQuizCarry()) { + drawQuizCarryChoiceLabels(ctx, worldToScreen, zDraw); + } + + if (isStack() && stackMini) { + drawStackMinigame(ctx, worldToScreen, zDraw); + } + + const gauntletObsDraw = isGauntlet() ? getGauntletObsDrawPositionsAt(performance.now()) : []; + if (isGauntlet() && gauntletObsDraw.length) { + const stx = Math.max(0, Math.floor(worldMinX / tileSize)); + const enx = Math.min(w - 1, Math.ceil(worldMaxX / tileSize)); + const sty = Math.max(0, Math.floor(worldMinY / tileSize)); + const eny = Math.min(h - 1, Math.ceil(worldMaxY / tileSize)); + for (let i = 0; i < gauntletObsDraw.length; i++) { + const o = gauntletObsDraw[i]; + if (!o) continue; + if (o.kind === 'lane' && typeof o.y === 'number') { + if (o.drawX < stx - 2 || o.drawX > enx + 2 || o.y < sty || o.y > eny) continue; + const size = tileSize * zDraw; + const inner = Math.max(2, size - 4); + const [sxC, syCellBottom] = worldToScreen((o.drawX + 0.5) * tileSize, (o.y + 1) * tileSize); + const dx = sxC - inner / 2; + const dy = syCellBottom - inner; + const laneRec = pickGauntletLaneImageRec(o.id); + if (laneRec && laneRec.img.complete && laneRec.img.naturalWidth > 0) { + try { + ctx.drawImage(laneRec.img, dx, dy, inner, inner); + } catch (e) { + ctx.fillStyle = '#f7768e'; + ctx.fillRect(dx, dy, inner, inner); + } + } else { + ctx.fillStyle = '#f7768e'; + ctx.fillRect(dx, dy, inner, inner); + } + } else if (o.kind === 'laser' && typeof o.drawX === 'number') { + if (o.drawX < stx - 2 || o.drawX > enx + 2) continue; + const { y0: laserRow0, y1: laserRow1 } = gauntletLaserRowHitRange(o, h); + const wx0 = o.drawX * tileSize; + const wx1 = (o.drawX + 1) * tileSize; + const wy0 = laserRow0 * tileSize; + const wy1 = (laserRow1 + 1) * tileSize; + const [sx0, syTop] = worldToScreen(wx0, wy0); + const [sx1, syBot] = worldToScreen(wx1, wy1); + const rx = Math.min(sx0, sx1); + const rw = Math.max(2, Math.abs(sx1 - sx0)); + const ry = Math.min(syTop, syBot); + const rh = Math.abs(syBot - syTop); + drawGauntletLaserColumnScreen(rx, ry, rw, rh); + } + } + } + + /** ป้ายติดตัว — กึ่งกลางลำตัว (ทับอก/เอว) แบบเดียวกับป้ายบนพื้น ไม่มีเส้นเชื่อม */ + function drawQuizCarryHeldSignBoard(sxFeet, feetSy, halfSpriteW, spriteTopY, signText, _faceDir, choiceIndex, imageUrl, imageElementOpt) { + const imgUrlSan = sanitizeQuizCarryImageUrlClient(imageUrl); + const gridHeldDraw = imageElementOpt && imageElementOpt.complete && imageElementOpt.naturalWidth > 0 ? imageElementOpt : null; + const raw = String(signText || '').trim(); + if (!raw && !imgUrlSan && !gridHeldDraw) return; + ctx.save(); + const heldPs = quizCarryPlaqueMapScaleClampedPlay(); + const bodyH = Math.max(18, feetSy - spriteTopY); + const midBodyY = spriteTopY + bodyH * 0.48; + const bodyCx = sxFeet; + const halfW = Math.max(10, halfSpriteW || 0); + const baseMaxW = Math.min(280, Math.max(100, halfW * 2.4 + tileSize * zDraw * 0.5)); + const maxPlaqueW = Math.min(340, Math.round(baseMaxW * heldPs)); + + const fontPx = Math.max(12, Math.min(22, tileSize * zDraw * 0.22 * heldPs)); + ctx.font = `600 ${fontPx}px system-ui, "Segoe UI", "Kanit", sans-serif`; + const padX = 12; + const padY = 9; + const maxInner = Math.min(maxPlaqueW - padX * 2, Math.max(72, tileSize * zDraw * 4.2)); + let lines; + if (!raw) { + lines = []; + } else { + lines = canvasWordWrapLines(ctx, raw, maxInner); + if (!lines.length) lines = [raw]; + if (lines.length > 3) lines = lines.slice(0, 3); + for (let i = 0; i < lines.length; i++) { + let s = lines[i]; + if (ctx.measureText(s).width <= maxInner) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxInner) s = s.slice(0, -1); + lines[i] = s + '…'; + } + } + let maxLineW = 0; + for (let i = 0; i < lines.length; i++) { + const lw = ctx.measureText(lines[i]).width; + if (lw > maxLineW) maxLineW = lw; + } + const lineH = fontPx * 1.22; + let w; + let h; + if (!lines.length && (imgUrlSan || gridHeldDraw)) { + w = Math.ceil(Math.min(maxPlaqueW, Math.max(104, maxInner + padX * 2))); + h = Math.ceil(Math.max(56, fontPx * 2.8 + padY * 2)); + } else { + w = Math.ceil(Math.max(maxLineW, 56) + padX * 2); + h = Math.ceil(lines.length * lineH + padY * 2); + w = Math.min(maxPlaqueW, Math.max(96, w)); + h = Math.max(40, h); + } + + let signX = bodyCx - w / 2; + let signY = midBodyY - h / 2; + + if (signX + w > canvas.width - 6) signX = canvas.width - w - 6; + if (signX < 6) signX = 6; + if (signY < 6) signY = 6; + if (signY + h > feetSy + 8) signY = feetSy + 8 - h; + + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, null, { imageUrl: imgUrlSan, imageElement: gridHeldDraw }); + ctx.restore(); + } + + function drawAvatar(ax, ay, isMe, name, characterId, direction, isWalking, playTint, gauntletAirTicks, gauntletScoreLabel, quizCarrySignPayload) { + const { cw, ch } = getCharacterFootprintWH(mapData); + const air = Number(gauntletAirTicks) || 0; + let liftWorldY = 0; + if (isGauntlet() && air > 0) { + liftWorldY = gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSize * 0.52; + } + const cxWorld = (ax + cw * 0.5) * tileSize; + /* ใช้ ay แบบต่อเนื่อง — ห้าม floor(ay) เดิมทำให้เท้ากระโดดทีละช่องขณะเดิน (กระตุกแนวตั้ง) */ + const cyBottomWorld = (ay + ch) * tileSize - liftWorldY; + const [sx, sy] = worldToScreen(cxWorld, cyBottomWorld); + const cellSpan = Math.max(cw, ch); + const r = Math.max(14, (tileSize * zDraw * cellSpan) / 2 - 2); + const size = r * 2.2; + const dir = direction || 'down'; + const rawImg = getAvatarImg(characterId, dir, timeMs, isWalking); + const charImg = playTint + ? getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, isWalking, playTint) + : rawImg; + /* รองรับทั้ง และ canvas จากย้อมสี (canvas ไม่มี naturalWidth → เดิมตกวงกลม) */ + let iw = 0, ih = 0; + if (charImg && charImg.tagName === 'CANVAS' && charImg.width > 0 && charImg.height > 0) { + iw = charImg.width; + ih = charImg.height; + } else if (charImg && charImg.complete && charImg.naturalWidth) { + iw = charImg.naturalWidth; + ih = charImg.naturalHeight; + } + let halfSpriteW = r; + let spriteTopY = sy - 2 * r; + if (iw > 0 && ih > 0) { + const scale = Math.min(size / iw, size / ih, 1); + const drawW = iw * scale; + const drawH = ih * scale; + halfSpriteW = drawW / 2; + spriteTopY = sy - drawH; + ctx.drawImage(charImg, 0, 0, iw, ih, sx - drawW / 2, sy - drawH, drawW, drawH); + } else { + const cy = sy - r; + ctx.fillStyle = isMe ? '#7aa2f7' : '#9ece6a'; + ctx.beginPath(); + ctx.arc(sx, cy, r, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = '#c0caf5'; + ctx.lineWidth = 2; + ctx.stroke(); + spriteTopY = cy - r; + } + ctx.fillStyle = '#c0caf5'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'center'; + const nameBase = name || ''; + const withScore = isGauntlet() && typeof gauntletScoreLabel === 'number' && gauntletScoreLabel >= 0 + ? (nameBase + ' · ' + gauntletScoreLabel) + : nameBase; + ctx.fillText(withScore, sx, sy + 10); + if (isQuizCarry() && quizCarrySignPayload) { + drawQuizCarryHeldSignBoard( + sx, sy, halfSpriteW, spriteTopY, + quizCarrySignPayload.text, dir, quizCarrySignPayload.choiceIndex, + quizCarrySignPayload.imageUrl, + quizCarrySignPayload.imageElement, + ); + } + } + function quizCarrySignForEntity(ent) { + if (!isQuizCarry() || !quizCarryCurrent || !ent || ent.quizCarryHeld == null) return null; + const idx = ent.quizCarryHeld; + const ch = quizCarryCurrent.choices; + if (!Array.isArray(ch) || idx < 0 || idx >= ch.length) return null; + const t = String(ch[idx] != null ? ch[idx] : '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(quizCarryCurrent, idx); + const gridHeldEl = findQuizCarryGridHeldImgForChoiceIndex(idx); + if (!t && !imgUrl && !gridHeldEl) return null; + return { text: t, choiceIndex: idx, imageUrl: imgUrl, imageElement: gridHeldEl || undefined }; + } + const safeX = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + const safeY = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + function peerVisualOffset(id) { + let h = 0; + for (let i = 0; i < (id || '').length; i++) h = (h * 31 + (id || '').charCodeAt(i)) >>> 0; + return { ax: ((h % 5) - 2) * 0.1, ay: ((Math.floor(h / 5) % 5) - 2) * 0.1 }; + } + const { ch: sortCh } = getCharacterFootprintWH(mapData); + /** ใช้แถวกริด (floor y) เป็นหลัก — ลดการสลับลำดับวาดทุกเฟรมเมื่อ y เป็น float เลอร์ป */ + function avatarDrawDepth(ent) { + return Math.floor(safeY(ent.y)) + sortCh; + } + if (!isStack() && !isSpaceShooter() && !isBalloonBoss()) { + let mt = me.playTint; + if (!mt || typeof mt.head !== 'string' || typeof mt.hair !== 'string' || typeof mt.body !== 'string') { + mt = playTintFromPeerId(String((myId != null && myId !== '') ? myId : (nick || 'local'))); + me.playTint = mt; + } + const meTag = isFrogger() ? (' (กบ) ' + froggerScore) : (isGauntlet() && meGauntletJumpTicks > 0 ? ' (กระโดด)' : (isJumpSurvive() && jumpSurviveEliminated ? ' (ตกรอบ)' : ' (คุณ)')); + const meWalking = isFrogger() ? false : (isGauntlet() ? (meGauntletJumpTicks > 0 || meGauntletJumpVis > 0.08) : !!me.isWalking); + const drawRows = []; + others.forEach((o, id) => { + if (!o) return; + drawRows.push({ isMe: false, id, o }); + }); + drawRows.push({ isMe: true, id: '__me', o: me }); + drawRows.sort((a, b) => { + const da = avatarDrawDepth(a.o); + const db = avatarDrawDepth(b.o); + if (da !== db) return da - db; + const xa = Math.floor(safeX(a.o.x) * 4); + const xb = Math.floor(safeX(b.o.x) * 4); + if (xa !== xb) return xa - xb; + const sa = a.isMe ? '\uFFFFme' : String(a.id); + const sb = b.isMe ? '\uFFFFme' : String(b.id); + return sa < sb ? -1 : sa > sb ? 1 : 0; + }); + drawRows.forEach((row) => { + if (!row.isMe) { + const id = row.id; + const o = row.o; + const off = isGauntlet() ? { ax: 0, ay: 0 } : peerVisualOffset(id); + const otherWalk = isGauntlet() + ? ((o.gauntletJumpTicks || 0) > 0 || (o.gauntletJumpVis || 0) > 0.08) + : (isPreviewBotId(id) + ? !!o.botIsWalking + : !!((o.tx != null && Math.abs((o.tx || o.x) - o.x) > 0.02) || (o.ty != null && Math.abs((o.ty || o.y) - o.y) > 0.02))); + const ot = o.playTint || playTintFromPeerId(id); + const faceDirOther = isGauntletFaceRightMapMno9kb07() ? 'right' : o.direction; + const botOut = isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated; + const peerName = botOut ? (o.nickname + ' (ตกรอบ)') : o.nickname; + if (botOut) ctx.save(); + if (botOut) ctx.globalAlpha = 0.4; + drawAvatar(safeX(o.x) + off.ax, safeY(o.y) + off.ay, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o)); + if (botOut) ctx.restore(); + } else { + if (isJumpSurvive() && jumpSurviveEliminated) ctx.save(); + if (isJumpSurvive() && jumpSurviveEliminated) ctx.globalAlpha = 0.4; + const faceDirMe = isGauntletFaceRightMapMno9kb07() ? 'right' : me.direction; + drawAvatar(safeX(me.x), safeY(me.y), true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me)); + if (isJumpSurvive() && jumpSurviveEliminated) ctx.restore(); + } + }); + } + if (isFrogger()) { + ctx.fillStyle = '#7aa2f7'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('โหมดกบ | คะแนน: ' + froggerScore, 10, 24); + } + if (isJumpSurvive() && !useCyberPlayHud()) { + ctx.fillStyle = 'rgba(148, 226, 213, 0.95)'; + ctx.font = '600 12px system-ui, "Segoe UI", "Kanit", sans-serif'; + ctx.textAlign = 'left'; + if (jumpSurviveEliminated) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.95)'; + ctx.fillText('คุณตกรอบแล้ว (ชนหรือหลุดเพดาน/พื้นจอ) · เล่นต่อไม่ได้ — ดูเพื่อนได้ · รอรอบใหม่หรือออกจากห้อง', 10, 22); + } else { + ctx.fillText('กระโดดให้รอด · Space / W / ↑ · A D / ← → · แพลตฟอร์มวน · ชนหรือหลุดบน/ล่างจอ = ตกรอบ (ดูเพื่อนต่อ)', 10, 22); + } + } + if (isGauntlet() && !useCyberPlayHud()) { + ctx.fillStyle = '#f7768e'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + if (isGauntletCrownHeistMapPlay()) { + ctx.fillText('พรมแดงสุดท้าย | คะแนน ' + (me.gauntletScore || 0) + ' · ชน -10 · ขอบซ้ายตาย · สูงสุด 6 คน', 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · จบเกมมีโบนัสอันดับ + เกรดทีม', 10, 42); + } else { + ctx.fillText('พรมแดงสุดท้าย | คะแนน: ' + (me.gauntletScore || 0) + ' (ข้าม 1 ชิ้น +1) · สูงสุด 6 คน', 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · ข้ามสำเร็จ → ขวา 1 + คะแนน · ชน → ซ้าย 1', 10, 42); + } + ctx.fillStyle = '#e0af68'; + if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) { + const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000)); + const mm = Math.floor(rem / 60); + const ss = rem % 60; + const clock = `${mm}:${String(ss).padStart(2, '0')}`; + ctx.fillText(`เหลือเวลา ${clock} · Time left ${clock}`, 10, 60); + } else if (gauntletRuntimeTimeLimitSec > 0) { + ctx.fillText('เวลา: กำลังซิงก์จากเซิร์ฟเวอร์... · Syncing timer...', 10, 60); + } else { + ctx.fillText('เวลา: ไม่จำกัด · No time limit (ตั้งได้ที่ Admin → เวลาเกม)', 10, 60); + } + } + if (isStack() && stackMini) { + if (previewFillBots) { + ctx.fillStyle = 'rgba(125, 220, 255, 0.9)'; + ctx.font = '600 12px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Stack · shared tower · Space / click = release on your turn', 10, 22); + drawStackPreviewCyberHud(ctx, canvas.width, canvas.height); + } else { + ctx.fillStyle = '#7dcfff'; + ctx.font = 'bold 15px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Stack | คะแนน ' + stackMini.score + ' · ชีวิต ' + stackMini.lives + ' · combo x' + (stackMini.combo || 0), 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / คลิก = ปล่อย', 10, 44); + } + } + if (isQuiz() || isQuizCarry() || isQuizBattle()) { + syncPlayQuizMapPanel(); + syncQuizCarryEmbedQuestionStrip(); + } + /* timeup ต้องซิงก์ทุกเฟรมแม้จบเซสชัน — อย่าให้อยู่ในเงื่อนไขด้านบนอย่างเดียว */ + syncQuizCarryTimeupDeskLayerPosition(); + if (previewFillBots && mapData && !useCyberPlayHud()) { + const human = countPlayHumans(); + const bots = [...others.keys()].filter(isPreviewBotId).length; + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = 'rgba(26,27,38,0.78)'; + const hudW = isStack() ? Math.min(canvas.width - 12, 520) : Math.min(canvas.width - 12, 320); + ctx.fillRect(6, canvas.height - 38, hudW, 30); + ctx.fillStyle = '#a9b1d6'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'left'; + let line = 'ทดสอบ: คนจริง ' + human + ' + บอท ' + bots + ' (เป้า ' + previewTargetHeadcount + ' คน)'; + if (isStack()) { + line += ' · ตึกร่วม + HUD ซ้าย/ขวา'; + } + ctx.fillText(line, 12, canvas.height - 18); + ctx.restore(); + } + if (isSpaceShooter()) { + drawSpaceShooterCombatLayer(ctx, worldToScreen, zDraw, timeMs); + } + if (isBalloonBoss()) { + drawBalloonBossCombatLayer(ctx, worldToScreen, zDraw, timeMs); + } + if (isQuizCarry()) { + drawQuizCarryEmbedCountdownHighlight(ctx, worldToScreen, zDraw, timeMs); + } + if (previewMode && editorEmbedReturn && isQuizCarry() && quizCarryEmbedCountdownEndAt > Date.now()) { + syncQuizCarryEmbedCountdownLayout(); + } + syncPlayCyberHud(); + syncQuizCarryGrabButton(); + } + + document.addEventListener('keydown', (e) => { + if (isMovementKey(e.code) && isChatFocused()) return; + if ((isSpaceShooter() || isBalloonBoss()) && mapData && !isChatFocused()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + } + if (isStack() && mapData && !isChatFocused()) { + if (e.code === 'Space') { + e.preventDefault(); + tryHumanStackDrop(); + return; + } + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + return; + } + } + if (isGauntlet() && mapData && !isChatFocused()) { + if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { + e.preventDefault(); + if (isGauntletCrownPregameBlockingPlay()) return; + if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return; + const now = Date.now(); + if (now - lastGauntletJumpKey < 200) return; + lastGauntletJumpKey = now; + socket.emit('gauntlet-jump'); + return; + } + if (['ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + return; + } + } + if (isJumpSurvive() && mapData && !isChatFocused()) { + if (isJumpSurviveMissionPregameBlockingPlay()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) e.preventDefault(); + return; + } + if (jumpSurviveEliminated) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) e.preventDefault(); + return; + } + if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { + e.preventDefault(); + if (!e.repeat) jumpSurviveJumpQueued = true; + return; + } + if (e.code === 'ArrowDown' || e.code === 'KeyS') { + e.preventDefault(); + return; + } + } + if (isQuizBattle() && mapData && !isChatFocused()) { + if (e.code === 'KeyE') { + e.preventDefault(); + if (!e.repeat && myId != null) tryOpenQuizBattleFromKey(); + return; + } + } + if (isQuizCarry() && mapData && !isChatFocused()) { + if (e.code === 'KeyF') { + e.preventDefault(); + if (!e.repeat && myId != null) tryQuizCarryInteractionForPlayer(myId, me.x, me.y, { fromKey: true }); + return; + } + } + keys[e.code] = true; + keys[e.key] = true; + if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault(); + if (isFrogger() && mapData && !isChatFocused()) { + const now = Date.now(); + if (now - lastFroggerKey < 280) return; + let dx = 0, dy = 0; + if (e.code === 'ArrowUp' || e.code === 'KeyW') { dy = -1; me.direction = 'up'; } + else if (e.code === 'ArrowDown' || e.code === 'KeyS') { dy = 1; me.direction = 'down'; } + else if (e.code === 'ArrowLeft' || e.code === 'KeyA') { dx = -1; me.direction = 'left'; } + else if (e.code === 'ArrowRight' || e.code === 'KeyD') { dx = 1; me.direction = 'right'; } + if (dx !== 0 || dy !== 0) { + const nx = Math.round(me.x) + dx, ny = Math.round(me.y) + dy; + if (nx >= 0 && nx < mapData.width && ny >= 0 && ny < mapData.height) { + const row = mapData.objects && mapData.objects[ny]; + if (!row || row[nx] !== 1) { + me.x = nx; me.y = ny; + lastFroggerKey = now; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + const lane = getLane(Math.floor(me.y)); + if (lane && lane.type === 'goal') { froggerScore++; respawnFrogger(); } + else if (checkFroggerCollision()) respawnFrogger(); + } + } + } + } + }); + document.addEventListener('keyup', (e) => { keys[e.code] = false; keys[e.key] = false; }); + + let lastSend = 0; + function tick() { + if (!mapData) { requestAnimationFrame(tick); return; } + if (isStack()) { + stackTickFrame(); + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isJumpSurvive()) { + if (isJumpSurviveMissionPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + jumpSurviveTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isSpaceShooter()) { + spaceShooterTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isBalloonBoss()) { + balloonBossTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isFrogger()) { + if (checkFroggerCollision()) respawnFrogger(); + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isGauntlet()) { + if (isGauntletCrownPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + me.isWalking = meGauntletJumpTicks > 0; + const stepGauntletJumpVis = (vis, tgt) => { + const t = Number(tgt) || 0; + let v = Number(vis) || 0; + const d = t - v; + const k = d < 0 ? 0.68 : 0.4; + return v + d * k; + }; + meGauntletJumpVis = stepGauntletJumpVis(meGauntletJumpVis, meGauntletJumpTicks); + const mp = lerpGauntletEntityPos(me.x, me.y, me.tx, me.ty); + me.x = mp.nx; + me.y = mp.ny; + others.forEach((o) => { + const op = lerpGauntletEntityPos(o.x, o.y, o.tx, o.ty); + o.x = op.nx; + o.y = op.ny; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks || 0; + o.gauntletJumpVis = stepGauntletJumpVis(o.gauntletJumpVis, o.gauntletJumpTicks || 0); + }); + draw(); + requestAnimationFrame(tick); + return; + } + /* บอท preview ขยับที่ o.x/o.y โดยตรง — ห้าม LERP กับ tx/ty เพราะ stepPreviewBots เคยตั้ง tx=ก่อนเดิน ทำให้โดนดึงถอย ~20%/เฟรม กระตุกและแทบไม่ไป */ + others.forEach((o, id) => { + if (previewFillBots && isPreviewBotId(id)) return; + if (o.tx != null) o.x += (o.tx - o.x) * LERP; + if (o.ty != null) o.y += (o.ty - o.y) * LERP; + }); + tickQuizCarryEmbedCountdown(); + tickQuizCarryRoundTimers(); + updateQuizCarryCarryPhaseHud(); + stepPreviewBots(); + if (isChatFocused()) { + me.isWalking = false; + enforceQuizBattleLaneOnMePlay(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isQuizCarryEmbedLobbyBlockingMovement() || isQuizCarryEmbedCountdownBlockingMovement()) { + me.isWalking = false; + playPath = []; + enforceQuizBattleLaneOnMePlay(); + draw(); + requestAnimationFrame(tick); + return; + } + const w = mapData.width || 20, h = mapData.height || 15; + let accX = 0, accY = 0; + let usePath = false; + const keyPressed = keys['ArrowUp'] || keys['KeyW'] || keys['ArrowDown'] || keys['KeyS'] || keys['ArrowLeft'] || keys['KeyA'] || keys['ArrowRight'] || keys['KeyD']; + if (playPath.length > 0 && keyPressed) playPath = []; + if (playPath.length > 0) { + const way = playPath[0]; + const dx = way.x - me.x, dy = way.y - me.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= PATH_ARRIVE_THRESH) { + playPath.shift(); + /* ข้าม waypoint ที่ซ้ำตำแหน่ง — กัน acc=0 ทั้งที่ยังมี path (เดินขึ้น/ลงแล้วแอนิเมชันค้าง) */ + while (playPath.length > 0) { + const w2 = playPath[0]; + const ux = w2.x - me.x, uy = w2.y - me.y; + if (Math.sqrt(ux * ux + uy * uy) > PATH_ARRIVE_THRESH) break; + playPath.shift(); + } + if (playPath.length === 0) { + me.isWalking = false; + me.x = Math.max(0, Math.min(w - 0.01, me.x)); + me.y = Math.max(0, Math.min(h - 0.01, me.y)); + enforceQuizBattleLaneOnMePlay(); + const t = Date.now(); + if (t - lastSend > 80) { lastSend = t; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } + maybeAutoOpenQuizBattle(); + draw(); + requestAnimationFrame(tick); + return; + } + usePath = true; + const next = playPath[0]; + accX = next.x - me.x; + accY = next.y - me.y; + } else { + usePath = true; + accX = dx; + accY = dy; + } + if (usePath && (accX !== 0 || accY !== 0)) { + if (Math.abs(accY) > Math.abs(accX)) me.direction = accY > 0 ? 'down' : 'up'; + else me.direction = accX > 0 ? 'right' : 'left'; + } + } + if (!usePath) { + if (keys['ArrowUp'] || keys['KeyW']) { accY = -1; me.direction = 'up'; } + 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'; } + } + const preWalkX = me.x, preWalkY = me.y; + if (accX !== 0 || accY !== 0) { + const len = Math.sqrt(accX * accX + accY * accY) || 1; + const step = Math.min(MOVE_SPEED, len); + const nx = me.x + (accX / len) * step; + const ny = me.y + (accY / len) * step; + /** Quiz Battle + เส้นทาง: ห้ามเลื่อนแกนเดียว (slide) — ไม่งั้นเดินทแยงหลุดออกนอกเลนได้ */ + const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData); + if (canWalkLikeLobby(nx, ny, me.x, me.y)) { + me.x = nx; + me.y = ny; + } else if (!pathStrict) { + if (canWalkLikeLobby(nx, me.y, me.x, me.y)) { + me.x = nx; + } else if (canWalkLikeLobby(me.x, ny, me.x, me.y)) { + me.y = ny; + } + } + } + me.x = Math.max(0, Math.min(w - 0.01, me.x)); + me.y = Math.max(0, Math.min(h - 0.01, me.y)); + enforceQuizBattleLaneOnMePlay(); + const movedThisTick = Math.abs(me.x - preWalkX) > 1e-5 || Math.abs(me.y - preWalkY) > 1e-5; + me.isWalking = !!(accX !== 0 || accY !== 0) || playPath.length > 0 || movedThisTick; + const now = Date.now(); + if (now - lastSend > 80) { lastSend = now; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } + maybeAutoOpenQuizBattle(); + draw(); + requestAnimationFrame(tick); + } + + const chatForm = document.getElementById('chat-form'); + if (chatForm) { + chatForm.addEventListener('submit', (e) => { + e.preventDefault(); + const input = document.getElementById('chat-input'); + const text = (input && input.value || '').trim(); + if (text) { socket.emit('chat', text); if (input) input.value = ''; } + }); + } + document.getElementById('btn-leave').addEventListener('click', () => { + socket.emit('leave-space'); + const mid = params.get('map'); + if (previewMode && mid) { + var edQ = '?id=' + encodeURIComponent(mid) + (editorEmbedReturn ? '&embed=1' : ''); + window.location.replace(BASE + '/editor.html' + edQ); + } else { + window.location.replace(BASE + '/lobby.html'); + } + }); + (function wireQuizBattleMcqModal() { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + if (!ov) return; + ov.addEventListener('click', (e) => { + const t = e.target; + if (!t) return; + if (t.classList && t.classList.contains('quiz-battle-choice')) { + const idx = parseInt(t.getAttribute('data-idx'), 10); + if (Number.isFinite(idx) && idx >= 0 && idx <= 2) trySubmitQuizBattleChoice(idx); + return; + } + if (t.id === 'quiz-battle-mcq-close') { + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + return; + } + if (t.classList && t.classList.contains('quiz-battle-mcq-backdrop')) { + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + } + }); + })(); + (function wireQuizCarryGrabButton() { + const btn = document.getElementById('quiz-carry-grab-btn'); + if (!btn) return; + btn.addEventListener('click', (ev) => { + if (!isQuizCarry() || !mapData || isChatFocused()) return; + if (!btn.classList.contains('quiz-carry-grab-btn--active')) return; + ev.preventDefault(); + if (myId == null) return; + tryQuizCarryInteractionForPlayer(myId, me.x, me.y, { fromKey: true }); + }); + })(); + + (function wireGauntletCrownHowtoPrimary() { + const btn = document.getElementById('btn-gch-ready'); + if (!btn) return; + btn.addEventListener('click', () => { + if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'howto') return; + const humans = quizCarryPregameHumanIds(); + if (!(humans.length === 1 || isMePlayHost())) return; + beginJumpSurviveMissionCountdownThenRun(); + return; + } + if (!isGauntletCrownHeistMapPlay()) return; + if (gauntletCrownPregamePhase !== 'howto') return; + if (myId == null || !isMePlayHost()) return; + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReady) { + if (humans.length === 1 && isMePlayHost()) { + beginGauntletCrownCountdownThenRun(); + return; + } + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { + if (!r || !r.ok) { /* ignore */ } + }); + return; + } + const sid = String(myId); + const next = !gauntletCrownLobbyReadyMap[sid]; + gauntletCrownLobbyReadyMap[sid] = next; + updateGauntletCrownHowtoHud(); + socket.emit('gauntlet-crown-lobby-ready', { ready: next }); + }); + })(); + + (function wireQuizCarryPregameOverlay() { + const primary = document.getElementById('quiz-carry-pregame-primary'); + if (!primary) return; + primary.addEventListener('click', () => { + if (!quizCarryPregameActive || myId == null) return; + if (!isMePlayHost()) return; + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]); + if (humansReady) { + /* ผู้เล่นจริงคนเดียว + พรีวิว: ไม่ต้องรอ ACK เซิร์ฟ (ห้องที่ map บนเซิร์ฟยังไม่ใช่ quiz_carry จะเคยล้มเหลว) */ + if (humans.length === 1 && isMePlayHost()) { + endQuizCarryEmbedPregameAndStart(); + return; + } + socket.emit('quiz-carry-lobby-start', {}, (r) => { + if (!r || !r.ok) showQuizCarryToast((r && r.error) || 'START ไม่ได้', false); + }); + return; + } + const sid = String(myId); + const next = !quizCarryLobbyReadyMap[sid]; + quizCarryLobbyReadyMap[sid] = next; + updateQuizCarryPregameHud(); + socket.emit('quiz-carry-lobby-ready', { ready: next }); + }); + })(); + window.addEventListener('beforeunload', () => socket.emit('leave-space')); + window.addEventListener('resize', () => { + if (!mapData || !canvas) return; + resizeCanvas(); + draw(); + }); +})(); diff --git a/www/html/Game/public/play.js.bak-20260607-114128 b/www/html/Game/public/play.js.bak-20260607-114128 new file mode 100644 index 0000000..25fe357 --- /dev/null +++ b/www/html/Game/public/play.js.bak-20260607-114128 @@ -0,0 +1,11777 @@ +(function () { + const BASE = '/Game'; + /** แผงคำถามบนแมป quiz_carry — ไฟล์อยู่ public/img/quiz-carry */ + const QUIZ_CARRY_MAP_FEEDBACK_IMG = { + correct: BASE + '/img/quiz-carry/icon-Correct.png', + incorrect: BASE + '/img/quiz-carry/icon-Incorrect.png', + scorePlus: BASE + '/img/quiz-carry/score+10.png', + }; + const QUIZ_MAP_CARRY_FEEDBACK_MS = 1400; + const params = new URLSearchParams(window.location.search); + const spaceId = params.get('space'); + const nick = params.get('nick') || 'ผู้เล่น'; + const previewMode = params.get('preview') === '1'; + const forceDefaultCharacter = params.get('defaultChar') === '1'; + const editorEmbedReturn = params.get('editorEmbed') === '1'; + const playMapIdFromQuery = (params.get('map') || '').trim(); + /** mapId จาก join / game-start — ใช้จับฉาก crown เมื่อ URL ไม่มี ?map= */ + let playSessionMapId = playMapIdFromQuery; + /** สรุปภารกิจแบบ mock (popup-result ฯลฯ) — ใช้เฉพาะฉากนี้จาก editor (id ใน URL เช่น editor.html?id=mnorwqx1) */ + const quizCarryMissionSummaryMapId = 'mnorwqx1'; + const quizCarryUseMissionSummaryOverlay = playMapIdFromQuery === quizCarryMissionSummaryMapId; + /** Gauntlet พรมแดง — ฉากนี้จาก editor (?map=mno9kb07): วาดตัวหันขวา + ใช้รูป lane/laser จาก game-timing */ + const GAUNTLET_FACE_RIGHT_MAP_ID = 'mno9kb07'; + /** Jump Survival ฉาก Jumper — UI สรุปภารกิจแบบ crown + รูปใน /img/Jumper/ (editor ?map=mnptfts2) */ + const JUMP_SURVIVE_MISSION_MAP_ID = 'mnptfts2'; + /** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */ + const LEGACY_PLACEHOLDER_CHARACTER_ID = ''; + let firstCharacterDefaultResolved = null; + const firstCharacterDefaultPromise = fetch(BASE + '/api/characters') + .then((r) => r.json()) + .then((list) => { + if (Array.isArray(list) && list.length > 0 && list[0] && list[0].id) { + firstCharacterDefaultResolved = String(list[0].id); + } else { + firstCharacterDefaultResolved = ''; + } + }) + .catch(() => { + firstCharacterDefaultResolved = ''; + }); + const lobbyLevelParam = params.get('lobbyLevel'); + const lobbyCaseParam = params.get('case'); + if (lobbyLevelParam || lobbyCaseParam) { + try { + window.__detectiveLobbyMeta = { level: lobbyLevelParam, caseId: lobbyCaseParam }; + } catch (e) { /* ignore */ } + } + + const POST_CASE_LOBBY_MAP_ID = 'mn8nx46h'; + + function isDetectiveMinigamePlay() { + if (params.get('detectiveReturn') === '1') return true; + try { + return sessionStorage.getItem('detectiveMinigameReturn') === '1'; + } catch (e) { + return false; + } + } + + function buildRoomLobbyReturnHref() { + let h = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + h += '&map=' + encodeURIComponent(POST_CASE_LOBBY_MAP_ID); + try { + const dr = localStorage.getItem('lastCreatedSpaceName'); + if (dr) h += '&displayRoom=' + encodeURIComponent(dr); + } catch (e) { /* ignore */ } + try { + const meta = window.__detectiveLobbyMeta; + if (meta && meta.level != null && String(meta.level).trim()) { + h += '&lobbyLevel=' + encodeURIComponent(String(meta.level).trim()); + } + if (meta && meta.caseId != null && String(meta.caseId).trim()) { + h += '&caseId=' + encodeURIComponent(String(meta.caseId).trim()); + } + } catch (e2) { /* ignore */ } + try { + if (sessionStorage.getItem('detectiveMinigameReturn') === '1') { + h += '&detectiveReturn=1'; + sessionStorage.removeItem('detectiveMinigameReturn'); + } + } catch (e3) { /* ignore */ } + if (params.get('detectiveReturn') === '1' && h.indexOf('detectiveReturn') < 0) { + h += '&detectiveReturn=1'; + } + return h; + } + + function finishDetectiveMinigameAndReturnLobby() { + quizCarrySessionEnded = true; + try { sessionStorage.setItem('detectiveMinigameReturn', '1'); } catch (e) { /* ignore */ } + const go = function () { + window.location.href = buildRoomLobbyReturnHref(); + }; + if (socket && socket.connected) { + socket.emit('detective-minigame-finished', {}, function (res) { + if (res && res.ok) go(); + else go(); + }); + } else { + go(); + } + } + /** ทดสอบจากเอดิเตอร์: เติมบอทให้ครบจำนวน (รวมผู้เล่นจริง) — ?fillTotal=6 (ค่าเริ่ม 6) */ + const previewFillBots = previewMode && editorEmbedReturn; + const previewTargetHeadcount = Math.min(24, Math.max(1, parseInt(params.get('fillTotal'), 10) || 6)); + let detectiveCaseFillBots = false; + let detectiveBotSlotCount = 0; + function playBotsEnabled() { + return previewFillBots || detectiveCaseFillBots; + } + function playBotTargetHeadcount() { + if (previewFillBots) return previewTargetHeadcount; + if (detectiveCaseFillBots) return countPlayHumans() + detectiveBotSlotCount; + return countPlayHumans(); + } + const PREVIEW_BOT_PREFIX = '__pv_bot_'; + let previewBotSeq = 0; + /** Stack preview: บอทไม่วาบบนแผนที่ — ใช้ไฮไลต์แถบแข่งแทน */ + let lastStackPreviewActorId = null; + let lastStackPreviewActorUntil = 0; + /** Stack preview: สลับตา Player 1 = มนุษย์, 2–6 = บอท (เฉพาะตาปัจจุบันเล่นได้) */ + let stackTurnCount = 6; + let stackPreviewTurnOrder = null; + let stackPreviewTurnIndex = 0; + let stackPreviewBotThinkUntil = 0; + + function rebuildStackPreviewTurnOrder() { + if (!playBotsEnabled() || !mapData || mapData.gameType !== 'stack') { + stackPreviewTurnOrder = null; + stackTurnCount = 1; + return; + } + const bots = [...others.keys()].filter(isPreviewBotId).sort(); + stackTurnCount = Math.max(1, Math.min(12, playBotTargetHeadcount())); + stackPreviewTurnOrder = [{ kind: 'human', seat: 1 }]; + for (let i = 0; i < stackTurnCount - 1; i++) { + stackPreviewTurnOrder.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null }); + } + stackPreviewTurnIndex = 0; + stackPreviewBotThinkUntil = 0; + } + + function advanceStackPreviewTurn() { + if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount) return; + stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % stackTurnCount; + stackPreviewBotThinkUntil = 0; + } + + function isStackPreviewHumanTurn() { + if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount) return true; + const cur = stackPreviewTurnOrder[stackPreviewTurnIndex]; + return !!(cur && cur.kind === 'human'); + } + + function getStackPreviewCurrentSeat() { + if (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount) return 1; + const cur = stackPreviewTurnOrder[stackPreviewTurnIndex]; + return cur && cur.seat ? cur.seat : stackPreviewTurnIndex + 1; + } + if (!spaceId) { window.location.replace(BASE + '/lobby.html'); return; } + + const socket = io({ path: BASE + '/socket.io' }); + const canvas = document.getElementById('game-canvas'); + const ctx = canvas.getContext('2d'); + let mapData = null, tileSize = 32, myId = null; + let stackMini = null; + let stackFall = null; + let lastStackTickMs = performance.now(); + let mapBackgroundImg = null; + /** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */ + let mapGridImageImgs = []; + let mapGridImageHeldImgs = []; + let me = { x: 1, y: 1, direction: 'down', nickname: nick, isWalking: false, playTint: null, gauntletScore: 0, gauntletEliminated: false, tx: null, ty: null, quizCarryHeld: null }; + const others = new Map(); + /** Stack preview HUD: เทอร์มินัลล็อก (cyber UI) */ + const STACK_PREVIEW_HUD_LOG_MAX = 8; + let stackPreviewHudLog = []; + function stackPreviewPushHudLog(line) { + if (!previewFillBots) return; + stackPreviewHudLog.push(String(line || '').slice(0, 96)); + while (stackPreviewHudLog.length > STACK_PREVIEW_HUD_LOG_MAX) stackPreviewHudLog.shift(); + } + function stackPreviewLogStackDrop(hit, actor) { + if (!previewFillBots || !hit) return; + const bid = actor && actor.botId; + let nm = 'NODE'; + if (actor && actor.human) nm = (me.nickname || 'YOU').toUpperCase().replace(/\s+/g, '_').slice(0, 12); + else if (bid && others.get(bid)) nm = String(others.get(bid).nickname || 'BOT').toUpperCase().replace(/\s+/g, '_').slice(0, 12); + if (hit.miss) stackPreviewPushHudLog(`>>> ${nm}: SECTOR_MISS`); + else if (hit.perfect) stackPreviewPushHudLog(`>>> ${nm}: PERFECT +${hit.pts} SYN`); + else stackPreviewPushHudLog(`>>> ${nm}: LOCK +${hit.pts}`); + } + const keys = {}; + const characterImages = {}; + function createDefaultAvatarImg() { + const c = document.createElement('canvas'); + c.width = 64; c.height = 64; + const ctx = c.getContext('2d'); + ctx.fillStyle = '#7aa2f7'; + ctx.beginPath(); + ctx.arc(32, 22, 14, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#9ece6a'; + ctx.beginPath(); + ctx.arc(32, 48, 18, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#1a1b26'; + ctx.beginPath(); + ctx.arc(28, 20, 3, 0, Math.PI * 2); + ctx.arc(36, 20, 3, 0, Math.PI * 2); + ctx.fill(); + const img = new Image(); + img.src = c.toDataURL('image/png'); + return img; + } + const defaultAvatarImg = createDefaultAvatarImg(); + const characterAnimations = {}; + const characterIdleAnimations = {}; + const CHARACTER_ANIM_FRAMES = 4; + const CHARACTER_ANIM_FRAME_MS = 200; + + /** จังหวะเดินลูปคงที่ 4 เฟรม — อย่า modulo ด้วยจำนวนเฟรมที่โหลดได้แล้ว (ไม่งั้นเฟรมกลางหาย/ลูปสั้นลง) */ + function walkAnimPhaseIndex(now, isWalking) { + const t = isWalking ? (typeof now === 'number' ? now : Date.now()) : 0; + return Math.floor(t / CHARACTER_ANIM_FRAME_MS) % CHARACTER_ANIM_FRAMES; + } + + /** เลือกเฟรมสูงสุดที่โหลดแล้วและ <= phase (เดิมถอย) */ + function pickLoadedWalkFrameIndex(anim, phase) { + if (!anim || !anim.frames || !anim.frames.length) return -1; + const maxK = Math.min(phase, anim.frames.length - 1, CHARACTER_ANIM_FRAMES - 1); + for (let k = maxK; k >= 0; k--) { + const f = anim.frames[k]; + if (f && f.complete && f.naturalWidth) return k; + } + return -1; + } + + /** ตัวละครอัปโหลดจากระบบมักเป็น char- + ตัวเลข (อาจมี suffix) — ไม่มี idle strip / idle layer ครบทุกทิศ */ + function isUploadedCharAssetId(id) { + if (!id) return false; + const s = String(id).trim(); + return /^char-\d/i.test(s); + } + + function useMultiFrameIdleSpriteSheets(id) { + if (!id) return true; + return !isUploadedCharAssetId(id); + } + + function ensureCharacterIdleAnim(id, dir) { + const d = dir || 'down'; + /* char-* ไม่มี *_idle_0.png — ใช้ strip เดิน dir_0..3 เป็น idle ต่อทิศ (มีลูป) แทนรูปเดียวที่มักไม่มี */ + if (!useMultiFrameIdleSpriteSheets(id)) { + const key = String(id) + '_' + d + '_idle'; + let anim = characterIdleAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterIdleAnimations[key] = anim; + const enc = encodeURIComponent(id); + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + enc + '_' + d + '_' + i + '.png'; + anim.frames.push(img); + } + const fb = new Image(); + fb.src = BASE + '/img/characters/' + enc + '_' + d + '_idle.png'; + anim.fallback = fb; + } + return anim; + } + const key = id + '_' + d + '_idle'; + let anim = characterIdleAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterIdleAnimations[key] = anim; + const enc = encodeURIComponent(id); + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + enc + '_' + d + '_idle_' + i + '.png'; + anim.frames.push(img); + } + const fb = new Image(); + fb.src = BASE + '/img/characters/' + enc + '_' + d + '_idle.png'; + anim.fallback = fb; + } + return anim; + } + + /** ลูป idle ขณะยืน — ใช้จังหวะเวลาเหมือนเดิน (CHARACTER_ANIM_FRAME_MS) */ + function idleAnimPhaseIndex(now) { + const t = typeof now === 'number' ? now : Date.now(); + return Math.floor(t / CHARACTER_ANIM_FRAME_MS) % CHARACTER_ANIM_FRAMES; + } + + function characterIdleSpritesVisible(id, dir) { + if (!id) return false; + const anim = ensureCharacterIdleAnim(id, dir || 'down'); + if (anim.fallback && anim.fallback.complete && anim.fallback.naturalWidth) return true; + for (let i = 0; i < anim.frames.length; i++) { + const f = anim.frames[i]; + if (f && f.complete && f.naturalWidth) return true; + } + return false; + } + + /** สุ่มสีตามเลเยอร์เดียวกับ character.html: bodyColor / hairColor / headColor (ลำดับซ้อนเหมือน composeLayeredFrame) */ + const PLAY_LAYER_ORDER = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face']; + const PLAY_LAYER_TINT_KEY = { bodyColor: 'body', headColor: 'head', hairColor: 'hair' }; + const PLAY_TINT_HEAD = ['#eaa78a', '#fbd5c4', '#fae9e1']; + const PLAY_TINT_HAIR = ['#d72520', '#ef8508', '#efe237', '#5bb443', '#2585cb', '#3f4ead', '#b53fd6', '#ef62b9']; + const PLAY_TINT_BODY = ['#fb4941', '#feaa11', '#fefe6d', '#adfd85', '#45fbfd', '#799afe', '#f87dff', '#fec1fe']; + const playLayerMode = {}; + /** คิว probe ครบ up/down/left/right แล้ว — กันทิศที่ไม่มีไฟล์ layer ไปตก fallback แถบทั้งทั้งที่ทิศอื่นมี */ + const playLayerAllDirsQueued = {}; + /** จาก GET /api/characters — hasLayerFiles สแกนจากชื่อไฟล์จริง (แม่นกว่า probe จาก อย่างเดียว) */ + let playCharLayerApi = { status: 'idle', map: null }; + /** id → Set เลเยอร์ที่มีไฟล์จริง (จาก GET /api/characters) — กันยิง URL เลเยอร์ที่ไม่เคยอัปโหลด → 404 รกคอนโซล */ + const playCharDiskLayersById = Object.create(null); + + function ensurePlayCharLayerListFetch() { + if (playCharLayerApi.status !== 'idle') return; + playCharLayerApi.status = 'loading'; + fetch(BASE + '/api/characters') + .then((r) => r.json()) + .then((list) => { + const map = Object.create(null); + if (Array.isArray(list)) { + list.forEach((c) => { + if (!c || !c.id) return; + map[c.id] = !!c.hasLayerFiles; + if (Array.isArray(c.layers) && c.layers.length) { + const st = new Set(); + c.layers.forEach((n) => { + if (typeof n === 'string' && n.length) st.add(n); + }); + if (st.size) playCharDiskLayersById[c.id] = st; + else delete playCharDiskLayersById[c.id]; + } else { + delete playCharDiskLayersById[c.id]; + } + }); + } + playCharLayerApi = { status: 'done', map }; + }) + .catch(() => { + playCharLayerApi = { status: 'done', map: Object.create(null) }; + }); + } + + /** @returns {boolean|null} true/false จาก API, null = ยังไม่โหลดหรือไม่มีรายการ id นี้ */ + function playCharLayerFromApi(characterId) { + if (playCharLayerApi.status !== 'done' || !characterId) return null; + if (Object.prototype.hasOwnProperty.call(playCharLayerApi.map, characterId)) { + return playCharLayerApi.map[characterId]; + } + return null; + } + + /** ถ้าเซิร์ฟเวอร์ส่งรายชื่อเลเยอร์ที่มีบนดิสก์ — ไม่โหลดเลเยอร์อื่น (ลด 404) */ + function playCharDiskLayersSet(characterId) { + if (!characterId || playCharLayerApi.status !== 'done') return null; + const s = playCharDiskLayersById[characterId]; + return s && s.size ? s : null; + } + + const playLayerImageCache = new Map(); + const playLayerCompositeCache = new Map(); + function pickRandomPlayTint() { + return { + head: PLAY_TINT_HEAD[Math.floor(Math.random() * PLAY_TINT_HEAD.length)], + hair: PLAY_TINT_HAIR[Math.floor(Math.random() * PLAY_TINT_HAIR.length)], + body: PLAY_TINT_BODY[Math.floor(Math.random() * PLAY_TINT_BODY.length)], + }; + } + + /** FNV-1a 32-bit — แยกดัชนี head/hair/body ได้กระจายกว่า hash*31 เดิม (กันบอท __pv_bot_N สีซ้ำ) */ + function hashStringFnv1a32(str) { + let h = 2166136261 >>> 0; + const s = String(str || ''); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619) >>> 0; + } + return h >>> 0; + } + + function playTintFromPeerId(id) { + const s = String(id || 'x'); + const h0 = hashStringFnv1a32(s); + const h1 = hashStringFnv1a32(s + '\n1hair'); + const h2 = hashStringFnv1a32(s + '\n2body'); + return { + head: PLAY_TINT_HEAD[h0 % PLAY_TINT_HEAD.length], + hair: PLAY_TINT_HAIR[h1 % PLAY_TINT_HAIR.length], + body: PLAY_TINT_BODY[h2 % PLAY_TINT_BODY.length], + }; + } + + function hexToRgb01(hex) { + const h = (hex || '').replace('#', '').trim(); + if (h.length !== 6) return [1, 1, 1]; + return [ + parseInt(h.slice(0, 2), 16) / 255, + parseInt(h.slice(2, 4), 16) / 255, + parseInt(h.slice(4, 6), 16) / 255, + ]; + } + + function tintPlayLayerImageData(imageData, tintHex) { + const rgb = hexToRgb01(tintHex); + const tr = rgb[0], tg = rgb[1], tb = rgb[2]; + const d = imageData.data; + for (let i = 0; i < d.length; i += 4) { + if (d[i + 3] < 12) continue; + const r = d[i], g = d[i + 1], b = d[i + 2]; + const L = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + d[i] = Math.min(255, Math.round(tr * 255 * L)); + d[i + 1] = Math.min(255, Math.round(tg * 255 * L)); + d[i + 2] = Math.min(255, Math.round(tb * 255 * L)); + } + } + + function drawPlayTintedLayer(ctx, w, h, img, tintHex) { + if (!tintHex) { + ctx.drawImage(img, 0, 0, w, h); + return; + } + const c = document.createElement('canvas'); + c.width = w; + c.height = h; + const x = c.getContext('2d'); + x.drawImage(img, 0, 0, w, h); + try { + const idata = x.getImageData(0, 0, w, h); + tintPlayLayerImageData(idata, tintHex); + x.putImageData(idata, 0, 0); + } catch (e) { + x.clearRect(0, 0, w, h); + x.drawImage(img, 0, 0, w, h); + } + ctx.drawImage(c, 0, 0, w, h); + } + + function ensurePlayLayerImage(url) { + let img = playLayerImageCache.get(url); + if (!img) { + img = new Image(); + img.src = url; + playLayerImageCache.set(url, img); + } + return img; + } + + /** ลำดับลอง URL: เฟรมปัจจุบันแบบ multi → เฟรม 0 → แบบ single (ไม่มี _0_) เพื่อกันชื่อไฟล์ไม่ตรงกับที่เซิร์ฟเวอร์เขียน */ + function layerUrlCandidates(id, dir, layerName, frameIndex) { + const enc = encodeURIComponent(id); + const base = BASE + '/img/characters/' + enc + '_' + dir; + const out = []; + function add(u) { + if (out.indexOf(u) === -1) out.push(u); + } + add(base + '_' + frameIndex + '_layer_' + layerName + '.png'); + if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png'); + add(base + '_layer_' + layerName + '.png'); + return out; + } + + /** เลเยอร์ idle: id__idle__layer_.png หรือ id__idle_layer_.png */ + function layerUrlCandidatesIdle(id, dir, layerName, frameIndex) { + const enc = encodeURIComponent(id); + const base = BASE + '/img/characters/' + enc + '_' + dir + '_idle'; + const out = []; + function add(u) { + if (out.indexOf(u) === -1) out.push(u); + } + add(base + '_' + frameIndex + '_layer_' + layerName + '.png'); + if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png'); + add(base + '_layer_' + layerName + '.png'); + return out; + } + + function shadowUrlCandidates(id, dir, frameIndex) { + const c = layerUrlCandidates(id, dir, 'shadow', frameIndex); + const def = BASE + '/img/default-shadow-' + dir + '.png'; + if (c.indexOf(def) === -1) c.push(def); + return c; + } + + function shadowUrlCandidatesIdle(id, dir, frameIndex) { + const c = layerUrlCandidatesIdle(id, dir, 'shadow', frameIndex); + const def = BASE + '/img/default-shadow-' + dir + '.png'; + if (c.indexOf(def) === -1) c.push(def); + return c; + } + + /** ลองทีละ URL — อย่า set src พร้อมกันหลายรูป (เดิมทำให้ legacy `id_dir_layer_x` โดน 404 ทุกเลเยอร์ทุกทิศทั้งที่มีแค่ `id_dir_0_layer_x`) */ + function resolvePlayLayerImage(urls) { + for (let i = 0; i < urls.length; i++) { + const img = ensurePlayLayerImage(urls[i]); + if (!img.complete) return { status: 'pending' }; + if (img.naturalWidth > 0) return { status: 'ok', img }; + } + return { status: 'missing' }; + } + + function ensurePlayLayerProbesAllDirections(characterId) { + if (!characterId || playLayerAllDirsQueued[characterId]) return; + playLayerAllDirsQueued[characterId] = true; + ['up', 'down', 'left', 'right'].forEach((d) => ensurePlayLayerProbe(characterId, d)); + } + + function playCharAnyDirectionLayered(characterId) { + if (!characterId) return false; + return ['up', 'down', 'left', 'right'].some((d) => playLayerMode[characterId + '|' + d] === 'layered'); + } + + /** รอเฉพาะตอนยังไม่เจอ layered เลย — ถ้ามีทิศหนึ่ง layered แล้ว ไม่ต้องรอทิศอื่น */ + function playCharLayerDiscoveryPending(characterId) { + if (!characterId) return true; + const api = playCharLayerFromApi(characterId); + if (api === true || api === false) return false; + if (playCharAnyDirectionLayered(characterId)) return false; + return ['up', 'down', 'left', 'right'].some((d) => { + const m = playLayerMode[characterId + '|' + d]; + return m === undefined || m === 'pending'; + }); + } + + function ensurePlayLayerProbe(characterId, dir) { + const key = characterId + '|' + dir; + if (playLayerMode[key] !== undefined && playLayerMode[key] !== 'pending') return; + if (playLayerMode[key] === 'pending') return; + playLayerMode[key] = 'pending'; + const cands = layerUrlCandidates(characterId, dir, 'bodyColor', 0); + let ci = 0; + function tryNextProbe() { + if (playLayerMode[key] !== 'pending') return; + if (ci >= cands.length) { + playLayerMode[key] = 'none'; + return; + } + const url = cands[ci++]; + const img = ensurePlayLayerImage(url); + const fin = () => { + if (playLayerMode[key] !== 'pending') return; + if (img.naturalWidth > 0) { + playLayerMode[key] = 'layered'; + return; + } + tryNextProbe(); + }; + img.onload = fin; + img.onerror = fin; + if (img.complete) fin(); + } + tryNextProbe(); + } + + function getCharacterAnimFrameIndex(id, dir, now, isWalking) { + if (!id) return 0; + const key = id + '_' + dir; + const anim = characterAnimations[key]; + if (!anim) return 0; + const phase = walkAnimPhaseIndex(now, isWalking); + const fi = pickLoadedWalkFrameIndex(anim, phase); + return fi >= 0 ? fi : 0; + } + + function tryComposePlayLayersFromFiles(rawImg, id, dir, frameIndex, tint, opts) { + const useIdle = opts && opts.idle; + if (!rawImg || !rawImg.naturalWidth || !rawImg.naturalHeight) return null; + const w = rawImg.naturalWidth, h = rawImg.naturalHeight; + const c = document.createElement('canvas'); + c.width = w; + c.height = h; + const cctx = c.getContext('2d'); + let anyTintColorLayer = false; + let skippedShadowWhilePending = false; + const diskLayers = playCharDiskLayersSet(id); + /* ยืน (opts.idle): ลอง compose จาก *_idle_*_layer_* เหมือนหน้า character upload — รวม char-* ที่อัปโหลด idle เลเยอร์; ถ้าไม่มีไฟล์จะ missing → ไม่มีเลเยอร์ย้อม → getPlayTintedAvatarSource fallback ไป walk compose */ + const idleLayerStripes = !!useIdle; + const resDir = dir; + const resFi = frameIndex; + for (let li = 0; li < PLAY_LAYER_ORDER.length; li++) { + const layerName = PLAY_LAYER_ORDER[li]; + if (diskLayers && layerName !== 'shadow' && !diskLayers.has(layerName)) continue; + /* char-* หันหลัง (up): ไม่มี face ทิศนี้ — ข้ามกันตาหน้าซ้อนหลังหัว; ซ้าย/ขวา/หน้าใช้ face ตามทิศได้ */ + if (layerName === 'face' && isUploadedCharAssetId(id) && dir === 'up') { + continue; + } + let urls; + if (layerName === 'shadow') { + urls = idleLayerStripes + ? shadowUrlCandidatesIdle(id, dir, frameIndex) + : shadowUrlCandidates(id, resDir, resFi); + } else { + urls = idleLayerStripes + ? layerUrlCandidatesIdle(id, dir, layerName, frameIndex) + : layerUrlCandidates(id, resDir, layerName, resFi); + } + const r = resolvePlayLayerImage(urls); + /* เงาโหลดช้า/404 บ่อย — อย่าให้บล็อกทั้งคอมโพส (ไม่งั้นตกไป fallback แถบแนวตั้งแทนเลเยอร์จริง) */ + if (r.status === 'pending' && layerName === 'shadow') { + skippedShadowWhilePending = true; + continue; + } + if (r.status === 'pending') return null; + if (r.status === 'missing') continue; + const tintHex = PLAY_LAYER_TINT_KEY[layerName] ? tint[PLAY_LAYER_TINT_KEY[layerName]] : null; + if (PLAY_LAYER_TINT_KEY[layerName]) anyTintColorLayer = true; + drawPlayTintedLayer(cctx, w, h, r.img, tintHex); + } + /* ถ้าโหลดได้แต่ไม่มีเลเยอร์สีเลย จะเหลือแต่ stroke → ตัวขาว — ใช้ PNG รวมแทน */ + if (!anyTintColorLayer) return null; + return { canvas: c, skipCache: skippedShadowWhilePending }; + } + + function getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, isWalking, tint) { + if (!tint || !characterId || !rawImg) return rawImg; + ensurePlayCharLayerListFetch(); + ensurePlayLayerProbesAllDirections(characterId); + const dirn = dir || 'down'; + const frameIndexWalk = getCharacterAnimFrameIndex(characterId, dirn, timeMs, isWalking); + const idlePhase = idleAnimPhaseIndex(typeof timeMs === 'number' ? timeMs : Date.now()); + + if (playCharLayerDiscoveryPending(characterId)) return rawImg; + + const apiFlag = playCharLayerFromApi(characterId); + /* เหมือนเดิม: มีเลเยอร์แยกไฟล์เมื่อ API บอก hasLayerFiles หรือ probe เจอ — gen สีทำบน canvas จาก *_layer_* เท่านั้น */ + const charHasLayerFiles = apiFlag === true + || (apiFlag == null && playCharAnyDirectionLayered(characterId)); + + /* มี PNG idle รวม — ใช้เมื่อไม่มีระบบเลเยอร์สี (ถ้ามีเลเยอร์ต้อง compose ไม่ return raw ขาว) */ + if (!charHasLayerFiles && !isWalking && characterIdleSpritesVisible(characterId, dirn)) { + return rawImg; + } + const cacheKeyWalk = [characterId, dirn, frameIndexWalk, tint.head, tint.hair, tint.body, 'ct3'].join('|'); + + if (charHasLayerFiles) { + if (!isWalking) { + const composeFrame = isUploadedCharAssetId(characterId) ? 0 : idlePhase; + const cacheKeyIdle = [characterId, dirn, composeFrame, tint.head, tint.hair, tint.body, 'ct3', 'idleL'].join('|'); + const hitI = playLayerCompositeCache.get(cacheKeyIdle); + if (hitI) return hitI; + const packI = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, composeFrame, tint, { idle: true }); + if (packI && packI.canvas) { + if (!packI.skipCache) playLayerCompositeCache.set(cacheKeyIdle, packI.canvas); + return packI.canvas; + } + } + const hit = playLayerCompositeCache.get(cacheKeyWalk); + if (hit) return hit; + const pack = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, frameIndexWalk, tint, { idle: false }); + if (pack && pack.canvas) { + if (!pack.skipCache) playLayerCompositeCache.set(cacheKeyWalk, pack.canvas); + return pack.canvas; + } + return rawImg; + } + + /* + * ไม่มีไฟล์เลเยอร์ตาม API/probe — แสดง PNG รวม (ไม่ย้อมทั้งตัว: ไม่เหมือนระบบ gen สีแบบเลเยอร์เดิม) + */ + return rawImg; + } + + function getCharacterImg(id, direction) { + if (!id) return null; + const key = id + '_' + (direction || 'down'); + if (characterImages[key]) return characterImages[key]; + const img = new Image(); + img.src = BASE + '/img/characters/' + encodeURIComponent(id) + '_' + (direction || 'down') + '.png'; + characterImages[key] = img; + return img; + } + + function getCharacterFrame(id, direction, now, isWalking) { + if (!id) return null; + const dir = direction || 'down'; + const t = typeof now === 'number' ? now : Date.now(); + if (!isWalking) { + const idleAnim = ensureCharacterIdleAnim(id, dir); + let idlePhase = idleAnimPhaseIndex(t); + if (isUploadedCharAssetId(id)) idlePhase = 0; + const idleFi = pickLoadedWalkFrameIndex(idleAnim, idlePhase); + if (idleFi >= 0) return idleAnim.frames[idleFi]; + const idfb = idleAnim.fallback; + if (idfb && idfb.complete && idfb.naturalWidth) return idfb; + } + const key = id + '_' + dir; + let anim = characterAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterAnimations[key] = anim; + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + encodeURIComponent(id) + '_' + dir + '_' + i + '.png'; + anim.frames.push(img); + } + anim.fallback = getCharacterImg(id, dir); + } + const phase = walkAnimPhaseIndex(now, isWalking); + const fi = pickLoadedWalkFrameIndex(anim, phase); + if (fi >= 0) return anim.frames[fi]; + const fb = anim.fallback; + if (fb && fb.complete && fb.naturalWidth) return fb; + return null; + } + + function getAvatarImg(characterId, direction, now, isWalking) { + const img = characterId ? getCharacterFrame(characterId, direction, now, isWalking) : null; + if (img) return img; + return defaultAvatarImg; + } + function getStoredCharacterId() { + try { + let v = localStorage.getItem('gameCharacterId'); + if (v === 'Chatest') v = ''; // legacy placeholder — ไม่มี sprite จริง + if (v) return v; + } catch (e) { /* ignore */ } + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; + } + function getPlayCharacterId() { + if (forceDefaultCharacter) { + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; + } + return getStoredCharacterId(); + } + const MOVE_SPEED = 0.15; + const PATH_ARRIVE_THRESH = 0.15; + + function isMovementKey(code) { + return ['KeyW','KeyA','KeyS','KeyD','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].indexOf(code) !== -1; + } + function isChatFocused() { + return false; + } + let zoom = 1.4; + let froggerScore = 0; + let lastFroggerKey = 0; + let gauntletObstacles = []; + /** อินเทอร์โพเลตการวาด obstacle ระหว่างแพ็กเก็ต sync (~220ms) */ + let gauntletObsRenderPrev = []; + let gauntletObsRenderNext = []; + let gauntletObsBlendT0 = 0; + let meGauntletJumpTicks = 0; + /** ค่าที่ใช้วาดการยกตัว (เลอร์ปจาก meGauntletJumpTicks ให้โค้งกระโดดไม่กระตุก) */ + let meGauntletJumpVis = 0; + /** ซิงก์จากเซิร์ฟเวอร์ (gauntlet-sync / GET /api/game-timing) */ + let gauntletRuntimeTickMs = 220; + let gauntletRuntimeJumpTicks = 16; + /** Stack: รอบสวิงต่อวินาที — จาก GET /api/game-timing · ค่าน้อย = สวิงช้า */ + let playStackSwingHz = 0.55; + /** Stack: ความกว้างบล็อก (tile) — null = คำนวณจากโซนลงบนแผนที่ */ + let playStackBlockWidthTiles = null; + /** กระโดดให้รอด: ความสูงกระโดด = ทวีคูณ × ความสูงตัว (ch×tile) — จาก GET /api/game-timing */ + let playJumpSurviveJumpHeightMult = 1.5; + /** จำกัดเวลารอบภารกิจกระโดดขึ้นแท่น (วินาที) — จาก GET /api/game-timing; 0 = ใช้ค่าเริ่ม 60 ในเกม */ + let playJumpSurviveMissionTimeSec = 0; + /** ยิงยานอวกาศ (space_shooter): เวลารอบจาก game-timing; 0 = ใช้ค่าเริ่ม 90 ในเกมถ้าแมปไม่ทับ */ + let playSpaceShooterMissionTimeSec = 0; + /** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync (พรมแดง Gauntlet เท่านั้น) */ + let gauntletRuntimeTimeLimitSec = 0; + /** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */ + let gauntletEndsAtMs = null; + /** Crown (mno9kb07): เซิร์ฟยังไม่ปล่อยรัน — หยุด tick / เวลา */ + let gauntletCrownRunHeldRemote = false; + /** null | 'howto' | 'countdown' | 'live' — พรีรันก่อน GO */ + let gauntletCrownPregamePhase = null; + let gauntletCrownLobbyReadyMap = {}; + let gauntletCrownCountdownTimer = null; + let lastGauntletJumpKey = 0; + /** รูป lane: หลาย URL สุ่มตาม id คงที่ · laser: แยกบน/ล่าง/เส้น + สี/ความหนา */ + let gauntletLaneImageUrls = []; + let gauntletLaserTopUrl = ''; + let gauntletLaserBottomUrl = ''; + let gauntletLaserLineUrl = ''; + let gauntletLaserFillColor = 'rgba(140,230,255,0.42)'; + let gauntletLaserStrokeColor = 'rgba(255,220,255,0.9)'; + let gauntletLaserLineWidthPx = 2; + const gauntletAssetImageCache = new Map(); + + /** แปลง URL จาก Admin/game-timing ให้โหลดได้ (nginx เสิร์ฟจาก /Game/...) */ + function normalizeGauntletAssetUrlForPlay(u) { + if (typeof u !== 'string') return ''; + const t = u.trim(); + if (!t) return ''; + if (/^https?:\/\//i.test(t)) return t; + const qIdx = t.indexOf('?'); + const base = (qIdx >= 0 ? t.slice(0, qIdx) : t).trim(); + const qs = qIdx >= 0 ? t.slice(qIdx) : ''; + if (!base) return ''; + if (base.startsWith('/')) return base + qs; + if (/^Game\//i.test(base)) return '/' + base.replace(/^\/+/, '') + qs; + return base + qs; + } + + function ensureGauntletAssetImage(url) { + const u = normalizeGauntletAssetUrlForPlay(typeof url === 'string' ? url : ''); + if (!u) return null; + let rec = gauntletAssetImageCache.get(u); + if (rec) return rec; + rec = { img: new Image(), ready: false }; + if (/^https?:\/\//i.test(u)) { + try { rec.img.crossOrigin = 'anonymous'; } catch (e) { /* ignore */ } + } + rec.img.onload = function () { rec.ready = true; }; + rec.img.onerror = function () { rec.ready = false; }; + rec.img.src = u; + gauntletAssetImageCache.set(u, rec); + return rec; + } + + function pickGauntletLaneImageRec(obsId) { + if (!gauntletLaneImageUrls.length) return null; + const n = gauntletLaneImageUrls.length; + const idx = Math.abs(Number(obsId) | 0) % n; + const u = gauntletLaneImageUrls[idx]; + return u ? ensureGauntletAssetImage(u) : null; + } + + function clampClientLaserColor(s, def) { + const t = String(s || '').trim().slice(0, 100); + if (!t || /[<>"'`]/.test(t)) return def; + return t; + } + + let playQuizPhaseLocal = null; + let playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + let playQuizText = ''; + let playQuizPhaseEndsAt = 0; + let playQuizTimerInterval = null; + /** Preview-only: real question pool + phased timer (matches server quiz-settings / map quizQuestions). */ + let previewQuizPool = []; + let previewQuizTiming = { readMs: 10000, answerMs: 5000, betweenMs: 3500 }; + let previewQuizStep = 'read'; + let previewQuizCurrent = null; + let playLiveQuizScores = {}; + /** คะแนนต่อคำตอบที่ถูกเมื่อส่งป้ายที่ฮับ/โซนส่ง — quiz_carry เท่านั้น */ + const QUIZ_CARRY_POINTS_PER_CORRECT = 10; + /** quiz_carry: หยิบตัวเลือกมาวางโซนกลาง — เล่นพร้อมกัน (ไม่สลับตา) */ + let quizCarryPool = []; + let quizCarryCurrent = null; + /** editor embed + preview: นับ 3–2–1 ก่อนโชว์คำถามและให้เดิน/หยิบได้ */ + let quizCarryEmbedPendingQuestion = null; + let quizCarryEmbedCountdownStartAt = 0; + let quizCarryEmbedCountdownEndAt = 0; + /** quiz_carry: epoch ให้หยิบตัวเลือกได้ · epoch ปิดรอบตอบ (ตั้งที่ Admin แท็บหยิบมาวาง) */ + let quizCarryOptionRevealAt = 0; + let quizCarryAnswerCloseAt = 0; + let quizCarryCarryTimingMs = { carryReadMs: 3000, carryAnswerMs: 5000 }; + /** พรีวิว (รวม editor embed): จำนวนข้อที่เล่นก่อนโอเวอร์เลย์จบ — 0 = ไม่จบอัตโนมัติ */ + let quizCarrySessionLength = 0; + let quizCarryRoundsCompleted = 0; + let quizCarrySessionEnded = false; + /** หลัง timeup บนโต๊ะ (embed): รอ 5s แล้วโชว์ result-complete / result-gameover */ + let quizCarryResultEndTimer = null; + /** หลังโชว์ result-end (embed preview): รอ 5s แล้วกลับ room-lobby — ใช้ร่วม quiz_carry + crown */ + let embedPreviewLobbyReturnTimer = null; + /** embed พรีวิว: รอ Ready / START ของโฮสต์ก่อนนับ 3–2–1 */ + let quizCarryPregameActive = false; + let playHostId = null; + /** ซิงก์จากเซิร์ฟเวอร์: socketId → กด Ready แล้ว */ + let quizCarryLobbyReadyMap = {}; + /** จาก /api/quiz-settings carryMapPanelTheme — ใช้กับ #quiz-map-question-panel เฉพาะ quiz_carry */ + let quizCarryMapPanelTheme = null; + /** จาก /api/quiz-settings carryEmbedCountdownTheme — สี/ขนาด overlay นับ 3-2-1 (embed) */ + let quizCarryEmbedCountdownTheme = null; + /** จาก /api/quiz-settings carryChoicePlaqueThemes — ป้าย canvas ต่อช่องตัวเลือก 0..15 (null = ใช้ค่าเริ่มต้นทุกช่อง) */ + let quizCarryChoicePlaqueThemes = null; + /** จาก carryChoicePlaqueMapScale — ขยายป้ายบนแมป + ฟอนต์ (0.85–2.5) */ + let quizCarryPlaqueMapScale = 1.25; + const quizCarryChoiceImageCache = new Map(); + /** สแนปจาก join-space (Node) — ใช้เมื่อ fetch HTTP quiz-settings ไม่ได้หรือไม่มีธีม */ + let quizCarryJoinSettingsSnap = null; + + /** quiz_carry — โซนตัวเลือกบนแมป / ธีมป้ายต่อช่องสูงสุด 16 */ + const QUIZ_CARRY_MAX_OPTION_SLOTS = 16; + + /** jump_survive — กล้องตามตัวในกรอบ + แพลตฟอร์มเลื่อนขึ้น (scroll) ไม่ลากทั้งฉาก */ + let jumpSurviveCamCenterX = 0; + let jumpSurviveCamCenterY = 0; + let jumpSurviveEliminated = false; + /** จับเวลาโหมดกระโดดให้รอด (วินาทีบน HUD) */ + let jumpSurviveSessionStartMs = 0; + let jumpSurvivePlatformScrollPx = 0; + let jumpSurviveVy = 0; + let jumpSurviveLastTickMs = 0; + let jumpSurviveJumpQueued = false; + let jumpSurviveOnGround = false; + let jumpSurviveLastEmitT = 0; + /** mnptfts2: howto → countdown → live → ended (คะแนน 100 ผู้รอดสุดท้าย) */ + let jumpSurviveMissionPhase = null; + let jumpSurviveGameEnded = false; + let jumperMissionCountdownTimer = null; + + /** space_shooter — ยิงหิน (จุดเกิดจากกริด P1–P6) */ + let spaceShooterBullets = []; + let spaceShooterAsteroids = []; + let spaceShooterPopups = []; + let spaceShooterLastTickMs = 0; + let spaceShooterSpawnAccMs = 0; + let spaceShooterFireCd = 0; + let spaceShooterSessionStartMs = 0; + let spaceShooterLastMoveEmit = 0; + let spaceShooterGameEnded = false; + + /** balloon_boss — ลูกโป้งยิงบอส (Mega Virus) */ + let balloonBossPendingShots = []; + let balloonBossPlayerBullets = []; + let balloonBossBossBullets = []; + let balloonBossSessionStartMs = 0; + let balloonBossLastTickMs = 0; + let balloonBossLastMoveEmit = 0; + let balloonBossPlayerFireCd = 0; + let balloonBossGameEnded = false; + let balloonBossHitFx = []; + let balloonBossBossFireAcc = 0; + /** quiz_battle — โดม MCQ กด E (ข้อจาก battleQuizMcq) */ + let quizBattleMcqPool = []; + let quizBattleAnsweredComps = new Set(); + let quizBattleModalCompId = null; + let qbAutoLastComp = null; // โดมล่าสุดที่ trigger อัตโนมัติ (กันเด้งซ้ำขณะยืนค้าง) + const SPACE_SHOOTER_SHIP_COLORS = ['#50fa7b', '#ff79c6', '#ff5555', '#f1fa8c', '#bd93f9', '#8be9fd']; + + document.getElementById('room-id').textContent = spaceId; + + function hidePlayQuizHud() { + const sb = document.getElementById('play-quiz-scoreboard'); + if (sb) sb.classList.add('is-hidden'); + const fb = document.getElementById('play-quiz-feedback'); + if (fb) { fb.classList.add('is-hidden'); fb.textContent = ''; } + } + + function renderPlayQuizScoreboard(scores) { + const wrap = document.getElementById('play-quiz-scoreboard'); + const ul = document.getElementById('play-quiz-scoreboard-list'); + if (!wrap || !ul || !mapData || (!isQuiz() && !isQuizCarry() && !isQuizBattle())) return; + if (!scores || typeof scores !== 'object') return; + wrap.classList.remove('is-hidden'); + ul.textContent = ''; + const merged = { ...scores }; + others.forEach((_, id) => { if (merged[id] == null) merged[id] = 0; }); + if (myId != null && merged[myId] == null) merged[myId] = 0; + const rows = []; + if (myId != null) { + rows.push({ id: myId, nick: me.nickname || 'คุณ', sc: merged[myId] != null ? merged[myId] : 0 }); + } + others.forEach((o, id) => { + rows.push({ id, nick: (o && o.nickname) ? String(o.nickname) : id, sc: merged[id] != null ? merged[id] : 0 }); + }); + rows.sort((a, b) => b.sc - a.sc || a.nick.localeCompare(b.nick, 'th')); + rows.forEach((row) => { + const li = document.createElement('li'); + if (row.id === myId) li.className = 'play-quiz-scoreboard-me'; + const spN = document.createElement('span'); + spN.className = 'play-quiz-scoreboard-name'; + spN.textContent = row.nick; + const spV = document.createElement('span'); + spV.className = 'play-quiz-scoreboard-val'; + spV.textContent = String(row.sc); + li.appendChild(spN); + li.appendChild(spV); + ul.appendChild(li); + }); + if (isQuizCarry() && useCyberPlayHud()) { + wrap.classList.add('is-hidden'); + } + } + + function initPlayLiveQuizScoresZeros() { + if ((!isQuiz() && !isQuizCarry() && !isQuizBattle()) || myId == null) return; + playLiveQuizScores = {}; + playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + renderPlayQuizScoreboard(playLiveQuizScores); + } + + function showPlayQuizFeedback(r) { + const el = document.getElementById('play-quiz-feedback'); + if (!el || !r || !r.results || myId == null) return; + let mine = null; + let botRight = 0, botWrong = 0; + for (let i = 0; i < r.results.length; i++) { + const row = r.results[i]; + if (row.id === myId) mine = row; + else if (isPreviewBotId(row.id)) { + if (row.right) botRight++; + else botWrong++; + } + } + if (!mine) return; + el.classList.remove('is-hidden'); + const botExtra = previewFillBots && (botRight + botWrong > 0) + ? ' · บอท ถูก ' + botRight + ' / ผิด ' + botWrong + ' (Bots: ' + botRight + ' right / ' + botWrong + ' wrong)' + : ''; + if (mine.right) { + el.className = 'play-quiz-feedback play-quiz-feedback-ok'; + el.textContent = 'คุณตอบถูก · คะแนนรวม ' + (typeof mine.score === 'number' ? mine.score : 0) + ' แต้ม' + botExtra; + } else { + el.className = 'play-quiz-feedback play-quiz-feedback-bad'; + el.textContent = (mine.choice == null + ? 'คุณไม่ได้ยืนในโซนตอบ — นับเป็นผิด' + : 'คุณตอบผิด — กลับจุดเกิด และเข้าโซนตอบไม่ได้อีก') + botExtra; + } + if (typeof window.__playQuizFeedbackT === 'number') clearTimeout(window.__playQuizFeedbackT); + window.__playQuizFeedbackT = setTimeout(() => { el.classList.add('is-hidden'); }, 4200); + } + + function getTileBoundsForOnesGrid(grid) { + if (!grid || !grid.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (let yy = 0; yy < grid.length; yy++) { + const row = grid[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (Number(row[xx]) === 1) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + } + } + } + if (minX === Infinity) return null; + return { minX, minY, maxX, maxY }; + } + + function getQuizQuestionAreaTileBounds(md) { + if (!md || md.gameType !== 'quiz') return null; + return getTileBoundsForOnesGrid(md.quizQuestionArea); + } + + /** quiz_carry: โซนทองพิเศษสำหรับข้อความคำถาม (ถ้าไม่วาด ใช้โซนกลางม่วงแทนใน syncPlayQuizMapPanel) */ + function getQuizCarryQuestionAreaTileBounds(md) { + if (!md || md.gameType !== 'quiz_carry') return null; + return getTileBoundsForOnesGrid(md.quizQuestionArea); + } + + function clearQuizMapPanelThemeInline(panel, textEl) { + if (!panel || !textEl) return; + panel.style.removeProperty('--qmap-bg'); + panel.style.removeProperty('--qmap-border'); + panel.style.removeProperty('--qmap-border-w'); + panel.style.removeProperty('--qmap-shadow'); + panel.style.removeProperty('background'); + panel.style.removeProperty('border'); + panel.style.removeProperty('border-color'); + panel.style.removeProperty('border-width'); + panel.style.removeProperty('border-style'); + panel.style.removeProperty('box-shadow'); + textEl.style.removeProperty('--qmap-text'); + textEl.style.removeProperty('--qmap-text-shadow'); + textEl.style.removeProperty('color'); + textEl.style.removeProperty('text-shadow'); + textEl.style.removeProperty('font-size'); + textEl.style.removeProperty('line-height'); + textEl.style.removeProperty('width'); + textEl.style.removeProperty('box-sizing'); + textEl.style.removeProperty('max-height'); + textEl.style.removeProperty('overflow'); + } + + function clearQuizMapQuestionCarryFeedback() { + if (typeof window.__quizMapCarryFeedbackT === 'number') { + clearTimeout(window.__quizMapCarryFeedbackT); + window.__quizMapCarryFeedbackT = null; + } + const icon = document.getElementById('quiz-map-q-feedback-icon'); + const scoreWrap = document.getElementById('quiz-map-q-feedback-score-wrap'); + const scoreEl = document.getElementById('quiz-map-q-feedback-score'); + if (icon) { + icon.classList.add('is-hidden'); + icon.removeAttribute('src'); + } + if (scoreEl) scoreEl.classList.remove('quiz-map-q-feedback-score--pop'); + if (scoreWrap) { + scoreWrap.classList.add('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'true'); + } + } + + /** ไอคอนถูก/ผิดที่ขอบบนแผง + score+10 ตรงกลางใต้ข้อความ (เฉพาะถูก) — ตาม mock quiz_carry */ + function showQuizMapQuestionCarryFeedback(isCorrect) { + if (!isQuizCarry()) return; + const panel = document.getElementById('quiz-map-question-panel'); + const icon = document.getElementById('quiz-map-q-feedback-icon'); + const scoreWrap = document.getElementById('quiz-map-q-feedback-score-wrap'); + const scoreEl = document.getElementById('quiz-map-q-feedback-score'); + if (!panel || !icon || panel.classList.contains('is-hidden')) return; + clearQuizMapQuestionCarryFeedback(); + icon.src = isCorrect ? QUIZ_CARRY_MAP_FEEDBACK_IMG.correct : QUIZ_CARRY_MAP_FEEDBACK_IMG.incorrect; + icon.classList.remove('is-hidden'); + if (isCorrect && scoreWrap && scoreEl) { + scoreEl.src = QUIZ_CARRY_MAP_FEEDBACK_IMG.scorePlus; + scoreWrap.classList.remove('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'false'); + scoreEl.classList.remove('quiz-map-q-feedback-score--pop'); + void scoreEl.offsetWidth; + scoreEl.classList.add('quiz-map-q-feedback-score--pop'); + } else if (scoreWrap) { + scoreWrap.classList.add('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'true'); + } + window.__quizMapCarryFeedbackT = setTimeout(() => { + window.__quizMapCarryFeedbackT = null; + clearQuizMapQuestionCarryFeedback(); + }, QUIZ_MAP_CARRY_FEEDBACK_MS); + } + + /** ค่าเริ่มต้นแผงคำถามบนแผนที่ (ตรงกับ server defaultCarryMapPanelTheme) — ใช้เมื่อยังไม่โหลดจาก API */ + function defaultCarryMapPanelThemePlay() { + return { + panelBg: 'rgba(12, 14, 28, 0.88)', + panelBorder: 'rgba(255, 214, 102, 0.7)', + borderWidthPx: 2, + textColor: '#f1f5f9', + questionFontMinPx: 10, + questionFontMaxPx: 24, + }; + } + + /** ตรงกับ drawQuizCarryChoiceLabels — carryChoicePlaqueMapScale */ + function quizCarryPlaqueMapScaleClampedPlay() { + const sc = Number(quizCarryPlaqueMapScale); + if (!Number.isFinite(sc)) return 1.25; + return Math.max(0.85, Math.min(2.5, sc)); + } + + /** + * ขนาดตัวอักษรแผงคำถาม DOM ให้เท่าป้ายคำตอบบนพื้น: Math.max(10, Math.min(24, tileSize*zoom*0.24*ps)) + * แล้ว clamp ด้วย questionFontMinPx / questionFontMaxPx จากธีม (ถ้าต้องการใหญ่กว่าป้ายได้โดยยกเพดาน max) + */ + function quizMapQuestionFontPxLikeCarryPlaques(th, zoomVal) { + const ts = tileSize * zoomVal; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + const base = Math.max(10, Math.min(24, ts * 0.24 * ps)); + const fb = defaultCarryMapPanelThemePlay(); + let mn = Number(th && th.questionFontMinPx); + let mx = Number(th && th.questionFontMaxPx); + if (!Number.isFinite(mn)) mn = fb.questionFontMinPx; + if (!Number.isFinite(mx)) mx = fb.questionFontMaxPx; + mn = Math.round(Math.max(8, Math.min(40, mn))); + mx = Math.round(Math.max(10, Math.min(72, mx))); + if (mx < mn) { + const s = mn; + mn = mx; + mx = s; + } + return Math.max(mn, Math.min(mx, base)); + } + + /** + * ลด px ทีละ 1 จนเนื้อหาไม่ล้นกล่อง (ไม่ใช้ scrollbar) + * zoom out กล่องเตี้ยมาก: อนุญาตต่ำกว่า questionFontMinPx ของธีมได้ถึง hardMin (≥6) ไม่ให้บรรทัดถูกตัด + * แผงใช้ justify-content:flex-start ใน CSS — ไม่จัดกึ่งกลางแนวตั้งแล้วโดน overflow ตัดบน-ล่าง + */ + function fitQuizMapQuestionFontToPanel(panel, textEl, startPx, minPx) { + const cs = window.getComputedStyle(panel); + const padT = parseFloat(cs.paddingTop) || 0; + const padB = parseFloat(cs.paddingBottom) || 0; + const padL = parseFloat(cs.paddingLeft) || 0; + const padR = parseFloat(cs.paddingRight) || 0; + const availH = Math.max(12, panel.clientHeight - padT - padB); + const availW = Math.max(24, panel.clientWidth - padL - padR); + const themeMin = Math.max(8, Math.floor(Number(minPx) || 8)); + const hardMin = Math.min(themeMin, Math.max(6, Math.floor(availH / 7))); + let px = Math.round(Math.min(80, Math.max(hardMin, Number(startPx) || hardMin))); + textEl.style.setProperty('line-height', '1.28', 'important'); + textEl.style.setProperty('width', '100%', 'important'); + textEl.style.setProperty('box-sizing', 'border-box', 'important'); + textEl.style.removeProperty('max-height'); + textEl.style.setProperty('overflow', 'hidden', 'important'); + for (let i = 0; i < 96 && px > hardMin; i++) { + textEl.style.setProperty('font-size', String(px) + 'px', 'important'); + const sh = textEl.scrollHeight; + const sw = textEl.scrollWidth; + if (sh <= availH + 2 && sw <= availW + 2) break; + px -= 1; + } + textEl.style.setProperty('font-size', String(px) + 'px', 'important'); + textEl.style.setProperty('max-height', availH + 'px', 'important'); + } + + /** ธีมจาก quiz-settings / join API — ถ้า JSON ไม่มีฟอนต์ให้ใช้ค่าเริ่มต้น */ + function parseCarryMapPanelThemeObject(raw) { + let t = raw; + if (typeof t === 'string') { + try { + t = JSON.parse(t); + } catch (e) { + t = null; + } + } + if (!t || typeof t !== 'object') return null; + const bw = Number(t.borderWidthPx); + const borderWidthPx = Number.isFinite(bw) ? Math.max(0, Math.min(12, Math.round(bw))) : 2; + const fb = defaultCarryMapPanelThemePlay(); + let qMin = Number(t.questionFontMinPx); + let qMax = Number(t.questionFontMaxPx); + if (!Number.isFinite(qMin)) qMin = fb.questionFontMinPx; + if (!Number.isFinite(qMax)) qMax = fb.questionFontMaxPx; + qMin = Math.round(Math.max(10, Math.min(40, qMin))); + qMax = Math.round(Math.max(14, Math.min(56, qMax))); + if (qMax < qMin) { + const s = qMin; + qMin = qMax; + qMax = s; + } + return { + panelBg: String(t.panelBg || '').trim().slice(0, 120), + panelBorder: String(t.panelBorder || '').trim().slice(0, 120), + borderWidthPx, + textColor: String(t.textColor || '').trim().slice(0, 120), + questionFontMinPx: qMin, + questionFontMaxPx: qMax, + }; + } + + /** + * ชั้นทับจาก carryMapPanelTheme ในไฟล์แมป — ฟอนต์ใส่เฉพาะเมื่อ JSON มี key จริง + * (กันธีมแมปมีแค่สีแล้วไปทับขนาดฟอนต์จาก Admin เป็นค่าเริ่ม 16/24) + */ + function parseCarryMapPanelThemeMapOverlay(raw) { + let t = raw; + if (typeof t === 'string') { + try { + t = JSON.parse(t); + } catch (e) { + t = null; + } + } + if (!t || typeof t !== 'object') return null; + const bw = Number(t.borderWidthPx); + const borderWidthPx = Number.isFinite(bw) ? Math.max(0, Math.min(12, Math.round(bw))) : 2; + const out = { + panelBg: String(t.panelBg || '').trim().slice(0, 120), + panelBorder: String(t.panelBorder || '').trim().slice(0, 120), + borderWidthPx, + textColor: String(t.textColor || '').trim().slice(0, 120), + }; + const hasMin = Object.prototype.hasOwnProperty.call(t, 'questionFontMinPx'); + const hasMax = Object.prototype.hasOwnProperty.call(t, 'questionFontMaxPx'); + if (hasMin || hasMax) { + const fb = defaultCarryMapPanelThemePlay(); + let qMin = hasMin ? Number(t.questionFontMinPx) : fb.questionFontMinPx; + let qMax = hasMax ? Number(t.questionFontMaxPx) : fb.questionFontMaxPx; + if (!Number.isFinite(qMin)) qMin = fb.questionFontMinPx; + if (!Number.isFinite(qMax)) qMax = fb.questionFontMaxPx; + qMin = Math.round(Math.max(10, Math.min(40, qMin))); + qMax = Math.round(Math.max(14, Math.min(56, qMax))); + if (qMax < qMin) { + const s = qMin; + qMin = qMax; + qMax = s; + } + out.questionFontMinPx = qMin; + out.questionFontMaxPx = qMax; + } + return out; + } + + function setQuizCarryMapPanelThemeFromApi(s) { + quizCarryMapPanelTheme = null; + if (!s || typeof s !== 'object') return; + const parsed = parseCarryMapPanelThemeObject(s.carryMapPanelTheme); + if (!parsed) return; + quizCarryMapPanelTheme = parsed; + } + + function defaultCarryChoicePlaqueThemePlay() { + return { + borderMode: 'neon', + fixedBorder: 'rgba(122, 200, 255, 0.9)', + fillBg: 'rgba(12, 10, 20, 0.88)', + textColor: 'rgba(248, 249, 255, 1)', + borderWidthPx: 2.5, + plaqueImageUrl: '', + }; + } + + function parseCarryChoicePlaqueThemeObject(raw) { + const d = defaultCarryChoicePlaqueThemePlay(); + if (!raw || typeof raw !== 'object') return d; + const safeColor = (x, fb) => { + const t = String(x == null ? '' : x).trim().slice(0, 120); + if (!t || /url\s*\(|expression|@import|\/\*|javascript|<|>|\\0/i.test(t)) return fb; + if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(t)) return t; + if (/^rgba?\(\s*[^)]+\)$/i.test(t)) return t.replace(/\s+/g, ' ').trim(); + return fb; + }; + const mode = String(raw.borderMode || '').toLowerCase() === 'fixed' ? 'fixed' : 'neon'; + let bw = Number(raw.borderWidthPx); + if (!Number.isFinite(bw)) bw = d.borderWidthPx; + bw = Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10; + return { + borderMode: mode, + fixedBorder: safeColor(raw.fixedBorder, d.fixedBorder), + fillBg: safeColor(raw.fillBg, d.fillBg), + textColor: safeColor(raw.textColor, d.textColor), + borderWidthPx: bw, + plaqueImageUrl: sanitizeQuizCarryImageUrlClient(raw.plaqueImageUrl), + }; + } + + function buildCarryChoicePlaqueThemesArrayFromApi(s) { + if (!s || typeof s !== 'object') return null; + if (Array.isArray(s.carryChoicePlaqueThemes) && s.carryChoicePlaqueThemes.length) { + const out = []; + for (let i = 0; i < QUIZ_CARRY_MAX_OPTION_SLOTS; i++) { + out.push(parseCarryChoicePlaqueThemeObject(s.carryChoicePlaqueThemes[i])); + } + return out; + } + if (s.carryChoicePlaqueTheme && typeof s.carryChoicePlaqueTheme === 'object') { + const one = parseCarryChoicePlaqueThemeObject(s.carryChoicePlaqueTheme); + return Array.from({ length: QUIZ_CARRY_MAX_OPTION_SLOTS }, () => ({ ...one })); + } + return null; + } + + function getEffectiveCarryChoicePlaqueThemeForChoice(choiceIndex) { + const i = Math.max(0, Math.min(QUIZ_CARRY_MAX_OPTION_SLOTS - 1, choiceIndex | 0)); + if (quizCarryChoicePlaqueThemes && quizCarryChoicePlaqueThemes[i]) { + return quizCarryChoicePlaqueThemes[i]; + } + return defaultCarryChoicePlaqueThemePlay(); + } + + function setQuizCarryChoicePlaqueThemeFromApi(s) { + quizCarryChoicePlaqueThemes = buildCarryChoicePlaqueThemesArrayFromApi(s); + } + + function sanitizeQuizCarryImageUrlClient(u) { + const s = String(u == null ? '' : u).trim().slice(0, 512); + if (!s || /[\s<>'"`]/.test(s)) return ''; + if (s.startsWith('/')) { + if (!/^\/[\w\-./?#=&%+]+$/i.test(s)) return ''; + return s; + } + const low = s.toLowerCase(); + if (!low.startsWith('https://') && !low.startsWith('http://')) return ''; + try { + const parsed = new URL(s); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return ''; + return s; + } catch (e) { + return ''; + } + } + + /** crossOrigin=anonymous กับ same-origin บางเซิร์ฟ/nginx ทำให้โหลดรูปล้ม — ใช้เฉพาะข้ามโดเมน */ + function quizCarryApplyImageCrossOrigin(img, rawUrl) { + if (!img) return; + try { + img.removeAttribute('crossorigin'); + const u = sanitizeQuizCarryImageUrlClient(rawUrl); + if (!u || u.startsWith('data:')) return; + let imgOrigin = ''; + if (u.startsWith('http://') || u.startsWith('https://')) { + imgOrigin = new URL(u).origin; + } else if (u.startsWith('/')) { + imgOrigin = window.location.origin; + } + if (imgOrigin && imgOrigin !== window.location.origin) { + img.crossOrigin = 'anonymous'; + } + } catch (e) { /* ignore */ } + } + + function preloadQuizCarryChoiceImages(q) { + if (!q) return; + if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) { + for (let i = 0; i < q.choiceImageUrls.length; i++) { + const raw = sanitizeQuizCarryImageUrlClient(q.choiceImageUrls[i]); + if (!raw) continue; + if (quizCarryChoiceImageCache.has(raw)) continue; + const im = new Image(); + quizCarryApplyImageCrossOrigin(im, raw); + im.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(raw, im); + try { + im.src = raw; + } catch (e) { /* ignore */ } + } + } + if (Array.isArray(q.choices)) { + for (let j = 0; j < q.choices.length; j++) { + const raw2 = getQuizCarryPlaqueImageUrlForIndex(q, j); + if (!raw2 || quizCarryChoiceImageCache.has(raw2)) continue; + const im2 = new Image(); + quizCarryApplyImageCrossOrigin(im2, raw2); + im2.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(raw2, im2); + try { + im2.src = raw2; + } catch (e) { /* ignore */ } + } + } + } + + function getQuizCarryChoiceImageCached(url) { + const u = sanitizeQuizCarryImageUrlClient(url); + if (!u) return null; + const im = quizCarryChoiceImageCache.get(u); + if (im && im.complete && im.naturalWidth > 0) return im; + return null; + } + + function getQuizCarryChoiceImageUrlForIndex(q, idx) { + if (!q || !Array.isArray(q.choiceImageUrls)) return ''; + const u = q.choiceImageUrls[idx]; + return sanitizeQuizCarryImageUrlClient(u); + } + + /** รูปป้าย: คำถามต่อช่องก่อน แล้วค่อยรูปจากธีมช่อง (carryChoicePlaqueThemes) */ + function getQuizCarryPlaqueImageUrlForIndex(q, idx) { + const perQ = getQuizCarryChoiceImageUrlForIndex(q, idx); + if (perQ) return perQ; + const th = getEffectiveCarryChoicePlaqueThemeForChoice(idx); + const u = th && th.plaqueImageUrl ? sanitizeQuizCarryImageUrlClient(th.plaqueImageUrl) : ''; + return u || ''; + } + + function defaultCarryEmbedCountdownThemePlay() { + return { + overlayBackdrop: 'rgba(8, 10, 20, 0.42)', + innerBg: 'rgba(12, 14, 28, 0.82)', + innerBorder: 'rgba(122, 162, 247, 0.45)', + innerBorderWpx: 1, + innerRadiusPx: 12, + digitColor: '#ffe066', + mapDigitCqmin: 78, + mapDigitCqh: 82, + mapDigitMaxPx: 200, + screenDigitVw: 28, + screenDigitMaxPx: 132, + }; + } + + function parseCarryEmbedCountdownThemeObject(raw) { + const d = defaultCarryEmbedCountdownThemePlay(); + if (!raw || typeof raw !== 'object') return d; + const clampN = (v, lo, hi, def) => { + const n = Number(v); + if (!Number.isFinite(n)) return def; + return Math.max(lo, Math.min(hi, Math.round(n))); + }; + const clipStr = (x, maxLen) => { + const t = String(x == null ? '' : x).trim().slice(0, maxLen); + return t || null; + }; + const safeColor = (x, fb) => { + const t = clipStr(x, 120); + if (!t || /url\s*\(|expression|@import|\/\*|javascript|<|>|\\0/i.test(t)) return fb; + if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(t)) return t; + if (/^rgba?\(\s*[^)]+\)$/i.test(t)) return t.replace(/\s+/g, ' ').trim(); + return fb; + }; + return { + overlayBackdrop: safeColor(raw.overlayBackdrop, d.overlayBackdrop), + innerBg: safeColor(raw.innerBg, d.innerBg), + innerBorder: safeColor(raw.innerBorder, d.innerBorder), + innerBorderWpx: clampN(raw.innerBorderWpx, 0, 12, d.innerBorderWpx), + innerRadiusPx: clampN(raw.innerRadiusPx, 0, 32, d.innerRadiusPx), + digitColor: safeColor(raw.digitColor, d.digitColor), + mapDigitCqmin: clampN(raw.mapDigitCqmin, 35, 100, d.mapDigitCqmin), + mapDigitCqh: clampN(raw.mapDigitCqh, 35, 100, d.mapDigitCqh), + mapDigitMaxPx: clampN(raw.mapDigitMaxPx, 48, 400, d.mapDigitMaxPx), + screenDigitVw: clampN(raw.screenDigitVw, 6, 44, d.screenDigitVw), + screenDigitMaxPx: clampN(raw.screenDigitMaxPx, 48, 220, d.screenDigitMaxPx), + }; + } + + function setQuizCarryEmbedCountdownThemeFromApi(s) { + quizCarryEmbedCountdownTheme = parseCarryEmbedCountdownThemeObject(s && s.carryEmbedCountdownTheme); + applyQuizCarryEmbedCountdownThemeToDom(); + } + + const CARRY_ECD_VAR_KEYS = [ + '--carry-ecd-overlay', + '--carry-ecd-inner-bg', + '--carry-ecd-inner-border', + '--carry-ecd-inner-border-w', + '--carry-ecd-inner-radius', + '--carry-ecd-digit-color', + '--carry-ecd-map-digit-fs', + '--carry-ecd-screen-vw', + '--carry-ecd-screen-max', + ]; + + function clearQuizCarryEmbedCountdownThemeDomVars() { + const root = document.getElementById('quiz-carry-embed-countdown'); + if (!root) return; + CARRY_ECD_VAR_KEYS.forEach((k) => { try { root.style.removeProperty(k); } catch (e) { /* ignore */ } }); + } + + /** ใส่ CSS variables บน #quiz-carry-embed-countdown — อ่านจาก quizCarryEmbedCountdownTheme */ + function applyQuizCarryEmbedCountdownThemeToDom() { + const root = document.getElementById('quiz-carry-embed-countdown'); + const numEl = document.getElementById('quiz-carry-embed-countdown-num'); + if (!root) return; + clearQuizCarryEmbedCountdownThemeDomVars(); + const th = quizCarryEmbedCountdownTheme || defaultCarryEmbedCountdownThemePlay(); + root.style.setProperty('--carry-ecd-overlay', th.overlayBackdrop); + root.style.setProperty('--carry-ecd-inner-bg', th.innerBg); + root.style.setProperty('--carry-ecd-inner-border', th.innerBorder); + root.style.setProperty('--carry-ecd-inner-border-w', `${Math.max(0, Math.min(12, Math.round(Number(th.innerBorderWpx) || 1)))}px`); + root.style.setProperty('--carry-ecd-inner-radius', `${Math.max(0, Math.min(32, Math.round(Number(th.innerRadiusPx) || 12)))}px`); + root.style.setProperty('--carry-ecd-digit-color', th.digitColor); + const cqmin = Math.max(35, Math.min(100, Math.round(Number(th.mapDigitCqmin) || 78))); + const cqh = Math.max(35, Math.min(100, Math.round(Number(th.mapDigitCqh) || 82))); + const mpx = Math.max(48, Math.min(400, Math.round(Number(th.mapDigitMaxPx) || 200))); + root.style.setProperty('--carry-ecd-map-digit-fs', `clamp(14px, min(${cqmin}cqmin, ${cqh}cqh), ${mpx}px)`); + const vw = Math.max(6, Math.min(44, Math.round(Number(th.screenDigitVw) || 28))); + const smx = Math.max(48, Math.min(220, Math.round(Number(th.screenDigitMaxPx) || 132))); + root.style.setProperty('--carry-ecd-screen-vw', `${vw}vw`); + root.style.setProperty('--carry-ecd-screen-max', `${smx}px`); + if (numEl) { + numEl.style.textShadow = '0 0 0.45em currentColor, 0 4px 14px rgba(0,0,0,0.55)'; + } + } + + /** default ← quiz-settings/API ← แมป (สีจากแมปทับได้; ฟอนต์จากแมปเฉพาะเมื่อแมประบุ key) */ + function getEffectiveCarryMapPanelThemeForApply() { + const d = defaultCarryMapPanelThemePlay(); + const api = quizCarryMapPanelTheme != null && typeof quizCarryMapPanelTheme === 'object' ? quizCarryMapPanelTheme : null; + let merged = api ? { ...d, ...api } : { ...d }; + if (mapData && mapData.carryMapPanelTheme != null) { + const mp = parseCarryMapPanelThemeMapOverlay(mapData.carryMapPanelTheme); + if (mp) merged = { ...merged, ...mp }; + } + return merged; + } + + function applyQuizCarryMapPanelThemeIfNeeded(panel, textEl) { + if (!panel || !textEl) return; + if (!isQuizCarry()) { + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + const th = getEffectiveCarryMapPanelThemeForApply(); + clearQuizMapPanelThemeInline(panel, textEl); + const bgStr = th.panelBg != null ? String(th.panelBg).trim() : ''; + const brStr = th.panelBorder != null ? String(th.panelBorder).trim() : ''; + const txStr = th.textColor != null ? String(th.textColor).trim() : ''; + const wRaw = Number(th.borderWidthPx); + const w = Number.isFinite(wRaw) ? Math.max(0, Math.min(12, Math.round(wRaw))) : 0; + const bgUse = bgStr || 'transparent'; + const brUse = brStr || 'transparent'; + const txUse = txStr || '#f1f5f9'; + panel.style.setProperty('--qmap-bg', bgUse); + panel.style.setProperty('--qmap-border', brUse); + panel.style.setProperty('--qmap-border-w', String(w) + 'px'); + panel.style.setProperty('--qmap-shadow', 'none'); + textEl.style.setProperty('--qmap-text', txUse); + textEl.style.setProperty('--qmap-text-shadow', 'none'); + /* โหลด play.html แบบแคชเก่า (ไม่มี var) ยังต้องทับได้ — ใช้ !important คู่กับตัวแปร */ + panel.style.setProperty('background', bgUse, 'important'); + panel.style.setProperty('border', String(w) + 'px solid ' + brUse, 'important'); + panel.style.setProperty('box-shadow', 'none', 'important'); + textEl.style.setProperty('color', txUse, 'important'); + textEl.style.setProperty('text-shadow', 'none', 'important'); + } + + function syncPlayQuizMapPanel() { + const panel = document.getElementById('quiz-map-question-panel'); + const textEl = document.getElementById('quiz-map-question-text'); + if (!panel || !textEl || !mapData) return; + /* เอดิเตอร์ embed: ซ่อนแผงทองเฉพาะโหมด quiz (ทับกลาง) — quiz_carry ต้องโชว์ตามโซนที่วาดบนแผนที่ */ + if (previewMode && editorEmbedReturn && !isQuizCarry()) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + if (isQuizBattle()) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + if (!isQuiz() && !isQuizCarry()) { + clearQuizMapQuestionCarryFeedback(); + return; + } + const bounds = isQuiz() + ? getQuizQuestionAreaTileBounds(mapData) + : (getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData)); + const text = (playQuizText || '').trim(); + if (!bounds || !text) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + 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; + textEl.textContent = text; + panel.style.left = Math.round(left) + 'px'; + panel.style.top = Math.round(top) + 'px'; + panel.style.width = Math.round(Math.max(48, wPx)) + 'px'; + panel.style.height = Math.round(Math.max(40, hPx)) + 'px'; + panel.classList.remove('is-hidden'); + panel.setAttribute('aria-hidden', 'false'); + applyQuizCarryMapPanelThemeIfNeeded(panel, textEl); + const thFont = getEffectiveCarryMapPanelThemeForApply(); + const startFontPx = quizMapQuestionFontPxLikeCarryPlaques(thFont, zoom); + const mnFont = Math.max(8, Math.round(Number(thFont.questionFontMinPx) || 10)); + fitQuizMapQuestionFontToPanel(panel, textEl, startFontPx, mnFont); + } + + /** โซนเดียวกับแผงคำถามบนแผนที่ — ยึด timeup ให้ตรงกล่อง #quiz-map-question-panel (ห้ามคำนวณซ้ำแล้วคลาด) */ + function syncQuizCarryTimeupDeskLayerPosition() { + const layer = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!layer || layer.classList.contains('is-hidden')) return; + const anchor = layer.querySelector('.qc-timeup-desk-anchor') + || layer.querySelector('.qc-timeup-desk-inner') + || (() => { + const img = document.getElementById('quiz-carry-timeup-txt-img'); + return img && img.parentElement && img.parentElement !== layer ? img.parentElement : null; + })(); + if (!anchor || !canvas) return; + const clearToFullStack = () => { + anchor.style.left = '0'; + anchor.style.top = '0'; + anchor.style.width = '100%'; + anchor.style.height = '100%'; + }; + const stack = document.getElementById('play-canvas-stack'); + const panel = document.getElementById('quiz-map-question-panel'); + if (stack && panel && !panel.classList.contains('is-hidden')) { + try { + const sr = stack.getBoundingClientRect(); + const pr = panel.getBoundingClientRect(); + const rw = pr.width; + const rh = pr.height; + if (rw >= 8 && rh >= 8 && sr.width > 0 && sr.height > 0) { + anchor.style.left = Math.round(pr.left - sr.left) + 'px'; + anchor.style.top = Math.round(pr.top - sr.top) + 'px'; + anchor.style.width = Math.round(rw) + 'px'; + anchor.style.height = Math.round(rh) + 'px'; + return; + } + } catch (e) { /* fallback ด้านล่าง */ } + } + const pl = panel && panel.style && String(panel.style.left || '').trim(); + const pt = panel && panel.style && String(panel.style.top || '').trim(); + const pw = panel && panel.style && String(panel.style.width || '').trim(); + const ph = panel && panel.style && String(panel.style.height || '').trim(); + if (pl && pt && pw && ph) { + anchor.style.left = pl; + anchor.style.top = pt; + anchor.style.width = pw; + anchor.style.height = ph; + return; + } + if (!mapData || mapData.gameType !== 'quiz_carry') { + clearToFullStack(); + return; + } + const bounds = getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData); + if (!bounds) { + clearToFullStack(); + return; + } + 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; + anchor.style.left = Math.round(left) + 'px'; + anchor.style.top = Math.round(top) + 'px'; + anchor.style.width = Math.round(Math.max(48, wPx)) + 'px'; + anchor.style.height = Math.round(Math.max(40, hPx)) + 'px'; + } + + function carryEmbedCountdownAnchorResolved() { + if (!mapData || !isQuizCarry()) return 'screen'; + const v = mapData.carryEmbedCountdownAnchor; + if (v === 'grid' || v === 'map') return v; + return 'screen'; + } + + function resetQuizCarryEmbedCountdownLayoutDom() { + const ov = document.getElementById('quiz-carry-embed-countdown'); + const inner = document.getElementById('quiz-carry-embed-countdown-inner'); + if (ov) ov.classList.remove('quiz-carry-embed-countdown--map-anchor'); + if (inner) { + inner.classList.remove('quiz-carry-embed-countdown-inner--map-rect'); + inner.style.position = ''; + inner.style.left = ''; + inner.style.top = ''; + inner.style.width = ''; + inner.style.height = ''; + inner.style.transform = ''; + inner.style.maxWidth = ''; + inner.style.boxSizing = ''; + inner.style.display = ''; + inner.style.margin = ''; + } + } + + /** พรีวิว embed: วางกล่อง 3–2–1 กลางจอ / ชิดโซนทองหรือโซนกลาง / ชิดกริด carryEmbedCountdownArea — ตาม carryEmbedCountdownAnchor */ + function syncQuizCarryEmbedCountdownLayout() { + if (!previewMode || !editorEmbedReturn || !mapData || !isQuizCarry()) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + const ov = document.getElementById('quiz-carry-embed-countdown'); + const inner = document.getElementById('quiz-carry-embed-countdown-inner'); + const canvasEl = document.getElementById('game-canvas'); + if (!ov || !inner || !canvasEl) return; + if (ov.classList.contains('is-hidden')) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + const anchor = carryEmbedCountdownAnchorResolved(); + if (anchor === 'screen') { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + let bounds = null; + if (anchor === 'grid') { + bounds = getTileBoundsForOnesGrid(mapData.carryEmbedCountdownArea); + if (!bounds) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + } else { + bounds = getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData); + if (!bounds) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + } + 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 r = canvasEl.getBoundingClientRect(); + const sl = r.left + l; + const st = r.top + t; + ov.classList.add('quiz-carry-embed-countdown--map-anchor'); + inner.classList.add('quiz-carry-embed-countdown-inner--map-rect'); + inner.style.position = 'fixed'; + inner.style.left = Math.round(sl) + 'px'; + inner.style.top = Math.round(st) + 'px'; + inner.style.width = Math.round(Math.max(100, wPx)) + 'px'; + inner.style.height = Math.round(Math.max(72, hPx)) + 'px'; + inner.style.transform = 'none'; + inner.style.boxSizing = 'border-box'; + inner.style.display = 'flex'; + inner.style.flexDirection = 'column'; + inner.style.alignItems = 'center'; + inner.style.justifyContent = 'center'; + inner.style.margin = '0'; + inner.style.maxWidth = 'none'; + } + + /** quiz_carry + embed: คำถามไปที่แผงทองบนแผนที่ / ใน overlay นับถอยหลัง — ไม่ใช้แถบบน */ + function syncQuizCarryEmbedQuestionStrip() { + const wrap = document.getElementById('quiz-carry-embed-q-strip'); + const p = document.getElementById('quiz-carry-embed-q-strip-text'); + if (!wrap || !p) return; + wrap.classList.remove('quiz-carry-embed-q-strip--countdown'); + wrap.classList.add('is-hidden'); + wrap.setAttribute('aria-hidden', 'true'); + } + + function updatePlayQuizTimerDisplay() { + const el = document.getElementById('quiz-game-timer'); + if (!el) return; + if (!playQuizPhaseEndsAt) { + el.textContent = ''; + return; + } + const s = Math.max(0, Math.ceil((playQuizPhaseEndsAt - Date.now()) / 1000)); + el.textContent = s + ' วินาที'; + } + + function clampPreviewMs(n, def, minV, maxV) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(minV, Math.min(maxV, Math.floor(v))); + } + + function clampCarrySessionLen(n, def) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(0, Math.min(500, Math.floor(v))); + } + + function buildQuizPoolFromMap(md) { + if (!md || !Array.isArray(md.quizQuestions)) return []; + return md.quizQuestions + .filter((q) => q && String(q.text || '').trim()) + .map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue })); + } + + function pickRandomQuizFromPool(pool) { + if (!pool || !pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function clearPreviewBotAnswerPaths() { + if (!previewFillBots) return; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botPath = []; + o.botAnswerWander = false; + }); + } + + function resetPreviewBotsQuizState() { + if (!previewFillBots) return; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.quizCannotTrue = false; + o.quizCannotFalse = false; + o.botPath = []; + o.botAnswerWander = false; + }); + } + + function applyPreviewReadPhase(q, poolLen) { + clearPreviewBotAnswerPaths(); + previewQuizStep = 'read'; + playQuizPhaseLocal = 'read'; + previewQuizCurrent = q; + playQuizText = q.text; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.readMs; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] อ่านคำถาม · สุ่มจากชุด ' + poolLen + ' ข้อ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'โซนทองบนแผนที่ = ข้อความคำถาม · ฟ้า = จริง · ชมพู = เท็จ'; + } + + function applyPreviewAnswerPhase() { + previewQuizStep = 'answer'; + playQuizPhaseLocal = 'answer'; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.answerMs; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] เดินไปโซน ถูก / ผิด'; + const leg = document.getElementById('quiz-play-legend'); + if (leg && previewQuizCurrent) { + leg.textContent = previewQuizCurrent.answerTrue + ? 'เฉลย (ทดสอบ): คำตอบที่ถูกคือ «จริง» — โซนสีฟ้า' + : 'เฉลย (ทดสอบ): คำตอบที่ถูกคือ «เท็จ» — โซนสีชมพู'; + } + previewBotsPrepareAnswerRound(); + } + + function applyPreviewBetweenPhase() { + clearPreviewBotAnswerPaths(); + previewQuizStep = 'between'; + playQuizPhaseLocal = null; + playQuizText = 'ข้อถัดไปกำลังจะมา…'; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.betweenMs; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] พักระหว่างข้อ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'รอสักครู่แล้วจะสุ่มคำถามใหม่จากชุดเดิม'; + } + + function quizCellOnPlay(grid, x, y) { + return !!(grid && grid[y] && grid[y][x] === 1); + } + + function spawnTileWalkablePlay(md, x, y) { + const w = md.width || 20; + const h = md.height || 15; + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + if (row && row[x] === 1) return false; + return true; + } + + /** เหมือน server pickRandomSpawnFromMap — สุ่มด้วย crypto.getRandomValues */ + function pickRandomSpawnFromMapPlay(md) { + const fallback = md.spawn || { x: 1, y: 1 }; + const fx = Number.isFinite(Number(fallback.x)) ? Number(fallback.x) : 1; + const fy = Number.isFinite(Number(fallback.y)) ? Number(fallback.y) : 1; + const grid = md.spawnArea; + if (!grid || !Array.isArray(grid)) return { x: fx, y: fy }; + const w = md.width || 20; + const h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && spawnTileWalkablePlay(md, x, y)) pool.push({ x, y }); + } + } + if (!pool.length) return { x: fx, y: fy }; + const u = new Uint32Array(1); + (typeof crypto !== 'undefined' && crypto.getRandomValues) + ? crypto.getRandomValues(u) + : (u[0] = (Math.floor(Math.random() * 0xffffffff) >>> 0)); + const pick = pool[u[0] % pool.length]; + return { x: pick.x, y: pick.y }; + } + + function parseLobbyPlayerSpawnsFromMapPlay(md) { + const w = md.width || 20; + const h = md.height || 15; + const out = [null, null, null, null, null, null]; + const raw = md && md.lobbyPlayerSpawns; + if (!Array.isArray(raw)) return out; + for (let i = 0; i < 6 && i < raw.length; i++) { + const cell = raw[i]; + if (!cell || typeof cell !== 'object') continue; + const x = Math.floor(Number(cell.x)); + const y = Math.floor(Number(cell.y)); + if (!Number.isFinite(x) || !Number.isFinite(y)) continue; + if (x < 0 || x >= w || y < 0 || y >= h) continue; + out[i] = { x, y }; + } + return out; + } + + /** สอดคล้องกับ server pickSpawnForJoin — ใช้พรีวิวบอท / ทดสอบ */ + function pickSpawnForJoinPlay(md, joinOrderIndex) { + if (!md) return { x: 1, y: 1 }; + const mode = md.lobbySpawnMode; + const ord = joinOrderIndex | 0; + if (mode === 'slots6' && ord >= 6) return pickRandomSpawnFromMapPlay(md); + const j = Math.min(Math.max(0, ord), 5); + if (mode === 'fixed' && md.spawn) { + const fx = Number.isFinite(Number(md.spawn.x)) ? Math.floor(Number(md.spawn.x)) : 1; + const fy = Number.isFinite(Number(md.spawn.y)) ? Math.floor(Number(md.spawn.y)) : 1; + const w = md.width || 20; + const h = md.height || 15; + const x = Math.max(0, Math.min(w - 1, fx)); + const y = Math.max(0, Math.min(h - 1, fy)); + if (spawnTileWalkablePlay(md, x, y)) return { x, y }; + return pickRandomSpawnFromMapPlay(md); + } + if (mode === 'slots6') { + const slots = parseLobbyPlayerSpawnsFromMapPlay(md); + const pick = slots[j]; + if (pick && spawnTileWalkablePlay(md, pick.x, pick.y)) return { x: pick.x, y: pick.y }; + return pickRandomSpawnFromMapPlay(md); + } + return pickRandomSpawnFromMapPlay(md); + } + + const GAUNTLET_PREVIEW_MAX = 6; + function gauntletSpawnYsPlay(md, playerCount) { + const h = md.height || 15; + const lo = 1; + const hi = Math.max(lo, h - 2); + const n = Math.min(GAUNTLET_PREVIEW_MAX, Math.max(1, playerCount)); + if (hi <= lo) return Array.from({ length: n }, () => lo); + const ys = []; + for (let i = 0; i < n; i++) { + ys.push(Math.round(lo + (i * (hi - lo)) / Math.max(1, n - 1))); + } + return ys; + } + function collectGauntletSpawnSlotsFromSpawnAreaPlay(md) { + const grid = md.spawnArea; + if (!grid || !Array.isArray(grid)) return []; + const w = md.width || 20; + const h = md.height || 15; + const slots = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && spawnTileWalkablePlay(md, x, y)) slots.push({ x, y }); + } + } + slots.sort((a, b) => a.y - b.y || a.x - b.x); + return slots; + } + function collectGauntletSpawnSlotsPlay(md) { + const explicit = md.gauntletPlayerSpawns; + if (Array.isArray(explicit) && explicit.length > 0) { + const w = md.width || 20; + const h = md.height || 15; + const slots = []; + for (const raw of explicit) { + if (slots.length >= GAUNTLET_PREVIEW_MAX) break; + const x = Math.floor(Number(raw && raw.x)); + const y = Math.floor(Number(raw && raw.y)); + if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x >= w || y < 0 || y >= h) continue; + if (!spawnTileWalkablePlay(md, x, y)) continue; + slots.push({ x, y }); + } + if (slots.length > 0) return slots; + } + return collectGauntletSpawnSlotsFromSpawnAreaPlay(md); + } + function gauntletResolveSpawnXForRowPlay(md, spawnColX, y, slotXFallback) { + const w = md.width || 20; + if (spawnTileWalkablePlay(md, spawnColX, y)) return spawnColX; + if (slotXFallback != null && spawnTileWalkablePlay(md, slotXFallback, y)) return slotXFallback; + for (let d = 0; d < w; d++) { + if (spawnColX + d < w && spawnTileWalkablePlay(md, spawnColX + d, y)) return spawnColX + d; + if (spawnColX - d >= 0 && spawnTileWalkablePlay(md, spawnColX - d, y)) return spawnColX - d; + } + return Math.max(0, Math.min(w - 1, spawnColX)); + } + function gauntletSpawnPositionsPlay(md, playerCount) { + const n = Math.min(GAUNTLET_PREVIEW_MAX, Math.max(1, playerCount)); + const slots = collectGauntletSpawnSlotsPlay(md); + const fallbackYs = gauntletSpawnYsPlay(md, n); + const spawnColX = slots.length ? Math.min(...slots.map((s) => s.x)) : 1; + const out = []; + for (let i = 0; i < n; i++) { + const y = i < slots.length + ? slots[i].y + : (fallbackYs[i] != null ? fallbackYs[i] : fallbackYs[fallbackYs.length - 1]); + const slotX = i < slots.length ? slots[i].x : null; + const x = gauntletResolveSpawnXForRowPlay(md, spawnColX, y, slotX); + out.push({ x, y }); + } + return out; + } + /** + * โหมดทดสอบพรมแดง: จัดตำแหน่งเกิดให้ตรง server (spawnArea หรือกระจาย y) + * @param {boolean} [onlyBots=false] ถ้า true = จัดแค่บอท (หลัง game-start; ไม่ทับ me/คนจริงจาก peersSnap) + */ + function applyGauntletPreviewSpawnLayout(onlyBots) { + if (!mapData || mapData.gameType !== 'gauntlet') return; + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + const humanCount = 1 + realIds.length; + const total = Math.min(GAUNTLET_PREVIEW_MAX, humanCount + botIds.length); + const pos = gauntletSpawnPositionsPlay(mapData, total); + if (onlyBots) { + let idx = humanCount; + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + return; + } + let idx = 0; + if (idx < pos.length) { + me.x = pos[idx].x; + me.y = pos[idx].y; + me.tx = me.x; + me.ty = me.y; + idx++; + } + realIds.forEach((rid) => { + const o = others.get(rid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + } + + function isPreviewBotId(id) { + return typeof id === 'string' && id.indexOf(PREVIEW_BOT_PREFIX) === 0; + } + + function countPlayHumans() { + let n = 1; + others.forEach((_, id) => { if (!isPreviewBotId(id)) n++; }); + return n; + } + + function rebalancePreviewBots() { + if (!playBotsEnabled() || !mapData) return; + if (isDetectiveMinigamePlay() && isQuizCarry()) { + /* บอทคดีสืบสวน — ใช้ stepQuizCarryPreviewBots */ + } + const human = countPlayHumans(); + const wantBots = Math.max(0, playBotTargetHeadcount() - human); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o) return; + if (o.botTier == null) { + const tierRoll = Math.random(); + o.botTier = tierRoll < 0.28 ? 'sharp' : tierRoll < 0.62 ? 'avg' : 'weak'; + } + if (o.quizCannotTrue == null) o.quizCannotTrue = false; + if (o.quizCannotFalse == null) o.quizCannotFalse = false; + if (!Array.isArray(o.botPath)) o.botPath = []; + if (o.botAnswerWander == null) o.botAnswerWander = false; + if (o.gauntletJumpTicks == null) o.gauntletJumpTicks = 0; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks; + if (o.gauntletScore == null) { + o.gauntletScore = (mapData && mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) ? 100 : 0; + } + if (o.gauntletEliminated == null) o.gauntletEliminated = false; + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)]; + o.botWanderDx = wd[0]; + o.botWanderDy = wd[1]; + } + if (typeof o.botWanderNextTurn !== 'number') { + o.botWanderNextTurn = Date.now() + 400 + Math.floor(Math.random() * 900); + } + }); + let botIds = [...others.keys()].filter(isPreviewBotId); + while (botIds.length > wantBots) { + const drop = botIds.pop(); + if (drop) others.delete(drop); + } + botIds = [...others.keys()].filter(isPreviewBotId); + while (botIds.length < wantBots) { + const id = PREVIEW_BOT_PREFIX + (++previewBotSeq); + const joinIdx = countPlayHumans() + botIds.length; + const sp = pickSpawnForJoinPlay(mapData, joinIdx); + const jx = (Math.random() - 0.5) * 0.4; + const jy = (Math.random() - 0.5) * 0.4; + let x = sp.x + 0.5 + jx; + let y = sp.y + 0.5 + jy; + if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(x, y); + x = pathSnap.x; + y = pathSnap.y; + } + const tierRoll = Math.random(); + const botTier = tierRoll < 0.28 ? 'sharp' : tierRoll < 0.62 ? 'avg' : 'weak'; + const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)]; + const crownStart = mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay(); + others.set(id, { + x, y, tx: x, ty: y, + direction: ['down', 'up', 'left', 'right'][Math.floor(Math.random() * 4)], + nickname: 'บอท', + characterId: getPlayCharacterId(), + playTint: playTintFromPeerId(id), + botTier, + gauntletJumpTicks: 0, + gauntletJumpVis: 0, + gauntletScore: crownStart ? 100 : 0, + gauntletEliminated: false, + quizCannotTrue: false, + quizCannotFalse: false, + quizCarryHeld: null, + botPath: [], + botAnswerWander: false, + botWanderDx: wd[0], + botWanderDy: wd[1], + botWanderNextTurn: Date.now() + 400 + Math.floor(Math.random() * 1000), + jumpSurviveEliminated: false, + spaceShooterScore: 0, + balloonBossScore: 0, + balloonBossBalloons: 5, + balloonBossEliminated: false, + botQuizCarryPathfindAfter: performance.now() + previewBotSeq * 75 + Math.floor(Math.random() * 160), + }); + botIds.push(id); + } + let k = 0; + [...others.keys()].filter(isPreviewBotId).sort().forEach((bid) => { + const o = others.get(bid); + if (!o) return; + const tag = o.botTier === 'sharp' ? '(ฉลาด)' : o.botTier === 'weak' ? '(พลาดบ่อย)' : '(กลาง)'; + o.nickname = 'บอท ' + (++k) + ' ' + tag; + }); + if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) { + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o) return; + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + o.tx = s.x; + o.ty = s.y; + }); + } + if (mapData.gameType === 'quiz_carry') { + const t0 = performance.now(); + [...others.keys()].filter(isPreviewBotId).forEach((bid, idx) => { + const o = others.get(bid); + if (!o) return; + o.botQuizCarryPathfindAfter = t0 + idx * 90 + Math.floor(Math.random() * 140); + }); + } + if (mapData.gameType === 'gauntlet') { + applyGauntletPreviewSpawnLayout(false); + emitGauntletPreviewRowsToServer(); + } + if (mapData.gameType === 'stack') { + applyStackPreviewSpawnLayout(); + rebuildStackPreviewTurnOrder(); + } + if (mapData.gameType === 'jump_survive') { + applyJumpSurvivePreviewSpawnLayout(false); + } + if (mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + if (quizCarryPregameActive && mapData && mapData.gameType === 'quiz_carry') updateQuizCarryPregameHud(); + } + + function pickRandomWalkableCellInAnswerGrid(grid, o) { + if (!grid || !mapData) return null; + const w = mapData.width || 20, h = mapData.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] !== 1) continue; + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, NaN, NaN, o)) pool.push({ x, y }); + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + /** ช่วงตอบ: บอทสุ่มเดินไปโซนถูก/ผิด/เดินมั่ว ตามระดับ (ฉลาด/กลาง/พลาดบ่อย) */ + function previewBotsPrepareAnswerRound() { + if (!previewFillBots || !mapData || !isQuiz() || !previewQuizCurrent) return; + const correctTrue = !!previewQuizCurrent.answerTrue; + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botPath = []; + o.botAnswerWander = false; + const tier = o.botTier || 'avg'; + let pCorrect = 0.52; + if (tier === 'sharp') pCorrect = 0.84; + else if (tier === 'weak') pCorrect = 0.22; + if (Math.random() < 0.14) { + o.botAnswerWander = true; + return; + } + const wantsCorrect = Math.random() < pCorrect; + const targetTrue = wantsCorrect ? correctTrue : !correctTrue; + const zoneGrid = targetTrue ? qt : qf; + const dest = pickRandomWalkableCellInAnswerGrid(zoneGrid, o); + if (!dest) { + o.botAnswerWander = true; + return; + } + const path = pathfindPlayForBot(o.x, o.y, dest.x + 0.5, dest.y + 0.5, o); + if (!path || path.length <= 1) { + o.botAnswerWander = true; + return; + } + o.botPath = path.slice(1); + }); + } + + /** คัดลอกตรรกะจาก server — ดีดออกนอกโซนจริง/เท็จ (โหมดทดสอบ preview บน play.html) */ + function findNearestOutsideQuizAnswerZonesPlay(md, sx, sy) { + const w = md.width || 20; + const h = md.height || 15; + const tGrid = md.quizTrueArea || []; + const fGrid = md.quizFalseArea || []; + const inAnswer = (x, y) => quizCellOnPlay(tGrid, x, y) || quizCellOnPlay(fGrid, x, y); + const walkable = (x, y) => { + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + return row && row[x] !== 1; + }; + const fx = Math.floor(Number(sx)); + const fy = Math.floor(Number(sy)); + if (!walkable(fx, fy)) { + const sp = md.spawn || { x: 1, y: 1 }; + return { x: (typeof sp.x === 'number' ? sp.x : 1) + 0.5, y: (typeof sp.y === 'number' ? sp.y : 1) + 0.5 }; + } + if (!inAnswer(fx, fy)) return { x: sx, y: sy }; + const q = [[fx, fy]]; + const seen = new Set([`${fx},${fy}`]); + const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; + while (q.length) { + const [cx, cy] = q.shift(); + for (let di = 0; di < dirs.length; di++) { + const nx = cx + dirs[di][0]; + const ny = cy + dirs[di][1]; + const k = `${nx},${ny}`; + if (seen.has(k)) continue; + if (!walkable(nx, ny)) continue; + seen.add(k); + if (!inAnswer(nx, ny)) { + return { x: nx + 0.5, y: ny + 0.5 }; + } + q.push([nx, ny]); + } + } + const sp = md.spawn || { x: 1, y: 1 }; + return { x: (typeof sp.x === 'number' ? sp.x : 1) + 0.5, y: (typeof sp.y === 'number' ? sp.y : 1) + 0.5 }; + } + + function quizResolveChoiceFromStanding(ox, oy, correctTrue) { + const tx = Math.floor(ox); + const ty = Math.floor(oy); + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + const inT = quizCellOnPlay(qt, tx, ty); + const inF = quizCellOnPlay(qf, tx, ty); + let choice = null; + if (inT && !inF) choice = true; + else if (inF && !inT) choice = false; + else if (inT && inF) choice = true; + const right = choice !== null && choice === correctTrue; + return { choice, right }; + } + + /** เฉลยรอบทดสอบบนเครื่อง — ให้พฤติกรรมใกล้เคียง server (ผิด = ล็อกโซน + ดีดออก) */ + function resolvePreviewQuizRound() { + if (!previewMode || !mapData || !isQuiz() || !previewQuizCurrent || myId == null) return; + const correctTrue = !!previewQuizCurrent.answerTrue; + const results = []; + const mine = quizResolveChoiceFromStanding(me.x, me.y, correctTrue); + let right = mine.right; + let choice = mine.choice; + if (mine.right) { + playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + 1; + } else { + playQuizPlayerLocal.cannotTrue = true; + playQuizPlayerLocal.cannotFalse = true; + const sp = pickRandomSpawnFromMapPlay(mapData); + const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5); + me.x = pos.x; + me.y = pos.y; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + results.push({ + id: myId, + right, + choice, + score: playLiveQuizScores[myId] != null ? playLiveQuizScores[myId] : 0, + }); + if (previewFillBots) { + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + const br = quizResolveChoiceFromStanding(o.x, o.y, correctTrue); + o.botPath = []; + o.botAnswerWander = false; + if (br.right) { + playLiveQuizScores[id] = (playLiveQuizScores[id] || 0) + 1; + } else { + o.quizCannotTrue = true; + o.quizCannotFalse = true; + const sp = pickRandomSpawnFromMapPlay(mapData); + const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5); + o.x = pos.x + (Math.random() - 0.5) * 0.35; + o.y = pos.y + (Math.random() - 0.5) * 0.35; + } + results.push({ + id, + right: br.right, + choice: br.choice, + score: playLiveQuizScores[id] != null ? playLiveQuizScores[id] : 0, + }); + }); + } + const r = { results, scores: { ...playLiveQuizScores } }; + renderPlayQuizScoreboard(playLiveQuizScores); + showPlayQuizFeedback(r); + } + + function advancePreviewQuizIfDue() { + if (!previewMode || !isQuiz() || !playQuizPhaseEndsAt || Date.now() < playQuizPhaseEndsAt) return; + const pool = previewQuizPool; + if (!pool || !pool.length) return; + if (previewQuizStep === 'read') { + applyPreviewAnswerPhase(); + return; + } + if (previewQuizStep === 'answer') { + resolvePreviewQuizRound(); + applyPreviewBetweenPhase(); + return; + } + if (previewQuizStep === 'between') { + const q = pickRandomQuizFromPool(pool); + if (q) applyPreviewReadPhase(q, pool.length); + } + } + + async function loadPreviewQuizAndStart() { + resetPreviewBotsQuizState(); + let settings = null; + try { + const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' }); + if (r.ok) settings = await r.json(); + } catch (e) { /* use map fallback */ } + let pool = []; + if (settings && Array.isArray(settings.questions) && settings.questions.length) { + pool = settings.questions + .filter((q) => q && String(q.text || '').trim()) + .map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue })); + } + if (!pool.length && mapData) pool = buildQuizPoolFromMap(mapData); + previewQuizPool = pool; + const dRead = 10000; + const dAns = 5000; + const dBet = 3500; + previewQuizTiming = { + readMs: clampPreviewMs(settings && settings.readMs, dRead, 1000, 300000), + answerMs: clampPreviewMs(settings && settings.answerMs, dAns, 1000, 300000), + betweenMs: clampPreviewMs(settings && settings.betweenMs, dBet, 0, 300000), + }; + previewQuizCurrent = null; + if (!pool.length) { + playQuizPhaseLocal = 'read'; + previewQuizStep = 'read'; + playQuizText = 'ยังไม่มีคำถามในชุด — ตั้งคำถามใน Admin → คำถามเกม หรือในแมป (quizQuestions)'; + playQuizPhaseEndsAt = 0; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] ไม่มีคำถามในระบบ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'โซนสีบนแผนที่ยังใช้ดูเลย์เอาต์ได้ตามปกติ'; + initPlayLiveQuizScoresZeros(); + startPlayQuizTimer(); + return; + } + const q = pickRandomQuizFromPool(pool); + if (q) applyPreviewReadPhase(q, pool.length); + initPlayLiveQuizScoresZeros(); + startPlayQuizTimer(); + } + + function startPlayQuizTimer() { + if (playQuizTimerInterval) clearInterval(playQuizTimerInterval); + playQuizTimerInterval = setInterval(() => { + updatePlayQuizTimerDisplay(); + advancePreviewQuizIfDue(); + }, 200); + updatePlayQuizTimerDisplay(); + } + + function teardownPlayQuizUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) ov.classList.add('is-hidden'); + const panel = document.getElementById('quiz-map-question-panel'); + const mapQText = document.getElementById('quiz-map-question-text'); + if (panel) { + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, mapQText); + } + playQuizPhaseLocal = null; + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + playQuizText = ''; + playQuizPhaseEndsAt = 0; + previewQuizPool = []; + previewQuizCurrent = null; + previewQuizStep = 'read'; + playLiveQuizScores = {}; + playPath = []; + hidePlayQuizHud(); + quizCarryJoinSettingsSnap = null; + quizCarryMapPanelTheme = null; + quizCarryChoicePlaqueThemes = null; + resetQuizCarryPlayState(); + resetQuizBattlePlayState(); + const grabBtnTeardown = document.getElementById('quiz-carry-grab-btn'); + if (grabBtnTeardown) { + grabBtnTeardown.classList.add('is-hidden'); + grabBtnTeardown.classList.remove('quiz-carry-grab-btn--active'); + grabBtnTeardown.classList.remove('quiz-carry-grab-btn--place'); + grabBtnTeardown.style.pointerEvents = ''; + const im = grabBtnTeardown.querySelector('img'); + if (im) im.src = '/Game/img/quiz-carry/btn-grab.png'; + } + } + + function setupPlayQuizUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else ov.classList.remove('is-hidden'); + } + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + const leg = document.getElementById('quiz-play-legend'); + if (previewMode) { + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] กำลังโหลดคำถาม…'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'ดึงชุดคำถามจาก /api/quiz-settings (หรือจากแมป)…'; + if (leg) leg.textContent = ''; + loadPreviewQuizAndStart(); + } else { + playQuizPhaseLocal = null; + playQuizText = 'รอคำถามจากโฮสต์…'; + playQuizPhaseEndsAt = 0; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'แมพตอบคำถาม'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'เข้าร่วมห้องแล้ว — เมื่อโฮสต์เริ่มเกม ข้อความและเวลาจะอัปเดตที่นี่ (เล่นจริงแนะนำผ่าน room-lobby)'; + if (leg) leg.textContent = 'โซนสีบนแผนที่คือจุดตอบเหมือนใน Lobby'; + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + } + } + + function isFrogger() { return mapData && mapData.gameType === 'frogger'; } + function isGauntlet() { return mapData && mapData.gameType === 'gauntlet'; } + function isGauntletFaceRightMapMno9kb07() { + return !!(isGauntlet() && playSessionMapId === GAUNTLET_FACE_RIGHT_MAP_ID); + } + /** Crown heist rules + UI (เดียวกับหันขวา — ฉาก mno9kb07) */ + function isGauntletCrownHeistMapPlay() { + return isGauntletFaceRightMapMno9kb07(); + } + let gauntletCrownHowtoVisible = false; + function isGauntletCrownPregameBlockingPlay() { + if (!isGauntletCrownHeistMapPlay()) return false; + if (gauntletCrownRunHeldRemote) return true; + if (gauntletCrownPregamePhase != null && gauntletCrownPregamePhase !== 'live') return true; + return false; + } + function isStack() { return mapData && mapData.gameType === 'stack'; } + function isJumpSurvive() { return mapData && mapData.gameType === 'jump_survive'; } + + function currentPlayMapId() { + const q = (playSessionMapId || '').trim(); + if (q) return q; + if (mapData && mapData.id != null && String(mapData.id).trim() !== '') return String(mapData.id).trim(); + return (playMapIdFromQuery || '').trim(); + } + + function isJumpSurviveMissionUiMapPlay() { + return isJumpSurvive() && currentPlayMapId() === JUMP_SURVIVE_MISSION_MAP_ID; + } + + function isJumpSurviveMissionPregameBlockingPlay() { + if (!isJumpSurviveMissionUiMapPlay()) return false; + return jumpSurviveMissionPhase != null && jumpSurviveMissionPhase !== 'live' && jumpSurviveMissionPhase !== 'ended'; + } + + function jumperAssetUrl(file) { + return BASE + '/img/Jumper/' + file; + } + + function hideConflictingOverlaysForJumpSurviveMission() { + hideConflictingOverlaysForGauntletCrown(); + } + + function jumpSurviveTimeLimitSecForMission() { + const m = Number(mapData && mapData.jumpSurviveTimeSec); + if (Number.isFinite(m) && m > 0 && m < 7200) return Math.floor(m); + const j = Number(playJumpSurviveMissionTimeSec); + if (Number.isFinite(j) && j > 0 && j < 7200) return Math.floor(j); + return 60; + } + + function jumpSurviveRemainingSecMission() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveSessionStartMs <= 0) return null; + const lim = jumpSurviveTimeLimitSecForMission(); + if (!(lim > 0)) return null; + const elapsed = (performance.now() - jumpSurviveSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function applyJumpSurviveJumperPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = jumperAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = jumperAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + } + + function showJumpSurviveMissionHowtoOverlay() { + if (!isJumpSurviveMissionUiMapPlay()) return; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + jumpSurviveMissionPhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.textContent = ''; + st.classList.remove('gch-status--jumper'); + st.classList.add('is-hidden'); + } + const btn = document.getElementById('btn-gch-ready'); + if (btn) { + const humans = quizCarryPregameHumanIds(); + const soleHuman = humans.length === 1; + const canGo = soleHuman || isMePlayHost(); + btn.classList.remove('is-start-phase'); + btn.classList.toggle('is-read-only', !canGo); + btn.disabled = !canGo; + btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host'; + btn.setAttribute('aria-pressed', 'false'); + } + } + + function beginJumpSurviveMissionCountdownThenRun() { + if (!isJumpSurviveMissionUiMapPlay()) return; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + jumpSurviveMissionPhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.classList.add('is-hidden'); + st.classList.remove('gch-status--jumper'); + } + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = function () { + if (cd) cd.classList.add('is-hidden'); + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + jumpSurviveMissionPhase = 'live'; + jumpSurviveGameEnded = false; + jumpSurviveEliminated = false; + jumpSurviveInitRuntime(); + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + numEl.textContent = String(n); + const step = function () { + n--; + if (n > 0) { + numEl.textContent = String(n); + jumperMissionCountdownTimer = setTimeout(step, 1000); + } else { + jumperMissionCountdownTimer = null; + runFinish(); + } + }; + jumperMissionCountdownTimer = setTimeout(step, 1000); + } + + /** Jumper ภารกิจ mnptfts2: เกรดจากจำนวนผู้รอด — 6=A … 3=D; น้อยกว่า 2 = F; พอดี 2 คน = E */ + function jumpSurviveGradeFromSurvivorCount(survivors) { + const s = Math.max(0, Math.floor(Number(survivors) || 0)); + if (s >= 6) return 'A'; + if (s === 5) return 'B'; + if (s === 4) return 'C'; + if (s === 3) return 'D'; + if (s === 2) return 'E'; + return 'F'; + } + + function jumpSurviveRollRewardCardForGrade(grade) { + if (grade === 'F') return null; + const r = Math.random(); + if (grade === 'A' || grade === 'B') { + if (r < 0.35) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.7) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'C' || grade === 'D') { + if (r < 0.25) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.45) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'E') { + if (r < 0.12) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (r < 0.25) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.45) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + + function jumpSurviveMissionTotalParticipants() { + let n = myId != null ? 1 : 0; + if (others && typeof others.size === 'number') n += others.size; + return n; + } + + function jumpSurviveMissionCountAlive() { + let n = 0; + if (myId != null && !jumpSurviveEliminated) n++; + if (others && typeof others.forEach === 'function') { + others.forEach(function (o) { + if (o && !o.jumpSurviveEliminated) n++; + }); + } + return n; + } + + /** จบทันทีเมื่อไม่มีผู้รอด หรือเหลือคนเดียว (หลายคนในรอบ) — โหมด solo 1 คนยังเล่นจนหมดเวลา */ + function jumpSurviveMissionMaybeEarlyFinish() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) return; + const total = jumpSurviveMissionTotalParticipants(); + const alive = jumpSurviveMissionCountAlive(); + if (alive === 0) { + endJumpSurviveMissionRound(); + return; + } + if (total >= 2 && alive === 1) { + endJumpSurviveMissionRound(); + } + } + + function jumpSurviveBuildMissionPayload() { + const rows = []; + const pushEnt = function (id, nickname, characterId, eliminated) { + const alive = !eliminated; + const baseScore = alive ? 100 : 0; + rows.push({ + id: id, + nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id), + characterId: characterId ?? null, + baseScore: baseScore, + eliminated: !!eliminated, + rankBonus: 0, + finalScore: baseScore, + }); + }; + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), jumpSurviveEliminated); + } + others.forEach(function (o, id) { + if (!o) return; + pushEnt(id, o.nickname, o.characterId, !!o.jumpSurviveEliminated); + }); + rows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + let survivorCount = 0; + for (let si = 0; si < rows.length; si++) { + if (rows[si] && !rows[si].eliminated) survivorCount++; + } + const participantCount = rows.length; + const grade = jumpSurviveGradeFromSurvivorCount(survivorCount); + const top = rows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: row.baseScore, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const rewardCard = jumpSurviveRollRewardCardForGrade(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'jumper', + survivorCount: survivorCount, + participantCount: participantCount, + }; + } + + function jumpSurviveMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'jumper' || !previewFillBots || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + rankBonus: 0, + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: o.jumpSurviveEliminated ? 0 : 100, + eliminated: !!o.jumpSurviveEliminated, + rankBonus: 0, + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + let survivorCount = 0; + for (let si = 0; si < baseRows.length; si++) { + if (baseRows[si] && !baseRows[si].eliminated) survivorCount++; + } + const participantCount = baseRows.length; + const grade = jumpSurviveGradeFromSurvivorCount(survivorCount); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: jumpSurviveRollRewardCardForGrade(grade), + totalParts: totalParts, + uiSkin: 'jumper', + survivorCount: survivorCount, + participantCount: participantCount, + }; + } + + function endJumpSurviveMissionRound() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveGameEnded) return; + jumpSurviveGameEnded = true; + jumpSurviveMissionPhase = 'ended'; + applyJumpSurviveJumperPanelImages(); + const mission = jumpSurviveBuildMissionPayload(); + showGauntletCrownMissionOverlay(mission); + } + function isSpaceShooter() { return mapData && mapData.gameType === 'space_shooter'; } + function isBalloonBoss() { return mapData && mapData.gameType === 'balloon_boss'; } + function isQuizCarry() { return mapData && mapData.gameType === 'quiz_carry'; } + function isQuizBattle() { return mapData && mapData.gameType === 'quiz_battle'; } + + function quizCarryNeonColorForChoice(choiceIndex) { + const i = Math.max(0, choiceIndex | 0); + const h = (i * 47 + 100) % 360; + return 'hsl(' + h + ', 72%, 62%)'; + } + + function quizCarryMinimapOptionFillCss(ov) { + const th = getEffectiveCarryChoicePlaqueThemeForChoice((Number(ov) | 0) - 1); + if (th.borderMode === 'fixed' && th.fixedBorder) { + const m = /^rgba?\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i.exec(String(th.fixedBorder).trim()); + if (m) { + const a0 = m[4] != null && m[4] !== '' ? Number(m[4]) : 1; + const aa = (Number.isFinite(a0) ? Math.max(0, Math.min(1, a0)) : 1) * 0.32; + const t = Math.round(aa * 1000) / 1000; + return 'rgba(' + m[1] + ',' + m[2] + ',' + m[3] + ',' + t + ')'; + } + } + const i = Math.max(0, ov - 1); + const h = (i * 47 + 100) % 360; + return 'hsla(' + h + ', 58%, 52%, 0.32)'; + } + + function normalizeQuizCarryQuestionFromAny(q) { + if (!q) return null; + let text = String(q.text || q.question || q.prompt || q.title || '').trim(); + if (Array.isArray(q.choices) && q.choices.length >= 2) { + const choices = q.choices.map((c) => String(c)); + let ci = Number(q.correctIndex); + if (!Number.isFinite(ci) || ci < 0 || ci >= choices.length) ci = 0; + if (!text) text = 'เลือกคำตอบที่ถูกต้อง — หยิบตัวเลือกไปวางโซนส่งคำตอบ'; + let chSlot = Number(q.countdownHighlightSlot); + if (!Number.isFinite(chSlot) || chSlot < 1 || chSlot > 16) chSlot = null; + else chSlot = Math.floor(chSlot); + let choiceImageUrls = null; + if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) { + const urls = choices.map((_, idx) => sanitizeQuizCarryImageUrlClient(q.choiceImageUrls[idx])); + if (urls.some((u) => u)) choiceImageUrls = urls; + } + return { text, choices, correctIndex: ci, countdownHighlightSlot: chSlot, choiceImageUrls }; + } + if (!text) return null; + const t = !!q.answerTrue; + return { text, choices: ['จริง', 'เท็จ'], correctIndex: t ? 0 : 1, countdownHighlightSlot: null }; + } + + function buildQuizCarryPoolFromMap(md) { + if (!md || !Array.isArray(md.quizQuestions)) return []; + const out = []; + for (let i = 0; i < md.quizQuestions.length; i++) { + const n = normalizeQuizCarryQuestionFromAny(md.quizQuestions[i]); + if (n) out.push(n); + } + return out; + } + + function getQuizCarryHubTileBounds(md) { + if (!md || md.gameType !== 'quiz_carry') return null; + const grid = md.quizCarryHubArea; + if (!grid || !grid.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (let yy = 0; yy < grid.length; yy++) { + const row = grid[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (row[xx] === 1) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + } + } + } + if (minX === Infinity) return null; + return { minX, minY, maxX, maxY }; + } + + /** จุดกลางโซนตัวเลือกหยิบมาวาง (ค่า grid 1..N = ลำดับ choices) เพื่อวาดข้อความบนแผนที่ */ + function getQuizCarryOptionClusterBounds(md, slot1toN) { + const g = md && md.quizCarryOptionArea; + if (!g || !g.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let n = 0, sumX = 0, sumY = 0; + for (let yy = 0; yy < g.length; yy++) { + const row = g[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (Number(row[xx]) === slot1toN) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + sumX += xx + 0.5; + sumY += yy + 0.5; + n++; + } + } + } + if (!n || minX === Infinity) return null; + return { + minX, minY, maxX, maxY, + cx: sumX / n, + cy: sumY / n, + }; + } + + function canvasWordWrapLines(ctx, text, maxWidth) { + const raw = String(text || '').trim(); + if (!raw) return []; + const words = raw.split(/\s+/); + const lines = []; + let cur = words[0]; + for (let i = 1; i < words.length; i++) { + const w = words[i]; + const test = cur + ' ' + w; + if (ctx.measureText(test).width <= maxWidth) cur = test; + else { + lines.push(cur); + cur = w; + } + } + lines.push(cur); + return lines; + } + + function quizCarryOptionHeldByAnyone(optionIdx) { + if (optionIdx == null || optionIdx < 0) return false; + if (me.quizCarryHeld === optionIdx) return true; + for (const o of others.values()) { + if (o && o.quizCarryHeld === optionIdx) return true; + } + return false; + } + + /** สไปรต์ทับช่องโซนกลาง (ม่วง) อย่างน้อย 1 ช่อง */ + function spriteOverlapsQuizCarryHubArea(md, sp) { + if (!md || md.gameType !== 'quiz_carry' || !sp) return false; + const hub = md.quizCarryHubArea; + if (!hub || !hub.length) return false; + const mw = md.width || 20, mh = md.height || 15; + const x0 = sp.x | 0, y0 = sp.y | 0, ww = sp.w | 0, hh = sp.h | 0; + for (let yy = y0; yy < y0 + hh; yy++) { + if (yy < 0 || yy >= mh) continue; + const row = hub[yy]; + if (!row) continue; + for (let xx = x0; xx < x0 + ww; xx++) { + if (xx < 0 || xx >= mw) continue; + if (row[xx] === 1) return true; + } + } + return false; + } + + /** ตัวเลือกหนึ่งข้อ — มีคนถือป้ายอยู่ได้แค่คนเดียว (หยิบซ้ำไม่ได้) */ + function pickQuizCarryTargetOptionIndexAvoidingTaken(preferredIdx) { + const n = quizCarryCurrent && quizCarryCurrent.choices ? quizCarryCurrent.choices.length : 0; + if (n < 1) return null; + let p = preferredIdx | 0; + if (p < 0 || p >= n) p = 0; + if (!quizCarryOptionHeldByAnyone(p)) return p; + const avail = []; + for (let i = 0; i < n; i++) { + if (!quizCarryOptionHeldByAnyone(i)) avail.push(i); + } + if (!avail.length) return null; + return avail[Math.floor(Math.random() * avail.length)]; + } + + /** + * วาดป้ายมุมมน + ขอบเรือง + ข้อความ (รูปแบบเดียวกันทั้งแมปและติดตัว) — สีจาก carryChoicePlaqueThemes[choiceIndex] · รูปจาก plaqueExtra.imageUrl + * @param tether ถ้ามี วาดเส้นขาวจาก (x0,y0) ถึง (x1,y1) ก่อนป้าย + * @param plaqueExtra optional { imageUrl } + */ + function drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, tether, plaqueExtra) { + const th = getEffectiveCarryChoicePlaqueThemeForChoice(choiceIndex); + const neon = th.borderMode === 'fixed' ? th.fixedBorder : quizCarryNeonColorForChoice(choiceIndex); + const fillCol = th.fillBg || 'rgba(12, 10, 20, 0.88)'; + const textCol = th.textColor || '#f8f9ff'; + const baseLw = Math.max(0.5, Math.min(8, Number(th.borderWidthPx))); + const imgUrl = plaqueExtra && plaqueExtra.imageUrl ? String(plaqueExtra.imageUrl) : ''; + const elImg = plaqueExtra && plaqueExtra.imageElement; + const imgFromEl = (elImg && elImg.complete && elImg.naturalWidth > 0) ? elImg : null; + const img = !imgFromEl && imgUrl ? getQuizCarryChoiceImageCached(imgUrl) : null; + const drawPlaqueImg = imgFromEl || ((img && img.complete && img.naturalWidth > 0) ? img : null); + const rr = Math.max(6, Math.min(12, Math.min(w, h) * 0.2)); + const simplePreviewPlaque = previewMode && editorEmbedReturn; + function pathBoard() { + ctx.beginPath(); + if (typeof ctx.roundRect === 'function') ctx.roundRect(signX, signY, w, h, rr); + else ctx.rect(signX, signY, w, h); + } + if (!simplePreviewPlaque) { + for (let g = 4; g >= 1; g--) { + ctx.strokeStyle = neon; + ctx.globalAlpha = 0.07 + g * 0.06; + ctx.lineWidth = baseLw + g * 3.2; + pathBoard(); + ctx.stroke(); + } + } + ctx.globalAlpha = 1; + ctx.fillStyle = fillCol; + pathBoard(); + ctx.fill(); + if (drawPlaqueImg) { + ctx.save(); + pathBoard(); + ctx.clip(); + const padImg = Math.max(2, padY * 0.65); + const ix = signX + padImg; + const iy = signY + padImg; + const iw = Math.max(0, w - padImg * 2); + const ih = Math.max(0, h - padImg * 2); + if (iw > 0 && ih > 0) { + const nw = drawPlaqueImg.naturalWidth; + const nh = drawPlaqueImg.naturalHeight; + const ir = nw / nh; + let dw; + let dh; + if (iw / ih > ir) { + dh = ih; + dw = dh * ir; + } else { + dw = iw; + dh = dw / ir; + } + const dx = ix + (iw - dw) / 2; + const dy = iy + (ih - dh) / 2; + ctx.drawImage(drawPlaqueImg, 0, 0, nw, nh, dx, dy, dw, dh); + } + ctx.restore(); + } + ctx.strokeStyle = neon; + if (simplePreviewPlaque) { + ctx.lineWidth = Math.max(1, baseLw * 0.85); + ctx.shadowBlur = 0; + pathBoard(); + ctx.stroke(); + } else { + ctx.lineWidth = baseLw; + ctx.shadowColor = neon; + ctx.shadowBlur = 14; + pathBoard(); + ctx.stroke(); + ctx.shadowBlur = 8; + pathBoard(); + ctx.stroke(); + ctx.shadowBlur = 0; + } + const attachX = signX + w / 2; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = textCol; + const lh = lineH || 12; + if (!simplePreviewPlaque && lines && lines.length) { + ctx.shadowColor = 'rgba(0,0,0,0.55)'; + ctx.shadowBlur = 3; + } + if (lines && lines.length) { + const n = lines.length; + const textBlockH = n * lh; + const y0 = signY + Math.max(padY, (h - textBlockH) / 2); + const firstLineCenterY = y0 + lh / 2; + for (let li = 0; li < n; li++) { + ctx.fillText(lines[li], attachX, firstLineCenterY + li * lh); + } + } + ctx.shadowBlur = 0; + if (tether && Number.isFinite(tether.x0) && Number.isFinite(tether.y0)) { + ctx.strokeStyle = 'rgba(255,255,255,0.95)'; + ctx.lineWidth = 1.5; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.shadowBlur = 0; + ctx.beginPath(); + ctx.moveTo(tether.x0, tether.y0); + ctx.lineTo(tether.x1, tether.y1); + ctx.stroke(); + } + } + + /** + * กล่องป้ายบนจอ (signX, signY, w, h) ให้ตรงกับ drawQuizCarryChoiceLabels สำหรับช่อง idx 0-based + */ + function computeQuizCarryChoicePlaqueScreenRect(ctx, worldToScreen, zoom, idx0Based) { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return null; + const q = quizCarryCurrent; + const choices = q.choices; + if (!choices || !choices.length || idx0Based < 0 || idx0Based >= choices.length) return null; + const i = idx0Based; + const b = getQuizCarryOptionClusterBounds(mapData, i + 1); + if (!b) return null; + const ts = tileSize * zoom; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + const choiceText = String(choices[i] || '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(q, i); + const gridIdleEl = findQuizCarryGridIdleImgForChoiceIndex(i); + const plaqueExtra = imgUrl ? { imageUrl: imgUrl } + : (gridIdleEl && gridIdleEl.complete && gridIdleEl.naturalWidth ? { imageElement: gridIdleEl } : {}); + if (!choiceText && !plaqueExtra.imageUrl && !plaqueExtra.imageElement) return null; + const tileSpanX = (b.maxX - b.minX + 1); + const tileSpanY = (b.maxY - b.minY + 1); + const minPlaqueW = Math.ceil(Math.max(72, tileSpanX * ts - 16) * ps); + const minPlaqueH = Math.ceil(Math.max(52, tileSpanY * ts - 12) * ps); + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + ctx.save(); + let signX; + let signY; + let w; + let h; + if (!choiceText && (plaqueExtra.imageUrl || plaqueExtra.imageElement)) { + const minW = minPlaqueW; + const minH = minPlaqueH; + signX = sx - minW / 2; + signY = sy - minH / 2; + w = minW; + h = minH; + ctx.restore(); + return { signX, signY, w, h }; + } + const line = choiceText; + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + let maxW = Math.max(56, tileSpanX * ts - 14); + const lineH = fontPx * 1.2; + let textLines = canvasWordWrapLines(ctx, line, maxW); + if (!textLines.length) textLines = [line]; + if (textLines.length * lineH > tileSpanY * ts - 8 && textLines.length > 1) { + const fp2 = Math.max(9, fontPx * 0.85); + ctx.font = '600 ' + fp2 + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + const lh2 = fp2 * 1.2; + maxW = Math.max(48, maxW - 4); + textLines = canvasWordWrapLines(ctx, line, maxW); + if (textLines.length * lh2 > tileSpanY * ts - 8) { + textLines = textLines.slice(0, Math.max(1, Math.floor((tileSpanY * ts - 8) / lh2))); + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + const maxLineW = Math.max(...textLines.map((t) => ctx.measureText(t).width), 40); + const padX = 10; + const padY = 8; + w = Math.ceil(maxLineW + padX * 2); + h = Math.ceil(textLines.length * lh2 + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + signX = sx - w / 2; + signY = sy - h / 2; + ctx.restore(); + return { signX, signY, w, h }; + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + let maxLineW = 0; + for (let ti = 0; ti < textLines.length; ti++) { + const lw = ctx.measureText(textLines[ti]).width; + if (lw > maxLineW) maxLineW = lw; + } + const padX = 10; + const padY = 8; + w = Math.ceil(Math.max(maxLineW, 52) + padX * 2); + h = Math.ceil(textLines.length * lineH + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + signX = sx - w / 2; + signY = sy - h / 2; + ctx.restore(); + return { signX, signY, w, h }; + } + + /** + * ขอบเรืองรอบช่องตัวเลือก (carryEmbedCountdownHighlight + carryEmbedCountdownHighlightColor) + * ขนาดกรอบเท่าป้ายบนแมป (เดียวกับ drawQuizCarryChoiceLabels) ไม่ใช่ทั้ง cluster กริด + */ + function drawQuizCarryEmbedCountdownHighlight(ctx, worldToScreen, zoom, timeMs) { + if (!isQuizCarry() || !mapData) return; + if (mapData.carryEmbedCountdownHighlight === false) return; + const optIdx = getQuizCarryLocalGrabbableOptionIndex(); + if (optIdx == null) return; + const pr = computeQuizCarryChoicePlaqueScreenRect(ctx, worldToScreen, zoom, optIdx); + if (!pr || pr.w < 4 || pr.h < 4) return; + const rx = pr.signX; + const ry = pr.signY; + const rw = pr.w; + const rh = pr.h; + const rr = Math.max(6, Math.min(16, Math.min(rw, rh) * 0.14)); + const colRaw = mapData.carryEmbedCountdownHighlightColor; + const strokeBase = typeof colRaw === 'string' && /^#[0-9a-fA-F]{6}$/.test(colRaw.trim()) ? colRaw.trim() : '#ffb44a'; + const pulse = 0.55 + 0.45 * Math.sin((timeMs || 0) / 180); + ctx.save(); + function pathRr() { + ctx.beginPath(); + if (typeof ctx.roundRect === 'function') ctx.roundRect(rx, ry, rw, rh, rr); + else ctx.rect(rx, ry, rw, rh); + } + pathRr(); + ctx.fillStyle = strokeBase; + ctx.globalAlpha = 0.12 * pulse; + ctx.fill(); + for (let g = 6; g >= 1; g--) { + ctx.strokeStyle = strokeBase; + ctx.globalAlpha = (0.07 + g * 0.055) * pulse; + ctx.lineWidth = 2.5 + g * 4; + pathRr(); + ctx.stroke(); + } + ctx.globalAlpha = 0.98; + ctx.strokeStyle = strokeBase; + ctx.lineWidth = 4; + ctx.shadowColor = strokeBase; + ctx.shadowBlur = 22 * pulse; + pathRr(); + ctx.stroke(); + ctx.shadowBlur = 12 * pulse; + pathRr(); + ctx.stroke(); + ctx.shadowBlur = 0; + ctx.globalAlpha = 1; + ctx.restore(); + } + + function drawQuizCarryChoiceLabels(ctx, worldToScreen, zoom) { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return; + if (!quizCarryOptionsPickableNow()) return; + const choices = quizCarryCurrent.choices; + if (!choices || !choices.length) return; + const maxSlots = choices.length; + const ts = tileSize * zoom; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + ctx.save(); + for (let i = 0; i < Math.min(choices.length, maxSlots); i++) { + if (quizCarryOptionHeldByAnyone(i)) continue; + const b = getQuizCarryOptionClusterBounds(mapData, i + 1); + if (!b) continue; + const choiceText = String(choices[i] || '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(quizCarryCurrent, i); + const gridIdleEl = findQuizCarryGridIdleImgForChoiceIndex(i); + const plaqueExtra = imgUrl ? { imageUrl: imgUrl } + : (gridIdleEl && gridIdleEl.complete && gridIdleEl.naturalWidth ? { imageElement: gridIdleEl } : {}); + if (!choiceText && !plaqueExtra.imageUrl && !plaqueExtra.imageElement) continue; + const tileSpanX = (b.maxX - b.minX + 1); + const tileSpanY = (b.maxY - b.minY + 1); + const minPlaqueW = Math.ceil(Math.max(72, tileSpanX * ts - 16) * ps); + const minPlaqueH = Math.ceil(Math.max(52, tileSpanY * ts - 12) * ps); + if (!choiceText && (plaqueExtra.imageUrl || plaqueExtra.imageElement)) { + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + const lineH = fontPx * 1.2; + const padY = 8; + const minW = minPlaqueW; + const minH = minPlaqueH; + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + drawQuizCarryNeonPlaque(ctx, sx - minW / 2, sy - minH / 2, minW, minH, [], lineH, padY, i, null, plaqueExtra); + continue; + } + const line = choiceText; + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + let maxW = Math.max(56, tileSpanX * ts - 14); + const lineH = fontPx * 1.2; + let textLines = canvasWordWrapLines(ctx, line, maxW); + if (!textLines.length) textLines = [line]; + if (textLines.length * lineH > tileSpanY * ts - 8 && textLines.length > 1) { + const fp2 = Math.max(9, fontPx * 0.85); + ctx.font = '600 ' + fp2 + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + const lh2 = fp2 * 1.2; + maxW = Math.max(48, maxW - 4); + textLines = canvasWordWrapLines(ctx, line, maxW); + if (textLines.length * lh2 > tileSpanY * ts - 8) { + textLines = textLines.slice(0, Math.max(1, Math.floor((tileSpanY * ts - 8) / lh2))); + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + const maxLineW = Math.max(...textLines.map((t) => ctx.measureText(t).width), 40); + const padX = 10; + const padY = 8; + let w = Math.ceil(maxLineW + padX * 2); + let h = Math.ceil(textLines.length * lh2 + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const signX = sx - w / 2; + const signY = sy - h / 2; + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, textLines, lh2, padY, i, null, plaqueExtra); + continue; + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + let maxLineW = 0; + for (let ti = 0; ti < textLines.length; ti++) { + const lw = ctx.measureText(textLines[ti]).width; + if (lw > maxLineW) maxLineW = lw; + } + const padX = 10; + const padY = 8; + let w = Math.ceil(Math.max(maxLineW, 52) + padX * 2); + let h = Math.ceil(textLines.length * lineH + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const signX = sx - w / 2; + const signY = sy - h / 2; + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, textLines, lineH, padY, i, null, plaqueExtra); + } + ctx.restore(); + } + + function quizCarryHubCellPlay(md, tx, ty) { + const g = md && md.quizCarryHubArea; + return !!(g && g[ty] && g[ty][tx] === 1); + } + + function quizCarryInteractiveCellPlay(md, tx, ty) { + const g = md && md.interactive; + return !!(g && g[ty] && g[ty][tx] === 1); + } + + function quizCarryMapHasAnswerInteractive(md) { + if (!md || !md.interactive || !md.interactive.length) return false; + const h = md.height || 15, w = md.width || 20; + for (let y = 0; y < h; y++) { + const row = md.interactive[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1) return true; + } + } + return false; + } + + /** @returns {number|null} index ใน choices จากช่องหมายเลข 1..N บนแมป */ + function quizCarryOptionIndexAtPlay(md, tx, ty) { + const g = md && md.quizCarryOptionArea; + if (!g || !g[ty]) return null; + const v = g[ty][tx]; + if (v < 1 || v > QUIZ_CARRY_MAX_OPTION_SLOTS) return null; + const idx = v - 1; + const n = quizCarryCurrent && quizCarryCurrent.choices ? quizCarryCurrent.choices.length : 0; + if (n < 1 || idx >= n) return null; + return idx; + } + + /** ร่างตัวละครทับช่องโซนกลาง (ม่วง) — ใช้บล็อกการเดิน */ + function quizCarryFootprintOverlapsHub(px, py) { + if (!mapData || !isQuizCarry()) return false; + const md = mapData; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryHubCellPlay(md, tx, ty)) return true; + } + return false; + } + + /** ยืนข้างนอกแต่ชิดโซนกลาง — ใช้ให้กด F ส่งคำตอบได้โดยไม่ต้องย่ำบนม่วง */ + function quizCarryFootprintAdjacentToHub(md, px, py) { + if (!md) return false; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryHubCellPlay(md, tx, ty)) continue; + for (let di = 0; di < dirs.length; di++) { + const nx = tx + dirs[di][0], ny = ty + dirs[di][1]; + if (quizCarryHubCellPlay(md, nx, ny)) return true; + } + } + return false; + } + + function quizCarryCanSubmitAtHub(md, px, py) { + if (!md) return false; + return quizCarryFootprintOverlapsHub(px, py) || quizCarryFootprintAdjacentToHub(md, px, py); + } + + function quizCarryFootprintOverlapsInteractive(md, px, py) { + if (!md) return false; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryInteractiveCellPlay(md, tx, ty)) return true; + } + return false; + } + + function quizCarryFootprintAdjacentToInteractive(md, px, py) { + if (!md) return false; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryInteractiveCellPlay(md, tx, ty)) continue; + for (let di = 0; di < dirs.length; di++) { + const nx = tx + dirs[di][0], ny = ty + dirs[di][1]; + if (quizCarryInteractiveCellPlay(md, nx, ny)) return true; + } + } + return false; + } + + function quizCarryCanSubmitAtInteractiveZone(md, px, py) { + if (!md) return false; + return quizCarryFootprintOverlapsInteractive(md, px, py) || quizCarryFootprintAdjacentToInteractive(md, px, py); + } + + /** ส่งคำตอบ: ถ้ามีโซน interactive (เขียว) ในแมป ใช้โซนนั้น — ไม่มี fallback ชิดโซนกลาง */ + function quizCarryCanSubmitAnswerAt(md, px, py) { + if (!md) return false; + if (quizCarryMapHasAnswerInteractive(md)) { + return quizCarryCanSubmitAtInteractiveZone(md, px, py); + } + return quizCarryCanSubmitAtHub(md, px, py); + } + + /** ข้อความใน HUD / โซนคำถาม — ไม่รวมตัวเลือก (แสดงบนแผนที่ตามจุดสี) */ + function formatQuizCarryQuestionHud(q) { + if (!q) return ''; + return String(q.text || q.question || q.prompt || q.title || '').trim(); + } + + function quizCarryRestoreEmbedPhaseLabel() { + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (!phaseEl || !mapData) return; + phaseEl.textContent = quizCarryMapHasAnswerInteractive(mapData) + ? 'กด F หรือปุ่ม GRAB หยิบบนโซนตัวเลือก · ส่งที่โซน interactive (เขียว) — เล่นพร้อมกันได้' + : 'กด F หรือปุ่ม GRAB หยิบบนโซนตัวเลือก · ส่งเมื่อชิดโซนกลาง (ม่วงเป็นกำแพง) — เล่นพร้อมกันได้'; + } + + function isQuizCarryEmbedCountdownBlockingMovement() { + return !!(previewMode && editorEmbedReturn && isQuizCarry() && quizCarryEmbedCountdownEndAt > Date.now()); + } + + function isQuizCarryEmbedLobbyBlockingMovement() { + return !!(previewMode && editorEmbedReturn && isQuizCarry() && quizCarryPregameActive); + } + + function quizCarryPregameHumanIds() { + const ids = []; + if (myId != null && !isPreviewBotId(myId)) ids.push(String(myId)); + others.forEach((_, id) => { + if (!isPreviewBotId(id)) ids.push(String(id)); + }); + return ids; + } + + function quizCarryPregameBotCount() { + let n = 0; + others.forEach((_, id) => { + if (isPreviewBotId(id)) n++; + }); + return n; + } + + function quizCarryPregameReadyNumerator() { + let n = quizCarryPregameBotCount(); + quizCarryPregameHumanIds().forEach((id) => { + if (quizCarryLobbyReadyMap[id]) n++; + }); + return n; + } + + function quizCarryPregameTotalPlayers() { + return quizCarryPregameHumanIds().length + quizCarryPregameBotCount(); + } + + function isMePlayHost() { + if (myId == null) return false; + if (playHostId == null) return true; + return String(playHostId) === String(myId); + } + + function hideQuizCarryPregameOverlay() { + const ov = document.getElementById('quiz-carry-pregame-overlay'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + } + + function showQuizCarryPregameOverlay() { + const ov = document.getElementById('quiz-carry-pregame-overlay'); + if (ov) { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + updateQuizCarryPregameHud(); + } + + function quizCarrySyncGuestReadyIfNeeded() { + if (!quizCarryPregameActive || myId == null || isMePlayHost()) return; + const sid = String(myId); + if (quizCarryLobbyReadyMap[sid]) return; + quizCarryLobbyReadyMap[sid] = true; + if (socket && socket.connected) socket.emit('quiz-carry-lobby-ready', { ready: true }); + } + + function updateQuizCarryPregameHud() { + if (!quizCarryPregameActive) return; + const st = document.getElementById('quiz-carry-pregame-status'); + const primary = document.getElementById('quiz-carry-pregame-primary'); + const primaryImg = document.getElementById('quiz-carry-pregame-primary-img'); + const num = quizCarryPregameReadyNumerator(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + if (st) { + st.textContent = 'Ready Status : ' + num + '/' + tot; + } + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]); + const meReady = !!(myId && quizCarryLobbyReadyMap[String(myId)]); + if (primaryImg) { + primaryImg.src = humansReady ? '/Game/img/quiz-carry/btn-start.png' : '/Game/img/quiz-carry/btn-ready.png'; + primaryImg.alt = humansReady ? 'START' : 'READY'; + } + if (primary) { + primary.classList.toggle('is-start-phase', humansReady); + primary.classList.toggle('is-read-only', !isMePlayHost()); + primary.classList.toggle('is-pressed', isMePlayHost() && meReady && !humansReady); + primary.disabled = !isMePlayHost(); + primary.setAttribute('aria-pressed', humansReady ? 'false' : (meReady ? 'true' : 'false')); + primary.title = isMePlayHost() + ? (humansReady ? 'START' : (meReady ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + } + + function beginQuizCarryEmbedPregame() { + if (!(previewMode && editorEmbedReturn && isQuizCarry())) return; + quizCarryPregameActive = true; + quizCarryLobbyReadyMap = {}; + quizCarryPregameHumanIds().forEach((id) => { quizCarryLobbyReadyMap[id] = false; }); + showQuizCarryPregameOverlay(); + if (socket && socket.connected) socket.emit('quiz-carry-lobby-sync-request'); + quizCarrySyncGuestReadyIfNeeded(); + updateQuizCarryPregameHud(); + } + + function endQuizCarryEmbedPregameAndStart() { + if (!quizCarryPregameActive) return; + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryPickNextQuestion(); + if (!(previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now())) { + quizCarryRestoreEmbedPhaseLabel(); + } + } + + function quizCarryOptionsPickableNow() { + if (!isQuizCarry() || !quizCarryCurrent) return true; + if (!quizCarryOptionRevealAt || quizCarryOptionRevealAt <= 0) return true; + return Date.now() >= quizCarryOptionRevealAt; + } + + function quizCarryApplyPhaseTimersForCurrentQuestion() { + if (!quizCarryCurrent) { + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + return; + } + const readMs = Math.max(0, Math.floor(Number(quizCarryCarryTimingMs.carryReadMs)) || 0); + const ansMs = Math.max(1000, Math.floor(Number(quizCarryCarryTimingMs.carryAnswerMs)) || 5000); + const t0 = Date.now(); + if (readMs <= 0) { + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = t0 + ansMs; + } else { + quizCarryOptionRevealAt = t0 + readMs; + quizCarryAnswerCloseAt = quizCarryOptionRevealAt + ansMs; + } + } + + function updateQuizCarryCarryPhaseHud() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!isQuizCarry() || !quizCarryCurrent) return; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (!phaseEl) return; + const now = Date.now(); + if (quizCarryOptionRevealAt > 0 && now < quizCarryOptionRevealAt) { + const remain = Math.max(0, Math.ceil((quizCarryOptionRevealAt - now) / 1000)); + phaseEl.textContent = 'อ่านคำถาม — ตัวเลือกขึ้นใน ' + remain + ' วิ'; + return; + } + if (quizCarryAnswerCloseAt > now) { + const remain = Math.max(0, Math.ceil((quizCarryAnswerCloseAt - now) / 1000)); + phaseEl.textContent = 'หยิบ/ส่งคำตอบ — เหลือ ' + remain + ' วิ'; + return; + } + quizCarryRestoreEmbedPhaseLabel(); + } + + function tickQuizCarryRoundTimers() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return; + if (quizCarryEmbedCountdownEndAt > Date.now()) return; + if (quizCarryEmbedPendingQuestion) return; + if (!quizCarryAnswerCloseAt || quizCarryAnswerCloseAt <= 0) return; + if (Date.now() < quizCarryAnswerCloseAt) return; + quizCarryAnswerCloseAt = 0; + quizCarryOptionRevealAt = 0; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + renderPlayQuizScoreboard(playLiveQuizScores); + quizCarryAfterRoundResolved(); + } + + /** นับ 3–2–1: ไม่โชว์ข้อความคำถามใน overlay — มีแค่เลข */ + function hideQuizCarryEmbedCountdownQuestionChrome() { + const cq = document.getElementById('quiz-carry-embed-countdown-q'); + const ck = document.getElementById('quiz-carry-embed-countdown-kicker'); + if (cq) { + cq.textContent = ''; + cq.style.display = 'none'; + } + if (ck) ck.style.display = 'none'; + } + + function hideQuizCarryEmbedCountdownOverlay() { + resetQuizCarryEmbedCountdownLayoutDom(); + const ov = document.getElementById('quiz-carry-embed-countdown'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + hideQuizCarryEmbedCountdownQuestionChrome(); + } + + function tickQuizCarryEmbedCountdown() { + if (!previewMode || !editorEmbedReturn || !isQuizCarry()) return; + if (!quizCarryEmbedPendingQuestion || quizCarryEmbedCountdownEndAt <= 0) return; + const now = Date.now(); + const ov = document.getElementById('quiz-carry-embed-countdown'); + const numEl = document.getElementById('quiz-carry-embed-countdown-num'); + if (now >= quizCarryEmbedCountdownEndAt) { + const pq = quizCarryEmbedPendingQuestion; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + if (!pq) return; + quizCarryCurrent = pq; + preloadQuizCarryChoiceImages(pq); + playQuizText = formatQuizCarryQuestionHud(pq); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + quizCarryRestoreEmbedPhaseLabel(); + others.forEach((o) => { + if (!o) return; + o.botQuizCarryPathfindAfter = 0; + }); + syncQuizCarryEmbedQuestionStrip(); + quizCarryApplyPhaseTimersForCurrentQuestion(); + return; + } + const elapsed = now - quizCarryEmbedCountdownStartAt; + const n = 3 - Math.min(2, Math.floor(elapsed / 1000)); + if (numEl) numEl.textContent = String(Math.max(1, Math.min(3, n))); + if (ov) { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + syncQuizCarryEmbedCountdownLayout(); + hideQuizCarryEmbedCountdownQuestionChrome(); + syncQuizCarryEmbedQuestionStrip(); + } + + function quizCarryPickNextQuestion() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!quizCarryPool.length) { + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + playQuizText = 'ไม่มีคำถาม — ใส่ quizQuestions ในแมป (choices + correctIndex) หรือตั้งใน Admin'; + return; + } + let q; + let guard = 0; + do { + q = quizCarryPool[Math.floor(Math.random() * quizCarryPool.length)]; + guard++; + } while (guard < 12 && quizCarryPool.length > 1 && quizCarryCurrent && q && q.text === quizCarryCurrent.text); + if (previewMode && editorEmbedReturn) { + quizCarryEmbedPendingQuestion = q; + preloadQuizCarryChoiceImages(q); + quizCarryEmbedCountdownStartAt = Date.now(); + quizCarryEmbedCountdownEndAt = quizCarryEmbedCountdownStartAt + 3000; + quizCarryCurrent = null; + playQuizText = ''; + clearQuizMapQuestionCarryFeedback(); + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + o.botPathStuckTicks = 0; + /* ต้องใช้ performance.now() ช่วงกับ stepQuizCarryPreviewBots — ห้ามใช้ Date.now() epoch ไม่งั้นบอทค้าง pathfind ตลอด */ + o.botQuizCarryPathfindAfter = 0; + }); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = '…'; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'รอเริ่มคำถาม — นับถอยหลัง 3 · 2 · 1'; + tickQuizCarryEmbedCountdown(); + syncQuizCarryEmbedQuestionStrip(); + return; + } + quizCarryCurrent = q; + preloadQuizCarryChoiceImages(q); + playQuizText = formatQuizCarryQuestionHud(q); + clearQuizMapQuestionCarryFeedback(); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + }); + quizCarryApplyPhaseTimersForCurrentQuestion(); + } + + /** หลังจบข้อหนึ่งข้อ (ถูกหรือหมดเวลา): พรีวิวนับรอบจนครบ carrySessionLength แล้วจบเซสชัน */ + function quizCarryAfterRoundResolved() { + if (!isQuizCarry()) return; + if (isDetectiveMinigamePlay()) { + finishDetectiveMinigameAndReturnLobby(); + return; + } + if (!previewMode) { + quizCarryPickNextQuestion(); + return; + } + quizCarryRoundsCompleted++; + const lim = quizCarrySessionLength; + if (lim > 0 && quizCarryRoundsCompleted >= lim) { + showQuizCarrySessionCompleteOverlay(); + return; + } + quizCarryPickNextQuestion(); + } + + function playQuizCarryAvatarUrlForMission(characterId) { + if (!characterId) return ''; + return BASE + '/img/characters/' + encodeURIComponent(String(characterId)) + '_down.png'; + } + + /** อวาตาร์สรุปภารกิจ / DOM — ใช้สี gen เดียวกับในเกม (เลเยอร์ + tint) */ + function applyQuizCarryRankAvatarTint(imgEl, characterId, peerId) { + if (!imgEl || !characterId) return; + let attempts = 0; + const fallbackUrl = playQuizCarryAvatarUrlForMission(characterId); + const tick = () => { + attempts++; + const nowT = Date.now(); + const rawImg = getCharacterFrame(characterId, 'down', nowT, false) || getCharacterImg(characterId, 'down'); + if (!rawImg) { + if (fallbackUrl) imgEl.src = fallbackUrl; + return; + } + if ((!rawImg.complete || !rawImg.naturalWidth) && attempts < 28) { + if (fallbackUrl) imgEl.src = fallbackUrl + '?p=' + String(attempts); + setTimeout(tick, 90); + return; + } + if (!rawImg.naturalWidth) { + if (fallbackUrl) imgEl.src = fallbackUrl; + return; + } + const tint = playTintFromPeerId(peerId != null ? String(peerId) : String(characterId)); + const out = getPlayTintedAvatarSource(rawImg, characterId, 'down', nowT, false, tint); + if (out && out.tagName === 'CANVAS' && out.width > 0) { + try { + imgEl.src = out.toDataURL('image/png'); + return; + } catch (e) { /* ต่อไปลอง walk หรือ URL */ } + } + if (out && out.src && out !== rawImg) { + imgEl.src = out.src; + return; + } + if (attempts < 28 && playCharLayerDiscoveryPending(characterId)) { + setTimeout(tick, 120); + return; + } + imgEl.src = rawImg.src || fallbackUrl; + }; + tick(); + } + + /** เกรดจากค่าเฉลี่ยคะแนนทีม (0–100 หลังปัด) — A 80–100, B 60–79, C 0–59 */ + function quizCarryGradeFromTeamAverage(avgRounded) { + if (avgRounded >= 80) return 'A'; + if (avgRounded >= 60) return 'B'; + return 'C'; + } + + function quizCarryRollCardRewardForGrade(grade) { + const u = Math.random() * 100; + if (grade === 'A') { + if (u < 30) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' }; + if (u < 80) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'B') { + if (u < 20) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' }; + if (u < 50) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'C') { + if (u < 20) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + return null; + } + + function quizCarryBuildMissionRankList() { + const ranks = []; + if (myId != null) { + ranks.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + score: Math.max(0, Number(playLiveQuizScores[myId]) || 0), + characterId: me.characterId || getPlayCharacterId(), + }); + } + others.forEach((o, id) => { + if (!o) return; + ranks.push({ + id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id), + score: Math.max(0, Number(playLiveQuizScores[id]) || 0), + characterId: o.characterId || null, + }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + return ranks.slice(0, 5); + } + + function cancelEmbedPreviewLobbyReturnTimer() { + if (embedPreviewLobbyReturnTimer != null) { + clearTimeout(embedPreviewLobbyReturnTimer); + embedPreviewLobbyReturnTimer = null; + } + } + + function cancelQuizCarryResultEndAfterTimeup() { + if (quizCarryResultEndTimer != null) { + clearTimeout(quizCarryResultEndTimer); + quizCarryResultEndTimer = null; + } + cancelEmbedPreviewLobbyReturnTimer(); + } + + /** หลังภาพ result-complete / gameover (editor embed + preview): รอ 5s แล้วไปล็อบบี้ห้องพรีวิว */ + function scheduleEmbedPreviewReturnToLobbyAfterResultEnd() { + cancelEmbedPreviewLobbyReturnTimer(); + if (!(previewMode && editorEmbedReturn)) return; + embedPreviewLobbyReturnTimer = setTimeout(function () { + embedPreviewLobbyReturnTimer = null; + window.location.href = buildRoomLobbyReturnHref(); + }, 5000); + } + + /** มีผู้เล่นอย่างน้อยหนึ่งคนได้คะแนน > 0 (= ส่งคำตอบถูกที่ฮับอย่างน้อย 1 ครั้ง; คะแนนต่อข้อคงที่ QUIZ_CARRY_POINTS_PER_CORRECT) */ + function quizCarryAnyPlayerHasCorrectAnswer() { + if (!playLiveQuizScores || typeof playLiveQuizScores !== 'object') return false; + const keys = Object.keys(playLiveQuizScores); + for (let i = 0; i < keys.length; i++) { + if (Math.max(0, Number(playLiveQuizScores[keys[i]]) || 0) > 0) return true; + } + return false; + } + + function hideQuizCarryResultEndLayer() { + cancelEmbedPreviewLobbyReturnTimer(); + const el = document.getElementById('quiz-carry-result-end-layer'); + if (!el) return; + el.classList.add('is-hidden'); + el.setAttribute('aria-hidden', 'true'); + } + + function showQuizCarryResultEndLayer(useComplete) { + const el = document.getElementById('quiz-carry-result-end-layer'); + const img = document.getElementById('quiz-carry-result-end-img'); + if (!el || !img) return; + const fname = useComplete ? 'result-complete.png' : 'result-gameover.png'; + img.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now()); + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + } + + /** Embed editor: หลัง timeup บนโต๊ะ — รอ 5 วิ แล้วโชว์ result-complete / result-gameover (ใช้ร่วม quiz_carry + crown) */ + function scheduleEmbedDeskResultAfterDelay(okResolver) { + cancelQuizCarryResultEndAfterTimeup(); + hideQuizCarryResultEndLayer(); + const resolve = typeof okResolver === 'function' ? okResolver : function () { return !!okResolver; }; + quizCarryResultEndTimer = setTimeout(function () { + quizCarryResultEndTimer = null; + let anyOk = false; + try { + anyOk = !!resolve(); + } catch (e) { + anyOk = false; + } + hideQuizCarryTimeupOnDeskLayer(); + showQuizCarryResultEndLayer(anyOk); + scheduleEmbedPreviewReturnToLobbyAfterResultEnd(); + }, 5000); + } + + function hideQuizCarryTimeupOnDeskLayer() { + cancelQuizCarryResultEndAfterTimeup(); + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) return; + const anchor = el.querySelector('.qc-timeup-desk-anchor') || el.querySelector('.qc-timeup-desk-inner'); + if (anchor) { + anchor.style.left = ''; + anchor.style.top = ''; + anchor.style.width = ''; + anchor.style.height = ''; + } + el.classList.add('is-hidden'); + el.setAttribute('aria-hidden', 'true'); + } + + /** หลังกดรับหลักฐาน (embed preview) — แสดง timeup กลางแคนวาส (Jumper mnptfts2 = img/Jumper/result-timeup.png) แยกจากป๊อปสรุปคะแนน + * @param {function():boolean} [embedResultOkFn] — ถ้าไม่ส่ง = ใช้คะแนน quiz_carry (คนตอบถูก); ส่งเมื่อ crown เพื่อเลือก complete vs gameover */ + function showQuizCarryTimeupOnDeskLayer(embedResultOkFn) { + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) return; + const img = document.getElementById('quiz-carry-timeup-txt-img'); + if (img) { + if (isJumpSurviveMissionUiMapPlay()) { + img.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + }; + img.src = jumperAssetUrl('result-timeup.png') + '?v=' + String(Date.now()); + } else { + img.onerror = null; + img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + } + } + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + try { + syncQuizCarryTimeupDeskLayerPosition(); + } catch (e) { /* ignore */ } + if (previewMode && editorEmbedReturn) { + scheduleEmbedDeskResultAfterDelay(embedResultOkFn || quizCarryAnyPlayerHasCorrectAnswer); + } + } + + function showQuizCarryMissionSummaryOverlay(opts) { + opts = opts || {}; + const forceF = !!opts.forceGradeF; + const overlay = document.getElementById('quiz-carry-mission-overlay'); + if (!overlay) return; + hideQuizCarryTimeupOnDeskLayer(); + const ranks = quizCarryBuildMissionRankList(); + const parts = ranks.map((r) => Math.max(0, Number(r.score) || 0)); + const total = parts.reduce((a, b) => a + b, 0); + const n = ranks.length; + const avgForGrade = n ? Math.min(100, Math.max(0, Math.round(total / n))) : 0; + const grade = forceF ? 'F' : quizCarryGradeFromTeamAverage(avgForGrade); + const reward = grade === 'F' ? null : quizCarryRollCardRewardForGrade(grade); + + const rowEl = document.getElementById('qc-mission-rank-row'); + const totalEl = document.getElementById('qc-mission-total-line'); + const gradeEl = document.getElementById('qc-mission-grade'); + const cardTextEl = document.getElementById('qc-mission-card-text'); + const checkEl = document.getElementById('qc-mission-check'); + const successTxt = document.getElementById('qc-mission-success-txt'); + const rankTagLabels = ['[1st]', '[2nd]', '[3rd]', '[4th]', '[5th]']; + if (rowEl) { + rowEl.innerHTML = ''; + ranks.forEach((r, idx) => { + const rank = idx + 1; + const cell = document.createElement('div'); + cell.className = 'qc-mission-rank-cell'; + const tag = document.createElement('div'); + tag.className = 'qc-mission-rank-tag'; + tag.textContent = rankTagLabels[rank - 1] || ('[' + rank + ']'); + const frame = document.createElement('div'); + frame.className = 'qc-mission-rank-frame'; + const img = document.createElement('img'); + img.className = 'qc-mission-rank-avatar'; + img.alt = ''; + applyQuizCarryRankAvatarTint(img, r.characterId, r.id); + img.onerror = function () { + this.onerror = null; + this.src = 'data:image/svg+xml,' + encodeURIComponent('?'); + }; + frame.appendChild(img); + const nick = document.createElement('div'); + nick.className = 'qc-mission-rank-nick'; + nick.textContent = r.nickname; + nick.title = r.nickname; + const sc = document.createElement('div'); + sc.className = 'qc-mission-rank-score'; + sc.textContent = String(r.score); + cell.appendChild(tag); + cell.appendChild(frame); + cell.appendChild(nick); + cell.appendChild(sc); + rowEl.appendChild(cell); + }); + } + if (totalEl) { + totalEl.textContent = ''; + if (!n) { + totalEl.textContent = '—'; + } else { + const lab = document.createElement('span'); + lab.textContent = 'คะแนนรวม '; + const nums = document.createElement('span'); + nums.className = 'qc-mission-total-nums'; + nums.textContent = '(' + parts.join('+') + ') = ' + total; + const tail = document.createElement('span'); + tail.textContent = ' · เฉลี่ยทีม ' + avgForGrade + '/100'; + totalEl.appendChild(lab); + totalEl.appendChild(nums); + totalEl.appendChild(tail); + } + } + if (gradeEl) { + gradeEl.textContent = grade; + gradeEl.className = 'qc-mission-grade qc-mission-grade--' + String(grade).toLowerCase(); + } + const okMission = grade !== 'F'; + if (checkEl) checkEl.style.display = okMission ? '' : 'none'; + if (successTxt) { + successTxt.style.display = okMission ? '' : 'none'; + if (okMission) successTxt.textContent = 'ภารกิจสำเร็จ'; + } + if (cardTextEl) { + cardTextEl.textContent = ''; + if (grade === 'F') { + const pl = document.createElement('div'); + pl.className = 'qc-mission-reward-plaque qc-mission-reward-plaque--muted'; + const t = document.createElement('div'); + t.className = 'qc-mission-reward-plaque-title'; + t.textContent = 'ไม่ได้รับการ์ด'; + const s = document.createElement('div'); + s.className = 'qc-mission-reward-plaque-sub'; + s.textContent = 'เกรด F'; + pl.appendChild(t); + pl.appendChild(s); + cardTextEl.appendChild(pl); + } else if (reward) { + const pl = document.createElement('div'); + pl.className = 'qc-mission-reward-plaque'; + const t = document.createElement('div'); + t.className = 'qc-mission-reward-plaque-title'; + t.textContent = reward.th; + const s = document.createElement('div'); + s.className = 'qc-mission-reward-plaque-sub'; + s.textContent = reward.en; + pl.appendChild(t); + pl.appendChild(s); + cardTextEl.appendChild(pl); + } + } + overlay.classList.remove('is-hidden'); + const btn = document.getElementById('btn-quiz-carry-mission-done'); + if (btn) { + btn.onclick = function () { + overlay.classList.add('is-hidden'); + if (previewMode && editorEmbedReturn) { + showQuizCarryTimeupOnDeskLayer(); + } else { + window.location.href = buildRoomLobbyReturnHref(); + } + }; + } + } + + function showQuizCarrySessionCompleteOverlay() { + if (quizCarrySessionEnded) return; + quizCarrySessionEnded = true; + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + playQuizText = 'ครบชุดคำถามแล้ว'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'ครบ ' + String(quizCarryRoundsCompleted) + ' ข้อ'; + const missionOv = document.getElementById('quiz-carry-mission-overlay'); + if (missionOv && quizCarryUseMissionSummaryOverlay) { + showQuizCarryMissionSummaryOverlay({ + forceGradeF: !!(playQuizPlayerLocal && playQuizPlayerLocal.eliminated), + }); + renderPlayQuizScoreboard(playLiveQuizScores); + return; + } + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) titleEl.textContent = 'เกมจบ · Game over'; + msgEl.textContent = 'ครบจำนวนคำถามที่ตั้งไว้ (' + String(quizCarryRoundsCompleted) + ' ข้อ) · Quiz carry session finished'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + score: Math.max(0, Number(playLiveQuizScores[myId]) || 0), + }); + } + others.forEach((o, id) => { + if (!o) return; + ranks.push({ + id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id), + score: Math.max(0, Number(playLiveQuizScores[id]) || 0), + }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + renderPlayQuizScoreboard(playLiveQuizScores); + } + + /** Toast กลางบน (หยิบ / ถูก / ผิด / คำใบ้) — ปิดการแสดงตามคำขอ UX */ + function showQuizCarryToast(msg, ok) { + if (typeof window.__quizCarryToastT === 'number') clearTimeout(window.__quizCarryToastT); + window.__quizCarryToastT = null; + const el = document.getElementById('play-quiz-feedback'); + if (!el) return; + el.classList.add('is-hidden'); + el.className = 'play-quiz-feedback'; + el.textContent = ''; + } + + /** + * หยิบ/ส่งคำตอบ — ต้องกด F (fromKey) สำหรับผู้เล่น · บอท preview เรียกแบบ silent เมื่อหยุดเดินแล้ว + * @returns {boolean} ทำ action สำเร็จหรือไม่ + */ + function tryQuizCarryInteractionForPlayer(actorId, ox, oy, opts) { + opts = opts || {}; + if (quizCarryPregameActive) return false; + if (quizCarrySessionEnded) return false; + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return false; + const md = mapData; + const footprint = quizTilesFootprintPlay(ox, oy); + const ent = actorId === myId ? me : others.get(actorId); + if (!ent) return false; + let pickupIdx = null; + for (const k of footprint) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + const opt = quizCarryOptionIndexAtPlay(md, tx, ty); + if (opt != null) pickupIdx = pickupIdx === null ? opt : pickupIdx; + } + const canSubmitAnswer = quizCarryCanSubmitAnswerAt(md, ox, oy); + const correct = quizCarryCurrent.correctIndex; + let held = ent.quizCarryHeld; + if (held != null && canSubmitAnswer) { + if (!quizCarryOptionsPickableNow()) return false; + ent.quizCarryHeld = null; + const ok = held === correct; + if (ok) { + playLiveQuizScores[actorId] = (playLiveQuizScores[actorId] || 0) + QUIZ_CARRY_POINTS_PER_CORRECT; + const lim = previewMode && isQuizCarry() ? quizCarrySessionLength : 0; + const willEnd = lim > 0 && (quizCarryRoundsCompleted + 1) >= lim; + const showMapFb = !opts.silent && actorId === myId; + if (showMapFb) showQuizMapQuestionCarryFeedback(true); + if (!opts.silent) { + const pts = QUIZ_CARRY_POINTS_PER_CORRECT; + showQuizCarryToast(willEnd ? ('ถูกต้อง +' + pts + ' แต้ม — ครบชุดคำถาม') : ('ถูกต้อง +' + pts + ' แต้ม — สุ่มคำถามใหม่'), true); + } + me.quizCarryHeld = null; + others.forEach((o) => { if (o) o.quizCarryHeld = null; }); + const afterOk = () => { quizCarryAfterRoundResolved(); }; + if (showMapFb) { + window.setTimeout(afterOk, 560); + } else { + afterOk(); + } + } else { + if (!opts.silent && actorId === myId) { + showQuizMapQuestionCarryFeedback(false); + } + if (!opts.silent) { + showQuizCarryToast('ผิด — คืนป้ายที่โซนตัวเลือกแล้ว กด F หยิบใหม่ได้', false); + } + } + renderPlayQuizScoreboard(playLiveQuizScores); + return true; + } + if (held == null && pickupIdx != null) { + if (!quizCarryOptionsPickableNow()) return false; + if (quizCarryOptionHeldByAnyone(pickupIdx)) { + if (opts.fromKey && actorId === myId && !opts.silent) { + showQuizCarryToast('ตัวเลือกนี้มีคนถืออยู่แล้ว — เลือกข้ออื่น', false); + } + return false; + } + ent.quizCarryHeld = pickupIdx; + const label = (quizCarryCurrent.choices && quizCarryCurrent.choices[pickupIdx]) || String(pickupIdx); + if (!opts.silent) { + showQuizCarryToast('หยิบ: ' + label, true); + } + return true; + } + if (opts.fromKey && actorId === myId && !opts.silent) { + const t = Date.now(); + if (t - (me.quizCarryHintT || 0) > 2200) { + me.quizCarryHintT = t; + if (held != null && !canSubmitAnswer) { + showQuizCarryToast(quizCarryMapHasAnswerInteractive(md) + ? 'ถือป้ายอยู่ — ยืนที่โซน interactive (เขียว) หรือชิดขอบแล้วกด F เพื่อส่ง' + : 'ถือป้ายอยู่ — ยืนชิดโซนกลาง (ม่วง) แล้วกด F เพื่อส่ง', false); + } else { + showQuizCarryToast(quizCarryMapHasAnswerInteractive(md) + ? 'ยืนโซนตัวเลือกแล้วกด F หยิบ · ยืนโซนเขียว (interactive) แล้วกด F ส่งเมื่อถือป้าย' + : 'ยืนโซนตัวเลือกแล้วกด F หยิบ · ยืนชิดโซนกลางแล้วกด F ส่งเมื่อถือป้าย', false); + } + } + } + return false; + } + + function quizCarryGrabCoreGatesOk() { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return false; + if (quizCarryPregameActive || quizCarrySessionEnded) return false; + if (myId == null) return false; + if (isQuizCarryEmbedCountdownBlockingMovement()) return false; + if (!quizCarryOptionsPickableNow()) return false; + return true; + } + + /** ดัชนีตัวเลือก 0-based ที่ผู้เล่นท้องถิ่นยืนอยู่และหยิบได้ — ไม่มีคืน null */ + function getQuizCarryLocalGrabbableOptionIndex() { + if (!quizCarryGrabCoreGatesOk()) return null; + if (me.quizCarryHeld != null) return null; + const md = mapData; + const footprint = quizTilesFootprintPlay(me.x, me.y); + let pickupIdx = null; + for (const k of footprint) { + const p = k.split(','); + const tx = +p[0]; + const ty = +p[1]; + const opt = quizCarryOptionIndexAtPlay(md, tx, ty); + if (opt != null) pickupIdx = pickupIdx === null ? opt : pickupIdx; + } + if (pickupIdx == null || quizCarryOptionHeldByAnyone(pickupIdx)) return null; + return pickupIdx; + } + + /** ยืนบนช่องตัวเลือกและหยิบได้ */ + function quizCarryGrabPickupAvailable() { + return getQuizCarryLocalGrabbableOptionIndex() != null; + } + + /** ถือป้ายแล้วยืนโซนส่งได้ */ + function quizCarryGrabSubmitAvailable() { + if (!quizCarryGrabCoreGatesOk()) return false; + if (me.quizCarryHeld == null) return false; + return quizCarryCanSubmitAnswerAt(mapData, me.x, me.y); + } + + /** true = กด F / ปุ่ม จะหยิบหรือส่งได้ทันที */ + function quizCarryGrabInteractionAvailable() { + return quizCarryGrabPickupAvailable() || quizCarryGrabSubmitAvailable(); + } + + function syncQuizCarryGrabButton() { + const btn = document.getElementById('quiz-carry-grab-btn'); + if (!btn) return; + if (!isQuizCarry() || !mapData) { + btn.classList.add('is-hidden'); + btn.classList.remove('quiz-carry-grab-btn--active'); + btn.classList.remove('quiz-carry-grab-btn--place'); + btn.setAttribute('aria-disabled', 'true'); + return; + } + const grabSrc = BASE + '/img/quiz-carry/btn-grab.png'; + const placeSrc = BASE + '/img/quiz-carry/btn-drop.png'; + const img = btn.querySelector('img'); + btn.classList.remove('is-hidden'); + const active = quizCarryGrabInteractionAvailable(); + btn.classList.toggle('quiz-carry-grab-btn--active', active); + btn.setAttribute('aria-disabled', active ? 'false' : 'true'); + btn.style.pointerEvents = active ? '' : 'none'; + if (img) { + if (active && quizCarryGrabSubmitAvailable()) { + img.src = placeSrc; + btn.classList.add('quiz-carry-grab-btn--place'); + btn.title = 'ส่ง / วางคำตอบ — เหมือนกด F'; + btn.setAttribute('aria-label', 'ส่งหรือวางคำตอบ'); + } else { + img.src = grabSrc; + btn.classList.remove('quiz-carry-grab-btn--place'); + btn.title = 'หยิบตัวเลือก — เหมือนกด F'; + btn.setAttribute('aria-label', 'หยิบตัวเลือก (Grab)'); + } + } + } + + function resetQuizCarryPlayState() { + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryPool = []; + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownStartAt = 0; + quizCarryEmbedCountdownEndAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + quizCarryRoundsCompleted = 0; + quizCarrySessionEnded = false; + hideQuizCarryEmbedCountdownOverlay(); + hideQuizCarryTimeupOnDeskLayer(); + hideQuizCarryResultEndLayer(); + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + } + + /** รวม carry จากดิสก์ — ใช้เฉพาะ GET ที่ Node รองรับ (path เดียวกับ server.js) ไม่เรียก .php เพื่อกัน 404 บน nginx/php-fpm */ + async function mergeQuizCarrySettingsFromDisk(s) { + const out = s && typeof s === 'object' ? s : {}; + try { + const rd = await fetch(BASE + '/api/quiz-carry-from-disk?_=' + Date.now(), { cache: 'no-store' }); + if (rd.ok) { + const disk = await rd.json(); + if (disk && typeof disk === 'object') { + if (disk.carryMapPanelTheme && typeof disk.carryMapPanelTheme === 'object') { + out.carryMapPanelTheme = disk.carryMapPanelTheme; + } + if (disk.carryEmbedCountdownTheme && typeof disk.carryEmbedCountdownTheme === 'object') { + out.carryEmbedCountdownTheme = disk.carryEmbedCountdownTheme; + } + if (Array.isArray(disk.carryChoicePlaqueThemes) && disk.carryChoicePlaqueThemes.length) { + out.carryChoicePlaqueThemes = disk.carryChoicePlaqueThemes; + } else if (disk.carryChoicePlaqueTheme && typeof disk.carryChoicePlaqueTheme === 'object') { + out.carryChoicePlaqueTheme = disk.carryChoicePlaqueTheme; + } + if (disk.carryReadMs != null) out.carryReadMs = disk.carryReadMs; + if (disk.carryAnswerMs != null) out.carryAnswerMs = disk.carryAnswerMs; + if (disk.carrySessionLength != null) out.carrySessionLength = disk.carrySessionLength; + const diskPlaqueScale = Number(disk.carryChoicePlaqueMapScale); + if (Number.isFinite(diskPlaqueScale)) { + out.carryChoicePlaqueMapScale = Math.max(0.85, Math.min(2.5, diskPlaqueScale)); + } + if (Array.isArray(disk.carryQuestions) && disk.carryQuestions.length > 0) { + out.carryQuestions = disk.carryQuestions; + } + } + } + } catch (e) { /* ignore */ } + return out; + } + + async function loadQuizCarryPoolAndStart() { + quizCarryRoundsCompleted = 0; + quizCarrySessionEnded = false; + hideQuizCarryTimeupOnDeskLayer(); + hideQuizCarryResultEndLayer(); + quizCarrySessionLength = 0; + let pool = []; + let s = {}; + const snap = quizCarryJoinSettingsSnap && typeof quizCarryJoinSettingsSnap === 'object' ? quizCarryJoinSettingsSnap : null; + try { + const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' }); + if (r.ok) { + const raw = await r.text(); + const trimmed = (raw || '').trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + const parsed = JSON.parse(trimmed); + if (parsed && typeof parsed === 'object') s = parsed; + } + } + } catch (e) { /* Node/HTML error — ต่อจาก merge ดิสก์ */ } + try { + s = await mergeQuizCarrySettingsFromDisk(s); + } catch (e) { /* ignore */ } + if (snap) { + if (s.carryReadMs == null && snap.carryReadMs != null) s.carryReadMs = snap.carryReadMs; + if (s.carryAnswerMs == null && snap.carryAnswerMs != null) s.carryAnswerMs = snap.carryAnswerMs; + if (s.carrySessionLength == null && snap.carrySessionLength != null) s.carrySessionLength = snap.carrySessionLength; + } + if (snap && snap.carryMapPanelTheme && typeof snap.carryMapPanelTheme === 'object') { + s.carryMapPanelTheme = snap.carryMapPanelTheme; + } + if (snap && snap.carryEmbedCountdownTheme && typeof snap.carryEmbedCountdownTheme === 'object') { + s.carryEmbedCountdownTheme = snap.carryEmbedCountdownTheme; + } + /* ไม่ทับ carryChoicePlaqueThemes จาก snap ถ้ามีแล้ว — snapshot ตอน join อาจเก่ากว่าไฟล์ที่เพิ่งบันทึกใน Admin (รูปป้ายหาย) */ + if (snap && (!Array.isArray(s.carryChoicePlaqueThemes) || !s.carryChoicePlaqueThemes.length)) { + if (Array.isArray(snap.carryChoicePlaqueThemes) && snap.carryChoicePlaqueThemes.length) { + s.carryChoicePlaqueThemes = snap.carryChoicePlaqueThemes; + } else if (snap.carryChoicePlaqueTheme && typeof snap.carryChoicePlaqueTheme === 'object') { + s.carryChoicePlaqueTheme = snap.carryChoicePlaqueTheme; + } + } + if (snap && (s.carryChoicePlaqueMapScale == null || !Number.isFinite(Number(s.carryChoicePlaqueMapScale)))) { + const sm = Number(snap.carryChoicePlaqueMapScale); + if (Number.isFinite(sm)) s.carryChoicePlaqueMapScale = sm; + } + { + const sc = Number(s.carryChoicePlaqueMapScale); + quizCarryPlaqueMapScale = Number.isFinite(sc) ? Math.max(0.85, Math.min(2.5, sc)) : 1.25; + } + try { + quizCarryCarryTimingMs = { + carryReadMs: clampPreviewMs(s.carryReadMs, 3000, 0, 120000), + carryAnswerMs: clampPreviewMs(s.carryAnswerMs, 5000, 1000, 300000), + }; + quizCarrySessionLength = clampCarrySessionLen(s.carrySessionLength, 0); + setQuizCarryMapPanelThemeFromApi(s); + setQuizCarryEmbedCountdownThemeFromApi(s); + setQuizCarryChoicePlaqueThemeFromApi(s); + for (let ti = 0; ti < QUIZ_CARRY_MAX_OPTION_SLOTS; ti++) { + const tUrl = sanitizeQuizCarryImageUrlClient(getEffectiveCarryChoicePlaqueThemeForChoice(ti).plaqueImageUrl); + if (!tUrl || quizCarryChoiceImageCache.has(tUrl)) continue; + const pim = new Image(); + quizCarryApplyImageCrossOrigin(pim, tUrl); + pim.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(tUrl, pim); + try { + pim.src = tUrl; + } catch (e) { /* ignore */ } + } + const carryOnly = s && Array.isArray(s.carryQuestions) && s.carryQuestions.length > 0; + const source = carryOnly ? s.carryQuestions : (s && Array.isArray(s.questions) ? s.questions : []); + for (let i = 0; i < source.length; i++) { + const n = normalizeQuizCarryQuestionFromAny(source[i]); + if (n) pool.push(n); + } + } catch (e) { /* map only */ } + if (!pool.length && mapData) pool = buildQuizCarryPoolFromMap(mapData); + quizCarryPool = pool; + for (let pi = 0; pi < pool.length; pi++) preloadQuizCarryChoiceImages(pool[pi]); + playLiveQuizScores = {}; + if (myId != null) playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + if (!pool.length) { + playQuizText = 'ไม่มีคำถาม — ตั้งใน Admin → คำถามหลายตัวเลือก หรือใส่ในแมป (quizQuestions)'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + try { + syncPlayQuizMapPanel(); + } catch (e) { /* ignore */ } + quizCarryJoinSettingsSnap = null; + return; + } + if (previewMode && editorEmbedReturn) { + beginQuizCarryEmbedPregame(); + } else if (isDetectiveMinigamePlay()) { + quizCarryPickNextQuestion(); + } else { + quizCarryPickNextQuestion(); + } + if (!(previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now())) { + quizCarryRestoreEmbedPhaseLabel(); + } + try { + syncPlayQuizMapPanel(); + } catch (e) { /* ignore */ } + quizCarryJoinSettingsSnap = null; + } + + function setupPlayQuizCarryUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) { + if (isDetectiveMinigamePlay() || (previewMode && editorEmbedReturn)) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } else { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + } + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + const leg = document.getElementById('quiz-play-legend'); + if (leg) { + leg.textContent = isDetectiveMinigamePlay() + ? '' + : (quizCarryMapHasAnswerInteractive(mapData) + ? 'แต่ละตัวเลือกมีคนถือได้คนเดียว — ส่งคำตอบที่โซน interactive (เขียว) · โซนกลางม่วงเป็นกำแพง · กด F หรือปุ่ม GRAB มุมขวาล่าง' + : 'แต่ละตัวเลือกมีคนถือได้คนเดียว — โซนกลางเป็นกำแพง · ยืนชิดขอบแล้วกด F หรือปุ่ม GRAB ส่งเมื่อถือป้าย'); + } + resetQuizCarryPlayState(); + if (previewMode) { + loadQuizCarryPoolAndStart(); + } else { + playQuizText = 'เข้าห้องแล้ว — คำถามจากแมป (เล่นจริงแนะนำซิงก์ผ่านโฮสต์ในอนาคต)'; + loadQuizCarryPoolAndStart(); + } + renderPlayQuizScoreboard(playLiveQuizScores); + syncQuizCarryGrabButton(); + } + + function pickRandomTileForQuizCarryOption(md, optionIndex, o) { + const w = md.width || 20, h = md.height || 15; + const want = optionIndex + 1; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.quizCarryOptionArea && md.quizCarryOptionArea[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === want && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileInQuizCarryHub(md, o) { + const w = md.width || 20, h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.quizCarryHubArea && md.quizCarryHubArea[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1 && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileInQuizCarryInteractive(md, o) { + const w = md.width || 20, h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.interactive && md.interactive[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1 && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileForQuizCarrySubmit(md, o) { + if (quizCarryMapHasAnswerInteractive(md)) { + const p = pickRandomTileInQuizCarryInteractive(md, o); + if (p) return p; + } + return pickRandomTileInQuizCarryHub(md, o); + } + + /** ช่วงไม่มีข้อ / รอตัวเลือก — ให้บอทเดินเล่นในแมปเหมือน preview ทั่วไป (กันยืนนิ่งทั้งเกม) */ + function quizCarryPreviewBotLobbyWanderStep(o, w, h) { + const now = Date.now(); + o.botIsWalking = false; + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600; + if (now >= o.botWanderNextTurn) { + o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200); + if (Math.random() < 0.55) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + } + const accX = o.botWanderDx; + const accY = o.botWanderDy; + if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up'; + else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left'; + const step = MOVE_SPEED; + const nx = o.x + accX * step; + const ny = o.y + accY * step; + const ox = o.x; + const oy = o.y; + if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + } + if (!Number.isFinite(o.x)) o.x = 0.5; + if (!Number.isFinite(o.y)) o.y = 0.5; + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) o.botIsWalking = true; + } + + function stepQuizCarryPreviewBots() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!playBotsEnabled() || !isQuizCarry() || !mapData) return; + const w = mapData.width || 20, h = mapData.height || 15; + const nowPf = performance.now(); + const pathfindMinGap = editorEmbedReturn ? 280 : 140; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + if (quizCarryFootprintOverlapsHub(o.x, o.y)) { + const sn = snapPositionOutOfQuizCarryHubIfNeeded(o.x, o.y); + if (Math.abs(sn.x - o.x) > 0.02 || Math.abs(sn.y - o.y) > 0.02) { + o.x = sn.x; + o.y = sn.y; + o.botPath = []; + o.botPathStuckTicks = 0; + o.botQuizCarryPathfindAfter = 0; + } + } + if (!o.botPath || o.botPath.length === 0) { + tryQuizCarryInteractionForPlayer(id, o.x, o.y, { silent: true }); + } + /* ช่วงนับถอยหลัง / เปลี่ยนข้อ — ยังให้เดินได้ */ + if (!quizCarryCurrent) { + quizCarryPreviewBotLobbyWanderStep(o, w, h); + return; + } + const tier = o.botTier || 'avg'; + const pCorrect = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.35 : 0.65; + const rawWant = Math.random() < pCorrect ? quizCarryCurrent.correctIndex + : Math.floor(Math.random() * Math.max(2, (quizCarryCurrent.choices || []).length)); + const wantIdx = pickQuizCarryTargetOptionIndexAvoidingTaken(rawWant); + if (!o.botPath || !o.botPath.length) { + if (typeof o.botQuizCarryPathfindAfter !== 'number') o.botQuizCarryPathfindAfter = 0; + if (nowPf < o.botQuizCarryPathfindAfter) { + quizCarryPreviewBotLobbyWanderStep(o, w, h); + return; + } + const jitter = (String(id).length * 31 + ((o.x | 0) ^ (o.y | 0)) * 7) % 160; + if (quizCarryOptionsPickableNow()) { + if (o.quizCarryHeld == null && wantIdx != null) { + const dest = pickRandomTileForQuizCarryOption(mapData, wantIdx, o); + if (dest) { + const path = pathfindPlayForBot(o.x, o.y, dest.x + 0.5, dest.y + 0.5, o); + if (path && path.length > 1) o.botPath = path.slice(1); + } + } else { + const sub = pickRandomTileForQuizCarrySubmit(mapData, o); + if (sub) { + const path = pathfindPlayForBot(o.x, o.y, sub.x + 0.5, sub.y + 0.5, o); + if (path && path.length > 1) o.botPath = path.slice(1); + } + } + o.botQuizCarryPathfindAfter = nowPf + pathfindMinGap + jitter; + } else { + o.botQuizCarryPathfindAfter = nowPf + 120; + quizCarryPreviewBotLobbyWanderStep(o, w, h); + } + } else { + stepPreviewBotAlongPath(o, w, h); + } + clampPlayEntityFootprintToMap(o, mapData); + }); + separateClumpedPreviewBots(); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const ob = others.get(bid); + if (ob) clampPlayEntityFootprintToMap(ob, mapData); + }); + } + + function getStackAreaBoundsPlay(area, w, h) { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let any = false; + for (let y = 0; y < h; y++) { + const row = area && area[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1) { + any = true; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + } + if (!any) return null; + return { + minX, maxX, minY, maxY, + cx: (minX + maxX + 1) / 2, + cy: (minY + maxY + 1) / 2, + }; + } + + function getStackCameraCentersPx() { + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const rel = getStackAreaBoundsPlay(mapData.stackReleaseArea, w, h); + let cx = (w / 2) * ts; + let cy = (h / 2) * ts; + if (land && rel) { + cx = land.cx * ts; + cy = ((land.cy + rel.cy) / 2) * ts; + } else if (land) { + cx = land.cx * ts; + cy = land.cy * ts; + } else if (rel) { + cx = rel.cx * ts; + cy = rel.cy * ts; + } + return { px: cx, py: cy }; + } + + /** ใช้ playStackBlockWidthTiles + แผนที่ — เรียกหลังโหลด /api/game-timing เมื่อหอว่าง */ + function reapplyStackMiniSizingFromGlobals() { + if (!mapData || !isStack() || !stackMini) return; + if (stackMini.layers && stackMini.layers.length > 0) return; + if (stackFall || stackMini.settling) return; + const wMap = mapData.width || 20; + const hMap = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap); + const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2)); + const iw = playStackBlockWidthTiles != null && Number.isFinite(playStackBlockWidthTiles) + ? Math.max(0.85, Math.min(3.2, playStackBlockWidthTiles)) + : autoW; + stackMini.initialWidthTiles = iw; + stackMini.widthTiles = iw; + } + + function resetStackMinigameState() { + stackMini = null; + if (!mapData || !isStack()) return; + const w = mapData.width || 20, h = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const rel = getStackAreaBoundsPlay(mapData.stackReleaseArea, w, h); + let towerCX = w * 0.5; + let swingAmp = 1.85; + let floorWorldY = (h - 1) * tileSize; + let swingWorldY = floorWorldY - tileSize * 1.35; + if (land) { + towerCX = land.cx; + floorWorldY = (land.maxY + 1) * tileSize; + const span = Math.max(1, land.maxX - land.minX + 1); + swingAmp = Math.min(span * 0.44, Math.max(0.6, span * 0.36)); + } + if (rel) { + swingWorldY = (rel.cy + 0.5) * tileSize; + const spanR = Math.max(1, rel.maxX - rel.minX + 1); + swingAmp = Math.max(swingAmp, Math.min(spanR * 0.42, 4.2)); + } + const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2)); + const initW = playStackBlockWidthTiles != null && Number.isFinite(playStackBlockWidthTiles) + ? Math.max(0.85, Math.min(3.2, playStackBlockWidthTiles)) + : autoW; + stackFall = null; + stackMini = { + topCenterX: towerCX, + widthTiles: initW, + initialWidthTiles: initW, + craneWorldX: towerCX * tileSize, + swingAmp, + phase: Math.random() * Math.PI * 2, + phaseSpeed: playStackSwingHz, + floorWorldY, + swingWorldY, + layerWorldH: Math.max(14, tileSize * 0.3), + blockStripH: Math.max(16, tileSize * 0.32), + layers: [], + lives: 3, + score: 0, + combo: 0, + lastDropAt: 0, + stackTiltRad: 0, + stackTiltVel: 0, + }; + if (previewFillBots) { + me.stackPreviewHumanPts = 0; + stackPreviewHudLog = []; + } + } + + function stackEaseDrop(t) { + t = Math.max(0, Math.min(1, t)); + return t * t; + } + + /** ตกช้า/นุ่มขึ้นเมื่อปล่อยเพี้ยนกลาง (คล้าย Tower of Babel — มีน้ำหนักก่อนนิ่ง) */ + function stackEaseDropPhys(t, sloppyN) { + t = Math.max(0, Math.min(1, t)); + if (sloppyN < 0.14) return t * t; + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; + } + + /** + * @param {object} hit + * @param {{ human?: boolean, botId?: string } | undefined} previewAttribution — โหมด preview: แยกคะแนนแสดงในแถบ (ตึกยังเป็นของร่วม) + */ + function applyStackHitFromDrop(hit, previewAttribution) { + if (!stackMini || !hit) return; + if (hit.miss) { + stackMini.lives = Math.max(0, stackMini.lives - 1); + stackMini.combo = 0; + if (stackMini.lives <= 0) { + stackMini.score = Math.max(0, stackMini.score - 2); + stackMini.lives = 3; + stackMini.layers = []; + stackMini.widthTiles = stackMini.initialWidthTiles; + const lb = getStackAreaBoundsPlay(mapData.stackLandArea, mapData.width || 20, mapData.height || 15); + stackMini.topCenterX = lb ? lb.cx : (mapData.width || 20) * 0.5; + stackMini.combo = 0; + stackMini.stackTiltRad = 0; + stackMini.stackTiltVel = 0; + } + } else { + const W = stackMini.initialWidthTiles; + stackMini.layers.push({ w: W, cx: hit.placedCx }); + stackMini.score += hit.pts; + if (previewFillBots && previewAttribution) { + if (previewAttribution.human) { + me.stackPreviewHumanPts = (me.stackPreviewHumanPts || 0) + hit.pts; + } else if (previewAttribution.botId) { + const ob = others.get(previewAttribution.botId); + if (ob) ob.stackBotScore = (ob.stackBotScore || 0) + hit.pts; + } + } + stackMini.combo = hit.perfect ? stackMini.combo + 1 : 0; + stackMini.widthTiles = W; + stackMini.topCenterX = hit.placedCx; + } + } + + /** + * บล็อกทุกชั้นหน้ากว้างเท่ากัน (initialWidthTiles) · พลาดเมื่อทับแท่นไม่พอ + * วาดชั้นเป็นสี่เหลี่ยมแนวนอน (ไม่หมุนต่อชั้น) เพื่อกองตรง ไม่บิดเป็นกองไม้ + */ + function computeStackDropResult(m) { + const raw = m.swingAmp * Math.sin(m.phase); + const wMap = mapData.width || 20; + const hMap = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap); + const fallW = m.initialWidthTiles; + let supportCx; + let supportW; + if (m.layers.length === 0) { + supportCx = land ? land.cx : wMap * 0.5; + supportW = land ? (land.maxX - land.minX + 1) : Math.max(8, fallW * 2.2); + } else { + const sup = m.layers[m.layers.length - 1]; + supportCx = sup.cx; + supportW = fallW; + } + const c1 = m.topCenterX + raw; + const half1 = fallW * 0.5; + const half0 = supportW * 0.5; + const left = Math.max(supportCx - half0, c1 - half1); + const right = Math.min(supportCx + half0, c1 + half1); + const overlap = right - left; + const missThreshold = + m.layers.length === 0 + ? Math.max(0.32, fallW * 0.11) + : Math.max(0.26, fallW * 0.17); + const miss = overlap < missThreshold; + const placedCx = c1; + const delta = c1 - supportCx; + const span = supportW * 0.5 + fallW * 0.5; + const offN = Math.abs(delta) / Math.max(0.01, span); + const perfect = !miss && offN < 0.075; + const bonus = perfect ? Math.min(4, m.combo || 0) : 0; + const pts = perfect ? (2 + bonus) : (miss ? 0 : 1); + const supportRatio = fallW > 0 ? overlap / fallW : 0; + return { + miss, + perfect, + pts, + landOffsetTiles: raw, + placedW: fallW, + placedCx, + overlap, + supportRatio, + delta, + }; + } + + /** + * เริ่มแอนิเมชันบล็อกร่วง (ผู้เล่น/บอทใช้ path เดียวกัน) + * @param {object} hit จาก computeStackDropResult + * @param {{ human?: boolean, botId?: string } | null} previewActor โหมด preview: ใครเป็นคนปล่อย (สำหรับคะแนน + เลื่อนตา) + * @returns {boolean} + */ + function startStackFallAnimation(hit, previewActor) { + if (!stackMini || !hit || stackFall || stackMini.settling) return false; + const m = stackMini; + const tw = m.widthTiles * tileSize; + const swingXW = (m.topCenterX + m.swingAmp * Math.sin(m.phase)) * tileSize; + const lh = m.layerWorldH || Math.max(14, tileSize * 0.3); + let y1 = m.floorWorldY - (m.layers.length + 1) * lh; + if (y1 <= m.swingWorldY) y1 = m.swingWorldY + lh * 0.55; + const landOff = hit.landOffsetTiles || 0; + const landCxWorld = (m.topCenterX + landOff) * tileSize; + const xLeft0 = swingXW - tw / 2; + const xLeft1 = landCxWorld - tw / 2; + const offN = Math.min(1, Math.abs(landOff) / Math.max(0.01, m.swingAmp)); + const dur = Math.max(400, Math.min(1280, 240 + (y1 - m.swingWorldY) * 0.92 + offN * 560)); + const tiltMax = hit.miss + ? 0.28 * (0.25 + offN) + : 0.16 * offN * (hit.perfect ? 0.1 : 1); + const isHumanPreview = !!(previewActor && previewActor.human); + const botIdPreview = previewActor && previewActor.botId ? previewActor.botId : null; + stackFall = { + t0: performance.now(), + dur, + xLeft0, + xLeft1, + tw, + y0: m.swingWorldY, + y1, + hit, + tiltMax, + sloppyN: offN, + previewHumanDrop: isHumanPreview, + previewBotId: botIdPreview, + }; + return true; + } + + /** @returns {boolean} เริ่มแอนิเมชันร่วงแล้วหรือไม่ */ + function tryHumanStackDrop() { + if (!stackMini || !isStack() || stackFall || stackMini.settling) return false; + if (playBotsEnabled() && mapData && mapData.gameType === 'stack' && + (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount)) { + rebuildStackPreviewTurnOrder(); + } + if (playBotsEnabled() && !isStackPreviewHumanTurn()) return false; + const now = Date.now(); + if (now - stackMini.lastDropAt < 480) return false; + const hit = computeStackDropResult(stackMini); + if (!startStackFallAnimation(hit, playBotsEnabled() ? { human: true } : null)) return false; + if (playBotsEnabled()) { + lastStackPreviewActorId = '__human__'; + lastStackPreviewActorUntil = Date.now() + 900; + } + return true; + } + + /** บอท preview: ปล่อยแบบมีแอนิเมชันร่วงเหมือนผู้เล่น — apply หลังแอนิเมชันจบ */ + function simulatePreviewBotStackDrop(botId, o) { + if (!stackMini || stackFall || stackMini.settling) return; + const hit = computeStackDropResult(stackMini); + if (!startStackFallAnimation(hit, playBotsEnabled() ? { botId } : null)) return; + o.stackBotGlowUntil = Date.now() + Math.max(900, 400 + (stackFall && stackFall.dur ? stackFall.dur : 800)); + lastStackPreviewActorId = botId; + lastStackPreviewActorUntil = Date.now() + Math.max(1000, 500 + (stackFall && stackFall.dur ? stackFall.dur : 800)); + } + + /** Stack preview: สลับตา — เฉพาะบอทที่ถึงตาถึงจะจำลองปล่อยหนึ่งครั้งแล้วเลื่อนตา */ + function stepPreviewStackTurnBased(nowMs) { + if (!playBotsEnabled() || !isStack() || !stackMini || stackFall || stackMini.settling) return; + if (mapData && mapData.gameType === 'stack' && + (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== stackTurnCount)) { + rebuildStackPreviewTurnOrder(); + } + const order = stackPreviewTurnOrder; + if (!order || order.length !== stackTurnCount) return; + const cur = order[stackPreviewTurnIndex]; + if (!cur || cur.kind !== 'bot') return; + if (!cur.botId) { + advanceStackPreviewTurn(); + return; + } + const o = others.get(cur.botId); + if (!o) { + advanceStackPreviewTurn(); + return; + } + if (stackPreviewBotThinkUntil === 0) { + stackPreviewBotThinkUntil = nowMs + 320 + Math.random() * 480; + return; + } + if (nowMs < stackPreviewBotThinkUntil) return; + stackPreviewBotThinkUntil = 0; + simulatePreviewBotStackDrop(cur.botId, o); + } + + function stackTickFrame() { + const now = performance.now(); + const dt = Math.min(0.06, (now - lastStackTickMs) / 1000); + lastStackTickMs = now; + if (stackMini && stackMini.settling) { + const s = stackMini.settling; + const u = (now - s.t0) / s.dur; + if (u >= 1) { + const wasHuman = !!s.previewHumanDrop; + const settleBotId = s.previewBotId || null; + applyStackHitFromDrop({ miss: true, perfect: false, pts: 0, landOffsetTiles: 0 }, undefined); + if (playBotsEnabled() && (wasHuman || settleBotId)) { + stackPreviewLogStackDrop({ miss: true, perfect: false, pts: 0 }, { human: wasHuman, botId: settleBotId }); + } + stackMini.settling = null; + stackMini.lastDropAt = Date.now(); + if (playBotsEnabled() && (wasHuman || settleBotId)) advanceStackPreviewTurn(); + } + } else if (stackFall) { + const t = (now - stackFall.t0) / stackFall.dur; + if (t >= 1) { + const wasHuman = !!stackFall.previewHumanDrop; + const fallBotId = stackFall.previewBotId || null; + const hit = stackFall.hit; + const stableMin = 0.52; + if (hit.miss) { + applyStackHitFromDrop(hit); + if (playBotsEnabled() && (wasHuman || fallBotId)) { + stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId }); + } + stackMini.lastDropAt = Date.now(); + stackFall = null; + if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn(); + } else if (hit.supportRatio != null && hit.supportRatio < stableMin) { + const stripH = stackMini.blockStripH || Math.max(16, tileSize * 0.32); + stackMini.settling = { + t0: performance.now(), + dur: Math.max(820, Math.min(2600, 720 + (1 - hit.supportRatio) * 3400)), + hit, + xLeft0: stackFall.xLeft1, + yTop0: stackFall.y1, + twWorld: stackFall.tw, + stripH, + delta: hit.delta || 0, + previewHumanDrop: wasHuman, + previewBotId: fallBotId, + }; + stackFall = null; + } else { + let previewAttr; + if (playBotsEnabled()) { + if (wasHuman) previewAttr = { human: true }; + else if (fallBotId) previewAttr = { botId: fallBotId }; + } + applyStackHitFromDrop(hit, previewAttr); + if (playBotsEnabled() && (wasHuman || fallBotId)) { + stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId }); + } + stackMini.lastDropAt = Date.now(); + stackFall = null; + if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn(); + } + } + } else if (stackMini) { + stackMini.phase += stackMini.phaseSpeed * dt * Math.PI * 2; + } + stepPreviewStackTurnBased(Date.now()); + } + + function applyStackPreviewSpawnLayout() { + if (!mapData || mapData.gameType !== 'stack') return; + const w = mapData.width || 20, h = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const cx = land ? land.cx : w / 2 - 0.5; + const cy = land ? land.cy : h / 2 - 0.5; + me.x = cx; + me.y = cy; + me.tx = cx; + me.ty = cy; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.x = cx + (Math.random() - 0.5) * 0.15; + o.y = cy + (Math.random() - 0.5) * 0.15; + o.tx = o.x; + o.ty = o.y; + o.stackBotScore = 0; + o.stackBotNextTry = Date.now() + 300 + Math.random() * 700; + }); + } + + function drawStackMinigame(ctx, worldToScreen, zoom) { + if (!stackMini) return; + const m = stackMini; + const layerWorldH = m.layerWorldH || Math.max(14, tileSize * 0.3); + const stripH = m.blockStripH || Math.max(16, tileSize * 0.32); + const floorY = m.floorWorldY; + const guideTopY = Math.max(0, m.swingWorldY - tileSize * 6.5); + const [gtx, gty] = worldToScreen(m.topCenterX * tileSize, guideTopY); + const [gbx, gby] = worldToScreen(m.topCenterX * tileSize, floorY); + ctx.strokeStyle = 'rgba(255,255,255,0.22)'; + ctx.lineWidth = Math.max(1, zoom * 0.85); + ctx.setLineDash([5, 7]); + ctx.beginPath(); + ctx.moveTo(gtx, gty); + ctx.lineTo(gbx, gby); + ctx.stroke(); + ctx.setLineDash([]); + ctx.lineWidth = 1; + + const nLay = m.layers.length; + for (let i = 0; i < nLay; i++) { + const L = m.layers[i]; + const lw = (L.w != null ? L.w : m.initialWidthTiles) * tileSize; + const cxW = L.cx * tileSize; + const yb = floorY - (i + 1) * layerWorldH; + const xl = cxW - lw / 2; + const [sx, sy] = worldToScreen(xl, yb); + const drawW = lw * zoom; + const drawH = layerWorldH * zoom; + const hue = (i * 47) % 360; + ctx.fillStyle = 'hsla(' + hue + ', 68%, 56%, 0.9)'; + ctx.fillRect(sx, sy, drawW, drawH); + ctx.strokeStyle = 'rgba(255,255,255,0.38)'; + ctx.lineWidth = 1; + ctx.strokeRect(sx, sy, drawW, drawH); + } + if (m.settling) { + const s = m.settling; + const u = Math.min(1, (performance.now() - s.t0) / s.dur); + const ue = u * u; + const rotSign = s.delta >= 0 ? 1 : -1; + const rotRad = ue * 1.22 * rotSign; + const yFall = ue * ue * tileSize * 6.5; + const xSlide = rotSign * ue * tileSize * 0.62; + const cxW = s.xLeft0 + s.twWorld / 2 + xSlide; + const cyW = s.yTop0 + s.stripH * 0.5 + yFall; + const [cxs, cys] = worldToScreen(cxW, cyW); + const drawW = s.twWorld * zoom; + const drawH = s.stripH * zoom; + const hue = (m.layers.length * 47) % 360; + ctx.save(); + ctx.translate(cxs, cys); + ctx.rotate(rotRad); + ctx.fillStyle = 'hsla(' + hue + ', 70%, 52%, 0.93)'; + ctx.fillRect(-drawW / 2, -drawH / 2, drawW, drawH); + ctx.strokeStyle = 'rgba(255, 200, 140, 0.9)'; + ctx.lineWidth = 2; + ctx.strokeRect(-drawW / 2, -drawH / 2, drawW, drawH); + ctx.restore(); + } else { + const twWorld = m.widthTiles * tileSize; + const drawStripPx = stripH * zoom; + let blockLeftWorld; + let blockTopWorld; + let fallTiltRad = 0; + if (stackFall) { + const tRaw = (performance.now() - stackFall.t0) / stackFall.dur; + const t = Math.min(1, tRaw); + const u = stackEaseDropPhys(t, stackFall.sloppyN); + blockLeftWorld = stackFall.xLeft0 + (stackFall.xLeft1 - stackFall.xLeft0) * u; + blockTopWorld = stackFall.y0 + (stackFall.y1 - stackFall.y0) * u; + fallTiltRad = stackFall.tiltMax * Math.sin(Math.PI * u); + } else { + const swingXW = (m.topCenterX + m.swingAmp * Math.sin(m.phase)) * tileSize; + blockLeftWorld = swingXW - twWorld / 2; + blockTopWorld = m.swingWorldY; + const cableTopY = Math.max(0, m.swingWorldY - tileSize * 6); + const craneX = m.craneWorldX != null ? m.craneWorldX : m.topCenterX * tileSize; + const cxMid = blockLeftWorld + twWorld / 2; + const [tcx, tcy] = worldToScreen(craneX, cableTopY); + const [bcx, bcy] = worldToScreen(cxMid, blockTopWorld); + ctx.strokeStyle = 'rgba(90, 88, 86, 0.75)'; + ctx.lineWidth = Math.max(2.2, zoom * 1.4); + ctx.setLineDash([]); + ctx.beginPath(); + ctx.moveTo(tcx, tcy); + ctx.lineTo(bcx, bcy); + ctx.stroke(); + ctx.strokeStyle = 'rgba(125, 207, 255, 0.35)'; + ctx.lineWidth = Math.max(1, zoom * 0.65); + ctx.setLineDash([3, 5]); + ctx.beginPath(); + ctx.moveTo(tcx, tcy); + ctx.lineTo(bcx, bcy); + ctx.stroke(); + ctx.setLineDash([]); + ctx.lineWidth = 1; + } + const hitMissTint = stackFall && stackFall.hit && stackFall.hit.miss; + ctx.fillStyle = hitMissTint ? 'rgba(247, 118, 190, 0.88)' : 'rgba(125, 207, 255, 0.95)'; + ctx.strokeStyle = hitMissTint ? 'rgba(255, 180, 210, 0.98)' : 'rgba(192, 202, 245, 0.98)'; + const cxW = blockLeftWorld + twWorld / 2; + const cyW = blockTopWorld + stripH * 0.5; + const [cxs, cys] = worldToScreen(cxW, cyW); + ctx.save(); + ctx.translate(cxs, cys); + ctx.rotate(fallTiltRad); + ctx.fillRect(-twWorld * zoom / 2, -drawStripPx / 2, twWorld * zoom, drawStripPx); + ctx.lineWidth = 2; + ctx.strokeRect(-twWorld * zoom / 2, -drawStripPx / 2, twWorld * zoom, drawStripPx); + ctx.lineWidth = 1; + ctx.restore(); + } + const [fsx, fsy] = worldToScreen(0, floorY); + const [fex, fey] = worldToScreen(mapData.width * tileSize, floorY); + ctx.strokeStyle = 'rgba(247, 118, 190, 0.65)'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(fsx, fsy); + ctx.lineTo(fex, fey); + ctx.stroke(); + ctx.lineWidth = 1; + } + + function applyGauntletTimingFromServer(payload) { + if (!payload || typeof payload !== 'object') return; + const tm = Number(payload.gauntletTickMs); + if (Number.isFinite(tm)) gauntletRuntimeTickMs = Math.max(80, Math.min(800, tm)); + const jt = Number(payload.gauntletJumpTicks); + if (Number.isFinite(jt)) gauntletRuntimeJumpTicks = Math.max(4, Math.min(40, jt)); + const tl = Number(payload.gauntletTimeLimitSec); + if (Number.isFinite(tl)) gauntletRuntimeTimeLimitSec = Math.max(0, Math.min(7200, tl)); + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletEndsAt')) { + if (payload.gauntletEndsAt == null) gauntletEndsAtMs = null; + else { + const ge = Number(payload.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(ge) ? ge : null; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletCrownRunHeld')) { + gauntletCrownRunHeldRemote = !!payload.gauntletCrownRunHeld; + if (!gauntletCrownRunHeldRemote && isGauntletCrownHeistMapPlay()) { + gauntletCrownPregamePhase = 'live'; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaneImageUrls') && Array.isArray(payload.gauntletLaneImageUrls)) { + gauntletLaneImageUrls = payload.gauntletLaneImageUrls + .filter((x) => typeof x === 'string') + .map((x) => normalizeGauntletAssetUrlForPlay(x)) + .filter((x) => x) + .slice(0, 24); + gauntletLaneImageUrls.forEach((x) => ensureGauntletAssetImage(x)); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserTopUrl')) { + gauntletLaserTopUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserTopUrl === 'string' ? payload.gauntletLaserTopUrl : ''); + ensureGauntletAssetImage(gauntletLaserTopUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserBottomUrl')) { + gauntletLaserBottomUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserBottomUrl === 'string' ? payload.gauntletLaserBottomUrl : ''); + ensureGauntletAssetImage(gauntletLaserBottomUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserLineUrl')) { + gauntletLaserLineUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserLineUrl === 'string' ? payload.gauntletLaserLineUrl : ''); + ensureGauntletAssetImage(gauntletLaserLineUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserFillColor')) { + gauntletLaserFillColor = clampClientLaserColor(payload.gauntletLaserFillColor, gauntletLaserFillColor); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserStrokeColor')) { + gauntletLaserStrokeColor = clampClientLaserColor(payload.gauntletLaserStrokeColor, gauntletLaserStrokeColor); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserLineWidthPx')) { + const lw = Number(payload.gauntletLaserLineWidthPx); + if (Number.isFinite(lw)) gauntletLaserLineWidthPx = Math.max(0, Math.min(24, Math.round(lw))); + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackSwingHz')) { + const sh = Number(payload.stackSwingHz); + if (Number.isFinite(sh)) playStackSwingHz = Math.max(0.08, Math.min(2.8, sh)); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackBlockWidthTiles')) { + const raw = payload.stackBlockWidthTiles; + if (raw == null || raw === '') playStackBlockWidthTiles = null; + else { + const nw = Number(raw); + playStackBlockWidthTiles = Number.isFinite(nw) && nw >= 0.85 + ? Math.max(0.85, Math.min(3.2, Math.round(nw * 100) / 100)) + : null; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurviveJumpHeightMult')) { + const jm = Number(payload.jumpSurviveJumpHeightMult); + if (Number.isFinite(jm)) { + playJumpSurviveJumpHeightMult = Math.round(Math.max(0.5, Math.min(4, jm)) * 100) / 100; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurviveMissionTimeSec')) { + const jt = Number(payload.jumpSurviveMissionTimeSec); + if (Number.isFinite(jt) && jt > 0) { + playJumpSurviveMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(jt))); + } else { + playJumpSurviveMissionTimeSec = 0; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterMissionTimeSec')) { + const st = Number(payload.spaceShooterMissionTimeSec); + if (Number.isFinite(st) && st > 0) { + playSpaceShooterMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st))); + } else { + playSpaceShooterMissionTimeSec = 0; + } + } + if (isStack() && stackMini) reapplyStackMiniSizingFromGlobals(); + } + + /** เลอร์ปตำแหน่งตัวละครต่อเฟรม (ยิ่งสูงยิ่งตามเป้าเร็ว) */ + const GAUNTLET_VIS_LERP = 0.42; + + function gauntletObsSmoothstep(t) { + t = Math.max(0, Math.min(1, t)); + return t * t * (3 - 2 * t); + } + + function cloneGauntletObsSnap(arr) { + if (!Array.isArray(arr)) return []; + return arr.map((o) => { + if (!o || o.id == null) return null; + const row = { id: o.id, kind: o.kind, x: o.x, y: o.y }; + if (o.kind === 'laser') { + if (o.y0 != null) row.y0 = o.y0; + if (o.y1 != null) row.y1 = o.y1; + } + return row; + }).filter(Boolean); + } + + function gauntletLaserRowHitRange(ob, mapH) { + const h = Math.max(1, Math.floor(Number(mapH)) || 15); + let y0 = ob && ob.y0 != null && Number.isFinite(Number(ob.y0)) ? Math.floor(Number(ob.y0)) : 0; + let y1 = ob && ob.y1 != null && Number.isFinite(Number(ob.y1)) ? Math.floor(Number(ob.y1)) : h - 1; + y0 = Math.max(0, Math.min(h - 1, y0)); + y1 = Math.max(0, Math.min(h - 1, y1)); + if (y1 < y0) { + const t = y0; + y0 = y1; + y1 = t; + } + return { y0, y1 }; + } + + function gauntletLaserOverlapsPlayerRow(ob, py, mapH) { + const { y0, y1 } = gauntletLaserRowHitRange(ob, mapH); + return py >= y0 && py <= y1; + } + + /** โค้งยกตัว: ขึ้นถึงจุดสูงสุดเร็ว ลงชัน (รู้สึกลงพื้นเร็วกว่าแบบสมมาตร) */ + function gauntletLiftHeightNorm(air, jumpTicksMax) { + const jm = Math.max(4, jumpTicksMax || 16); + const a = Math.max(0, Math.min(jm, Number(air) || 0)); + const t = a / jm; + const peakAt = 0.28; + if (t >= peakAt) { + const span = 1 - peakAt; + const u = span > 0 ? (t - peakAt) / span : 0; + return Math.max(0, 1 - u * u * 1.2); + } + return Math.min(1, t / peakAt); + } + + function getGauntletObsDrawPositionsAt(nowMs) { + if (!gauntletObsRenderNext.length) return []; + const elapsed = nowMs - gauntletObsBlendT0; + const alpha = gauntletObsSmoothstep(elapsed / gauntletRuntimeTickMs); + const prevMap = new Map(gauntletObsRenderPrev.map((o) => [o.id, o])); + const out = []; + for (let i = 0; i < gauntletObsRenderNext.length; i++) { + const n = gauntletObsRenderNext[i]; + if (!n) continue; + const p = prevMap.get(n.id); + let drawX = n.x; + if (p && Number.isFinite(p.x) && Number.isFinite(n.x)) drawX = p.x + (n.x - p.x) * alpha; + const row = { id: n.id, kind: n.kind, drawX, y: n.y }; + if (n.kind === 'laser') { + if (n.y0 != null) row.y0 = n.y0; + if (n.y1 != null) row.y1 = n.y1; + } + out.push(row); + } + return out; + } + + function pushGauntletObsRenderFrame(newObs) { + const now = performance.now(); + const next = cloneGauntletObsSnap(newObs); + if (!gauntletObsRenderNext.length) { + gauntletObsRenderPrev = next.slice(); + gauntletObsRenderNext = next.slice(); + } else { + const curDraw = getGauntletObsDrawPositionsAt(now); + const curMap = new Map(curDraw.map((o) => [o.id, o.drawX])); + gauntletObsRenderPrev = next.map((n) => { + const row = { + id: n.id, + kind: n.kind, + x: curMap.has(n.id) ? curMap.get(n.id) : n.x, + y: n.y, + }; + if (n.kind === 'laser') { + if (n.y0 != null) row.y0 = n.y0; + if (n.y1 != null) row.y1 = n.y1; + } + return row; + }); + gauntletObsRenderNext = next; + } + gauntletObsBlendT0 = now; + } + + function lerpGauntletEntityPos(x, y, tx, ty) { + let nx = x; + let ny = y; + if (tx != null && Number.isFinite(tx)) { + nx += (tx - nx) * GAUNTLET_VIS_LERP; + if (Math.abs(tx - nx) < 0.02) nx = tx; + } + if (ty != null && Number.isFinite(ty)) { + ny += (ty - ny) * GAUNTLET_VIS_LERP; + if (Math.abs(ty - ny) < 0.02) ny = ty; + } + return { nx, ny }; + } + + /** + * บอท preview ใน Gauntlet: ตัดสินใจกระโดด (client-only; เซิร์ฟเวอร์ไม่รู้จักบอท) + * — ตอบสนองสิ่งกีดขวางที่ชนเซลล์เดียวกัน + กระโดดล่วงหน้าเมื่อ threat อยู่คอลัมน์ถัดไป + */ + function maybePreviewBotGauntletJump(o, obstacles, w, h) { + if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return; + let px = Math.floor(Number(o.x)) || 0; + let py = Math.floor(Number(o.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + if ((o.gauntletJumpTicks || 0) > 0) return; + const { ch: gauntletCh } = getCharacterFootprintWH(mapData); + let sameCell = false; + let incomingCol = false; + for (let i = 0; i < obstacles.length; i++) { + const obs = obstacles[i]; + if (!obs) continue; + if (obs.kind === 'lane' && typeof obs.y === 'number') { + if (obs.x === px && obs.y >= py && obs.y < py + gauntletCh) sameCell = true; + if (obs.x === px + 1 && obs.y >= py && obs.y < py + gauntletCh) incomingCol = true; + } + if (obs.kind === 'laser' && typeof obs.x === 'number') { + if (obs.x === px && gauntletLaserOverlapsPlayerRow(obs, py, h)) sameCell = true; + if (obs.x === px + 1 && gauntletLaserOverlapsPlayerRow(obs, py, h)) incomingCol = true; + } + } + const tier = o.botTier || 'avg'; + const roll = Math.random(); + const pSame = tier === 'sharp' ? 0.96 : tier === 'weak' ? 0.62 : 0.86; + const pAhead = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.48 : 0.72; + if (sameCell && roll < pSame) { + o.gauntletJumpTicks = gauntletRuntimeJumpTicks; + return; + } + if (incomingCol && roll < pAhead) o.gauntletJumpTicks = gauntletRuntimeJumpTicks; + } + + /** หนึ่ง tick เดียวกับ runGauntletTick ฝั่งเซิร์ฟเวอร์ (เฉพาะ collision + เลื่อน x) */ + function applyGauntletPhysicsToPreviewBot(o, obstacles, w, h) { + if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return; + let px = Math.floor(Number(o.x)) || 0; + let py = Math.floor(Number(o.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + const air = (o.gauntletJumpTicks || 0) > 0; + const { ch: gauntletCh2 } = getCharacterFootprintWH(mapData); + const crown = isGauntletCrownHeistMapPlay(); + let advanceX = false; + let hitBack = false; + for (let i = 0; i < obstacles.length; i++) { + const ob = obstacles[i]; + if (!ob) continue; + if (ob.kind === 'lane' && typeof ob.y === 'number' && ob.x === px && ob.y >= py && ob.y < py + gauntletCh2) { + if (air) advanceX = true; + else hitBack = true; + } + if (ob.kind === 'laser' && typeof ob.x === 'number' && ob.x === px) { + if (!gauntletLaserOverlapsPlayerRow(ob, py, h)) { + /* เลเซอร์ไม่ครอบแถวนี้ */ + } else if (air) { + advanceX = true; + } else { + hitBack = true; + } + } + } + if (advanceX) { + px = Math.min(w - 2, px + 1); + o.gauntletJumpTicks = 0; + if (!crown) { + o.gauntletScore = (o.gauntletScore || 0) + 1; + } + } else if (hitBack) { + if (crown) { + if (px <= 0) { + o.gauntletEliminated = true; + o.gauntletScore = 0; + } else { + o.gauntletScore = Math.max(0, (o.gauntletScore || 0) - 10); + px = Math.max(0, px - 1); + } + } else { + px = Math.max(0, px - 1); + } + } + if ((o.gauntletJumpTicks || 0) > 0) o.gauntletJumpTicks--; + o.tx = px; + o.ty = py; + } + + function applyGauntletPreviewBotsAfterSync() { + if (!previewFillBots || !mapData || mapData.gameType !== 'gauntlet') return; + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return; + const w = mapData.width || 20; + const h = mapData.height || 15; + others.forEach((o, id) => { + if (!isPreviewBotId(id) || !o) return; + maybePreviewBotGauntletJump(o, gauntletObstacles, w, h); + applyGauntletPhysicsToPreviewBot(o, gauntletObstacles, w, h); + }); + } + + /** แจ้งเซิร์ฟเวอร์แถว y ที่มีคุณ+บอททดสอบ — lane spawn ใช้เฉพาะแถวที่มี peer; บอทไม่ใช่ peer */ + function emitGauntletPreviewRowsToServer() { + if (!previewFillBots || !mapData || mapData.gameType !== 'gauntlet') return; + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return; + const h = mapData.height || 15; + const ysSet = new Set(); + const addY = (v) => { + const fy = Math.floor(Number(v)); + if (Number.isFinite(fy) && fy >= 0 && fy < h) ysSet.add(fy); + }; + addY(me.y); + others.forEach((o, id) => { + if (isPreviewBotId(id)) addY(o.y); + }); + socket.emit('gauntlet-preview-rows', { ys: [...ysSet] }); + } + + /** ซ่อน overlay โหมดอื่นที่ค้างจาก embed / session ก่อน — กัน UI ซ้อนไม่ตรง mock */ + function hideConflictingOverlaysForGauntletCrown() { + hideQuizCarryPregameOverlay(); + quizCarryPregameActive = false; + const hideIds = [ + 'quiz-game-overlay', + 'quiz-carry-mission-overlay', + 'gauntlet-ended-overlay', + 'quiz-battle-mcq-overlay', + 'quiz-carry-embed-countdown', + 'gauntlet-crown-countdown', + 'quiz-carry-embed-q-strip', + 'play-quiz-scoreboard', + 'play-quiz-feedback', + ]; + for (let i = 0; i < hideIds.length; i++) { + const el = document.getElementById(hideIds[i]); + if (el) el.classList.add('is-hidden'); + } + const gcm = document.getElementById('gauntlet-crown-mission-overlay'); + if (gcm) gcm.classList.add('is-hidden'); + } + + function gcmRankBadgeUrl(rank) { + const r = Math.floor(Number(rank)) || 99; + if (r === 1) return BASE + '/img/gauntlet-assets/result-1st.png'; + if (r === 2) return BASE + '/img/gauntlet-assets/result-2nd.png'; + if (r === 3) return BASE + '/img/gauntlet-assets/result-3rd.png'; + return BASE + '/img/gauntlet-assets/result-txt-2.png'; + } + + function gauntletCrownPregameReadyNumerator() { + let n = quizCarryPregameBotCount(); + quizCarryPregameHumanIds().forEach((id) => { + if (gauntletCrownLobbyReadyMap[id]) n++; + }); + return n; + } + + function gauntletCrownSyncGuestReadyIfNeeded() { + if (gauntletCrownPregamePhase !== 'howto') return; + if (myId == null || isMePlayHost()) return; + const sid = String(myId); + if (gauntletCrownLobbyReadyMap[sid]) return; + gauntletCrownLobbyReadyMap[sid] = true; + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: true }); + } + + function updateGauntletCrownHowtoHud() { + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!btn || !isGauntletCrownHeistMapPlay() || gauntletCrownPregamePhase !== 'howto') return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + if (st) { + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + } + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + function beginGauntletCrownCountdownThenRun() { + if (!isGauntletCrownHeistMapPlay()) return; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + gauntletCrownPregamePhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = () => { + if (cd) cd.classList.add('is-hidden'); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + if (socket && socket.connected) { + socket.emit('gauntlet-crown-begin-run', (res) => { + if (res && res.ok) gauntletCrownPregamePhase = 'live'; + }); + } else { + gauntletCrownPregamePhase = 'live'; + } + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + numEl.textContent = String(n); + const step = () => { + n--; + if (n > 0) { + numEl.textContent = String(n); + gauntletCrownCountdownTimer = setTimeout(step, 1000); + } else { + gauntletCrownCountdownTimer = null; + runFinish(); + } + }; + gauntletCrownCountdownTimer = setTimeout(step, 1000); + } + + function showGauntletCrownHowtoOverlay() { + if (!isGauntletCrownHeistMapPlay()) return; + hideConflictingOverlaysForGauntletCrown(); + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + gauntletCrownPregamePhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateGauntletCrownHowtoHud(); + } + + /** Editor embed: quiz_carry / crown ใช้เกรด F = gameover — Jumper: ภาพจบใช้เฉพาะเมื่อไม่มีผู้รอด (0 คน) */ + function gauntletCrownEmbedMissionAnyOk(mission) { + if (!mission) return false; + if (mission.uiSkin === 'jumper') { + const sc = Number(mission.survivorCount); + return Number.isFinite(sc) ? sc > 0 : (String(mission.grade || '').trim().toUpperCase().charAt(0) !== 'F'); + } + const g = String(mission.grade || '').trim().toUpperCase().charAt(0); + return g !== 'F'; + } + + function gauntletCrownRankBonusLocal(rankOrdinal) { + if (rankOrdinal === 1) return 100; + if (rankOrdinal === 2) return 80; + if (rankOrdinal === 3) return 60; + return 0; + } + + function gauntletCrownRollRewardCardLocal(grade) { + const r = Math.random(); + if (grade === 'A') { + if (r < 0.3) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' }; + if (r < 0.8) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'B') { + if (r < 0.2) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' }; + if (r < 0.5) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (r < 0.2) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + + /** + * Editor embed + preview bots: รวมแถวอันดับกับบอท (สูงสุด 5 ช่อง) และคำนวณรวม/เฉลี่ย/เกรดให้สอดคล้องกับแถวที่โชว์ + * — อันดับในช่องใช้ลำดับ 1..5 หลังเรียงคะแนน (เหมือนแนว quiz_carry mission row) + */ + function gauntletCrownBuildDisplayMission(mission) { + if (!mission || !Array.isArray(mission.ranked)) return mission; + if (mission.uiSkin === 'jumper') return mission; + if (!(previewFillBots && isGauntletCrownHeistMapPlay() && others && typeof others.forEach === 'function')) return mission; + + const seen = new Set(); + const baseRows = []; + mission.ranked.forEach((r) => { + if (!r) return; + const sid = String(r.id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: r.id, + nickname: (r.nickname && String(r.nickname).trim()) ? String(r.nickname).trim() : 'ผู้เล่น', + characterId: r.characterId ?? null, + baseScore: r.eliminated ? 0 : Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + }); + }); + others.forEach((o, id) => { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: o.gauntletEliminated ? 0 : Math.max(0, Number(o.gauntletScore) | 0), + eliminated: !!o.gauntletEliminated, + }); + }); + + baseRows.sort((a, b) => { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + const nick = String(a.nickname).localeCompare(String(b.nickname), 'th'); + if (nick !== 0) return nick; + return String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map((row, idx) => { + const pos = idx + 1; + const rankBonus = gauntletCrownRankBonusLocal(pos); + const rankLabel = pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos); + const finalScore = row.baseScore + rankBonus; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: row.eliminated, + rank: pos, + rankLabel: rankLabel, + rankBonus: rankBonus, + finalScore: finalScore, + }; + }); + + const totalSum = ranked.reduce(function (s, r) { return s + r.finalScore; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + const rewardCard = gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: ranked.map(function (r) { return r.finalScore; }), + }; + } + + function showGauntletCrownMissionOverlay(mission) { + const ov = document.getElementById('gauntlet-crown-mission-overlay'); + const rowEl = document.getElementById('gcm-rank-row'); + const totalEl = document.getElementById('gcm-total'); + const gradeEl = document.getElementById('gcm-grade'); + const bonusEl = document.getElementById('gcm-bonus'); + const btn = document.getElementById('btn-gcm-done'); + if (!ov || !rowEl || !mission || !Array.isArray(mission.ranked)) return; + let disp = gauntletCrownBuildDisplayMission(mission); + if (mission.uiSkin === 'jumper') { + disp = jumpSurviveMergePreviewBotsMission(disp) || disp; + } + if (!disp || !Array.isArray(disp.ranked)) return; + rowEl.innerHTML = ''; + disp.ranked.forEach((r) => { + const cell = document.createElement('div'); + cell.className = 'gcm-cell'; + const tag = document.createElement('div'); + tag.className = 'gcm-rank-tag'; + const rk = Math.floor(Number(r.rank)) || 0; + if (rk >= 1 && rk <= 3) { + const tagImg = document.createElement('img'); + tagImg.src = gcmRankBadgeUrl(rk); + tagImg.alt = '[' + String(r.rankLabel || rk) + ']'; + tagImg.style.maxWidth = '100%'; + tagImg.style.height = 'auto'; + tag.appendChild(tagImg); + } else { + tag.textContent = '[' + String(r.rankLabel || rk) + ']'; + } + const frame = document.createElement('div'); + frame.className = 'gcm-frame'; + const img = document.createElement('img'); + img.className = 'gcm-av'; + img.alt = ''; + const cid = r.characterId ? String(r.characterId) : ''; + if (cid) { + const im = getCharacterImg(cid, 'down'); + if (im && im.src) img.src = im.src; + } else { + img.src = defaultAvatarImg.src; + } + img.onerror = function () { + this.onerror = null; + this.src = defaultAvatarImg.src; + }; + frame.appendChild(img); + if (r.eliminated) { + const st = document.createElement('img'); + st.className = 'gcm-stamp'; + const jSkin = mission && mission.uiSkin === 'jumper'; + st.src = jSkin ? jumperAssetUrl('stamp-sacrifice.png') : (BASE + '/img/gauntlet-assets/result-Lose_stamp.png'); + st.alt = jSkin ? 'ผู้เสียสละ' : 'เสียชีวิต'; + st.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/result-Lose_stamp.png'; + this.alt = 'เสียชีวิต'; + }; + frame.appendChild(st); + } + const nick = document.createElement('div'); + nick.className = 'gcm-nick'; + nick.textContent = r.nickname || '—'; + const sc = document.createElement('div'); + sc.className = 'gcm-sc'; + const jumpPlain = mission && mission.uiSkin === 'jumper'; + const botScorePlain = previewFillBots && isGauntletCrownHeistMapPlay() && isPreviewBotId(r.id); + sc.textContent = (jumpPlain || botScorePlain) + ? String(Math.max(0, Number(r.baseScore) || 0)) + : String(r.baseScore || 0) + '(+' + String(r.rankBonus || 0) + ')'; + cell.appendChild(tag); + cell.appendChild(frame); + cell.appendChild(nick); + cell.appendChild(sc); + rowEl.appendChild(cell); + }); + if (totalEl) { + const parts = (disp.totalParts || []).map((n) => String(Math.max(0, Number(n) || 0))); + const sum = Math.max(0, Number(disp.totalSum) || 0); + const avg = Math.max(0, Math.floor(Number(disp.averageScore) || 0)); + const jumperSkin = mission && mission.uiSkin === 'jumper'; + if (jumperSkin) { + const sc = Math.max(0, Math.floor(Number(disp.survivorCount) || 0)); + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้รอด ' + sc + '/' + pc + ' → เกรด'; + } else { + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · เฉลี่ย ' + avg + ' → เกรด'; + } + } + if (gradeEl) { + const g = String(disp.grade || 'C').toUpperCase().charAt(0); + gradeEl.textContent = g; + gradeEl.className = 'gcm-grade gcm-grade--' + g.toLowerCase(); + } + if (bonusEl) { + bonusEl.innerHTML = ''; + const gLetter = String(disp.grade || 'C').toUpperCase().charAt(0); + const missionOk = gLetter !== 'F'; + if (missionOk) { + const row = document.createElement('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + row.style.gap = '8px'; + const chk = document.createElement('img'); + chk.src = BASE + '/img/gauntlet-assets/result-check.png'; + chk.alt = ''; + chk.width = 26; + chk.height = 26; + const sp = document.createElement('span'); + sp.style.color = '#9ece6a'; + sp.style.fontWeight = '700'; + sp.textContent = 'ภารกิจสำเร็จ'; + row.appendChild(chk); + row.appendChild(sp); + bonusEl.appendChild(row); + } + const rc = disp.rewardCard; + if (missionOk && rc && rc.th) { + const pl = document.createElement('div'); + pl.className = 'gcm-plaque'; + pl.innerHTML = '' + String(rc.th) + '' + String(rc.en || '') + ''; + bonusEl.appendChild(pl); + } else if (!missionOk) { + const pl = document.createElement('div'); + pl.className = 'gcm-plaque'; + pl.style.borderColor = 'rgba(148, 153, 168, 0.45)'; + pl.style.opacity = '0.95'; + pl.innerHTML = 'ไม่ได้รับการ์ดเกรด ' + gLetter + ' · ภารกิจไม่ผ่านเกณฑ์'; + bonusEl.appendChild(pl); + } + } + ov.classList.remove('is-hidden'); + function goLobby() { + ov.classList.add('is-hidden'); + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = function () { + if (previewMode && editorEmbedReturn) { + ov.classList.add('is-hidden'); + showQuizCarryTimeupOnDeskLayer(function () { return gauntletCrownEmbedMissionAnyOk(disp); }); + } else { + goLobby(); + } + }; + } + } + + function isLobby() { return mapData && mapData.gameType === 'lobby'; } + function isQuiz() { return mapData && mapData.gameType === 'quiz'; } + function getLane(y) { + if (!mapData || !mapData.lanes) return null; + const lane = mapData.lanes.find(l => l.y === y); + return lane || (mapData.lanes[y] != null ? mapData.lanes[y] : null); + } + function getVehiclePositions(lane, width, timeMs) { + if (!lane || (lane.type !== 'road' && lane.type !== 'water')) return []; + const speed = (lane.speed != null ? lane.speed : 1) * 0.05; + const dir = lane.dir === -1 ? -1 : 1; + const spacing = lane.spacing != null ? lane.spacing : (lane.type === 'road' ? 3 : 2.5); + const period = width + 2; + const count = Math.max(2, Math.floor(period / spacing)); + const positions = []; + for (let i = 0; i < count; i++) { + const p = i * spacing + (timeMs * speed * dir) / 60; + const pNorm = ((p % period) + period) % period; + const vx = pNorm - 1; + positions.push(Math.max(-1, Math.min(width, vx))); + } + return positions; + } + function checkFroggerCollision() { + const fy = Math.floor(me.y); + const lane = getLane(fy); + if (!lane) return false; + if (lane.type === 'road') { + const positions = getVehiclePositions(lane, mapData.width, Date.now()); + for (let i = 0; i < positions.length; i++) { + if (Math.abs(positions[i] - me.x) < 1.1) return true; + } + } + if (lane.type === 'water') { + const positions = getVehiclePositions(lane, mapData.width, Date.now()); + let onLog = false; + for (let i = 0; i < positions.length; i++) { + if (Math.abs(positions[i] - me.x) < 1.2) { onLog = true; break; } + } + if (!onLog) return true; + } + return false; + } + function respawnFrogger() { + const sp = mapData.spawn || { x: 1, y: mapData.height - 1 }; + me.x = sp.x; me.y = sp.y; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + + /** รองรับ x/y เป็น string จาก Socket — ถ้าเช็คแค่ typeof number จะร่วงไป mapData.spawn ทุกครั้ง (จุดเกิดไม่สุ่ม) */ + function peerXYFromJoin(peer, spawnFb) { + const sx = Number(spawnFb && spawnFb.x); + const sy = Number(spawnFb && spawnFb.y); + const defX = Number.isFinite(sx) ? sx : 1; + const defY = Number.isFinite(sy) ? sy : 1; + if (!peer) return { x: defX, y: defY }; + const x = Number(peer.x); + const y = Number(peer.y); + if (Number.isFinite(x) && Number.isFinite(y)) return { x, y }; + return { x: defX, y: defY }; + } + + /** Hub = 0/1 · กริดตัวเลือก = 0 หรือเลข 1..16 (ห้ามบังคับ === 1 เหมือน hub — เลข 2–16จะหาย) */ + function normalizeQuizCarryLayersInPlay(md) { + if (!md || md.gameType !== 'quiz_carry') return; + const w = md.width || 20, h = md.height || 15; + const maxOpt = QUIZ_CARRY_MAX_OPTION_SLOTS; + const srcHub = md.quizCarryHubArea || []; + const hubRows = []; + for (let y = 0; y < h; y++) { + const r = srcHub[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + hubRows.push(row); + } + md.quizCarryHubArea = hubRows; + const srcOpt = md.quizCarryOptionArea || []; + const optRows = []; + for (let y = 0; y < h; y++) { + const r = srcOpt[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= maxOpt ? Math.floor(n) : 0); + } + optRows.push(row); + } + md.quizCarryOptionArea = optRows; + const srcCd = md.carryEmbedCountdownArea || []; + const cdRows = []; + for (let y = 0; y < h; y++) { + const r = srcCd[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + row.push(Number(v) === 1 ? 1 : 0); + } + cdRows.push(row); + } + md.carryEmbedCountdownArea = cdRows; + } + + /** โดม = ช่องติดกัน (4 ทิศ) ต่อ 1 ข้อ · comp id เรียงตามการค้นพบบนแมป */ + function normalizeQuizBattleDomeInPlay(md) { + if (!md || md.gameType !== 'quiz_battle') return; + const w = md.width || 20, h = md.height || 15; + const src = md.quizBattleDomeArea || []; + const grid = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + grid.push(row); + } + md.quizBattleDomeArea = grid; + const comp = Array(h).fill(0).map(() => Array(w).fill(0)); + let nextId = 0; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (grid[y][x] !== 1 || comp[y][x]) continue; + nextId++; + const stack = [[x, y]]; + comp[y][x] = nextId; + while (stack.length) { + const c = stack.pop(); + const cx = c[0], cy = c[1]; + for (let i = 0; i < 4; i++) { + const dx = (i === 0 ? 1 : i === 1 ? -1 : 0); + const dy = (i === 2 ? 1 : i === 3 ? -1 : 0); + const nx = cx + dx, ny = cy + dy; + if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue; + if (grid[ny][nx] !== 1 || comp[ny][nx]) continue; + comp[ny][nx] = nextId; + stack.push([nx, ny]); + } + } + } + } + md.quizBattleDomeComp = comp; + } + + /** เส้นทาง Quiz Battle — วาดอย่างน้อย 1 ช่องแล้วจะเดินได้เฉพาะบนเส้นทาง (เปลี่ยนแค่รูปพื้นหลังได้) */ + function normalizeQuizBattlePathInPlay(md) { + if (!md || md.gameType !== 'quiz_battle') return; + const w = md.width || 20, h = md.height || 15; + const src = md.quizBattlePathArea || []; + const grid = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + grid.push(row); + } + md.quizBattlePathArea = grid; + } + + function quizBattlePathModeActive(md) { + if (!md || md.gameType !== 'quiz_battle' || !md.quizBattlePathArea) return false; + const g = md.quizBattlePathArea; + for (let y = 0; y < g.length; y++) { + const row = g[y]; + if (!row) continue; + for (let x = 0; x < row.length; x++) if (row[x] === 1) return true; + } + return false; + } + + function quizBattleFootprintFullyOnPath(md, px, py) { + if (!md || !quizBattlePathModeActive(md)) return true; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return false; + const tiles = quizTilesFootprintPlay(px, py); + if (tiles.size === 0) return false; + const g = md.quizBattlePathArea; + for (const k of tiles) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!g[ty] || g[ty][tx] !== 1) return false; + } + /* กันเดินลง/ข้ามขอบเลน: เท้า (≈ py+0.5 / px+0.5) ต้องอยู่บนเส้นทาง — หยุดก่อนล้ำช่องถัดไปที่ไม่ใช่ path */ + const h = md.height || 15, w = md.width || 20; + const { cw, ch } = getCharacterFootprintWH(md); + const fy = Math.floor(py + 0.5); + if (fy >= 0 && fy < h) { + const minTx = Math.floor(px), maxTx = Math.min(w - 1, minTx + cw - 1); + for (let tx = Math.max(0, minTx); tx <= maxTx; tx++) { + if (!g[fy] || g[fy][tx] !== 1) return false; + } + } + const fx = Math.floor(px + 0.5); + if (fx >= 0 && fx < w) { + const minTy = Math.floor(py), maxTy = Math.min(h - 1, minTy + ch - 1); + for (let ty = Math.max(0, minTy); ty <= maxTy; ty++) { + if (!g[ty] || g[ty][fx] !== 1) return false; + } + } + return true; + } + + /** สแน็ปบนเลน — เช็คแค่กำแพง + อยู่บน path (ให้ตรงกับ server) ไม่ใช้ blockPlayer/ผู้เล่นคนอื่น เพราะจะทำให้หาจุดสแน็ปไม่ได้แล้วค้างนอกเลน */ + function quizBattleSnapTargetValidPlay(x, y) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + for (const k of quizTilesWallCollisionFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (quizBattlePathModeActive(mapData) && !quizBattleFootprintFullyOnPath(mapData, x, y)) return false; + return true; + } + + function snapPositionOntoQuizBattlePathIfNeeded(px, py) { + if (!mapData || !isQuizBattle() || !quizBattlePathModeActive(mapData)) return { x: px, y: py }; + if (quizBattleSnapTargetValidPlay(px, py)) return { x: px, y: py }; + const w = mapData.width || 20, h = mapData.height || 15; + const g = mapData.quizBattlePathArea; + let bestX = null, bestY = null, bestD = Infinity; + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + const nx = tx + 0.01; + const ny = ty + 0.01; + if (!quizBattleSnapTargetValidPlay(nx, ny)) continue; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + /* footprint ใหญ่กว่าเลน (เช่น 2×2 บนทางแคบ 1 ช่อง): หาช่อง path ใกล้สุดที่เดินได้อย่างน้อยที่เซ็นเตอร์ — ดีกว่าปล่อยค้างนอกเลน */ + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + if (!spawnTileWalkablePlay(mapData, tx, ty)) continue; + const nx = tx + 0.5; + const ny = ty + 0.5; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + return { x: px, y: py }; + } + + /** บังคับให้ผู้เล่นอยู่บนเลน — เรียกก่อน return กลาง tick (แชท / path หมด) เพราะเดิมข้ามบล็อกสแน็ปท้าย tick */ + function enforceQuizBattleLaneOnMePlay() { + if (!mapData || !isQuizBattle() || !quizBattlePathModeActive(mapData)) return; + if (quizBattleSnapTargetValidPlay(me.x, me.y)) return; + const snapped = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = snapped.x; + me.y = snapped.y; + } + + function hideQuizBattleMcqModal() { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + } + + function flashQuizBattleFeedback(text, ok) { + const el = document.getElementById('play-quiz-feedback'); + if (!el) return; + el.textContent = text || ''; + el.classList.remove('is-hidden', 'play-quiz-feedback-ok', 'play-quiz-feedback-bad'); + el.classList.add(ok ? 'play-quiz-feedback-ok' : 'play-quiz-feedback-bad'); + if (typeof window.__quizBattleFbT === 'number') clearTimeout(window.__quizBattleFbT); + window.__quizBattleFbT = setTimeout(() => { el.classList.add('is-hidden'); }, 1800); + } + + function getQuizBattleDomeCompIdsUnderFootprint(px, py) { + if (!mapData || !isQuizBattle()) return []; + const comp = mapData.quizBattleDomeComp; + const grid = mapData.quizBattleDomeArea; + if (!comp || !grid) return []; + const ids = []; + const seen = Object.create(null); + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + const cid = comp[ty] && comp[ty][tx]; + if (grid[ty] && grid[ty][tx] === 1 && cid > 0 && !seen[cid]) { + seen[cid] = 1; + ids.push(cid); + } + } + return ids; + } + + function getPrimaryQuizBattleDomeCompAt(px, py) { + const arr = getQuizBattleDomeCompIdsUnderFootprint(px, py); + if (!arr.length) return null; + return Math.min.apply(null, arr); + } + + function showQuizBattleMcqModal(q) { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + const textEl = document.getElementById('quiz-battle-mcq-text'); + if (!ov || !textEl || !q) return; + textEl.textContent = q.text || ''; + const labels = ['A', 'B', 'C']; + for (let i = 0; i < 3; i++) { + const btn = ov.querySelector('.quiz-battle-choice[data-idx="' + i + '"]'); + if (btn) { + const ch = (q.choices && q.choices[i]) ? String(q.choices[i]) : ''; + btn.textContent = labels[i] + (ch ? (': ' + ch.slice(0, 96)) : ''); + } + } + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + + function trySubmitQuizBattleChoice(idx) { + if (quizBattleModalCompId == null || !mapData || !isQuizBattle()) return; + const compId = quizBattleModalCompId; + const pool = quizBattleMcqPool; + if (!pool.length) { + flashQuizBattleFeedback('ยังไม่มีคำถาม — ตั้ง battleQuizMcq ใน Admin → Quiz Battle', false); + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + return; + } + const q = pool[(compId - 1) % pool.length]; + if (!q) return; + const ok = Number(q.correctIndex) === Number(idx); + if (ok) { + quizBattleAnsweredComps.add(compId); + if (myId != null) { + if (!playLiveQuizScores) playLiveQuizScores = {}; + playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + 1; + renderPlayQuizScoreboard(playLiveQuizScores); + } + /* ส่งให้ server เก็บคะแนน (authoritative) แล้ว broadcast อันดับให้ทุกคน — แบบ ZEP */ + try { if (typeof socket !== 'undefined' && socket) socket.emit('quizbattle-solve', { compId }); } catch (e) { /* ignore */ } + updateQuizBattleProgressHud(); + flashQuizBattleFeedback('ถูกต้อง · +1', true); + } else { + flashQuizBattleFeedback('ยังไม่ถูก — ลองใหม่ (กด E อีกครั้ง)', false); + } + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + } + + /** ZEP-style: เดินถึงสถานี (โดม) ที่ยังไม่ตอบ → เปิดคำถามอัตโนมัติ (ครั้งเดียวต่อการเข้า · เดินออกแล้วกลับมาใหม่ถึงเด้งอีก) */ + function maybeAutoOpenQuizBattle() { + if (!isQuizBattle() || myId == null) return; + const compNow = getPrimaryQuizBattleDomeCompAt(me.x, me.y); + if (compNow == null) { qbAutoLastComp = null; return; } + const ov = document.getElementById('quiz-battle-mcq-overlay'); + const modalOpen = quizBattleModalCompId != null || (ov && !ov.classList.contains('is-hidden')); + if (modalOpen) return; + if (compNow === qbAutoLastComp) return; // เข้าโดมนี้รอบนี้ไปแล้ว + qbAutoLastComp = compNow; + if (quizBattleAnsweredComps.has(compNow)) return; // ตอบถูกแล้ว + const pool = quizBattleMcqPool; + if (!pool.length) return; + const q = pool[(compNow - 1) % pool.length]; + if (!q) return; + quizBattleModalCompId = compNow; + showQuizBattleMcqModal(q); + } + + function tryOpenQuizBattleFromKey() { + if (!isQuizBattle() || !mapData || myId == null) return; + const compId = getPrimaryQuizBattleDomeCompAt(me.x, me.y); + if (compId == null) { + flashQuizBattleFeedback('ยังไม่ยืนบนโดมคำถาม', false); + return; + } + if (quizBattleAnsweredComps.has(compId)) { + flashQuizBattleFeedback('ตอบโดมนี้ถูกแล้ว', false); + return; + } + const pool = quizBattleMcqPool; + if (!pool.length) { + flashQuizBattleFeedback('ยังไม่มีคำถามในระบบ (Admin → Quiz Battle)', false); + return; + } + const q = pool[(compId - 1) % pool.length]; + if (!q) return; + quizBattleModalCompId = compId; + showQuizBattleMcqModal(q); + } + + function pushSanitizedBattleMcqFromPayload(s) { + if (!s || !Array.isArray(s.battleQuizMcq)) return 0; + let n = 0; + for (let i = 0; i < s.battleQuizMcq.length; i++) { + const raw = s.battleQuizMcq[i]; + if (!raw || !String(raw.text || '').trim()) continue; + const ch = Array.isArray(raw.choices) ? raw.choices : []; + if (ch.length !== 3) continue; + const a = String(ch[0] || '').trim(); + const b = String(ch[1] || '').trim(); + const c = String(ch[2] || '').trim(); + if (!a || !b || !c) continue; + let ci = Number(raw.correctIndex); + if (!Number.isFinite(ci)) ci = 0; + ci = Math.max(0, Math.min(2, Math.floor(ci))); + quizBattleMcqPool.push({ + text: String(raw.text).trim(), + choices: [a, b, c], + correctIndex: ci, + }); + n++; + } + return n; + } + + /** + * โหลดชุดข้อ A/B/C — ลอง Node ก่อน แล้ว fallback PHP อ่านจากดิสก์ (กรณี nginx ไม่ส่ง /api/quiz-settings ถึง Node) + */ + async function loadQuizBattleMcqPool() { + quizBattleMcqPool = []; + const bust = '_=' + Date.now(); + const tryUrls = [ + BASE + '/api/quiz-settings?' + bust, + BASE + '/api-quiz-battle-mcq.php?' + bust, + ]; + for (let u = 0; u < tryUrls.length; u++) { + try { + const r = await fetch(tryUrls[u], { cache: 'no-store' }); + if (!r.ok) continue; + const s = await r.json(); + if (pushSanitizedBattleMcqFromPayload(s) > 0) break; + } catch (e) { /* next url */ } + } + } + + function resetQuizBattlePlayState() { + quizBattleMcqPool = []; + quizBattleAnsweredComps = new Set(); + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + hideQuizBattleProgressHud(); + } + + /* ===== Quiz Battle ฉากเดิน (ZEP) — Progress + Ranking sync จาก server ===== */ + let quizBattleScoresBound = false; + function ensureQuizBattleScoresListener() { + if (quizBattleScoresBound || typeof socket === 'undefined' || !socket) return; + quizBattleScoresBound = true; + socket.on('quizbattle-scores', (data) => { + if (!data || typeof data.scores !== 'object' || !isQuizBattle()) return; + const sc = data.scores || {}; + if (!playLiveQuizScores) playLiveQuizScores = {}; + /* server เป็นเจ้าของคะแนน (จำนวนโดมที่ปลดได้) — sync ทับของทุกคน เพื่ออันดับตรงกันแบบ ZEP */ + Object.keys(sc).forEach((id) => { playLiveQuizScores[id] = sc[id]; }); + renderPlayQuizScoreboard(playLiveQuizScores); + updateQuizBattleProgressHud(); + }); + } + + function quizBattleTotalDomes() { + if (!mapData || !isQuizBattle()) return 0; + const comp = mapData.quizBattleDomeComp; + if (!comp) return 0; + let mx = 0; + for (let y = 0; y < comp.length; y++) { + const row = comp[y] || []; + for (let x = 0; x < row.length; x++) if (row[x] > mx) mx = row[x]; + } + return mx; + } + + function updateQuizBattleProgressHud() { + if (!isQuizBattle()) return; + let el = document.getElementById('quiz-battle-progress-hud'); + if (!el) { + el = document.createElement('div'); + el.id = 'quiz-battle-progress-hud'; + el.style.cssText = 'position:fixed;top:12px;left:12px;z-index:60;background:rgba(8,12,28,0.82);color:#fff;font-family:Kanit,Sarabun,system-ui,sans-serif;font-weight:700;padding:8px 18px;border-radius:999px;border:1px solid rgba(255,255,255,0.2);font-size:18px;box-shadow:0 4px 14px rgba(0,0,0,0.35);pointer-events:none'; + document.body.appendChild(el); + } + const total = quizBattleTotalDomes(); + const mine = (myId != null && playLiveQuizScores && playLiveQuizScores[myId] != null) + ? playLiveQuizScores[myId] + : (quizBattleAnsweredComps ? quizBattleAnsweredComps.size : 0); + el.textContent = 'Progress ' + mine + '/' + total + (total > 0 && mine >= total ? ' ✓' : ''); + el.style.color = (total > 0 && mine >= total) ? '#34d399' : '#fff'; + } + + function hideQuizBattleProgressHud() { + const el = document.getElementById('quiz-battle-progress-hud'); + if (el) el.remove(); + } + + function setupPlayQuizBattleUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + /* ทดสอบจากเอดิเตอร์: อย่าเปิดแผงคำถามเต็มจอล่าง — บังมองเห็นบอท/แผนที่ (overlay ยังอัปเดตข้อความไว้ถ้าเปิด preview แบบไม่ embed) */ + if (ov) { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else ov.classList.remove('is-hidden'); + } + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'Quiz Battle'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'เดินทับโดม (สีฟ้า–ขอบแดง) แล้วกด E เพื่อตอบ A / B / C'; + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + const leg = document.getElementById('quiz-play-legend'); + if (leg) { + leg.textContent = quizBattlePathModeActive(mapData) + ? 'โหมดเส้นทาง: เดินได้เฉพาะช่องเส้นทาง (ม่วงบนแผนที่) — วางโดมบนเส้นทาง · ไม่วาดเส้นทางใน Editor = เดินอิสระ · เปลี่ยนรูปพื้นหลังได้ทีหลัง' + : 'คำถามจาก Admin → Quiz Battle (battleQuizMcq) · ช่องโดมติดกัน = 1 ข้อ · ถูกแล้วได้ +1 ต่อโดม'; + } + quizBattleAnsweredComps = new Set(); + quizBattleModalCompId = null; + quizBattleMcqPool = []; + hideQuizBattleMcqModal(); + playLiveQuizScores = {}; + if (myId != null) playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + renderPlayQuizScoreboard(playLiveQuizScores); + /* sync คะแนน/อันดับข้ามผู้เล่นจาก server (ZEP) + โชว์ Progress N/total */ + ensureQuizBattleScoresListener(); + updateQuizBattleProgressHud(); + try { if (typeof socket !== 'undefined' && socket) socket.emit('quizbattle-hello', (res) => { + if (res && res.scores && typeof res.scores === 'object') { + Object.keys(res.scores).forEach((id) => { playLiveQuizScores[id] = res.scores[id]; }); + renderPlayQuizScoreboard(playLiveQuizScores); + updateQuizBattleProgressHud(); + } + }); } catch (e) { /* ignore */ } + loadQuizBattleMcqPool().then(() => { + const q2 = document.getElementById('quiz-game-question'); + if (!q2) return; + if (!quizBattleMcqPool.length) { + q2.textContent = 'ยังไม่มีคำถาม — ไปที่ Admin แท็บ Quiz Battle แล้วบันทึกข้อ A/B/C (ครบคำถาม + A + B + C) · หรือเช็คว่าเซิร์ฟเวอร์เกม (Node) รันอยู่ · ลองรีเฟรชแบบ hard refresh (Ctrl+F5)'; + } else { + const q0 = quizBattleMcqPool[0]; + const snippet = q0 && q0.text ? String(q0.text).trim().slice(0, 100) : ''; + const more = q0 && q0.text && String(q0.text).trim().length > 100 ? '…' : ''; + q2.textContent = snippet + ? ('โหลดคำถามแล้ว ' + quizBattleMcqPool.length + ' ข้อ · ตัวอย่าง: 「' + snippet + more + '」\nเข้าโดมสีฟ้า (ขอบแดง) แล้วกด E เพื่อเลือก A / B / C') + : ('โหลดคำถามแล้ว ' + quizBattleMcqPool.length + ' ข้อ — เดินเข้าโดม (ฟ้า–ขอบแดง) แล้วกด E เพื่อตอบ A / B / C'); + } + const ov2 = document.getElementById('quiz-game-overlay'); + if (ov2 && previewMode && editorEmbedReturn) ov2.classList.add('is-hidden'); + }); + } + + function normalizeJumpSurvivePlatformAreaInPlay(md) { + if (!md || md.gameType !== 'jump_survive') return; + const w = md.width || 20, h = md.height || 15; + const src = md.jumpSurvivePlatformArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + md.jumpSurvivePlatformArea = rows; + } + + function normalizeShooterSpawnSlotsInPlay(md) { + if (!md || md.gameType !== 'space_shooter') return; + const w = md.width || 20, h = md.height || 15; + const src = md.shooterSpawnSlots || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= 6 ? Math.floor(n) : 0); + } + rows.push(row); + } + md.shooterSpawnSlots = rows; + } + + function getShooterSpawnWorldCenterFromMap(md, slot1to6, ts) { + if (!md || !md.shooterSpawnSlots) return null; + const w = md.width || 20, h = md.height || 15; + const g = md.shooterSpawnSlots; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (g[y] && g[y][x] === slot1to6) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; + } + } + return null; + } + + function buildSpaceShooterParticipantRefsPlay() { + const refs = []; + if (me) refs.push({ id: myId, ref: me }); + const humanIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + humanIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + botIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + return refs.filter((r) => r.ref); + } + + function applySpaceShooterSpawnLayoutPlay() { + if (!mapData || mapData.gameType !== 'space_shooter') return; + normalizeShooterSpawnSlotsInPlay(mapData); + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const { cw, ch } = getCharacterFootprintWH(mapData); + const mw = w * ts, mh = h * ts; + const refs = buildSpaceShooterParticipantRefsPlay(); + const n = refs.length || 1; + refs.forEach((entry, idx) => { + const ent = entry.ref; + const slot = (idx % 6) + 1; + const cen = getShooterSpawnWorldCenterFromMap(mapData, slot, ts); + let cx, cy; + if (cen) { + cx = cen.cx; + cy = cen.cy; + } else { + cx = ((idx + 1) / (n + 1)) * mw; + cy = Math.min(mh - ts * 1.2, (h - 1.5) * ts); + } + cx = Math.max(ts * 0.5, Math.min(mw - ts * 0.5, cx)); + cy = clampSpaceShooterWorldCy(cy, mh); + ent.spaceShooterCx = cx; + ent.spaceShooterCy = cy; + if (typeof ent.spaceShooterScore !== 'number' || !Number.isFinite(ent.spaceShooterScore)) ent.spaceShooterScore = 0; + ent.x = cx / ts - cw * 0.5; + ent.y = cy / ts - ch * 0.92; + ent.tx = ent.x; + ent.ty = ent.y; + ent.direction = 'up'; + }); + } + + function spaceShooterTimeLimitSecPlay() { + const t = Number(mapData && mapData.spaceShooterTimeSec); + if (Number.isFinite(t) && t === 0) return 0; + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + const g = Number(playSpaceShooterMissionTimeSec); + if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g); + return 90; + } + + function spaceShooterRemainingSecPlay() { + const lim = spaceShooterTimeLimitSecPlay(); + if (lim <= 0) return null; + const elapsed = (performance.now() - spaceShooterSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function endSpaceShooterTimeUp() { + if (spaceShooterGameEnded || !mapData || mapData.gameType !== 'space_shooter') return; + spaceShooterGameEnded = true; + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterSpawnAccMs = 0; + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) titleEl.textContent = 'หมดเวลา · Time up'; + msgEl.textContent = 'ยิงยานอวกาศจบแล้ว · SPACE SHOOTER finished — อันดับจากคะแนนยิงหิน'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ id: myId, nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', score: Math.max(0, me.spaceShooterScore | 0) }); + } + others.forEach((o, id) => { + ranks.push({ id, nickname: (o && o.nickname) ? String(o.nickname).trim() : id, score: Math.max(0, o.spaceShooterScore | 0) }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + } + + function syncSpaceShooterFootFromShipCenter(ent) { + if (!ent || !mapData || ent.spaceShooterCx == null || ent.spaceShooterCy == null) return; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + ent.x = ent.spaceShooterCx / ts - cw * 0.5; + ent.y = ent.spaceShooterCy / ts - ch * 0.92; + } + + /** จำกัดกลางยานแนวตั้งให้อยู่แค่ครึ่งล่างของแมพ (ไม่ขึ้นเกินกึ่งกลางความสูง) */ + function clampSpaceShooterWorldCy(cy, mhPx) { + if (!Number.isFinite(cy) || !Number.isFinite(mhPx) || mhPx <= 0) return cy; + const edge = 20; + const lo = mhPx * 0.5; + const hi = Math.max(lo + 4, mhPx - edge); + return Math.max(lo, Math.min(hi, cy)); + } + + function stepSpaceShooterPreviewBots(dt) { + if (spaceShooterGameEnded || !previewFillBots || !mapData || mapData.gameType !== 'space_shooter') return; + const ts = tileSize; + const mw = (mapData.width || 20) * ts; + const mh = (mapData.height || 15) * ts; + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o || o.spaceShooterCx == null) return; + let nearest = null, nd = 1e9; + for (let i = 0; i < spaceShooterAsteroids.length; i++) { + const a = spaceShooterAsteroids[i]; + if (!a) continue; + const d = Math.abs(a.x - o.spaceShooterCx) + Math.abs(a.y - o.spaceShooterCy) * 0.15; + if (d < nd) { nd = d; nearest = a; } + } + let vx = 0; + if (nearest) vx = nearest.x > o.spaceShooterCx ? 1 : nearest.x < o.spaceShooterCx ? -1 : 0; + else vx = Math.sin(performance.now() / 900 + bid.length) > 0 ? 1 : -1; + let vy = 0; + if (nearest && Math.abs(nearest.y - o.spaceShooterCy) > ts * 0.25) { + vy = nearest.y > o.spaceShooterCy ? 1 : -1; + } else if (!nearest) { + vy = Math.sin(performance.now() / 700 + bid.charCodeAt(0)) > 0 ? 1 : -1; + } + const spd = (o.botTier === 'sharp' ? 195 : o.botTier === 'weak' ? 115 : 155); + o.spaceShooterCx = Math.max(18, Math.min(mw - 18, o.spaceShooterCx + vx * spd * dt)); + o.spaceShooterCy = clampSpaceShooterWorldCy(o.spaceShooterCy + vy * spd * 0.7 * dt, mh); + syncSpaceShooterFootFromShipCenter(o); + o.tx = o.x; + o.ty = o.y; + if (typeof o.spaceShooterBotFireCd !== 'number') o.spaceShooterBotFireCd = 0; + o.spaceShooterBotFireCd -= dt; + if (o.spaceShooterBotFireCd <= 0 && nearest && nd < ts * 5) { + o.spaceShooterBotFireCd = o.botTier === 'sharp' ? 0.22 : o.botTier === 'weak' ? 0.48 : 0.34; + spaceShooterBullets.push({ x: o.spaceShooterCx, y: o.spaceShooterCy - 16, vy: -500, ownerId: bid }); + } + }); + } + + function spaceShooterTickFrame() { + if (!mapData || mapData.gameType !== 'space_shooter') return; + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const mw = w * ts, mh = h * ts; + const { cw, ch } = getCharacterFootprintWH(mapData); + const now = performance.now(); + const dt = Math.min(0.055, spaceShooterLastTickMs ? (now - spaceShooterLastTickMs) / 1000 : 0.016); + spaceShooterLastTickMs = now; + + if (spaceShooterGameEnded) { + const wallClock = Date.now(); + for (let pi = spaceShooterPopups.length - 1; pi >= 0; pi--) { + if (wallClock >= spaceShooterPopups[pi].until) spaceShooterPopups.splice(pi, 1); + } + return; + } + const remSec = spaceShooterRemainingSecPlay(); + if (remSec != null && remSec <= 0) { + endSpaceShooterTimeUp(); + return; + } + + const interval = Math.max(320, Number(mapData.spaceShooterAsteroidIntervalMs) || 1040); + spaceShooterSpawnAccMs += dt * 1000; + while (spaceShooterSpawnAccMs >= interval) { + spaceShooterSpawnAccMs -= interval; + spaceShooterAsteroids.push({ + x: 36 + Math.random() * (mw - 72), + y: -45 - Math.random() * 90, + r: 13 + Math.random() * 17, + vy: 58 + Math.random() * 62, + hp: 1, + }); + } + + others.forEach((o, id) => { + if (isPreviewBotId(id)) return; + if (o.tx != null) o.x += (o.tx - o.x) * 0.28; + if (o.ty != null) o.y += (o.ty - o.y) * 0.28; + o.spaceShooterCx = (o.x + cw * 0.5) * ts; + o.spaceShooterCy = clampSpaceShooterWorldCy((o.y + ch * 0.92) * ts, mh); + }); + + let vx = 0; + let vy = 0; + if (!isChatFocused()) { + if (keys['ArrowLeft'] || keys['KeyA']) vx -= 1; + if (keys['ArrowRight'] || keys['KeyD']) vx += 1; + if (keys['ArrowUp'] || keys['KeyW']) vy -= 1; + if (keys['ArrowDown'] || keys['KeyS']) vy += 1; + } + const moveSpd = 280; + if (me.spaceShooterCx != null) { + me.spaceShooterCx = Math.max(18, Math.min(mw - 18, me.spaceShooterCx + vx * moveSpd * dt)); + } + if (me.spaceShooterCy != null) { + me.spaceShooterCy = clampSpaceShooterWorldCy(me.spaceShooterCy + vy * moveSpd * dt, mh); + } + syncSpaceShooterFootFromShipCenter(me); + + spaceShooterFireCd -= dt; + const fireHeld = !isChatFocused() && !!keys['Space']; + if (fireHeld && spaceShooterFireCd <= 0 && me.spaceShooterCy != null) { + spaceShooterFireCd = 0.21; + spaceShooterBullets.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 20, vy: -580, ownerId: myId }); + } + + stepSpaceShooterPreviewBots(dt); + + for (let i = spaceShooterBullets.length - 1; i >= 0; i--) { + const b = spaceShooterBullets[i]; + b.y += b.vy * dt; + if (b.y < -50 || b.x < -30 || b.x > mw + 30) spaceShooterBullets.splice(i, 1); + } + for (let ai = spaceShooterAsteroids.length - 1; ai >= 0; ai--) { + const a = spaceShooterAsteroids[ai]; + a.y += a.vy * dt; + if (a.y > mh + 100) spaceShooterAsteroids.splice(ai, 1); + } + + for (let bi = spaceShooterBullets.length - 1; bi >= 0; bi--) { + const b = spaceShooterBullets[bi]; + let hit = -1; + for (let ai = 0; ai < spaceShooterAsteroids.length; ai++) { + const a = spaceShooterAsteroids[ai]; + const dx = b.x - a.x, dy = b.y - a.y; + const rr = (a.r + 7) * (a.r + 7); + if (dx * dx + dy * dy <= rr) { hit = ai; break; } + } + if (hit >= 0) { + const a = spaceShooterAsteroids[hit]; + a.hp = (a.hp || 1) - 1; + spaceShooterBullets.splice(bi, 1); + if (a.hp <= 0) { + spaceShooterAsteroids.splice(hit, 1); + const add = 5; + if (b.ownerId === myId) { + me.spaceShooterScore = Math.max(0, (me.spaceShooterScore || 0) + add); + spaceShooterPopups.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 }); + } else { + const o = others.get(b.ownerId); + if (o) { + o.spaceShooterScore = Math.max(0, (o.spaceShooterScore || 0) + add); + spaceShooterPopups.push({ x: o.spaceShooterCx, y: o.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 }); + } + } + } + } + } + + const wallClock = Date.now(); + for (let pi = spaceShooterPopups.length - 1; pi >= 0; pi--) { + /* until ใช้ Date.now() — ห้ามเทียบกับ performance.now() (สเกลคนละระบบ → ลบไม่ออก) */ + if (wallClock >= spaceShooterPopups[pi].until) spaceShooterPopups.splice(pi, 1); + } + + me.direction = 'up'; + me.isWalking = vx !== 0 || vy !== 0; + const tEmit = Date.now(); + if (tEmit - spaceShooterLastMoveEmit > 90 && socket && myId != null) { + spaceShooterLastMoveEmit = tEmit; + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + spaceShooterScore: Math.max(0, me.spaceShooterScore | 0), + }); + } + } + + function drawSpaceShooterCombatLayer(ctx, worldToScreen, zDraw, timeMs) { + if (!mapData || !isSpaceShooter()) return; + const refs = buildSpaceShooterParticipantRefsPlay(); + const wallNow = typeof timeMs === 'number' ? timeMs : Date.now(); + + for (let i = 0; i < spaceShooterAsteroids.length; i++) { + const a = spaceShooterAsteroids[i]; + const [sx, sy] = worldToScreen(a.x, a.y); + const sr = Math.max(6, a.r * zDraw); + const grd = ctx.createRadialGradient(sx - sr * 0.25, sy - sr * 0.25, sr * 0.1, sx, sy, sr); + grd.addColorStop(0, 'rgba(90, 85, 95, 0.95)'); + grd.addColorStop(0.55, 'rgba(35, 32, 42, 0.98)'); + grd.addColorStop(1, 'rgba(18, 16, 24, 1)'); + ctx.fillStyle = grd; + ctx.beginPath(); + ctx.arc(sx, sy, sr, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 90, 60, 0.75)'; + ctx.lineWidth = Math.max(1.5, zDraw * 1.2); + ctx.beginPath(); + ctx.arc(sx, sy, sr * 0.88, 0.6, 1.9); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(sx + sr * 0.15, sy + sr * 0.1, sr * 0.35, 0, Math.PI * 2); + ctx.stroke(); + ctx.shadowColor = 'rgba(255, 60, 40, 0.55)'; + ctx.shadowBlur = sr * 0.45; + ctx.strokeStyle = 'rgba(255, 120, 80, 0.35)'; + ctx.beginPath(); + ctx.arc(sx, sy, sr + 3 * zDraw, 0, Math.PI * 2); + ctx.stroke(); + ctx.shadowBlur = 0; + } + + for (let i = 0; i < spaceShooterBullets.length; i++) { + const b = spaceShooterBullets[i]; + for (let k = 0; k < 4; k++) { + const t = k / 4; + const yy = b.y + t * 22; + const [bx, by] = worldToScreen(b.x, yy); + ctx.fillStyle = `rgba(255, ${180 - k * 35}, ${80 - k * 15}, ${0.95 - k * 0.18})`; + ctx.beginPath(); + ctx.arc(bx, by, Math.max(2, 3.2 * zDraw - k * 0.4), 0, Math.PI * 2); + ctx.fill(); + } + } + + refs.forEach((entry, idx) => { + const ent = entry.ref; + if (ent.spaceShooterCx == null || ent.spaceShooterCy == null) return; + const [sx, sy] = worldToScreen(ent.spaceShooterCx, ent.spaceShooterCy); + const col = SPACE_SHOOTER_SHIP_COLORS[idx % SPACE_SHOOTER_SHIP_COLORS.length]; + const bodyW = 14 * zDraw; + const bodyH = 22 * zDraw; + ctx.save(); + ctx.translate(sx, sy); + ctx.fillStyle = col; + ctx.strokeStyle = 'rgba(180, 255, 255, 0.85)'; + ctx.lineWidth = Math.max(1.2, zDraw); + ctx.beginPath(); + ctx.moveTo(0, -bodyH * 0.55); + ctx.lineTo(-bodyW * 0.55, bodyH * 0.42); + ctx.lineTo(0, bodyH * 0.22); + ctx.lineTo(bodyW * 0.55, bodyH * 0.42); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = 'rgba(255, 220, 120, 0.95)'; + ctx.beginPath(); + ctx.moveTo(-bodyW * 0.22, bodyH * 0.38); + ctx.lineTo(0, bodyH * 0.72); + ctx.lineTo(bodyW * 0.22, bodyH * 0.38); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + const name = (ent.nickname || '').slice(0, 10); + if (name) { + ctx.font = `${Math.max(9, 10 * zDraw)}px system-ui, "Kanit", sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(224, 242, 255, 0.92)'; + ctx.strokeStyle = 'rgba(0, 20, 40, 0.85)'; + ctx.lineWidth = 3; + ctx.strokeText(name, sx, sy - bodyH * 0.62); + ctx.fillText(name, sx, sy - bodyH * 0.62); + ctx.textAlign = 'left'; + } + }); + + for (let i = 0; i < spaceShooterPopups.length; i++) { + const p = spaceShooterPopups[i]; + if (wallNow >= p.until) continue; + const [px, py] = worldToScreen(p.x, p.y); + const dur = 700; + const age = 1 - Math.max(0, Math.min(1, (p.until - wallNow) / dur)); + ctx.save(); + ctx.globalAlpha = Math.max(0, 1 - age * 0.92); + ctx.font = `bold ${Math.max(12, 16 * zDraw)}px Orbitron, ui-sans-serif, sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = '#ffe066'; + ctx.strokeStyle = 'rgba(120, 60, 0, 0.6)'; + ctx.lineWidth = 3; + ctx.strokeText(p.text || '+5', px, py - age * 18); + ctx.fillText(p.text || '+5', px, py - age * 18); + ctx.restore(); + } + } + + const BALLOON_BOSS_PLAYER_COLORS = ['#ff79c6', '#7aa2f7', '#f9e2af', '#f7768e', '#9ece6a', '#bb9af7']; + + function normalizeBalloonBossPlayerSlotsInPlay(md) { + if (!md || md.gameType !== 'balloon_boss') return; + const w = md.width || 20, h = md.height || 15; + const src = md.balloonBossPlayerSlots || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= 6 ? Math.floor(n) : 0); + } + rows.push(row); + } + md.balloonBossPlayerSlots = rows; + } + + function getBalloonBossPlayerSpawnWorldCenterFromMap(md, slot1to6, ts) { + if (!md || !md.balloonBossPlayerSlots) return null; + const w = md.width || 20, h = md.height || 15; + const g = md.balloonBossPlayerSlots; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (g[y] && g[y][x] === slot1to6) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; + } + } + return null; + } + + function getBalloonBossBossWorldCenterPlay(md, ts) { + const bx = md && md.balloonBossBossSpawn; + if (bx && Number.isFinite(Number(bx.x)) && Number.isFinite(Number(bx.y))) { + return { cx: (Number(bx.x) + 0.5) * ts, cy: (Number(bx.y) + 0.5) * ts }; + } + const w = md.width || 20, h = md.height || 15; + return { cx: (w * 0.5) * ts, cy: (h * 0.38) * ts }; + } + + function buildBalloonBossParticipantRefsPlay() { + const refs = []; + if (me) refs.push({ id: myId, ref: me }); + const humanIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + humanIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + botIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + return refs.filter((r) => r.ref); + } + + function balloonBossBalloonsStartPlay() { + const n = Number(mapData && mapData.balloonBossBalloonsPerPlayer); + return Math.max(1, Math.min(12, Number.isFinite(n) ? Math.floor(n) : 5)); + } + + function balloonBossMaxHpPlay() { + const n = Number(mapData && mapData.balloonBossMaxHp); + return Math.max(20, Math.min(9999, Number.isFinite(n) ? Math.floor(n) : 100)); + } + + function balloonBossFireDelayMsPlay() { + const n = Number(mapData && mapData.balloonBossFireDelayMs); + return Math.max(120, Math.min(2000, Number.isFinite(n) ? Math.floor(n) : 380)); + } + + /** ความเร่งยาน (px/s²) — ต่ำ = คุมยาก ลื่น */ + function balloonBossShipAccelPlay() { + const n = Number(mapData && mapData.balloonBossShipAccel); + return Math.max(80, Math.min(520, Number.isFinite(n) ? n : 210)); + } + + /** ความเร็วสูงสุดยาน (px/s) */ + function balloonBossShipMaxSpeedPlay() { + const n = Number(mapData && mapData.balloonBossShipMaxSpeed); + return Math.max(60, Math.min(320, Number.isFinite(n) ? n : 158)); + } + + /** แรงหน่วงต่อวินาที (velocity *= 1 - min(1, drag*dt)) — สูง = หยุดเร็วขึ้น */ + function balloonBossShipDragPlay() { + const n = Number(mapData && mapData.balloonBossShipDrag); + return Math.max(0.4, Math.min(6, Number.isFinite(n) ? n : 1.65)); + } + + function balloonBossTimeLimitSecPlay() { + const t = Number(mapData && mapData.balloonBossTimeSec); + if (Number.isFinite(t) && t === 0) return 0; + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + return 120; + } + + function balloonBossRemainingSecPlay() { + const lim = balloonBossTimeLimitSecPlay(); + if (lim <= 0) return null; + const elapsed = (performance.now() - balloonBossSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function balloonBossTeamDamagePlay() { + let s = Math.max(0, me.balloonBossScore | 0); + others.forEach((o) => { s += Math.max(0, o.balloonBossScore | 0); }); + return s; + } + + function syncBalloonBossFootFromCenter(ent) { + if (!ent || !mapData || ent.balloonBossCx == null || ent.balloonBossCy == null) return; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + ent.x = ent.balloonBossCx / ts - cw * 0.5; + ent.y = ent.balloonBossCy / ts - ch * 0.92; + } + + function clampBalloonBossWorld(cx, cy, mw, mh) { + const e = 22; + return { + cx: Math.max(e, Math.min(mw - e, cx)), + cy: Math.max(e, Math.min(mh - e, cy)), + }; + } + + function applyBalloonBossSpawnLayoutPlay() { + if (!mapData || mapData.gameType !== 'balloon_boss') return; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const { cw, ch } = getCharacterFootprintWH(mapData); + const mw = w * ts, mh = h * ts; + const refs = buildBalloonBossParticipantRefsPlay(); + const n = refs.length || 1; + const bStart = balloonBossBalloonsStartPlay(); + refs.forEach((entry, idx) => { + const ent = entry.ref; + const slot = (idx % 6) + 1; + const cen = getBalloonBossPlayerSpawnWorldCenterFromMap(mapData, slot, ts); + let cx, cy; + if (cen) { + cx = cen.cx; + cy = cen.cy; + } else { + const t = (idx + 1) / (n + 1); + cx = t * mw; + cy = Math.min(mh - ts * 1.2, (h - 1.2) * ts); + } + const cl = clampBalloonBossWorld(cx, cy, mw, mh); + ent.balloonBossCx = cl.cx; + ent.balloonBossCy = cl.cy; + if (typeof ent.balloonBossScore !== 'number' || !Number.isFinite(ent.balloonBossScore)) ent.balloonBossScore = 0; + if (typeof ent.balloonBossBalloons !== 'number' || !Number.isFinite(ent.balloonBossBalloons)) ent.balloonBossBalloons = bStart; + if (typeof ent.balloonBossHitIframe !== 'number') ent.balloonBossHitIframe = 0; + ent.balloonBossVelX = 0; + ent.balloonBossVelY = 0; + ent.x = ent.balloonBossCx / ts - cw * 0.5; + ent.y = ent.balloonBossCy / ts - ch * 0.92; + ent.tx = ent.x; + ent.ty = ent.y; + ent.direction = 'up'; + }); + } + + function endBalloonBossGame(reason) { + if (balloonBossGameEnded || !mapData || mapData.gameType !== 'balloon_boss') return; + balloonBossGameEnded = true; + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) { + titleEl.textContent = reason === 'victory' ? 'ชนะบอส! · Boss defeated' : 'หมดเวลา · Time up'; + } + msgEl.textContent = reason === 'victory' + ? 'MEGA VIRUS ถูกกำจัดแล้ว — อันดับจากความเสียหายที่ทำกับบอส' + : 'หมดเวลา — อันดับจากความเสียหายที่ทำกับบอส'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ id: myId, nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', score: Math.max(0, me.balloonBossScore | 0) }); + } + others.forEach((o, id) => { + ranks.push({ id, nickname: (o && o.nickname) ? String(o.nickname).trim() : id, score: Math.max(0, o.balloonBossScore | 0) }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — DMG ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + } + + function stepBalloonBossPreviewBots(dt, mw, mh, bossCx, bossCy, ts) { + if (!previewFillBots || !mapData || mapData.gameType !== 'balloon_boss') return; + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o || o.balloonBossEliminated || o.balloonBossCx == null) return; + const wob = Math.sin(performance.now() / 800 + bid.length) * 0.55; + const wob2 = Math.cos(performance.now() / 1100 + bid.charCodeAt(0)) * 0.45; + const spd = 48; + let nx = o.balloonBossCx + wob * spd * dt; + let ny = o.balloonBossCy + wob2 * spd * 0.72 * dt; + const cl = clampBalloonBossWorld(nx, ny, mw, mh); + o.balloonBossCx = cl.cx; + o.balloonBossCy = cl.cy; + syncBalloonBossFootFromCenter(o); + o.tx = o.x; + o.ty = o.y; + if (typeof o.balloonBossBotNextFire === 'number') o.balloonBossBotNextFire -= dt; + else o.balloonBossBotNextFire = 0.8 + Math.random() * 0.9; + if (o.balloonBossBotNextFire <= 0) { + o.balloonBossBotNextFire = 0.9 + Math.random() * 1.1; + const delayMs = balloonBossFireDelayMsPlay(); + const dx = bossCx - o.balloonBossCx, dy = bossCy - o.balloonBossCy; + const d = Math.sqrt(dx * dx + dy * dy) || 1; + balloonBossPendingShots.push({ + releaseAt: performance.now() + delayMs, + sx: o.balloonBossCx, sy: o.balloonBossCy, + vx: (dx / d) * 520, vy: (dy / d) * 520, + ownerId: bid, + }); + } + }); + } + + function balloonBossTickFrame() { + if (!mapData || mapData.gameType !== 'balloon_boss') return; + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const mw = w * ts, mh = h * ts; + const { cw, ch } = getCharacterFootprintWH(mapData); + others.forEach((o, id) => { + if (isPreviewBotId(id)) return; + if (!o || o.balloonBossEliminated) return; + if (o.tx != null) o.x += (o.tx - o.x) * 0.24; + if (o.ty != null) o.y += (o.ty - o.y) * 0.24; + const cx = (o.x + cw * 0.5) * ts; + const cy = (o.y + ch * 0.92) * ts; + const cl = clampBalloonBossWorld(cx, cy, mw, mh); + o.balloonBossCx = cl.cx; + o.balloonBossCy = cl.cy; + }); + const now = performance.now(); + const dt = Math.min(0.055, balloonBossLastTickMs ? (now - balloonBossLastTickMs) / 1000 : 0.016); + balloonBossLastTickMs = now; + const bossC = getBalloonBossBossWorldCenterPlay(mapData, ts); + const bossR = Math.max(36, ts * 1.15); + const maxHp = balloonBossMaxHpPlay(); + const teamDmg = balloonBossTeamDamagePlay(); + + if (me.balloonBossEliminated) { + me.balloonBossVelX = 0; + me.balloonBossVelY = 0; + } + if (balloonBossGameEnded) { + for (let hi = balloonBossHitFx.length - 1; hi >= 0; hi--) { + balloonBossHitFx[hi].t -= dt; + if (balloonBossHitFx[hi].t <= 0) balloonBossHitFx.splice(hi, 1); + } + return; + } + const remSec = balloonBossRemainingSecPlay(); + if (remSec != null && remSec <= 0) { + endBalloonBossGame('time'); + return; + } + if (teamDmg >= maxHp) { + endBalloonBossGame('victory'); + return; + } + + for (let hi = balloonBossHitFx.length - 1; hi >= 0; hi--) { + balloonBossHitFx[hi].t -= dt; + if (balloonBossHitFx[hi].t <= 0) balloonBossHitFx.splice(hi, 1); + } + + const aliveRefs = buildBalloonBossParticipantRefsPlay().filter((e) => e.ref && !e.ref.balloonBossEliminated); + + for (let pi = balloonBossPendingShots.length - 1; pi >= 0; pi--) { + const pnd = balloonBossPendingShots[pi]; + if (now >= pnd.releaseAt) { + balloonBossPlayerBullets.push({ x: pnd.sx, y: pnd.sy, vx: pnd.vx, vy: pnd.vy, ownerId: pnd.ownerId }); + balloonBossPendingShots.splice(pi, 1); + } + } + + const bossFireEvery = 0.92; + balloonBossBossFireAcc += dt; + while (balloonBossBossFireAcc >= bossFireEvery) { + balloonBossBossFireAcc -= bossFireEvery; + if (aliveRefs.length) { + const wave = Math.floor(now / 1000); + const tgt = aliveRefs[wave % aliveRefs.length].ref; + if (tgt && tgt.balloonBossCx != null) { + const dx = tgt.balloonBossCx - bossC.cx, dy = tgt.balloonBossCy - bossC.cy; + const d = Math.sqrt(dx * dx + dy * dy) || 1; + const spd = 290; + balloonBossBossBullets.push({ x: bossC.cx, y: bossC.cy, vx: (dx / d) * spd, vy: (dy / d) * spd }); + } + } + } + + function applyBalloonBossHitToEntity(ent) { + if (!ent || ent.balloonBossEliminated) return; + if (ent.balloonBossHitIframe > now) return; + ent.balloonBossHitIframe = now + 520; + ent.balloonBossBalloons = Math.max(0, (ent.balloonBossBalloons | 0) - 1); + if (ent.balloonBossBalloons <= 0) { + ent.balloonBossEliminated = true; + ent.balloonBossBalloons = 0; + } + if (ent === me && socket) { + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + balloonBossBalloons: me.balloonBossBalloons, + balloonBossEliminated: !!me.balloonBossEliminated, + balloonBossScore: Math.max(0, me.balloonBossScore | 0), + }); + } + } + + for (let bi = balloonBossBossBullets.length - 1; bi >= 0; bi--) { + const b = balloonBossBossBullets[bi]; + b.x += b.vx * dt; + b.y += b.vy * dt; + if (b.x < -80 || b.x > mw + 80 || b.y < -80 || b.y > mh + 80) { + balloonBossBossBullets.splice(bi, 1); + continue; + } + let hit = false; + aliveRefs.forEach((entry) => { + if (hit) return; + const ent = entry.ref; + if (!ent || ent.balloonBossCx == null) return; + const dx = b.x - ent.balloonBossCx, dy = b.y - ent.balloonBossCy; + if (dx * dx + dy * dy <= (26 * 26)) { + hit = true; + applyBalloonBossHitToEntity(ent); + } + }); + if (hit) balloonBossBossBullets.splice(bi, 1); + } + + for (let i = balloonBossPlayerBullets.length - 1; i >= 0; i--) { + const b = balloonBossPlayerBullets[i]; + b.x += b.vx * dt; + b.y += b.vy * dt; + if (b.x < -60 || b.x > mw + 60 || b.y < -60 || b.y > mh + 60) { + balloonBossPlayerBullets.splice(i, 1); + continue; + } + const dx = b.x - bossC.cx, dy = b.y - bossC.cy; + if (dx * dx + dy * dy <= bossR * bossR) { + balloonBossHitFx.push({ x: bossC.cx, y: bossC.cy, t: 0.22 }); + const add = 1; + if (b.ownerId === myId) { + me.balloonBossScore = Math.max(0, (me.balloonBossScore | 0) + add); + } else { + const o = others.get(b.ownerId); + if (o) o.balloonBossScore = Math.max(0, (o.balloonBossScore | 0) + add); + } + balloonBossPlayerBullets.splice(i, 1); + } + } + + let inX = 0, inY = 0; + if (!me.balloonBossEliminated && !isChatFocused()) { + if (keys['ArrowLeft'] || keys['KeyA']) inX -= 1; + if (keys['ArrowRight'] || keys['KeyD']) inX += 1; + if (keys['ArrowUp'] || keys['KeyW']) inY -= 1; + if (keys['ArrowDown'] || keys['KeyS']) inY += 1; + } + const accel = balloonBossShipAccelPlay(); + const maxSpd = balloonBossShipMaxSpeedPlay(); + const dragPerSec = balloonBossShipDragPlay(); + if (typeof me.balloonBossVelX !== 'number' || !Number.isFinite(me.balloonBossVelX)) me.balloonBossVelX = 0; + if (typeof me.balloonBossVelY !== 'number' || !Number.isFinite(me.balloonBossVelY)) me.balloonBossVelY = 0; + let nvx = me.balloonBossVelX; + let nvy = me.balloonBossVelY; + if (inX !== 0 || inY !== 0) { + const il = Math.sqrt(inX * inX + inY * inY) || 1; + nvx += (inX / il) * accel * dt; + nvy += (inY / il) * accel * dt; + } + const drag = Math.min(1, dragPerSec * dt); + nvx *= (1 - drag); + nvy *= (1 - drag); + const curSpd = Math.sqrt(nvx * nvx + nvy * nvy); + if (curSpd > maxSpd) { + const s = maxSpd / curSpd; + nvx *= s; + nvy *= s; + } + me.balloonBossVelX = nvx; + me.balloonBossVelY = nvy; + if (!me.balloonBossEliminated && me.balloonBossCx != null) { + const ncx = me.balloonBossCx + nvx * dt; + const ncy = me.balloonBossCy + nvy * dt; + const cl = clampBalloonBossWorld(ncx, ncy, mw, mh); + if (Math.abs(cl.cx - ncx) > 0.5) me.balloonBossVelX = 0; + if (Math.abs(cl.cy - ncy) > 0.5) me.balloonBossVelY = 0; + me.balloonBossCx = cl.cx; + me.balloonBossCy = cl.cy; + } + syncBalloonBossFootFromCenter(me); + me.direction = 'up'; + me.isWalking = Math.abs(me.balloonBossVelX) > 8 || Math.abs(me.balloonBossVelY) > 8 || inX !== 0 || inY !== 0; + + balloonBossPlayerFireCd -= dt; + const wantFire = !isChatFocused() && !!keys['Space'] && !me.balloonBossEliminated; + if (wantFire && balloonBossPlayerFireCd <= 0 && me.balloonBossCy != null) { + balloonBossPlayerFireCd = 0.35; + const delayMs = balloonBossFireDelayMsPlay(); + const dx = bossC.cx - me.balloonBossCx, dy = bossC.cy - me.balloonBossCy; + const d = Math.sqrt(dx * dx + dy * dy) || 1; + balloonBossPendingShots.push({ + releaseAt: now + delayMs, + sx: me.balloonBossCx, sy: me.balloonBossCy, + vx: (dx / d) * 480, vy: (dy / d) * 480, + ownerId: myId, + }); + } + + stepBalloonBossPreviewBots(dt, mw, mh, bossC.cx, bossC.cy, ts); + + const tEmit = Date.now(); + if (tEmit - balloonBossLastMoveEmit > 95 && socket && myId != null) { + balloonBossLastMoveEmit = tEmit; + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + balloonBossScore: Math.max(0, me.balloonBossScore | 0), + balloonBossBalloons: Math.max(0, me.balloonBossBalloons | 0), + balloonBossEliminated: !!me.balloonBossEliminated, + }); + } + } + + function drawBalloonBossCombatLayer(ctx, worldToScreen, zDraw, timeMs) { + if (!mapData || !isBalloonBoss()) return; + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const mw = w * ts, mh = h * ts; + const bossC = getBalloonBossBossWorldCenterPlay(mapData, ts); + const maxHp = balloonBossMaxHpPlay(); + const dmg = balloonBossTeamDamagePlay(); + const hpLeft = Math.max(0, maxHp - dmg); + const bossR = Math.max(36, ts * 1.15); + const refs = buildBalloonBossParticipantRefsPlay(); + const leaderId = (() => { + let best = null, bs = -1; + refs.forEach((e) => { + const sc = Math.max(0, e.ref.balloonBossScore | 0); + if (sc > bs) { bs = sc; best = e.id; } + }); + return best; + })(); + + for (let hi = 0; hi < balloonBossHitFx.length; hi++) { + const fx = balloonBossHitFx[hi]; + const [hx, hy] = worldToScreen(fx.x, fx.y); + const pulse = 1 + (fx.t || 0) * 3; + ctx.save(); + ctx.globalAlpha = Math.min(1, (fx.t || 0) * 4); + ctx.strokeStyle = 'rgba(255, 220, 120, 0.9)'; + ctx.lineWidth = 3 * zDraw; + ctx.beginPath(); + ctx.arc(hx, hy, bossR * zDraw * 0.5 * pulse, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + } + + const [bsx, bsy] = worldToScreen(bossC.cx, bossC.cy); + ctx.save(); + ctx.translate(bsx, bsy); + ctx.strokeStyle = 'rgba(0, 255, 255, 0.75)'; + ctx.lineWidth = Math.max(2, 2.5 * zDraw); + ctx.beginPath(); + for (let k = 0; k < 6; k++) { + const ang = (k / 6) * Math.PI * 2 - Math.PI / 2; + const px = Math.cos(ang) * bossR * zDraw * 1.05; + const py = Math.sin(ang) * bossR * zDraw * 1.05; + if (k === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + } + ctx.closePath(); + ctx.stroke(); + ctx.fillStyle = 'rgba(0, 40, 60, 0.35)'; + ctx.fill(); + + const skullR = bossR * zDraw * 0.62; + const grd = ctx.createRadialGradient(-skullR * 0.2, -skullR * 0.25, skullR * 0.1, 0, 0, skullR); + grd.addColorStop(0, 'rgba(55, 50, 62, 0.98)'); + grd.addColorStop(1, 'rgba(22, 18, 30, 1)'); + ctx.fillStyle = grd; + ctx.beginPath(); + ctx.arc(0, 0, skullR, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = 'rgba(255, 40, 60, 0.95)'; + ctx.shadowColor = 'rgba(255, 0, 0, 0.8)'; + ctx.shadowBlur = 12 * zDraw; + ctx.beginPath(); + ctx.arc(-skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2); + ctx.arc(skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + + ctx.fillStyle = 'rgba(255, 100, 200, 0.9)'; + ctx.font = `bold ${Math.max(10, 11 * zDraw)}px ui-monospace, monospace`; + ctx.textAlign = 'center'; + ctx.fillText('MEGA VIRUS : [ ' + hpLeft + ' / ' + maxHp + ' ]', 0, -bossR * zDraw - 14); + const barW = bossR * zDraw * 2.2; + const barH = Math.max(5, 6 * zDraw); + ctx.fillStyle = 'rgba(0,0,0,0.55)'; + ctx.fillRect(-barW / 2, -bossR * zDraw - 8, barW, barH); + ctx.fillStyle = 'rgba(255, 80, 200, 0.85)'; + ctx.fillRect(-barW / 2, -bossR * zDraw - 8, barW * (hpLeft / maxHp), barH); + ctx.strokeStyle = 'rgba(180, 255, 255, 0.5)'; + ctx.strokeRect(-barW / 2, -bossR * zDraw - 8, barW, barH); + ctx.restore(); + + function drawBalloonsCluster(sx, sy, count, col, z) { + const n = Math.max(0, Math.min(8, count | 0)); + for (let b = 0; b < n; b++) { + const ox = (b - (n - 1) / 2) * 7 * z; + const oy = -18 * z - Math.abs(b - (n - 1) / 2) * 2 * z; + ctx.fillStyle = col; + ctx.beginPath(); + ctx.ellipse(sx + ox, sy + oy, 6.5 * z, 8 * z, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255,255,255,0.35)'; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + + refs.forEach((entry, idx) => { + const ent = entry.ref; + if (ent.balloonBossCx == null || ent.balloonBossCy == null) return; + const [sx, sy] = worldToScreen(ent.balloonBossCx, ent.balloonBossCy); + const col = BALLOON_BOSS_PLAYER_COLORS[idx % BALLOON_BOSS_PLAYER_COLORS.length]; + const z = zDraw; + ctx.save(); + if (ent.balloonBossEliminated) ctx.globalAlpha = 0.38; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.65)'; + ctx.lineWidth = Math.max(1.2, 1.5 * z); + ctx.beginPath(); + ctx.arc(sx, sy, 22 * z, 0, Math.PI * 2); + ctx.stroke(); + drawBalloonsCluster(sx, sy, ent.balloonBossBalloons | 0, col, z); + const nm = String(ent.nickname || '').slice(0, 12); + ctx.font = `${Math.max(9, 10 * z)}px system-ui, "Kanit", sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(248, 250, 252, 0.95)'; + ctx.strokeStyle = 'rgba(0, 10, 30, 0.85)'; + ctx.lineWidth = 3; + let label = nm; + if (entry.id === leaderId && leaderId != null) label = '♔ ' + nm; + ctx.strokeText(label, sx, sy + 28 * z); + ctx.fillText(label, sx, sy + 28 * z); + const charImg = getPlayTintedAvatarSource( + getAvatarImg(ent.characterId, 'up', timeMs, !!ent.isWalking), + ent.characterId, 'up', timeMs, !!ent.isWalking, + ent.playTint || playTintFromPeerId(entry.id) + ); + const size = 26 * z; + if (charImg && ((charImg.tagName === 'CANVAS' && charImg.width > 0) || (charImg.complete && charImg.naturalWidth))) { + const iw = charImg.tagName === 'CANVAS' ? charImg.width : charImg.naturalWidth; + const ih = charImg.tagName === 'CANVAS' ? charImg.height : charImg.naturalHeight; + const sc = Math.min(size / iw, size / ih, 1); + const dw = iw * sc, dh = ih * sc; + ctx.drawImage(charImg, 0, 0, iw, ih, sx - dw / 2, sy - dh + 4 * z, dw, dh); + } else { + ctx.fillStyle = col; + ctx.fillRect(sx - 10 * z, sy - 18 * z, 20 * z, 22 * z); + } + ctx.restore(); + }); + + for (let i = 0; i < balloonBossPlayerBullets.length; i++) { + const b = balloonBossPlayerBullets[i]; + const [bx, by] = worldToScreen(b.x, b.y); + ctx.save(); + ctx.translate(bx, by); + ctx.rotate(Math.atan2(b.vy, b.vx) + Math.PI / 2); + ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.beginPath(); + ctx.moveTo(0, -8 * zDraw); + ctx.lineTo(-5 * zDraw, 6 * zDraw); + ctx.lineTo(5 * zDraw, 6 * zDraw); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = 'rgba(0, 255, 200, 0.5)'; + ctx.font = `${Math.max(7, 8 * zDraw)}px monospace`; + ctx.textAlign = 'center'; + ctx.fillText('01001', 0, 12 * zDraw); + ctx.restore(); + } + for (let i = 0; i < balloonBossBossBullets.length; i++) { + const b = balloonBossBossBullets[i]; + const [bx, by] = worldToScreen(b.x, b.y); + ctx.fillStyle = 'rgba(255, 60, 120, 0.92)'; + ctx.beginPath(); + ctx.arc(bx, by, Math.max(3, 4.5 * zDraw), 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 200, 255, 0.6)'; + ctx.stroke(); + } + } + + function jumpSurviveCollectPlatformCells(md) { + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + const out = []; + if (!pa) return out; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (pa[y] && pa[y][x] === 1) out.push({ x, y }); + } + } + return out; + } + + /** ความสูงแมปเป็นคาบวนของแพลตฟอร์ม — แถวที่เลื่อนออกบนจะวนมาโผล่ล่าง */ + function jumpSurvivePlatformPeriodPx(md) { + return (md.height || 15) * tileSize; + } + + /** + * ยืนบนหน้าแพลตฟอร์มช่อง (tx,ty) — เลือกชั้น k ที่ใกล้ hintY (กล้อง / กลางแมป) + */ + function jumpSurviveGridPosStandingOnPlatform(md, tx, ty, scrollPx) { + const ts = tileSize; + const h = md.height || 15; + const period = jumpSurvivePlatformPeriodPx(md); + const { cw, ch } = getCharacterFootprintWH(md); + const rawTop = ty * ts - scrollPx; + const hintY = (jumpSurviveCamCenterY > 1) + ? jumpSurviveCamCenterY + : h * ts * 0.72; + const k = Math.round((hintY - rawTop - ts * 0.35) / period); + const platTop = rawTop + k * period; + const feetY = platTop; + const pxc = (tx + 0.5) * ts; + return { + x: pxc / ts - cw * 0.5, + y: feetY / ts - ch, + }; + } + + function jumpSurviveGridPosFromTileCenter(md, tx, ty) { + return jumpSurviveGridPosStandingOnPlatform(md, tx, ty, 0); + } + + function jumpSurviveRandomStandGridPos(md) { + const cells = jumpSurviveCollectPlatformCells(md); + const sc = jumpSurvivePlatformScrollPx; + if (cells.length) { + const c = cells[Math.floor(Math.random() * cells.length)]; + return jumpSurviveGridPosStandingOnPlatform(md, c.x, c.y, sc); + } + const sp = md.spawn || { x: 1, y: 1 }; + return jumpSurviveGridPosStandingOnPlatform(md, Number(sp.x) || 1, Number(sp.y) || 1, sc); + } + + /** ทดสอบจากเอดิเตอร์: วางคน+บอทบนแพลตฟอร์ม (ไม่ให้เกิดบน spawnArea บนฟ้าแล้วลอย) */ + function applyJumpSurvivePreviewSpawnLayout(onlyBots) { + if (!mapData || mapData.gameType !== 'jump_survive') return; + const cells = jumpSurviveCollectPlatformCells(mapData); + cells.sort((a, b) => b.y - a.y || a.x - b.x); + const sc = jumpSurvivePlatformScrollPx; + const pool = cells.length + ? cells.map((c) => jumpSurviveGridPosStandingOnPlatform(mapData, c.x, c.y, sc)) + : [jumpSurviveGridPosStandingOnPlatform(mapData, Number(mapData.spawn && mapData.spawn.x) || 1, Number(mapData.spawn && mapData.spawn.y) || 1, sc)]; + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + + function stampJumpBotState(o) { + if (!o) return; + o.jumpSurviveVy = 0; + o.jumpSurviveOnGround = true; + o.jumpSurviveEliminated = false; + o.tx = o.x; + o.ty = o.y; + } + + if (onlyBots) { + let idx = 1 + realIds.length; + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || !pool.length) return; + const p = pool[idx % pool.length]; + idx++; + o.x = p.x + (Math.random() - 0.5) * 0.08; + o.y = p.y + (Math.random() - 0.5) * 0.08; + stampJumpBotState(o); + }); + return; + } + + let idx = 0; + function assignEnt(ent) { + if (!ent || !pool.length) return; + const p = pool[idx % pool.length]; + idx++; + ent.x = p.x + (Math.random() - 0.5) * 0.08; + ent.y = p.y + (Math.random() - 0.5) * 0.08; + stampJumpBotState(ent); + } + assignEnt(me); + realIds.forEach((rid) => assignEnt(others.get(rid))); + botIds.forEach((bid) => assignEnt(others.get(bid))); + jumpSurviveInitRuntime(); + } + + /** + * ฟิสิกส์กระโดดร่วม (ผู้เล่น + บอท preview) + * @returns {{ pxc: number, feetY: number, vy: number, onGround: boolean, died: boolean }} + */ + function jumpSurvivePhysicsIntegrate(md, opts) { + const { + dt, + cw, ch, + pxc0, feetY0, vy0, wasOnGround, + jumpQueued, + moveLeft, moveRight, + camCenterY, + halfViewH, + } = opts; + const w = md.width || 20, h = md.height || 15; + const ts = tileSize; + const bw = ts * 0.34; + const bh = ts * 0.9; + let pxc = pxc0; + let feetY = feetY0; + let vy = vy0; + let onGround = wasOnGround; + + const gFull = Number(md.jumpSurviveGravity) > 0 ? Number(md.jumpSurviveGravity) : 3200; + const g = gFull * dt; + let jumpV0; + if (Number(md.jumpSurviveJumpImpulse) > 0) { + jumpV0 = -Number(md.jumpSurviveJumpImpulse); + } else { + const mult = (typeof playJumpSurviveJumpHeightMult === 'number' && playJumpSurviveJumpHeightMult > 0) + ? playJumpSurviveJumpHeightMult + : 1.5; + const charHpixels = ch * ts; + const impulse = Math.sqrt(Math.max(1e-6, 2 * gFull * mult * charHpixels)); + jumpV0 = -Math.min(impulse, ts * 36); + } + let moveX = (Number(md.jumpSurviveMovePxPerSec) > 0 ? Number(md.jumpSurviveMovePxPerSec) : 200) * dt; + if (isJumpSurviveMissionUiMapPlay()) moveX *= 1.35; + + if (jumpQueued && onGround) { + vy = jumpV0; + onGround = false; + } + vy += g; + + let nx = pxc; + if (moveLeft) nx -= moveX; + if (moveRight) nx += moveX; + nx = Math.max(bw + 1, Math.min(w * ts - bw - 1, nx)); + + const headY0 = feetY - bh; + const tryL = nx - bw; + const tryR = nx + bw; + if (!jumpSurviveOverlapsSolidForHorzMove(md, tryL, headY0, tryR, feetY)) { + pxc = nx; + } + + feetY += vy * dt; + let headTop = feetY - bh; + if (jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + if (vy > 0) { + for (let s = 0; s < 22; s++) { + feetY -= ts * 0.035; + headTop = feetY - bh; + if (!jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + vy = 0; + onGround = true; + break; + } + } + } else if (vy < 0) { + for (let s = 0; s < 22; s++) { + feetY += ts * 0.035; + headTop = feetY - bh; + if (!jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + vy = 0; + break; + } + } + } + } else { + onGround = false; + } + + headTop = feetY - bh; + const headBandBottom = headTop + bh * 0.42; + if (jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY) && + jumpSurviveOverlapsObjectWall(md, pxc - bw, headTop, pxc + bw, headBandBottom)) { + return { pxc, feetY, vy, onGround, died: true }; + } + + headTop = feetY - bh; + const topKill = camCenterY - halfViewH - ts * 0.35; + const botKill = camCenterY + halfViewH + ts * 0.85; + if (feetY < topKill || feetY > botKill || headTop < -ts * 2) { + return { pxc, feetY, vy, onGround, died: true }; + } + + return { pxc, feetY, vy, onGround, died: false }; + } + + function jumpSurviveEntityFromHitbox(o, md, pxc, feetYpx) { + const { cw, ch } = getCharacterFootprintWH(md); + o.x = pxc / tileSize - cw * 0.5; + o.y = feetYpx / tileSize - ch; + } + + function jumpSurviveInitRuntime() { + jumpSurviveLastTickMs = performance.now(); + jumpSurviveSessionStartMs = performance.now(); + jumpSurviveVy = 0; + jumpSurviveJumpQueued = false; + jumpSurviveOnGround = false; + jumpSurvivePlatformScrollPx = 0; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + if (isJumpSurviveMissionUiMapPlay()) { + const w = mapData.width || 20, h = mapData.height || 15; + jumpSurviveCamCenterX = (w * ts) * 0.5; + jumpSurviveCamCenterY = (h * ts) * 0.5; + } else { + jumpSurviveCamCenterX = (me.x + cw * 0.5) * ts; + jumpSurviveCamCenterY = (me.y + ch * 0.5) * ts; + } + } + + function jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const x0 = Math.floor(left / ts); + const x1 = Math.floor((right - 1e-6) / ts); + const y0 = Math.floor(top / ts); + const y1 = Math.floor((bottom - 1e-6) / ts); + for (let ty = y0; ty <= y1; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (tx < 0 || tx >= w || ty >= h) return true; + if (ty < 0) continue; + const ob = md.objects && md.objects[ty] && md.objects[ty][tx]; + if (ob === 1) return true; + } + } + return false; + } + + function jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, scrollPx) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + if (!pa) return false; + const period = jumpSurvivePlatformPeriodPx(md); + if (period < ts) return false; + const x0 = Math.max(0, Math.floor(left / ts)); + const x1 = Math.min(w - 1, Math.floor((right - 1e-6) / ts)); + const vm = ts * 2; + for (let ty = 0; ty < h; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (!pa[ty] || pa[ty][tx] !== 1) continue; + const rawTop = ty * ts - scrollPx; + const kMin = Math.ceil((top - rawTop - ts - vm) / period); + const kMax = Math.floor((bottom - rawTop + vm) / period); + for (let k = kMin; k <= kMax; k++) { + const platTop = rawTop + k * period; + const platBot = platTop + ts; + const tileLeft = tx * ts; + const tileRight = (tx + 1) * ts; + if (platBot > top && platTop < bottom && right > tileLeft && left < tileRight) return true; + } + } + } + return false; + } + + function jumpSurviveOverlapsSolid(md, left, top, right, bottom) { + if (jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom)) return true; + return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, jumpSurvivePlatformScrollPx); + } + + /** + * ชนแนวนอน: objects เต็มกล่อง; แพลตฟอร์มไม่นับช่วงปลายเท้าเล็กน้อย + * (ถ้าใช้กล่องเต็มกับแพลตฟอร์ม ขณะยืนบนแพลตฟอร์มมักทับช่องแนวตั้ง — เดินซ้ายขวาถูกปฏิเสธทั้งก้อน) + */ + function jumpSurviveOverlapsSolidForHorzMove(md, left, top, right, bottom) { + if (jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom)) return true; + const ts = tileSize; + const footSlop = ts * 0.16; + const platformBottom = bottom - footSlop; + if (platformBottom <= top + ts * 0.05) return false; + return jumpSurviveOverlapsPlatforms(md, left, top, right, platformBottom, jumpSurvivePlatformScrollPx); + } + + /** Head/body overlaps กำแพง (objects=1) เท่านั้น — ใช้ตรวจชนฝาบนแล้วตาย */ + function jumpSurviveOverlapsObjectWall(md, left, top, right, bottom) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const x0 = Math.floor(left / ts); + const x1 = Math.floor((right - 1e-6) / ts); + const y0 = Math.floor(top / ts); + const y1 = Math.floor((bottom - 1e-6) / ts); + for (let ty = y0; ty <= y1; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (tx < 0 || tx >= w || ty < 0 || ty >= h) continue; + const ob = md.objects && md.objects[ty] && md.objects[ty][tx]; + if (ob === 1) return true; + } + } + return false; + } + + function jumpSurviveSyncMeFromHitbox(pxc, feetYpx) { + jumpSurviveEntityFromHitbox(me, mapData, pxc, feetYpx); + } + + function jumpSurviveCameraTargetPx(md) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + if (isJumpSurviveMissionUiMapPlay()) { + return { px: (w * ts) * 0.5, py: (h * ts) * 0.5 }; + } + const { cw, ch } = getCharacterFootprintWH(md); + const cx = (e) => (e.x + cw * 0.5) * ts; + const cy = (e) => (e.y + ch * 0.5) * ts; + if (!jumpSurviveEliminated) return { px: cx(me), py: cy(me) }; + const aliveHumanIds = [...others.keys()].filter((id) => { + if (isPreviewBotId(id)) return false; + const o = others.get(id); + return o && !o.jumpSurviveEliminated; + }); + const aliveBotIds = [...others.keys()].filter((id) => { + if (!isPreviewBotId(id)) return false; + const o = others.get(id); + return o && !o.jumpSurviveEliminated; + }); + const useIds = aliveHumanIds.length ? aliveHumanIds : aliveBotIds; + if (!useIds.length) return { px: cx(me), py: cy(me) }; + let sx = 0, sy = 0; + useIds.forEach((id) => { + const o = others.get(id); + if (!o) return; + sx += cx(o); + sy += cy(o); + }); + const n = useIds.length; + return { px: sx / n, py: sy / n }; + } + + /** มีแพลตฟอร์มใต้จุดเล็กน้อยข้างหน้าแนว wishDir หรือไม่ — กันบอทเดินหลุมแล้วตกจอ */ + function jumpSurviveBotHasFloorAhead(md, pxc, feetY, wishDir, scrollPx) { + if (!wishDir) return true; + const ts = tileSize; + const bw = ts * 0.34; + const probeX = pxc + wishDir * (bw + ts * 0.82); + const pad = ts * 0.24; + const left = probeX - pad; + const right = probeX + pad; + const top = feetY - ts * 0.45; + const bottom = feetY + ts * 8; + return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, scrollPx); + } + + function stepJumpSurvivePreviewBots(dt, halfViewH) { + if (!previewFillBots || !mapData || !isJumpSurvive()) return; + const md = mapData; + const { cw, ch } = getCharacterFootprintWH(md); + const ts = tileSize; + const wpx = (md.width || 20) * ts; + const centerPx = wpx * 0.5; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + if (o.jumpSurviveEliminated) return; + if (typeof o.jumpSurviveVy !== 'number') o.jumpSurviveVy = 0; + if (o.jumpSurviveOnGround == null) o.jumpSurviveOnGround = false; + if (typeof o.jumpSurviveNextJumpAi !== 'number') o.jumpSurviveNextJumpAi = Date.now() + 300 + Math.floor(Math.random() * 500); + if (typeof o.jumpSurviveWishNext !== 'number') o.jumpSurviveWishNext = Date.now() + 200; + if (o.jumpSurviveWishX == null) o.jumpSurviveWishX = 0; + + let pxc = (o.x + cw * 0.5) * ts; + let feetY = (o.y + ch) * ts; + + const camCy = jumpSurviveCamCenterY; + const mission = isJumpSurviveMissionUiMapPlay(); + const lowDanger = feetY > camCy + halfViewH - ts * 8.5; + + if (Date.now() >= o.jumpSurviveWishNext) { + const wMin = mission && lowDanger ? 180 : 380; + const wSpan = mission && lowDanger ? 480 : 700; + o.jumpSurviveWishNext = Date.now() + wMin + Math.floor(Math.random() * wSpan); + const tier = o.botTier || 'avg'; + if (mission && lowDanger && Math.random() < 0.55) { + o.jumpSurviveWishX = 0; + } else if (pxc < centerPx - ts * 1.8) o.jumpSurviveWishX = 1; + else if (pxc > centerPx + ts * 1.8) o.jumpSurviveWishX = -1; + else if (tier === 'sharp') { + const roam = mission ? 0.42 : 0.55; + o.jumpSurviveWishX = Math.random() < roam ? (Math.random() < 0.5 ? -1 : 1) : 0; + } else { + const roam = mission ? 0.28 : 0.4; + o.jumpSurviveWishX = Math.random() < roam ? (Math.random() < 0.5 ? -1 : 1) : 0; + } + } + + let moveLeft = o.jumpSurviveWishX < 0; + let moveRight = o.jumpSurviveWishX > 0; + let hazardJump = false; + if (o.jumpSurviveOnGround) { + const sc = jumpSurvivePlatformScrollPx; + if (moveRight && !jumpSurviveBotHasFloorAhead(md, pxc, feetY, 1, sc)) { + o.jumpSurviveWishX = Math.random() < 0.65 ? -1 : 0; + hazardJump = true; + } else if (moveLeft && !jumpSurviveBotHasFloorAhead(md, pxc, feetY, -1, sc)) { + o.jumpSurviveWishX = Math.random() < 0.65 ? 1 : 0; + hazardJump = true; + } + moveLeft = o.jumpSurviveWishX < 0; + moveRight = o.jumpSurviveWishX > 0; + } + + let jumpQ = false; + if (hazardJump) o.jumpSurviveNextJumpAi = Math.min(o.jumpSurviveNextJumpAi, Date.now()); + if (o.jumpSurviveOnGround && Date.now() >= o.jumpSurviveNextJumpAi) { + o.jumpSurviveNextJumpAi = Date.now() + 300 + Math.floor(Math.random() * (hazardJump ? 520 : 1100)); + const tier = o.botTier || 'avg'; + let jp = tier === 'sharp' ? 0.5 : tier === 'weak' ? 0.25 : 0.36; + if (mission) jp += 0.12; + if (lowDanger) jp += 0.22; + jp = Math.min(0.9, jp); + if (hazardJump || Math.random() < jp) jumpQ = true; + } + + const r = jumpSurvivePhysicsIntegrate(md, { + dt, + cw, + ch, + pxc0: pxc, + feetY0: feetY, + vy0: o.jumpSurviveVy, + wasOnGround: o.jumpSurviveOnGround, + jumpQueued: jumpQ, + moveLeft, + moveRight, + camCenterY: jumpSurviveCamCenterY, + halfViewH, + }); + + if (r.died) { + o.jumpSurviveEliminated = true; + o.jumpSurviveVy = 0; + jumpSurviveEntityFromHitbox(o, md, r.pxc, r.feetY); + o.tx = o.x; + o.ty = o.y; + o.botIsWalking = false; + return; + } + o.jumpSurviveVy = r.vy; + o.jumpSurviveOnGround = r.onGround; + jumpSurviveEntityFromHitbox(o, md, r.pxc, r.feetY); + o.tx = o.x; + o.ty = o.y; + if (moveRight) o.direction = 'right'; + else if (moveLeft) o.direction = 'left'; + o.botIsWalking = Math.abs(r.vy) > 40 || moveLeft || moveRight; + }); + } + + function jumpSurviveTickFrame() { + if (!mapData || !isJumpSurvive()) return; + if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) return; + const remEnd = jumpSurviveRemainingSecMission(); + if (remEnd !== null && remEnd <= 0) { + endJumpSurviveMissionRound(); + return; + } + } + const md = mapData; + const ts = tileSize; + const now = performance.now(); + const dt = Math.min(0.05, Math.max(0, (now - jumpSurviveLastTickMs) / 1000)); + jumpSurviveLastTickMs = now; + + const halfViewH = canvas.height / (2 * zoom); + const rise = (Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42) * dt; + jumpSurvivePlatformScrollPx += rise; + + const camTgt = jumpSurviveCameraTargetPx(md); + const ck = Math.min(1, dt * 7); + jumpSurviveCamCenterX += (camTgt.px - jumpSurviveCamCenterX) * ck; + jumpSurviveCamCenterY += (camTgt.py - jumpSurviveCamCenterY) * ck; + + if (jumpSurviveEliminated) { + stepJumpSurvivePreviewBots(dt, halfViewH); + jumpSurviveMissionMaybeEarlyFinish(); + return; + } + + const { cw, ch } = getCharacterFootprintWH(md); + let pxc = (me.x + cw * 0.5) * ts; + let feetY = (me.y + ch) * ts; + + const jq = jumpSurviveJumpQueued; + jumpSurviveJumpQueued = false; + + const r = jumpSurvivePhysicsIntegrate(md, { + dt, + cw, + ch, + pxc0: pxc, + feetY0: feetY, + vy0: jumpSurviveVy, + wasOnGround: jumpSurviveOnGround, + jumpQueued: jq, + moveLeft: !!(keys['ArrowLeft'] || keys['KeyA']), + moveRight: !!(keys['ArrowRight'] || keys['KeyD']), + camCenterY: jumpSurviveCamCenterY, + halfViewH, + }); + + if (r.died) { + jumpSurviveEliminated = true; + jumpSurviveVy = 0; + jumpSurviveSyncMeFromHitbox(r.pxc, r.feetY); + ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'KeyA', 'KeyW', 'KeyS', 'KeyD'].forEach((c) => { keys[c] = false; }); + } else { + jumpSurviveVy = r.vy; + jumpSurviveOnGround = r.onGround; + jumpSurviveSyncMeFromHitbox(r.pxc, r.feetY); + } + + stepJumpSurvivePreviewBots(dt, halfViewH); + + if (!jumpSurviveEliminated) { + me.isWalking = Math.abs(jumpSurviveVy) > 40 || !!(keys['ArrowLeft'] || keys['KeyA'] || keys['ArrowRight'] || keys['KeyD']); + if (keys['ArrowRight'] || keys['KeyD']) me.direction = 'right'; + else if (keys['ArrowLeft'] || keys['KeyA']) me.direction = 'left'; + + if (Date.now() - jumpSurviveLastEmitT > 80) { + jumpSurviveLastEmitT = Date.now(); + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + } + + jumpSurviveMissionMaybeEarlyFinish(); + } + + function gridImageLibIdleUrlPlay(entry) { + if (entry == null) return ''; + if (typeof entry === 'string') return entry; + if (typeof entry === 'object' && entry.idle) return String(entry.idle); + return ''; + } + function gridImageLibHeldUrlPlay(entry) { + if (entry == null || typeof entry === 'string') return ''; + if (typeof entry === 'object' && entry.held && String(entry.held).length) return String(entry.held); + return ''; + } + + function normalizeGridImageCellsOnMap(md) { + if (!md) return; + const w = md.width || 20, h = md.height || 15; + if (!Array.isArray(md.gridImageLibrary)) md.gridImageLibrary = []; + md.gridImageLibrary = md.gridImageLibrary + .map((raw) => { + if (typeof raw === 'string' && raw.length > 0 && raw.length < 30000000) return { idle: raw, held: null }; + if (raw && typeof raw === 'object' && typeof raw.idle === 'string' && raw.idle.length > 0 && raw.idle.length < 30000000) { + let held = typeof raw.held === 'string' && raw.held.length > 0 && raw.held.length < 30000000 ? raw.held : null; + if (held && held === raw.idle) held = null; + return { idle: raw.idle, held }; + } + return null; + }) + .filter(Boolean); + const libLen = md.gridImageLibrary.length; + let placements = []; + if (Array.isArray(md.gridImageSprites) && md.gridImageSprites.length) { + placements = md.gridImageSprites.map((raw) => { + const i = Math.max(0, Math.floor(Number(raw.i))); + const x = Math.floor(Number(raw.x)); + const y = Math.floor(Number(raw.y)); + const ww = Math.max(1, Math.floor(Number(raw.w)) || 1); + const hh = Math.max(1, Math.floor(Number(raw.h)) || 1); + const rawB = raw.bindCarryOption; + const bn = typeof rawB === 'number' ? rawB : parseInt(String(rawB), 10); + const base = { i, x, y, w: ww, h: hh }; + if (Number.isFinite(bn) && bn >= 1 && bn <= QUIZ_CARRY_MAX_OPTION_SLOTS) base.bindCarryOption = bn; + return base; + }).filter((s) => Number.isFinite(s.i) && s.i >= 0 && s.i < libLen && + Number.isFinite(s.x) && Number.isFinite(s.y) && s.w >= 1 && s.h >= 1 && + s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0); + placements = placements.map((s) => { + let x = s.x, y = s.y, ww = s.w, hh = s.h; + if (x < 0) { ww += x; x = 0; } + if (y < 0) { hh += y; y = 0; } + if (x + ww > w) ww = w - x; + if (y + hh > h) hh = h - y; + const out = { i: s.i, x, y, w: Math.max(1, ww), h: Math.max(1, hh) }; + if (s.bindCarryOption != null) out.bindCarryOption = s.bindCarryOption; + return out; + }).filter((s) => s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0); + } else { + const cells = md.gridImageCells; + if (Array.isArray(cells) && cells.length === h && libLen > 0) { + for (let yy = 0; yy < h; yy++) { + const row = cells[yy]; + if (!row) continue; + for (let xx = 0; xx < w; xx++) { + if (row[xx] == null) continue; + const iv = parseInt(row[xx], 10); + if (!Number.isFinite(iv) || iv < 0 || iv >= libLen) continue; + placements.push({ i: iv, x: xx, y: yy, w: 1, h: 1 }); + } + } + } + } + md.gridImagePlacements = placements; + const outCells = Array(h).fill(0).map(() => Array(w).fill(null)); + placements.forEach((sp) => { + for (let yy = sp.y; yy < sp.y + sp.h; yy++) { + for (let xx = sp.x; xx < sp.x + sp.w; xx++) { + if (yy >= 0 && yy < h && xx >= 0 && xx < w) outCells[yy][xx] = sp.i; + } + } + }); + md.gridImageCells = outCells; + } + + function loadMapGridImages() { + mapGridImageImgs = []; + mapGridImageHeldImgs = []; + if (!mapData || !Array.isArray(mapData.gridImageLibrary) || !mapData.gridImageLibrary.length) return; + mapData.gridImageLibrary.forEach((entry, i) => { + mapGridImageImgs[i] = null; + mapGridImageHeldImgs[i] = null; + const idle = gridImageLibIdleUrlPlay(entry); + if (!idle) return; + const im = new Image(); + im.onload = () => { try { draw(); } catch (e) {} }; + im.onerror = () => {}; + im.src = idle; + mapGridImageImgs[i] = im; + const held = gridImageLibHeldUrlPlay(entry); + if (held && held !== idle) { + const hm = new Image(); + hm.onload = () => { try { draw(); } catch (e) {} }; + hm.onerror = () => {}; + hm.src = held; + mapGridImageHeldImgs[i] = hm; + } + }); + } + + /** โซนตัวเลือกที่ทับสไปรต์มากที่สุด — quiz_carry สลับรูป idle/held · ถ้าไม่ทับเลยลองขยายขอบ 1 ช่อง (มาร์กเกอร์ชิดขอบสไปรต์) */ + function dominantQuizCarryOptionInSpriteRect(md, sp) { + if (!md || md.gameType !== 'quiz_carry' || !md.quizCarryOptionArea) return null; + const g = md.quizCarryOptionArea; + const mw = md.width || 20, mh = md.height || 15; + const bx = sp.x | 0, by = sp.y | 0, bw = sp.w | 0, bh = sp.h | 0; + const counts = new Map(); + function addOverlapRect(x0, y0, ww, hh) { + for (let yy = y0; yy < y0 + hh; yy++) { + if (yy < 0 || yy >= mh) continue; + const row = g[yy]; + if (!row) continue; + for (let xx = x0; xx < x0 + ww; xx++) { + if (xx < 0 || xx >= mw) continue; + const v = row[xx]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + if (!Number.isFinite(n) || n < 1 || n > QUIZ_CARRY_MAX_OPTION_SLOTS) continue; + const idx = n - 1; + counts.set(idx, (counts.get(idx) || 0) + 1); + } + } + } + addOverlapRect(bx, by, bw, bh); + if (!counts.size) { + const pad = 1; + const x0 = Math.max(0, bx - pad); + const y0 = Math.max(0, by - pad); + const x1 = Math.min(mw, bx + bw + pad); + const y1 = Math.min(mh, by + bh + pad); + addOverlapRect(x0, y0, x1 - x0, y1 - y0); + } + if (!counts.size) return null; + let best = null; + let bestN = -1; + counts.forEach((n, k) => { + if (best == null || n > bestN || (n === bestN && k < best)) { + bestN = n; + best = k; + } + }); + return best; + } + + /** 0-based choice index สำหรับสลับ idle/held — ใช้ bindCarryOption จากแมปก่อน แล้วค่อยนับทับโซน */ + function quizCarryOptionIndexForGridSprite(md, sp) { + if (!sp) return null; + const rawB = sp.bindCarryOption; + if (rawB != null && rawB !== '') { + const n = typeof rawB === 'number' ? rawB : parseInt(String(rawB), 10); + if (Number.isFinite(n) && n >= 1 && n <= QUIZ_CARRY_MAX_OPTION_SLOTS) return n - 1; + } + return dominantQuizCarryOptionInSpriteRect(md, sp); + } + + /** รูป held จากคลังกริดสำหรับข้อนี้ — ใช้วาดป้ายติดตัว (ไม่ผ่าน sanitize URL) */ + function findQuizCarryGridHeldImgForChoiceIndex(choiceIdx) { + if (choiceIdx == null || choiceIdx < 0 || !isQuizCarry() || !mapData) return null; + const pl = mapData.gridImagePlacements; + if (!Array.isArray(pl) || !pl.length) return null; + for (let gi = 0; gi < pl.length; gi++) { + const sp = pl[gi]; + const opt = quizCarryOptionIndexForGridSprite(mapData, sp); + if (opt !== choiceIdx) continue; + const gh = mapGridImageHeldImgs[sp.i]; + if (gh && gh.complete && gh.naturalWidth) return gh; + } + return null; + } + + /** รูป idle จากคลังกริดสำหรับข้อนี้ — วาดบนป้ายตัวเลือกบนพื้นเมื่อไม่มีรูปจากคำถาม/ธีม */ + function findQuizCarryGridIdleImgForChoiceIndex(choiceIdx) { + if (choiceIdx == null || choiceIdx < 0 || !isQuizCarry() || !mapData) return null; + const pl = mapData.gridImagePlacements; + if (!Array.isArray(pl) || !pl.length) return null; + for (let gi = 0; gi < pl.length; gi++) { + const sp = pl[gi]; + const opt = quizCarryOptionIndexForGridSprite(mapData, sp); + if (opt !== choiceIdx) continue; + const gid = mapGridImageImgs[sp.i]; + if (gid && gid.complete && gid.naturalWidth) return gid; + } + return null; + } + + function applyMapAndStart(gameMapData, res) { + const prevWasSpaceShooter = mapData && mapData.gameType === 'space_shooter'; + const prevWasBalloonBoss = mapData && mapData.gameType === 'balloon_boss'; + mapData = gameMapData; + if (res && res.mapId != null && String(res.mapId).trim() !== '') { + playSessionMapId = String(res.mapId).trim(); + } + if (res && res.hostId != null) playHostId = res.hostId; + if (mapData.gameType !== 'space_shooter') { + spaceShooterGameEnded = false; + if (prevWasSpaceShooter) { + const gend = document.getElementById('gauntlet-ended-overlay'); + if (gend) gend.classList.add('is-hidden'); + } + } + if (mapData.gameType !== 'balloon_boss') { + balloonBossGameEnded = false; + if (prevWasBalloonBoss) { + const gend = document.getElementById('gauntlet-ended-overlay'); + if (gend) gend.classList.add('is-hidden'); + } + } + if (!mapData.lanes && mapData.gameType === 'frogger') mapData.lanes = []; + if (!mapData.interactive) mapData.interactive = []; + if (mapData.gameType === 'quiz') { + if (!mapData.quizTrueArea) mapData.quizTrueArea = []; + if (!mapData.quizFalseArea) mapData.quizFalseArea = []; + if (!mapData.quizQuestionArea) mapData.quizQuestionArea = []; + } + if (mapData.gameType === 'stack') { + if (!mapData.stackReleaseArea) mapData.stackReleaseArea = []; + if (!mapData.stackLandArea) mapData.stackLandArea = []; + } + if (mapData.gameType === 'quiz_carry') { + if (!mapData.quizCarryHubArea) mapData.quizCarryHubArea = []; + if (!mapData.quizCarryOptionArea) mapData.quizCarryOptionArea = []; + if (!mapData.carryEmbedCountdownArea) mapData.carryEmbedCountdownArea = []; + if (!mapData.quizQuestions) mapData.quizQuestions = []; + if (!mapData.quizQuestionArea) mapData.quizQuestionArea = []; + normalizeQuizCarryLayersInPlay(mapData); + } + if (mapData.gameType === 'quiz_battle') { + if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = []; + if (!mapData.quizBattlePathArea) mapData.quizBattlePathArea = []; + normalizeQuizBattlePathInPlay(mapData); + normalizeQuizBattleDomeInPlay(mapData); + } + tileSize = mapData.tileSize || 32; + mapBackgroundImg = null; + if (mapData.backgroundImage) { + mapBackgroundImg = new Image(); + mapBackgroundImg.src = mapData.backgroundImage; + } + normalizeGridImageCellsOnMap(mapData); + loadMapGridImages(); + var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน' + : (isGauntlet() ? ' | พรมแดงสุดท้าย (Last Light)' + : (isLobby() ? ' | โถงรอ' : (isQuiz() ? ' | ตอบคำถาม' : (isQuizCarry() ? ' | หยิบคำตอบมาวางกลาง' : (isQuizBattle() ? ' | Quiz Battle · โดม E' : (isStack() ? ' | Stack ซ้อนตึก' : (isJumpSurvive() ? ' | กระโดดให้รอด' : (isSpaceShooter() ? ' | ยิงยานอวกาศ' : (isBalloonBoss() ? ' | ลูกโป้งยิงบอส' : ''))))))))); + var prevTag = previewMode ? '[ทดสอบ] ' : ''; + document.getElementById('room-id').textContent = prevTag + spaceId + modeLabel; + const plist = Array.isArray(res.peers) ? res.peers : []; + const myPeer = plist.find(p => p.id === myId); + const myPos = peerXYFromJoin(myPeer, mapData.spawn); + me.x = myPos.x; + me.y = myPos.y; + if (isQuizCarry()) { + const hubSnap = snapPositionOutOfQuizCarryHubIfNeeded(me.x, me.y); + me.x = hubSnap.x; + me.y = hubSnap.y; + } + if (isQuizBattle()) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = pathSnap.x; + me.y = pathSnap.y; + } + me.direction = myPeer?.direction ?? 'down'; + me.characterId = myPeer?.characterId || null; + meGauntletJumpTicks = typeof myPeer?.gauntletJumpTicks === 'number' ? myPeer.gauntletJumpTicks : 0; + meGauntletJumpVis = meGauntletJumpTicks; + { + const sc0 = Number(myPeer && myPeer.gauntletScore); + me.gauntletScore = Number.isFinite(sc0) ? Math.max(0, sc0) : 0; + me.gauntletEliminated = !!(myPeer && myPeer.gauntletEliminated); + } + { + const sh0 = Number(myPeer && myPeer.spaceShooterScore); + me.spaceShooterScore = Number.isFinite(sh0) ? Math.max(0, sh0) : 0; + } + { + const bSt = balloonBossBalloonsStartPlay(); + const bs0 = Number(myPeer && myPeer.balloonBossScore); + me.balloonBossScore = Number.isFinite(bs0) ? Math.max(0, bs0) : 0; + const bb0 = Number(myPeer && myPeer.balloonBossBalloons); + me.balloonBossBalloons = Number.isFinite(bb0) ? Math.max(0, Math.floor(bb0)) : bSt; + me.balloonBossEliminated = !!(myPeer && myPeer.balloonBossEliminated); + } + gauntletObstacles = []; + gauntletObsRenderPrev = []; + gauntletObsRenderNext = []; + gauntletObsBlendT0 = 0; + me.playTint = playTintFromPeerId(String((myId != null && myId !== '') ? myId : (nick || 'local'))); + others.clear(); + plist.forEach(p => { + if (p.id !== myId) { + const pos = peerXYFromJoin(p, mapData.spawn); + let px = pos.x, py = pos.y; + if (isQuizCarry()) { + const hubSnap = snapPositionOutOfQuizCarryHubIfNeeded(px, py); + px = hubSnap.x; + py = hubSnap.y; + } + if (isQuizBattle()) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(px, py); + px = pathSnap.x; + py = pathSnap.y; + } + others.set(p.id, { + x: px, y: py, tx: px, ty: py, direction: p.direction, nickname: p.nickname, characterId: p.characterId, + playTint: playTintFromPeerId(p.id), + gauntletJumpTicks: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0, + gauntletJumpVis: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0, + gauntletScore: (() => { const s = Number(p.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + gauntletEliminated: !!p.gauntletEliminated, + jumpSurviveEliminated: false, + spaceShooterScore: (() => { const s = Number(p.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossScore: (() => { const s = Number(p.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBalloons: (() => { + const s = Number(p.balloonBossBalloons); + return Number.isFinite(s) ? Math.max(0, Math.floor(s)) : balloonBossBalloonsStartPlay(); + })(), + balloonBossEliminated: !!p.balloonBossEliminated, + quizCarryHeld: null, + }); + } + }); + if (mapData.gameType === 'gauntlet') { + me.tx = me.x; + me.ty = me.y; + if (res.gauntletEndsAt != null) { + const geJoin = Number(res.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(geJoin) ? geJoin : null; + } else { + gauntletEndsAtMs = null; + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => {}); + } else { + me.tx = null; + me.ty = null; + gauntletEndsAtMs = null; + } + rebalancePreviewBots(); + if (mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + if (isStack()) { + lastStackTickMs = performance.now(); + resetStackMinigameState(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + else reapplyStackMiniSizingFromGlobals(); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => { + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }); + } else { + stackMini = null; + } + playPath = []; + me.isWalking = false; + if (mapData.gameType === 'jump_survive') { + if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = []; + if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = []; + normalizeJumpSurvivePlatformAreaInPlay(mapData); + jumpSurviveEliminated = false; + jumpSurviveGameEnded = false; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + if (isJumpSurviveMissionUiMapPlay()) { + jumpSurviveMissionPhase = 'howto'; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + setTimeout(function () { showJumpSurviveMissionHowtoOverlay(); }, 50); + } else { + jumpSurviveMissionPhase = null; + jumpSurviveInitRuntime(); + } + } + if (mapData.gameType === 'space_shooter') { + spaceShooterGameEnded = false; + normalizeShooterSpawnSlotsInPlay(mapData); + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterPopups = []; + spaceShooterLastTickMs = performance.now(); + spaceShooterSpawnAccMs = 0; + spaceShooterFireCd = 0; + spaceShooterSessionStartMs = performance.now(); + spaceShooterLastMoveEmit = 0; + } + if (mapData.gameType === 'balloon_boss') { + balloonBossGameEnded = false; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossHitFx = []; + balloonBossLastTickMs = performance.now(); + balloonBossPlayerFireCd = 0; + balloonBossSessionStartMs = performance.now(); + balloonBossLastMoveEmit = 0; + balloonBossBossFireAcc = 0; + } + if (isQuiz()) setupPlayQuizUi(); + else if (isQuizCarry()) setupPlayQuizCarryUi(); + else if (isQuizBattle()) setupPlayQuizBattleUi(); + else teardownPlayQuizUi(); + if (mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) { + hideConflictingOverlaysForGauntletCrown(); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccR = document.getElementById('gauntlet-crown-countdown'); + if (gccR) gccR.classList.add('is-hidden'); + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50); + } else if (mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay()) { + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccRj = document.getElementById('gauntlet-crown-countdown'); + if (gccRj) gccRj.classList.add('is-hidden'); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + } else { + const hov = document.getElementById('gauntlet-crown-howto-overlay'); + if (hov) { + hov.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + } + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccR2 = document.getElementById('gauntlet-crown-countdown'); + if (gccR2) gccR2.classList.add('is-hidden'); + } + resizeCanvas(); draw(); tick(); + } + + socket.on('connect', () => { + firstCharacterDefaultPromise.then(() => { + socket.emit('join-space', { spaceId, nickname: nick, characterId: getPlayCharacterId() }, (res) => { + if (!res || !res.ok) { + const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้'; + alert(errMsg); + const caseLocked = /เริ่มคดี|ไม่รับผู้เล่น/.test(errMsg); + const detectiveReturn = params.get('detectiveReturn') === '1' + || (function () { try { return sessionStorage.getItem('detectiveMinigameReturn') === '1'; } catch (e) { return false; } })(); + if (caseLocked || detectiveReturn) { + window.location.replace(buildRoomLobbyReturnHref()); + } else { + window.location.replace(BASE + '/lobby.html'); + } + return; + } + myId = socket.id; + let botSlots = parseInt(res.botSlotCount, 10); + if ((!botSlots || botSlots < 1) && res.maxPlayers > 0 && res.maxPlayers < 6) { + botSlots = 6 - res.maxPlayers; + } + if (botSlots > 0) { + detectiveCaseFillBots = true; + detectiveBotSlotCount = Math.min(6, Math.max(0, botSlots)); + } + if (isDetectiveMinigamePlay()) { + try { document.documentElement.classList.add('play-detective-minigame'); } catch (eD) { /* ignore */ } + } + if (res.quizCarrySettingsSnap && typeof res.quizCarrySettingsSnap === 'object') { + quizCarryJoinSettingsSnap = res.quizCarrySettingsSnap; + if (res.quizCarrySettingsSnap.carryMapPanelTheme && typeof res.quizCarrySettingsSnap.carryMapPanelTheme === 'object') { + setQuizCarryMapPanelThemeFromApi({ carryMapPanelTheme: res.quizCarrySettingsSnap.carryMapPanelTheme }); + } + if (res.quizCarrySettingsSnap.carryEmbedCountdownTheme && typeof res.quizCarrySettingsSnap.carryEmbedCountdownTheme === 'object') { + setQuizCarryEmbedCountdownThemeFromApi({ carryEmbedCountdownTheme: res.quizCarrySettingsSnap.carryEmbedCountdownTheme }); + } + if (Array.isArray(res.quizCarrySettingsSnap.carryChoicePlaqueThemes) && res.quizCarrySettingsSnap.carryChoicePlaqueThemes.length) { + setQuizCarryChoicePlaqueThemeFromApi({ carryChoicePlaqueThemes: res.quizCarrySettingsSnap.carryChoicePlaqueThemes }); + } else if (res.quizCarrySettingsSnap.carryChoicePlaqueTheme && typeof res.quizCarrySettingsSnap.carryChoicePlaqueTheme === 'object') { + setQuizCarryChoicePlaqueThemeFromApi({ carryChoicePlaqueTheme: res.quizCarrySettingsSnap.carryChoicePlaqueTheme }); + } + const joinSc = Number(res.quizCarrySettingsSnap.carryChoicePlaqueMapScale); + if (Number.isFinite(joinSc)) { + quizCarryPlaqueMapScale = Math.max(0.85, Math.min(2.5, joinSc)); + } + } + if (previewMode && editorEmbedReturn && res.mapData && res.mapData.gameType === 'lobby') { + const q = []; + q.push('space=' + encodeURIComponent(spaceId)); + q.push('nick=' + encodeURIComponent(nick)); + const mid = params.get('map'); + if (mid) q.push('map=' + encodeURIComponent(mid)); + q.push('preview=1'); + q.push('editorEmbed=1'); + if (params.get('defaultChar') != null) q.push('defaultChar=' + encodeURIComponent(params.get('defaultChar'))); + window.location.replace(BASE + '/room-lobby.html?' + q.join('&')); + return; + } + const playMapId = params.get('map'); + if (playMapId) { + fetch(BASE + '/api/maps/' + encodeURIComponent(playMapId)) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (data) applyMapAndStart(data, res); + else applyMapAndStart(res.mapData, res); + }) + .catch(() => applyMapAndStart(res.mapData, res)); + } else { + applyMapAndStart(res.mapData, res); + } + }); + }); + }); + + const LERP = 0.2; + socket.on('user-joined', (data) => { + if (!data || isPreviewBotId(data.id)) return; + const x = Number(data.x); + const y = Number(data.y); + const px = Number.isFinite(x) ? x : 1; + const py = Number.isFinite(y) ? y : 1; + others.set(data.id, { + x: px, y: py, tx: px, ty: py, + direction: data.direction || 'down', nickname: data.nickname, + characterId: data.characterId ?? null, + playTint: playTintFromPeerId(data.id), + gauntletJumpTicks: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0, + gauntletJumpVis: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0, + gauntletScore: (() => { const s = Number(data.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + jumpSurviveEliminated: false, + spaceShooterScore: (() => { const s = Number(data.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossScore: (() => { const s = Number(data.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBalloons: (() => { + const s = Number(data.balloonBossBalloons); + return Number.isFinite(s) ? Math.max(0, Math.floor(s)) : balloonBossBalloonsStartPlay(); + })(), + balloonBossEliminated: !!data.balloonBossEliminated, + quizCarryHeld: null, + }); + if (previewFillBots) rebalancePreviewBots(); + if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud(); + }); + socket.on('host-changed', (d) => { + if (d && d.hostId != null) playHostId = d.hostId; + if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud(); + }); + socket.on('quiz-carry-lobby-sync', (d) => { + if (!d || typeof d.readyMap !== 'object') return; + quizCarryLobbyReadyMap = { ...d.readyMap }; + if (quizCarryPregameActive && isQuizCarry()) { + quizCarrySyncGuestReadyIfNeeded(); + updateQuizCarryPregameHud(); + } + }); + socket.on('quiz-carry-lobby-started', () => { + if (!(previewMode && editorEmbedReturn && isQuizCarry())) return; + endQuizCarryEmbedPregameAndStart(); + }); + socket.on('gauntlet-crown-lobby-sync', (d) => { + if (!d || typeof d.readyMap !== 'object') return; + gauntletCrownLobbyReadyMap = { ...d.readyMap }; + if (gauntletCrownPregamePhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateGauntletCrownHowtoHud(); + } + }); + socket.on('gauntlet-crown-lobby-started', () => { + if (!isGauntletCrownHeistMapPlay()) return; + beginGauntletCrownCountdownThenRun(); + }); + socket.on('user-move', (data) => { + if (data && myId != null && data.id === myId) { + /* Gauntlet: ตำแหน่งอ้างอิงจาก gauntlet-sync + เลอร์ปเท่านั้น — ไม่งั้น echo จาก move จะสแนป me.x/y โดยไม่อัปเดต tx/ty ทำให้เลอร์ปผิดและชนเซิร์ฟเวอร์ไม่ตรง */ + if (mapData && mapData.gameType === 'gauntlet') { + return; + } + if (mapData && mapData.gameType === 'stack') { + return; + } + if (mapData && mapData.gameType === 'jump_survive') { + return; + } + if (mapData && mapData.gameType === 'space_shooter') { + if (data.spaceShooterScore != null) { + const s = Number(data.spaceShooterScore); + if (Number.isFinite(s)) me.spaceShooterScore = Math.max(0, s); + } + return; + } + if (mapData && mapData.gameType === 'balloon_boss') { + if (data.balloonBossScore != null) { + const s = Number(data.balloonBossScore); + if (Number.isFinite(s)) me.balloonBossScore = Math.max(0, s); + } + if (data.balloonBossBalloons != null) { + const b = Number(data.balloonBossBalloons); + if (Number.isFinite(b)) me.balloonBossBalloons = Math.max(0, Math.floor(b)); + } + if (data.balloonBossEliminated != null) me.balloonBossEliminated = !!data.balloonBossEliminated; + return; + } + if (mapData && mapData.gameType === 'quiz_battle') { + if (data.x != null) { + const x = Number(data.x); + if (Number.isFinite(x)) me.x = x; + } + if (data.y != null) { + const y = Number(data.y); + if (Number.isFinite(y)) me.y = y; + } + const s = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = s.x; + me.y = s.y; + if (data.direction) me.direction = data.direction; + if (data.characterId != null) me.characterId = data.characterId; + playPath = []; + return; + } + if (data.x != null) { + const x = Number(data.x); + if (Number.isFinite(x)) me.x = x; + } + if (data.y != null) { + const y = Number(data.y); + if (Number.isFinite(y)) me.y = y; + } + if (data.direction) me.direction = data.direction; + if (data.characterId != null) me.characterId = data.characterId; + playPath = []; + return; + } + const o = others.get(data.id); + if (o) { + if (mapData && mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData) && !isPreviewBotId(data.id)) { + const px = Number(data.x); + const py = Number(data.y); + if (Number.isFinite(px) && Number.isFinite(py)) { + const sn = snapPositionOntoQuizBattlePathIfNeeded(px, py); + o.tx = sn.x; + o.ty = sn.y; + o.x = sn.x; + o.y = sn.y; + } else { + o.tx = data.x; + o.ty = data.y; + } + } else { + o.tx = data.x; + o.ty = data.y; + } + o.direction = data.direction || o.direction; + if (data.characterId != null) o.characterId = data.characterId; + if (mapData && mapData.gameType === 'space_shooter' && data.spaceShooterScore != null) { + const s = Number(data.spaceShooterScore); + if (Number.isFinite(s)) o.spaceShooterScore = Math.max(0, s); + } + if (mapData && mapData.gameType === 'balloon_boss') { + if (data.balloonBossScore != null) { + const s = Number(data.balloonBossScore); + if (Number.isFinite(s)) o.balloonBossScore = Math.max(0, s); + } + if (data.balloonBossBalloons != null) { + const b = Number(data.balloonBossBalloons); + if (Number.isFinite(b)) o.balloonBossBalloons = Math.max(0, Math.floor(b)); + } + if (data.balloonBossEliminated != null) o.balloonBossEliminated = !!data.balloonBossEliminated; + } + } + }); + socket.on('user-left', (data) => { + if (!data || isPreviewBotId(data.id)) return; + others.delete(data.id); + if (previewFillBots) rebalancePreviewBots(); + }); + socket.on('chat', (data) => { + const box = document.getElementById('chat-messages'); + if (!box) return; + const div = document.createElement('div'); + div.className = 'chat-msg'; + div.textContent = (data.nickname || '') + ': ' + (data.text || ''); + box.appendChild(div); + box.scrollTop = 1e9; + }); + + socket.on('quiz-phase', (p) => { + if (previewMode) return; + if (!p || !mapData || !isQuiz()) return; + if (p.text) playQuizText = p.text; + playQuizPhaseLocal = p.phase; + playQuizPhaseEndsAt = p.endsAt || 0; + const phaseEl = document.getElementById('quiz-game-phase-label'); + const qEl = document.getElementById('quiz-game-question'); + if (phaseEl) { + phaseEl.textContent = p.phase === 'read' + ? ('อ่านคำถาม ข้อ ' + (p.questionIndex || '') + '/' + (p.questionTotal || '')) + : ('เดินไปโซน ถูก / ผิด — ข้อ ' + (p.questionIndex || '') + '/' + (p.questionTotal || '')); + } + if (qEl) qEl.textContent = playQuizText || ''; + const ov = document.getElementById('quiz-game-overlay'); + if (ov) ov.classList.remove('is-hidden'); + if (playQuizTimerInterval) clearInterval(playQuizTimerInterval); + playQuizTimerInterval = setInterval(updatePlayQuizTimerDisplay, 200); + updatePlayQuizTimerDisplay(); + if (!previewMode && isQuiz() && !Object.keys(playLiveQuizScores).length) initPlayLiveQuizScoresZeros(); + }); + + socket.on('quiz-result', (r) => { + if (previewMode || !r) return; + if (r.scores) { + playLiveQuizScores = { ...r.scores }; + renderPlayQuizScoreboard(playLiveQuizScores); + } + showPlayQuizFeedback(r); + }); + + socket.on('quiz-player-state', (st) => { + if (previewMode) return; + if (!st) return; + playQuizPlayerLocal = { + cannotTrue: !!st.cannotTrue, + cannotFalse: !!st.cannotFalse, + eliminated: !!st.eliminated, + score: typeof st.score === 'number' ? st.score : (playQuizPlayerLocal.score || 0), + }; + }); + + socket.on('quiz-ended', () => { + if (previewMode && isQuiz()) return; + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + playQuizPhaseLocal = null; + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) ov.classList.add('is-hidden'); + const panel = document.getElementById('quiz-map-question-panel'); + if (panel) { + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + } + playQuizText = ''; + playQuizPhaseEndsAt = 0; + playLiveQuizScores = {}; + hidePlayQuizHud(); + }); + + socket.on('gauntlet-sync', (data) => { + if (!mapData || mapData.gameType !== 'gauntlet' || !data) return; + applyGauntletTimingFromServer(data); + if (Array.isArray(data.obstacles)) { + gauntletObstacles = data.obstacles; + pushGauntletObsRenderFrame(data.obstacles); + } + if (!Array.isArray(data.players)) return; + data.players.forEach((p) => { + if (!p || p.id == null || myId == null) return; + const pid = String(p.id); + const mid = String(myId); + if (pid === mid) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) me.tx = px; + if (Number.isFinite(py)) me.ty = py; + if (p.direction) me.direction = p.direction; + const jt = Number(p.gauntletJumpTicks); + meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + meGauntletJumpVis = meGauntletJumpTicks; + const sc = Number(p.gauntletScore); + me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + me.gauntletEliminated = !!p.gauntletEliminated; + } else { + const o = others.get(p.id) ?? others.get(pid); + if (o) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) o.tx = px; + if (Number.isFinite(py)) o.ty = py; + if (p.direction) o.direction = p.direction; + const jt = Number(p.gauntletJumpTicks); + o.gauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks; + const sc = Number(p.gauntletScore); + o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + o.gauntletEliminated = !!p.gauntletEliminated; + } + } + }); + if (previewFillBots) { + applyGauntletPreviewBotsAfterSync(); + emitGauntletPreviewRowsToServer(); + } + }); + + socket.on('gauntlet-ended', (data) => { + cancelQuizCarryResultEndAfterTimeup(); + gauntletEndsAtMs = null; + gauntletCrownRunHeldRemote = false; + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc = document.getElementById('gauntlet-crown-countdown'); + if (gcc) gcc.classList.add('is-hidden'); + const hto = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto) hto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + if (data && data.crownMission) { + showGauntletCrownMissionOverlay(data.crownMission); + return; + } + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) { + titleEl.textContent = data && data.reason === 'time' ? 'หมดเวลา · Time up' : 'เกมจบ · Game over'; + } + msgEl.textContent = (data && data.message) || 'เกมพรมแดงจบแล้ว'; + listEl.innerHTML = ''; + const ranks = (data && data.rankings) || []; + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + }); + + socket.on('game-start', (ev) => { + if (!ev || !ev.mapId) return; + const applySnap = (md) => { + mapData = md; + if (ev.mapId != null && String(ev.mapId).trim() !== '') { + playSessionMapId = String(ev.mapId).trim(); + } + if (mapData.gameType !== 'space_shooter') { + spaceShooterGameEnded = false; + } + if (mapData.gameType !== 'balloon_boss') { + balloonBossGameEnded = false; + } + if (!mapData.lanes && mapData.gameType === 'frogger') mapData.lanes = []; + tileSize = mapData.tileSize || 32; + gauntletObstacles = []; + gauntletObsRenderPrev = []; + gauntletObsRenderNext = []; + gauntletObsBlendT0 = 0; + mapBackgroundImg = null; + if (mapData.backgroundImage) { + mapBackgroundImg = new Image(); + mapBackgroundImg.src = mapData.backgroundImage; + } + normalizeGridImageCellsOnMap(mapData); + loadMapGridImages(); + if (mapData.gameType === 'gauntlet' && ev.peersSnap && Array.isArray(ev.peersSnap)) { + ev.peersSnap.forEach((p) => { + if (p.id != null && myId != null && String(p.id) === String(myId)) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) { me.x = px; me.tx = px; } + if (Number.isFinite(py)) { me.y = py; me.ty = py; } + me.direction = p.direction || me.direction; + const jt = Number(p.gauntletJumpTicks); + meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + meGauntletJumpVis = meGauntletJumpTicks; + const sc = Number(p.gauntletScore); + me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + me.gauntletEliminated = !!p.gauntletEliminated; + } else { + const o = others.get(p.id) ?? others.get(String(p.id)); + if (o) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) { o.x = px; o.tx = px; } + if (Number.isFinite(py)) { o.y = py; o.ty = py; } + o.direction = p.direction || o.direction; + const jt = Number(p.gauntletJumpTicks); + o.gauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + o.gauntletJumpVis = o.gauntletJumpTicks; + const sc = Number(p.gauntletScore); + o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + o.gauntletEliminated = !!p.gauntletEliminated; + } + } + }); + } else { + meGauntletJumpTicks = 0; + meGauntletJumpVis = 0; + me.gauntletScore = 0; + me.gauntletEliminated = false; + others.forEach((o) => { + o.gauntletJumpTicks = 0; + o.gauntletJumpVis = 0; + o.gauntletScore = 0; + o.gauntletEliminated = false; + }); + } + if (mapData.gameType === 'gauntlet') { + if (ev.gauntletEndsAt != null) { + const ge = Number(ev.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(ge) ? ge : null; + } else { + gauntletEndsAtMs = null; + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => {}); + if (String(ev.mapId || '') === GAUNTLET_FACE_RIGHT_MAP_ID) { + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc0 = document.getElementById('gauntlet-crown-countdown'); + if (gcc0) gcc0.classList.add('is-hidden'); + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60); + } else { + const hto2 = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto2) hto2.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc1 = document.getElementById('gauntlet-crown-countdown'); + if (gcc1) gcc1.classList.add('is-hidden'); + } + } else { + gauntletEndsAtMs = null; + const skipHideCrownShell = mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay(); + if (!skipHideCrownShell) { + const hto3 = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto3) hto3.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + } + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc2 = document.getElementById('gauntlet-crown-countdown'); + if (gcc2) gcc2.classList.add('is-hidden'); + } + if (mapData.gameType !== 'gauntlet') { + me.tx = null; + me.ty = null; + } + const gauntletOv = document.getElementById('gauntlet-ended-overlay'); + if (gauntletOv) gauntletOv.classList.add('is-hidden'); + const gcmOv = document.getElementById('gauntlet-crown-mission-overlay'); + if (gcmOv) gcmOv.classList.add('is-hidden'); + if (mapData.gameType === 'stack') { + if (!mapData.stackReleaseArea) mapData.stackReleaseArea = []; + if (!mapData.stackLandArea) mapData.stackLandArea = []; + rebalancePreviewBots(); + lastStackTickMs = performance.now(); + resetStackMinigameState(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + else reapplyStackMiniSizingFromGlobals(); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => { + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }); + } else { + stackMini = null; + } + if (mapData.gameType === 'jump_survive') { + if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = []; + if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = []; + normalizeJumpSurvivePlatformAreaInPlay(mapData); + jumpSurviveEliminated = false; + jumpSurviveGameEnded = false; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + if (isJumpSurviveMissionUiMapPlay()) { + jumpSurviveMissionPhase = 'howto'; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + setTimeout(function () { showJumpSurviveMissionHowtoOverlay(); }, 60); + } else { + jumpSurviveMissionPhase = null; + jumpSurviveInitRuntime(); + } + } + if (mapData.gameType === 'space_shooter') { + spaceShooterGameEnded = false; + normalizeShooterSpawnSlotsInPlay(mapData); + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterPopups = []; + spaceShooterLastTickMs = performance.now(); + spaceShooterSpawnAccMs = 0; + spaceShooterFireCd = 0; + spaceShooterSessionStartMs = performance.now(); + spaceShooterLastMoveEmit = 0; + } + if (mapData.gameType === 'balloon_boss') { + balloonBossGameEnded = false; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossHitFx = []; + balloonBossLastTickMs = performance.now(); + balloonBossPlayerFireCd = 0; + balloonBossSessionStartMs = performance.now(); + balloonBossLastMoveEmit = 0; + balloonBossBossFireAcc = 0; + me.balloonBossScore = 0; + me.balloonBossBalloons = balloonBossBalloonsStartPlay(); + me.balloonBossEliminated = false; + others.forEach((o) => { + o.balloonBossScore = 0; + o.balloonBossBalloons = balloonBossBalloonsStartPlay(); + o.balloonBossEliminated = false; + }); + } + if (mapData.gameType === 'quiz_battle') { + if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = []; + if (!mapData.quizBattlePathArea) mapData.quizBattlePathArea = []; + normalizeQuizBattlePathInPlay(mapData); + normalizeQuizBattleDomeInPlay(mapData); + if (quizBattlePathModeActive(mapData)) { + const ms = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = ms.x; + me.y = ms.y; + others.forEach((o) => { + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + }); + } + } + var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน' + : (isGauntlet() ? ' | พรมแดงสุดท้าย (Last Light)' + : (isLobby() ? ' | โถงรอ' : (isQuiz() ? ' | ตอบคำถาม' : (isQuizCarry() ? ' | หยิบคำตอบมาวางกลาง' : (isQuizBattle() ? ' | Quiz Battle · โดม E' : (isStack() ? ' | Stack ซ้อนตึก' : (isJumpSurvive() ? ' | กระโดดให้รอด' : (isSpaceShooter() ? ' | ยิงยานอวกาศ' : (isBalloonBoss() ? ' | ลูกโป้งยิงบอส' : ''))))))))); + var prevTag = previewMode ? '[ทดสอบ] ' : ''; + document.getElementById('room-id').textContent = prevTag + spaceId + modeLabel; + if (isQuiz()) setupPlayQuizUi(); + else if (isQuizCarry()) setupPlayQuizCarryUi(); + else if (isQuizBattle()) setupPlayQuizBattleUi(); + else teardownPlayQuizUi(); + resizeCanvas(); + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + if (previewFillBots && mapData.gameType === 'gauntlet') { + applyGauntletPreviewSpawnLayout(true); + emitGauntletPreviewRowsToServer(); + } + if (previewFillBots && mapData.gameType === 'jump_survive') { + applyJumpSurvivePreviewSpawnLayout(true); + } + if (previewFillBots && mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (previewFillBots && mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + }; + fetch(BASE + '/api/maps/' + encodeURIComponent(ev.mapId)) + .then((r) => (r.ok ? r.json() : null)) + .then((md) => { if (md) applySnap(md); }); + }); + + function resizeCanvas() { + const vw = window.innerWidth || document.documentElement.clientWidth || 800; + 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; + 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); + if (ch < 120) ch = Math.max(240, vh - headerH - 6); + if (cw < 120) cw = Math.max(320, vw); + canvas.width = Math.max(320, cw); + canvas.height = Math.max(240, ch); + syncQuizCarryEmbedCountdownLayout(); + } + + // ให้ scroll wheel ใช้ซูมเข้า/ออกแบบง่าย ๆ + canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + const factor = e.deltaY < 0 ? 1.1 : 0.9; + zoom = Math.max(0.7, Math.min(2.5, zoom * factor)); + }, { passive: false }); + canvas.addEventListener('click', (e) => { + if (!mapData || !isStack() || isChatFocused()) return; + tryHumanStackDrop(); + }); + canvas.addEventListener('dblclick', (e) => { + if (!mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss() || isChatFocused() || isQuizCarryEmbedCountdownBlockingMovement()) return; + const r = canvas.getBoundingClientRect(); + const sx = e.clientX - r.left; + 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 tx = Math.floor(gx / tileSize); + const ty = Math.floor(gy / tileSize); + const mw = mapData.width || 20, mh = mapData.height || 15; + if (tx < 0 || tx >= mw || ty < 0 || ty >= mh) return; + if (!canWalkLikeLobby(tx + 0.5, ty + 0.5)) return; + const path = pathfindPlay(me.x, me.y, tx + 0.5, ty + 0.5); + if (path.length <= 1) return; + playPath = path.slice(1); + }); + + function getCharacterFootprintWH(md) { + if (!md) return { cw: 1, ch: 1 }; + let cw = Math.floor(Number(md.characterCellsW)); + let ch = Math.floor(Number(md.characterCellsH)); + const hasW = Number.isFinite(cw) && cw >= 1; + const hasH = Number.isFinite(ch) && ch >= 1; + if (hasW && hasH) { + return { cw: Math.max(1, Math.min(4, cw)), ch: Math.max(1, Math.min(4, ch)) }; + } + const leg = Math.max(1, Math.min(4, Math.floor(Number(md.characterCells)) || 1)); + return { cw: leg, ch: leg }; + } + + /** + * ขนาดช่องที่ใช้ชนกำแพง (objects=1) เท่านั้น — ชิดเท้า (ล่าง) + กลางแนวนอน · เล็กกว่า footprint = ส่วนบนไม่ติดกำแพง + */ + function getCharacterCollisionFootprintWH(md) { + const { cw, ch } = getCharacterFootprintWH(md); + let colW = Math.floor(Number(md.characterCollisionW)); + let colH = Math.floor(Number(md.characterCollisionH)); + if (!Number.isFinite(colW) || colW < 1) colW = cw; + if (!Number.isFinite(colH) || colH < 1) colH = ch; + colW = Math.max(1, Math.min(cw, colW)); + colH = Math.max(1, Math.min(ch, colH)); + return { cw, ch, colW, colH }; + } + + /** กันตัวละคร footprint ล้นออกนอกแมป — เดิมใช้ w-0.01 ทำให้ cw/ch>1 ชนขอบแล้วตรรกะเดินค้าง */ + function clampPlayEntityFootprintToMap(oEnt, md) { + if (!oEnt || !md) return; + const w = md.width || 20, h = md.height || 15; + const { cw, ch } = getCharacterFootprintWH(md); + const maxX = Math.max(0, w - cw + 0.999); + const maxY = Math.max(0, h - ch + 0.999); + if (!Number.isFinite(oEnt.x)) oEnt.x = 0.5; + if (!Number.isFinite(oEnt.y)) oEnt.y = 0.5; + oEnt.x = Math.max(0, Math.min(maxX, oEnt.x)); + oEnt.y = Math.max(0, Math.min(maxY, oEnt.y)); + } + + /** แยกบอท preview ที่ทับกันบนจุดเดียว (มักชนกันที่ขอบแมป) */ + function separateClumpedPreviewBots() { + if (!previewFillBots || !mapData) return; + const ids = [...others.keys()].filter(isPreviewBotId); + const minD = 0.52; + const pushAmt = MOVE_SPEED * 0.5; + for (let i = 0; i < ids.length; i++) { + for (let j = i + 1; j < ids.length; j++) { + const a = others.get(ids[i]); + const b = others.get(ids[j]); + if (!a || !b) continue; + let ddx = b.x - a.x, ddy = b.y - a.y; + let d = Math.sqrt(ddx * ddx + ddy * ddy); + if (d >= minD) continue; + if (d < 1e-5) { + ddx = (i + j * 2) % 2 === 0 ? 1 : -1; + ddy = ((i + j) % 3) - 1; + d = Math.sqrt(ddx * ddx + ddy * ddy) || 1; + } + const ux = -ddx / d, uy = -ddy / d; + const vx = ddx / d, vy = ddy / d; + const ax2 = a.x + ux * pushAmt, ay2 = a.y + uy * pushAmt; + const bx2 = b.x + vx * pushAmt, by2 = b.y + vy * pushAmt; + if (canWalkLikeLobbyForBot(ax2, ay2, a.x, a.y, a)) { a.x = ax2; a.y = ay2; } + if (canWalkLikeLobbyForBot(bx2, by2, b.x, b.y, b)) { b.x = bx2; b.y = by2; } + clampPlayEntityFootprintToMap(a, mapData); + clampPlayEntityFootprintToMap(b, mapData); + } + } + } + + function quizTilesFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch } = getCharacterFootprintWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const maxTx = Math.min(w - 1, minTx + cw - 1); + const maxTy = Math.min(h - 1, minTy + ch - 1); + for (let ty = minTy; ty <= maxTy; ty++) { + for (let tx = minTx; tx <= maxTx; tx++) { + if (tx >= 0 && ty >= 0) s.add(tx + ',' + ty); + } + } + return s; + } + + /** ร่าง (cw×ch) ของผู้เล่นที่ (px,py) ครอบคลุมช่อง (tx,ty) หรือไม่ — ใช้กับ blockPlayer แทนการเช็คแค่มุมซ้ายบน */ + function playEntityFootprintContainsTile(px, py, tx, ty) { + return quizTilesFootprintPlay(px, py).has(tx + ',' + ty); + } + + /** + * กล่องตรวจโซนห้ามซ้อน (blockPlayer) — วางเหมือนชนกำแพง: กลางแนวนอน + ชิดเท้า + * blockPlayerSeparationW/H: 0 = ใช้เต็มความกว้าง/สูงตัว · ค่าอื่น = จำกัด 1..cw / 1..ch + * รองรับ legacy blockPlayerSeparation เมื่อยังไม่มี W/H ใน JSON + */ + function resolveBlockPlayerNoOverlapBoxWH(md) { + const m = md || mapData; + if (!m) return { cw: 1, ch: 1, boxW: 1, boxH: 1 }; + const { cw, ch } = getCharacterFootprintWH(m); + let w = m.blockPlayerSeparationW != null && m.blockPlayerSeparationW !== '' ? Math.floor(Number(m.blockPlayerSeparationW)) : NaN; + let h = m.blockPlayerSeparationH != null && m.blockPlayerSeparationH !== '' ? Math.floor(Number(m.blockPlayerSeparationH)) : NaN; + const leg = m.blockPlayerSeparation != null && m.blockPlayerSeparation !== '' ? Math.floor(Number(m.blockPlayerSeparation)) : NaN; + const hasW = Number.isFinite(w) && w >= 0; + const hasH = Number.isFinite(h) && h >= 0; + if (!hasW && !hasH && Number.isFinite(leg) && leg >= 1) { + w = Math.min(leg, cw); + h = Math.min(leg, ch); + } else { + if (!hasW) w = 0; + if (!hasH) h = 0; + } + const boxW = !w || w <= 0 ? cw : Math.max(1, Math.min(cw, w)); + const boxH = !h || h <= 0 ? ch : Math.max(1, Math.min(ch, h)); + return { cw, ch, boxW, boxH }; + } + + /** ช่องกริดที่ถือว่า “ผู้เล่นที่ (px,py) กินพื้นที่” สำหรับตรวจโซน blockPlayer — กล่องเดียวกับแนวชนกำแพง */ + function quizTilesBlockPlayerPeerFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch, boxW, boxH } = resolveBlockPlayerNoOverlapBoxWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const offX = minTx + Math.floor((cw - boxW) / 2); + const offY = minTy + (ch - boxH); + for (let ty = offY; ty < offY + boxH; ty++) { + for (let tx = offX; tx < offX + boxW; tx++) { + if (tx >= 0 && ty >= 0 && tx < w && ty < h) s.add(tx + ',' + ty); + } + } + return s; + } + + function playPeerBlockPlayerOccupiesTile(px, py, tx, ty) { + return quizTilesBlockPlayerPeerFootprintPlay(px, py).has(tx + ',' + ty); + } + + /** Footprint ชนกำแพง (objects=1) เท่านั้น — hub / interactive / blockPlayer / quiz ใช้ quizTilesFootprintPlay = เต็ม characterCells */ + function quizTilesWallCollisionFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch, colW, colH } = getCharacterCollisionFootprintWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const offX = minTx + Math.floor((cw - colW) / 2); + const offY = minTy + (ch - colH); + for (let ty = offY; ty < offY + colH; ty++) { + for (let tx = offX; tx < offX + colW; tx++) { + if (tx >= 0 && ty >= 0 && tx < w && ty < h) s.add(tx + ',' + ty); + } + } + return s; + } + + function quizAnswerTileForbiddenForLock(lock, tx, ty) { + if (!lock || lock.eliminated) return false; + if (!mapData) return false; + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + if (lock.cannotTrue && qt && qt[ty] && qt[ty][tx] === 1) return true; + if (lock.cannotFalse && qf && qf[ty] && qf[ty][tx] === 1) return true; + return false; + } + + function quizAnswerTileForbiddenPlay(tx, ty) { + if (!playQuizPlayerLocal) return false; + return quizAnswerTileForbiddenForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, tx, ty); + } + + /** ยืนทับโซนต้องห้ามเมื่อโดนล็อก — ใช้กับ pathfind ปลายทาง / คลิก */ + function quizLockFootprintBlocksForLock(lock, px, py) { + if (!mapData || !isQuiz() || !lock || lock.eliminated) return false; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const txi = +p[0], tyi = +p[1]; + if (quizAnswerTileForbiddenForLock(lock, txi, tyi)) return true; + } + return false; + } + + function quizLockFootprintBlocksPlay(px, py) { + if (!playQuizPlayerLocal) return false; + return quizLockFootprintBlocksForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, px, py); + } + + /** บล็อกเฉพาะการ «เข้า» ช่องตอบใหม่ — ให้เดินออกจากโซนได้ถ้าเคยยืนผิดแล้ว */ + function quizLockWouldEnterForbiddenForLock(lock, ox, oy, nx, ny) { + if (!mapData || !isQuiz() || !lock || lock.eliminated) return false; + const fromS = quizTilesFootprintPlay(ox, oy); + const toS = quizTilesFootprintPlay(nx, ny); + for (const k of toS) { + if (fromS.has(k)) continue; + const p = k.split(','); + const txi = +p[0], tyi = +p[1]; + if (quizAnswerTileForbiddenForLock(lock, txi, tyi)) return true; + } + return false; + } + + function quizLockWouldEnterForbiddenPlay(ox, oy, nx, ny) { + if (!playQuizPlayerLocal) return false; + return quizLockWouldEnterForbiddenForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, ox, oy, nx, ny); + } + + function botQuizLock(o) { + return { + cannotTrue: !!(o && o.quizCannotTrue), + cannotFalse: !!(o && o.quizCannotFalse), + eliminated: false, + }; + } + + /** Same walkability as room-lobby `canWalkLobby` (LobbyA / hall). */ + function canWalkLikeLobby(x, y, fromX, fromY) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + const wallTiles = quizTilesWallCollisionFootprintPlay(x, y); + for (const k of wallTiles) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (wallTiles.size === 0) return false; + const bp = mapData.blockPlayer; + if (bp) { + for (const k of quizTilesFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!bp[ty] || bp[ty][tx] !== 1) continue; + for (const [, o] of others) { + if (playPeerBlockPlayerOccupiesTile(o.x, o.y, tx, ty)) return false; + } + } + } + if (isQuiz() && playQuizPlayerLocal && !playQuizPlayerLocal.eliminated) { + const hasFrom = typeof fromX === 'number' && typeof fromY === 'number' && !Number.isNaN(fromX) && !Number.isNaN(fromY); + if (hasFrom) { + if (quizLockWouldEnterForbiddenPlay(fromX, fromY, x, y)) return false; + } else if (quizLockFootprintBlocksPlay(x, y)) { + return false; + } + } + if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false; + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + if (!quizBattleFootprintFullyOnPath(mapData, x, y)) return false; + } + return true; + } + + function canWalkLikeLobbyForBot(x, y, fromX, fromY, o) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + const wallTilesB = quizTilesWallCollisionFootprintPlay(x, y); + for (const k of wallTilesB) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (wallTilesB.size === 0) return false; + const bp = mapData.blockPlayer; + if (bp) { + for (const k of quizTilesFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!bp[ty] || bp[ty][tx] !== 1) continue; + for (const [, peer] of others) { + if (o && peer === o) continue; + if (playPeerBlockPlayerOccupiesTile(peer.x, peer.y, tx, ty)) return false; + } + if (o && playPeerBlockPlayerOccupiesTile(me.x, me.y, tx, ty)) return false; + } + } + if (isQuiz()) { + const lock = botQuizLock(o); + if (lock.cannotTrue || lock.cannotFalse) { + const hasFrom = typeof fromX === 'number' && typeof fromY === 'number' && !Number.isNaN(fromX) && !Number.isNaN(fromY); + if (hasFrom) { + if (quizLockWouldEnterForbiddenForLock(lock, fromX, fromY, x, y)) return false; + } else if (quizLockFootprintBlocksForLock(lock, x, y)) { + return false; + } + } + } + if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false; + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + if (!quizBattleFootprintFullyOnPath(mapData, x, y)) return false; + } + return true; + } + + /** ถ้าสปอว์น/โหลดมาทับโซน hub ให้หาจุดใกล้ ๆ ที่ footprint ไม่ทับ hub */ + function snapPositionOutOfQuizCarryHubIfNeeded(x, y) { + if (!mapData || !isQuizCarry()) return { x, y }; + if (!quizCarryFootprintOverlapsHub(x, y)) return { x, y }; + const w = mapData.width || 20, h = mapData.height || 15; + const maxR = Math.max(w, h) + 6; + for (let r = 1; r <= maxR; r++) { + for (let dy = -r; dy <= r; dy++) { + for (let dx = -r; dx <= r; dx++) { + if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue; + const nx = x + dx * 0.45; + const ny = y + dy * 0.45; + if (nx < 0 || ny < 0 || nx > w - 0.02 || ny > h - 0.02) continue; + if (quizCarryFootprintOverlapsHub(nx, ny)) continue; + if (!canWalkLikeLobby(nx, ny, NaN, NaN)) continue; + return { x: nx, y: ny }; + } + } + } + return { x, y }; + } + + function stepPreviewBotAlongPath(o, w, h) { + const path = o.botPath; + if (!path || !path.length) return; + const way = path[0]; + const dx = way.x - o.x, dy = way.y - o.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= PATH_ARRIVE_THRESH) { + path.shift(); + while (path.length > 0) { + const w2 = path[0]; + const ux = w2.x - o.x, uy = w2.y - o.y; + if (Math.sqrt(ux * ux + uy * uy) > PATH_ARRIVE_THRESH) break; + path.shift(); + } + clampPlayEntityFootprintToMap(o, mapData); + return; + } + const len = dist || 1; + const pathStepMul = (previewFillBots && editorEmbedReturn) ? 0.56 : 1; + const step = Math.min(MOVE_SPEED * 1.28 * pathStepMul, len); + const nx = o.x + (dx / len) * step; + const ny = o.y + (dy / len) * step; + if (Math.abs(dy) > Math.abs(dx)) o.direction = dy > 0 ? 'down' : 'up'; + else if (Math.abs(dx) > 1e-6) o.direction = dx > 0 ? 'right' : 'left'; + const ox = o.x, oy = o.y; + const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData); + if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else if (!pathStrict) { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + /* เส้นตรงไป waypoint อาจตัดมุม hub / block — ลองก้าวแนวแกนตามทิศหลักอย่างใดอย่างหนึ่ง */ + const cstep = Math.min(MOVE_SPEED * 1.2 * pathStepMul, Math.max(Math.abs(dx), Math.abs(dy), 1e-4)); + const sx = dx > 1e-4 ? 1 : dx < -1e-4 ? -1 : 0; + const sy = dy > 1e-4 ? 1 : dy < -1e-4 ? -1 : 0; + const tryAxisX = () => { + if (sx === 0) return false; + if (!canWalkLikeLobbyForBot(o.x + sx * cstep, o.y, o.x, o.y, o)) return false; + o.x += sx * cstep; + return true; + }; + const tryAxisY = () => { + if (sy === 0) return false; + if (!canWalkLikeLobbyForBot(o.x, o.y + sy * cstep, o.x, o.y, o)) return false; + o.y += sy * cstep; + return true; + }; + if (Math.abs(dx) >= Math.abs(dy)) { + if (!tryAxisX()) tryAxisY(); + } else { + if (!tryAxisY()) tryAxisX(); + } + } + } + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) { + o.botIsWalking = true; + o.botPathStuckTicks = 0; + } else { + o.botPathStuckTicks = (o.botPathStuckTicks | 0) + 1; + if ((o.botPathStuckTicks | 0) >= 20) { + o.botPathStuckTicks = 0; + if (path.length) path.shift(); + if (isQuizCarry() && previewFillBots) o.botQuizCarryPathfindAfter = 0; + } + } + } + + function pickRandomPreviewBotWanderDir() { + const dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]]; + return dirs[Math.floor(Math.random() * dirs.length)]; + } + + function stepPreviewBots() { + if (!playBotsEnabled() || !mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss()) return; + if (isQuizCarry()) { + stepQuizCarryPreviewBots(); + return; + } + const w = mapData.width || 20, h = mapData.height || 15; + const now = Date.now(); + const inAnswerPhase = previewMode && isQuiz() && previewQuizStep === 'answer'; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botIsWalking = false; + if (inAnswerPhase && o.botPath && o.botPath.length > 0 && !o.botAnswerWander) { + stepPreviewBotAlongPath(o, w, h); + return; + } + if (inAnswerPhase && o.botPath && o.botPath.length === 0 && !o.botAnswerWander) { + return; + } + /* ก่อนตอบ / พัก / บอทสับสน: เดินทุกเฟรมตามทิศ (เหมือนผู้เล่นกดค้าง) — เดิมรอเป็นจังหวะแล้วก้าวทีละครั้งเลยดูกระตุก */ + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600; + if (now >= o.botWanderNextTurn) { + o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200); + if (Math.random() < 0.55) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + } + const accX = o.botWanderDx; + const accY = o.botWanderDy; + if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up'; + else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left'; + const step = MOVE_SPEED; + const nx = o.x + accX * step; + const ny = o.y + accY * step; + const ox = o.x, oy = o.y; + const pathStrictW = isQuizBattle() && quizBattlePathModeActive(mapData); + if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else if (!pathStrictW) { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + if (!Number.isFinite(o.x)) o.x = 0.5; + if (!Number.isFinite(o.y)) o.y = 0.5; + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) o.botIsWalking = true; + }); + separateClumpedPreviewBots(); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const ob = others.get(bid); + if (ob) clampPlayEntityFootprintToMap(ob, mapData); + }); + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + o.tx = s.x; + o.ty = s.y; + }); + } + } + + /** A* เหมือน room-lobby — double-click ไปจุดบนแผนที่ */ + function pathfindPlay(fromX, fromY, toX, toY) { + if (!mapData) return []; + const w = mapData.width || 20, h = mapData.height || 15; + const fx = Math.floor(fromX), fy = Math.floor(fromY); + const tx = Math.floor(toX), ty = Math.floor(toY); + if (tx < 0 || tx >= w || ty < 0 || ty >= h || !canWalkLikeLobby(tx + 0.5, ty + 0.5)) return []; + if (fx === tx && fy === ty) return [{ x: tx + 0.5, y: ty + 0.5 }]; + const key = (gx, gy) => gx + ',' + gy; + const open = [{ gx: fx, gy: fy, f: 0, g: 0 }]; + const closed = new Set(); + const cameFrom = {}; + const gScore = { [key(fx, fy)]: 0 }; + const heuristic = (ax, ay) => Math.abs(ax - tx) + Math.abs(ay - ty); + const dirs = [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }]; + while (open.length) { + open.sort((a, b) => a.f - b.f); + const cur = open.shift(); + const ck = key(cur.gx, cur.gy); + if (closed.has(ck)) continue; + closed.add(ck); + if (cur.gx === tx && cur.gy === ty) { + const path = []; + let u = cur; + while (u) { + path.unshift({ x: u.gx + 0.5, y: u.gy + 0.5 }); + u = cameFrom[key(u.gx, u.gy)]; + } + return path; + } + for (const d of dirs) { + const nx = cur.gx + d.dx, ny = cur.gy + d.dy; + if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + if (!canWalkLikeLobby(nx + 0.5, ny + 0.5, cur.gx + 0.5, cur.gy + 0.5)) continue; + const nk = key(nx, ny); + if (closed.has(nk)) continue; + const g = (gScore[ck] ?? Infinity) + 1; + if (g >= (gScore[nk] ?? Infinity)) continue; + gScore[nk] = g; + cameFrom[nk] = cur; + open.push({ gx: nx, gy: ny, f: g + heuristic(nx, ny), g }); + } + } + return []; + } + + function pathfindPlayForBot(fromX, fromY, toX, toY, o) { + if (!mapData) return []; + const w = mapData.width || 20, h = mapData.height || 15; + const fx = Math.floor(fromX), fy = Math.floor(fromY); + const tx = Math.floor(toX), ty = Math.floor(toY); + if (tx < 0 || tx >= w || ty < 0 || ty >= h || !canWalkLikeLobbyForBot(tx + 0.5, ty + 0.5, NaN, NaN, o)) return []; + if (fx === tx && fy === ty) return [{ x: tx + 0.5, y: ty + 0.5 }]; + const key = (gx, gy) => gx + ',' + gy; + const open = [{ gx: fx, gy: fy, f: 0, g: 0 }]; + const closed = new Set(); + const cameFrom = {}; + const gScore = { [key(fx, fy)]: 0 }; + const heuristic = (ax, ay) => Math.abs(ax - tx) + Math.abs(ay - ty); + const dirs = [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }]; + while (open.length) { + open.sort((a, b) => a.f - b.f); + const cur = open.shift(); + const ck = key(cur.gx, cur.gy); + if (closed.has(ck)) continue; + closed.add(ck); + if (cur.gx === tx && cur.gy === ty) { + const path = []; + let u = cur; + while (u) { + path.unshift({ x: u.gx + 0.5, y: u.gy + 0.5 }); + u = cameFrom[key(u.gx, u.gy)]; + } + return path; + } + for (const d of dirs) { + const nx = cur.gx + d.dx, ny = cur.gy + d.dy; + if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + if (!canWalkLikeLobbyForBot(nx + 0.5, ny + 0.5, cur.gx + 0.5, cur.gy + 0.5, o)) continue; + const nk = key(nx, ny); + if (closed.has(nk)) continue; + const g = (gScore[ck] ?? Infinity) + 1; + if (g >= (gScore[nk] ?? Infinity)) continue; + gScore[nk] = g; + cameFrom[nk] = cur; + open.push({ gx: nx, gy: ny, f: g + heuristic(nx, ny), g }); + } + } + return []; + } + + let playPath = []; + + function drawGauntletLaserColumnScreen(rx, ry, rw, rh) { + ctx.save(); + ctx.fillStyle = gauntletLaserFillColor; + ctx.fillRect(rx, ry, rw, rh); + const topRec = gauntletLaserTopUrl ? ensureGauntletAssetImage(gauntletLaserTopUrl) : null; + const botRec = gauntletLaserBottomUrl ? ensureGauntletAssetImage(gauntletLaserBottomUrl) : null; + const lineRec = gauntletLaserLineUrl ? ensureGauntletAssetImage(gauntletLaserLineUrl) : null; + let topH = 0; + let botH = 0; + if (topRec && topRec.img.complete && topRec.img.naturalWidth > 0) { + topH = Math.min(rh * 0.4, rw * topRec.img.naturalHeight / topRec.img.naturalWidth); + } + if (botRec && botRec.img.complete && botRec.img.naturalWidth > 0) { + botH = Math.min(rh * 0.4, rw * botRec.img.naturalHeight / botRec.img.naturalWidth); + } + /* เส้นกลาง tile ทั้งความสูงคอลัมน์ แล้วค่อยวาดหัว/ท้ายทับ — ให้ลำแสงต่อเนื่องแบบสินทรัพย์รวม (รูปอ้างอิง) */ + if (lineRec && lineRec.img.complete && lineRec.img.naturalWidth > 0 && rh > 1) { + const iw = lineRec.img.naturalWidth; + const ih = lineRec.img.naturalHeight; + const scale = rw / iw; + const step = Math.max(1, ih * scale); + let y = ry; + while (y < ry + rh) { + const piece = Math.min(step, ry + rh - y); + const srcH = piece / scale; + try { + ctx.drawImage(lineRec.img, 0, 0, iw, srcH, rx, y, rw, piece); + } catch (e) { /* ignore */ } + y += piece; + } + } + if (topH > 0 && topRec) { + try { ctx.drawImage(topRec.img, rx, ry, rw, topH); } catch (e) { /* ignore */ } + } + if (botH > 0 && botRec) { + try { ctx.drawImage(botRec.img, rx, ry + rh - botH, rw, botH); } catch (e) { /* ignore */ } + } + const lw = Number(gauntletLaserLineWidthPx) || 0; + if (lw > 0) { + ctx.strokeStyle = gauntletLaserStrokeColor; + ctx.lineWidth = lw; + ctx.strokeRect(rx + lw / 2, ry + lw / 2, rw - lw, rh - lw); + } + ctx.restore(); + } + + /** Stack preview: HUD แนว cyberdeck — SCORE ซ้าย, ตาปัจจุบันขวา, เทอร์มินัลล่างซ้าย */ + function drawStackPreviewCyberHud(ctx, cw, ch) { + if (!playBotsEnabled() || !stackMini || !mapData || !isStack()) return; + const now = Date.now(); + const pad = 12; + const currentSeat = getStackPreviewCurrentSeat(); + const order = stackPreviewTurnOrder; + const mono = '11px ui-monospace, "Cascadia Mono", Consolas, monospace'; + const monoSm = '9px ui-monospace, Consolas, monospace'; + const lives = stackMini.lives != null ? stackMini.lives : 3; + const lifeSlots = 3; + const teamScore = stackMini.score || 0; + const combo = stackMini.combo || 0; + + function roundRectPath(x, y, w, h, r) { + const rr = Math.min(r, w / 2, h / 2); + ctx.beginPath(); + ctx.moveTo(x + rr, y); + ctx.arcTo(x + w, y, x + w, y + h, rr); + ctx.arcTo(x + w, y + h, x, y + h, rr); + ctx.arcTo(x, y + h, x, y, rr); + ctx.arcTo(x, y, x + w, y, rr); + ctx.closePath(); + } + + function drawHudAvatar(cx, baselineY, maxSize, characterId, playTint) { + const timeMs = now; + const dir = 'down'; + const rawImg = getAvatarImg(characterId, dir, timeMs, false); + const charImg = playTint ? getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, false, playTint) : rawImg; + let iw = 0; let ih = 0; + if (charImg && charImg.tagName === 'CANVAS' && charImg.width > 0 && charImg.height > 0) { + iw = charImg.width; + ih = charImg.height; + } else if (charImg && charImg.complete && charImg.naturalWidth) { + iw = charImg.naturalWidth; + ih = charImg.naturalHeight; + } + const ax = cx - maxSize / 2; + const ay = baselineY - maxSize; + ctx.save(); + roundRectPath(ax, ay, maxSize, maxSize, 5); + ctx.clip(); + if (iw > 0 && ih > 0) { + const scale = Math.min(maxSize / iw, maxSize / ih, 1); + const dw = iw * scale; + const dh = ih * scale; + ctx.drawImage(charImg, 0, 0, iw, ih, cx - dw / 2, baselineY - dh, dw, dh); + } else { + ctx.fillStyle = 'rgba(80, 200, 255, 0.35)'; + ctx.fillRect(ax, ay, maxSize, maxSize); + } + ctx.restore(); + ctx.strokeStyle = 'rgba(0, 234, 255, 0.5)'; + ctx.lineWidth = 1.5; + roundRectPath(ax, ay, maxSize, maxSize, 5); + ctx.stroke(); + } + + function resolveEntryVisual(entry) { + if (entry.kind === 'human') { + return { + name: String(me.nickname || 'YOU').toUpperCase(), + score: me.stackPreviewHumanPts || 0, + characterId: me.characterId, + playTint: me.playTint || playTintFromPeerId(String(myId || 'me')), + }; + } + const o = entry.botId ? others.get(entry.botId) : null; + return { + name: o ? String(o.nickname || 'BOT').replace(/\s+/g, ' ').toUpperCase().slice(0, 12) : '—', + score: o ? (o.stackBotScore || 0) : 0, + characterId: o ? o.characterId : null, + playTint: o ? (o.playTint || playTintFromPeerId(entry.botId)) : null, + }; + } + + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + + const boardW = Math.min(248, Math.max(200, cw * 0.36)); + const rowH = 42; + const headH = 38; + const boardH = headH + stackTurnCount * rowH + 10; + + ctx.fillStyle = 'rgba(8, 12, 28, 0.9)'; + roundRectPath(pad, 8, boardW, boardH, 8); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 230, 255, 0.5)'; + ctx.lineWidth = 1.5; + roundRectPath(pad, 8, boardW, boardH, 8); + ctx.stroke(); + + ctx.fillStyle = '#5cefff'; + ctx.font = 'bold 12px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText('SCORE', pad + 14, 16); + ctx.fillStyle = 'rgba(94, 239, 255, 0.55)'; + ctx.font = monoSm; + ctx.fillText('shared stack · P1–P6', pad + 62, 18); + + let rowY = 8 + headH; + const drawRows = (entries) => { + entries.forEach((entry) => { + const isCurrent = entry.seat === currentSeat; + const v = resolveEntryVisual(entry); + let actionFlash = false; + if (entry.kind === 'human') { + actionFlash = lastStackPreviewActorId === '__human__' && now < lastStackPreviewActorUntil; + } else if (entry.botId) { + const ob = others.get(entry.botId); + actionFlash = !!(ob && ((now < (ob.stackBotGlowUntil || 0)) || (entry.botId === lastStackPreviewActorId && now < lastStackPreviewActorUntil))); + } + if (isCurrent) { + ctx.fillStyle = 'rgba(255, 0, 180, 0.1)'; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 255, 220, 0.9)'; + ctx.lineWidth = 2; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.stroke(); + } else if (actionFlash) { + ctx.strokeStyle = 'rgba(255, 214, 102, 0.65)'; + ctx.lineWidth = 1.5; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.stroke(); + } + const avS = 34; + drawHudAvatar(pad + 10 + avS / 2, rowY + avS - 3, avS, v.characterId, v.playTint); + ctx.fillStyle = isCurrent ? '#e0f7ff' : '#89b4fa'; + ctx.font = '600 10px ui-sans-serif, system-ui, sans-serif'; + ctx.textBaseline = 'middle'; + ctx.fillText('P' + entry.seat, pad + 14 + avS + 6, rowY + 12); + ctx.fillStyle = '#c0caf5'; + ctx.font = 'bold 10px ' + mono; + ctx.fillText(v.name.slice(0, 11), pad + 14 + avS + 6, rowY + 28); + ctx.fillStyle = actionFlash ? '#ffe066' : '#7dfcff'; + ctx.font = 'bold 13px ' + mono; + ctx.textAlign = 'right'; + ctx.fillText(String(v.score), pad + boardW - 12, rowY + rowH / 2); + ctx.textAlign = 'left'; + rowY += rowH; + }); + }; + + if (order && order.length === stackTurnCount) { + drawRows(order); + } else { + const bots = [...others.keys()].filter(isPreviewBotId).sort(); + const rows = [{ kind: 'human', seat: 1 }]; + for (let i = 0; i < stackTurnCount - 1; i++) { + rows.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null }); + } + drawRows(rows); + } + + const rightW = 136; + let rightX = cw - pad - rightW; + const topY = 8; + if (rightX < pad + boardW + 14) { + rightX = Math.max(pad, cw - pad - rightW); + } + const bigAv = Math.min(78, Math.max(56, ch * 0.11)); + const turnPanelH = bigAv + 118; + ctx.fillStyle = 'rgba(8, 12, 28, 0.9)'; + roundRectPath(rightX, topY, rightW, turnPanelH, 8); + ctx.fill(); + ctx.strokeStyle = 'rgba(187, 154, 247, 0.55)'; + ctx.lineWidth = 1.5; + roundRectPath(rightX, topY, rightW, turnPanelH, 8); + ctx.stroke(); + + let curEntry = null; + if (order && order.length === stackTurnCount) { + curEntry = order.find((e) => e.seat === currentSeat) || null; + } + const curV = curEntry ? resolveEntryVisual(curEntry) : resolveEntryVisual({ kind: 'human', seat: 1 }); + const framePad = 8; + const avCx = rightX + rightW / 2; + const bigFrameTop = topY + framePad - 2; + const bigFrameH = bigAv + 8; + ctx.strokeStyle = 'rgba(255, 120, 200, 0.75)'; + ctx.lineWidth = 2; + roundRectPath(rightX + framePad - 2, topY + framePad - 2, rightW - (framePad - 2) * 2, bigAv + 8, 6); + ctx.stroke(); + drawHudAvatar(avCx, bigFrameTop + bigFrameH - 4, bigAv, curV.characterId, curV.playTint); + + ctx.fillStyle = '#f5c2e7'; + ctx.font = 'bold 10px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.fillText('ACTIVE NODE', rightX + rightW / 2, topY + bigAv + 22); + ctx.fillStyle = '#7dfcff'; + ctx.font = 'bold 11px ' + mono; + ctx.fillText('P' + currentSeat + ' · ' + curV.name.slice(0, 10), rightX + rightW / 2, topY + bigAv + 38); + + ctx.fillStyle = '#bb9af7'; + ctx.font = '600 9px ui-sans-serif, system-ui, sans-serif'; + ctx.fillText('SYSTEM INTEGRITY', rightX + rightW / 2, topY + bigAv + 58); + ctx.font = '14px sans-serif'; + let heartStr = ''; + for (let i = 0; i < lifeSlots; i++) { + heartStr += i < lives ? '♥ ' : '♡ '; + } + ctx.fillStyle = lives <= 1 ? '#f7768e' : '#9ece6a'; + ctx.fillText(heartStr.trim(), rightX + rightW / 2, topY + bigAv + 74); + + ctx.fillStyle = 'rgba(125, 252, 255, 0.85)'; + ctx.font = monoSm; + ctx.fillText('TEAM ' + teamScore + ' · COMBO x' + combo, rightX + rightW / 2, topY + bigAv + 96); + + const termW = Math.min(380, Math.max(220, cw * 0.42)); + const termH = 86; + const termY = Math.max(boardH + 24, ch - termH - 52); + ctx.fillStyle = 'rgba(18, 8, 32, 0.88)'; + roundRectPath(pad, termY, termW, termH, 6); + ctx.fill(); + ctx.strokeStyle = 'rgba(187, 100, 255, 0.45)'; + ctx.lineWidth = 1.5; + roundRectPath(pad, termY, termW, termH, 6); + ctx.stroke(); + + ctx.fillStyle = '#c099ff'; + ctx.font = '600 10px ' + mono; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText('// NODE_LOG', pad + 10, termY + 8); + ctx.fillStyle = '#7dcfff'; + ctx.font = monoSm; + let ly = termY + 26; + const logLines = stackPreviewHudLog.length + ? stackPreviewHudLog.slice(-5) + : ['>>> Awaiting stack sync…', '>>> Space / click = release (your turn only)']; + logLines.forEach((ln) => { + ctx.fillText(ln, pad + 10, ly); + ly += 12; + }); + + const decryptPct = Math.min(100, Math.floor((stackMini.layers.length || 0) * 11 + Math.min(40, teamScore * 3))); + const barY = termY + termH - 18; + const barW = termW - 20; + ctx.fillStyle = 'rgba(0,0,0,0.35)'; + ctx.fillRect(pad + 10, barY, barW, 8); + ctx.fillStyle = 'rgba(0, 255, 200, 0.65)'; + ctx.fillRect(pad + 10, barY, Math.max(4, barW * decryptPct / 100), 8); + ctx.strokeStyle = 'rgba(0, 234, 255, 0.4)'; + ctx.strokeRect(pad + 10, barY, barW, 8); + ctx.fillStyle = '#94e2d5'; + ctx.font = monoSm; + ctx.fillText('TOWER_LINK ' + decryptPct + '%', pad + 10, barY - 14); + + ctx.restore(); + } + + function useCyberPlayHud() { + return !!(mapData && (isJumpSurvive() || isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry())); + } + + /** Cyber HUD: quiz_carry ใช้ playLiveQuizScores (ถูก +QUIZ_CARRY_POINTS_PER_CORRECT ตอนส่งป้ายถูกที่ฮับ) */ + function cyberHudQuizCarryPeerScore(peerId) { + if (peerId == null) return 0; + return Math.max(0, Number(playLiveQuizScores[peerId]) || 0); + } + + function syncPlayCyberHud() { + const root = document.getElementById('play-cyber-hud'); + const stackEl = document.getElementById('play-canvas-stack'); + const on = useCyberPlayHud(); + if (root) { + root.classList.toggle('is-hidden', !on); + root.setAttribute('aria-hidden', on ? 'false' : 'true'); + } + if (stackEl) stackEl.classList.toggle('play-cyber-vignette', !!on); + const gw = document.querySelector('.game-wrap'); + if (gw) gw.classList.toggle('play-cyber-active', !!on); + if (!on || !root) return; + const timeLabelEl = root.querySelector('.play-cyber-time-label'); + if (timeLabelEl) timeLabelEl.style.display = isQuizCarry() ? 'none' : ''; + const timeVal = document.getElementById('play-cyber-time-val'); + const timeSub = document.getElementById('play-cyber-time-sub'); + const portrait = document.getElementById('play-cyber-portrait-img'); + const statusEl = document.getElementById('play-cyber-self-status'); + const hintEl = document.getElementById('play-cyber-hint'); + const previewEl = document.getElementById('play-cyber-preview-line'); + + if (timeSub) { + timeSub.textContent = isQuizCarry() + ? 'QUIZ CARRY · COURT — หยิบป้ายถูกแล้วส่งที่ฮับ (F / Grab)' + : (isGauntlet() ? 'GAUNTLET · SURVIVAL RUN' + : (isSpaceShooter() ? 'SPACE SHOOTER · ARCADE' + : (isBalloonBoss() ? 'BALLOON BOSS · MEGA VIRUS' + : (isJumpSurviveMissionUiMapPlay() ? 'JUMPER · SURVIVE — เหลือเวลา (วินาที) · TIME (sec)' : 'JUMP SURVIVE · NODE UPLINK')))); + } + if (timeVal) { + if (isQuizCarry()) { + if (quizCarrySessionEnded) { + timeVal.textContent = '0'; + } else if (previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now()) { + const rem = Math.max(0, Math.ceil((quizCarryEmbedCountdownEndAt - Date.now()) / 1000)); + timeVal.textContent = String(rem); + } else if (quizCarryPregameActive) { + timeVal.textContent = '···'; + } else if (!quizCarryCurrent) { + timeVal.textContent = '—'; + } else if (quizCarryOptionRevealAt > 0 && Date.now() < quizCarryOptionRevealAt) { + timeVal.textContent = String(Math.max(0, Math.ceil((quizCarryOptionRevealAt - Date.now()) / 1000))); + } else if (quizCarryAnswerCloseAt > Date.now()) { + timeVal.textContent = String(Math.max(0, Math.ceil((quizCarryAnswerCloseAt - Date.now()) / 1000))); + } else { + timeVal.textContent = '0'; + } + } else if (isGauntlet()) { + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) { + timeVal.textContent = '···'; + } else if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) { + const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000)); + const mm = Math.floor(rem / 60); + const ss = rem % 60; + timeVal.textContent = `${mm}:${String(ss).padStart(2, '0')}`; + } else if (gauntletRuntimeTimeLimitSec > 0) { + timeVal.textContent = '···'; + } else { + timeVal.textContent = '∞'; + } + } else if (isSpaceShooter()) { + if (spaceShooterGameEnded) { + timeVal.textContent = '0'; + } else { + const rem = spaceShooterRemainingSecPlay(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else if (isBalloonBoss()) { + if (balloonBossGameEnded) { + timeVal.textContent = '0'; + } else { + const rem = balloonBossRemainingSecPlay(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) { + timeVal.textContent = jumpSurviveMissionPhase === 'howto' ? '···' : (jumpSurviveMissionPhase === 'countdown' ? '···' : '0'); + } else { + const rem = jumpSurviveRemainingSecMission(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else { + const t = jumpSurviveSessionStartMs > 0 + ? Math.max(0, Math.floor((performance.now() - jumpSurviveSessionStartMs) / 1000)) + : 0; + timeVal.textContent = String(t); + } + } + + if (portrait) { + const cid = me.characterId || getPlayCharacterId(); + const dir = (isGauntletFaceRightMapMno9kb07() ? 'right' : (me.direction || 'down')); + const nowT = Date.now(); + const walk = !!me.isWalking; + const rawImg = getAvatarImg(cid, dir, nowT, walk); + const tint = me.playTint || playTintFromPeerId(String(myId != null ? myId : 'me')); + const comp = rawImg && cid && tint + ? getPlayTintedAvatarSource(rawImg, cid, dir, nowT, walk, tint) + : rawImg; + if (comp && comp.tagName === 'CANVAS' && comp.width > 0) { + try { + portrait.src = comp.toDataURL('image/png'); + } catch (e) { + if (rawImg && rawImg.src) portrait.src = rawImg.src; + } + } else if (comp && comp.src) { + portrait.src = comp.src; + } else if (rawImg && rawImg.src) { + portrait.src = rawImg.src; + } + } + if (statusEl) { + if (isQuizCarry() && quizCarrySessionEnded) { + statusEl.textContent = 'SESSION COMPLETE · จบชุดคำถาม'; + } else if (isJumpSurvive() && jumpSurviveEliminated) { + statusEl.textContent = 'SPECTATOR · LINK SEVERED'; + } else if (isBalloonBoss() && me.balloonBossEliminated) { + statusEl.textContent = 'SPECTATOR · BALLOONS GONE'; + } else { + statusEl.textContent = 'OPERATOR ONLINE'; + } + } + if (hintEl) { + if (isQuizCarry()) { + hintEl.textContent = quizCarrySessionEnded + ? 'ชุดคำถามจบแล้ว · Session ended — ดูคะแนนด้าน SCORE' + : ('ยืนโซนตัวเลือกแล้วกด F/Grab หยิบ · ถือป้ายถูกแล้วไปฮับส่ง — ถูก +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' แต้ม (correct +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' on hub)'); + } else if (isJumpSurvive()) { + if (isJumpSurviveMissionUiMapPlay()) { + hintEl.textContent = jumpSurviveEliminated + ? 'คุณตกน้ำแล้ว — ดูเพื่อนต่อ — รอจบเพื่อรับคะแนน (ผู้รอดได้ 100 คะแนน)' + : 'กระโดดตามแท่น — หลีกหนาม/กับดักด้านล่าง · หมดเวลาแล้วสรุปคะแนน (ผู้รอด 100) · Space / W / ↑ กระโดด · A D / ← → เดิน'; + } else { + hintEl.textContent = jumpSurviveEliminated + ? 'Spectate remaining nodes · Exit room to reset session' + : 'Space / W / ↑ jump · A D / arrows move · Hit ceiling/floor = out (watch others)'; + } + } else if (isSpaceShooter()) { + hintEl.textContent = spaceShooterGameEnded + ? 'จบแล้ว · Session ended — ดูอันดับบน overlay / see rankings on overlay' + : 'A D / ← → = move · W S / ↑ ↓ = up/down (lower half of map only) · Space = fire · +5 per hit'; + } else if (isBalloonBoss()) { + hintEl.textContent = balloonBossGameEnded + ? 'จบแล้ว · Session ended — see overlay' + : (me.balloonBossEliminated + ? 'คุณตกรอบแล้ว — ดูเพื่อนเล่น · You are out — spectate' + : 'ยานมีแรงเฉื่อย — A D / arrows / W S เร่งทิศทาง · ปล่อยปุ่มแล้วยังไหล (แรงหน่วง) · Space = ยิง (ดีเลย์) · ลูกโป้งหมด = ตกรอบ'); + } else if (isGauntletCrownHeistMapPlay()) { + hintEl.textContent = 'Space / W / ↑ jump · Start 100 pts · Hit -10 · Fall off left edge = out · Rank bonus at end'; + } else { + hintEl.textContent = 'Space / W / ↑ jump · Lanes + lasers · Clear = right + score'; + } + } + if (previewEl) { + if (previewFillBots && mapData) { + const human = countPlayHumans(); + const bots = [...others.keys()].filter(isPreviewBotId).length; + previewEl.textContent = `SIM · humans ${human} + bots ${bots} · target ${previewTargetHeadcount}`; + previewEl.classList.remove('is-hidden'); + } else { + previewEl.textContent = ''; + previewEl.classList.add('is-hidden'); + } + } + + const ul = document.getElementById('play-cyber-score-list'); + if (!ul) return; + const safeYv = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + const jumpMissionHud = isJumpSurvive() && isJumpSurviveMissionUiMapPlay(); + const rows = []; + rows.push({ + id: myId, + nickname: me.nickname || nick, + characterId: me.characterId || getPlayCharacterId(), + score: isQuizCarry() ? cyberHudQuizCarryPeerScore(myId) + : (isGauntlet() ? (me.gauntletScore || 0) + : (isSpaceShooter() ? (me.spaceShooterScore || 0) + : (isBalloonBoss() ? (me.balloonBossScore || 0) + : (jumpMissionHud ? (jumpSurviveEliminated ? 0 : 100) : 0)))), + y: safeYv(me.y), + eliminated: !!(isJumpSurvive() && jumpSurviveEliminated) || !!(isBalloonBoss() && me.balloonBossEliminated) + || !!(isGauntletCrownHeistMapPlay() && me.gauntletEliminated), + isMe: true, + }); + others.forEach((o, id) => { + rows.push({ + id, + nickname: o.nickname || id.slice(0, 8), + characterId: o.characterId, + score: isQuizCarry() ? cyberHudQuizCarryPeerScore(id) + : (isGauntlet() ? (o.gauntletScore || 0) + : (isSpaceShooter() ? (o.spaceShooterScore || 0) + : (isBalloonBoss() ? (o.balloonBossScore || 0) + : (jumpMissionHud ? (o.jumpSurviveEliminated ? 0 : 100) : 0)))), + y: safeYv(o.y), + eliminated: !!(isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated) + || !!(isBalloonBoss() && o.balloonBossEliminated) + || !!(isGauntletCrownHeistMapPlay() && o.gauntletEliminated), + isMe: false, + }); + }); + let leaderId = null; + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud) { + let mx = -1; + for (let i = 0; i < rows.length; i++) { + if (rows[i].score > mx) { + mx = rows[i].score; + leaderId = rows[i].id; + } + } + } else { + let best = Infinity; + for (let i = 0; i < rows.length; i++) { + if (!rows[i].eliminated && rows[i].y < best) { + best = rows[i].y; + leaderId = rows[i].id; + } + } + } + rows.sort((a, b) => { + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud) { + if (b.score !== a.score) return b.score - a.score; + if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1; + return String(a.nickname || '').localeCompare(String(b.nickname || ''), 'th'); + } + if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1; + return a.y - b.y; + }); + + ul.innerHTML = ''; + const cap = 8; + for (let r = 0; r < Math.min(cap, rows.length); r++) { + const row = rows[r]; + const li = document.createElement('li'); + li.className = 'play-cyber-score-row' + (row.isMe ? ' is-me' : '') + (row.eliminated ? ' is-out' : ''); + const av = document.createElement('img'); + av.className = 'play-cyber-score-av'; + av.alt = ''; + const cid = row.characterId ? String(row.characterId) : ''; + if (!cid) { + av.src = defaultAvatarImg.src; + } else { + const im = getCharacterImg(cid, 'down'); + if (im && im.src) av.src = im.src; + } + const mid = document.createElement('div'); + mid.className = 'play-cyber-score-mid'; + const name = document.createElement('span'); + name.className = 'play-cyber-score-name'; + if (row.id === leaderId && leaderId != null) { + const crown = document.createElement('span'); + crown.className = 'play-cyber-lead-crown'; + crown.textContent = '♔'; + crown.title = 'Leader'; + name.appendChild(crown); + } + name.appendChild(document.createTextNode(String(row.nickname || '').slice(0, 14))); + const st = document.createElement('span'); + st.className = 'play-cyber-score-state'; + st.textContent = isQuizCarry() + ? '' + : (row.eliminated + ? (isBalloonBoss() ? 'OUT' : (isGauntletCrownHeistMapPlay() ? 'เสียชีวิต' : 'OFFLINE')) + : (isBalloonBoss() + ? ('♥' + Math.max(0, row.isMe ? (me.balloonBossBalloons | 0) : ((() => { + const ox = others.get(row.id); + return ox ? (ox.balloonBossBalloons | 0) : 0; + })()))) + : 'LINK_OK')); + mid.appendChild(name); + mid.appendChild(st); + const sc = document.createElement('span'); + sc.className = 'play-cyber-score-val'; + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud) { + sc.textContent = String(row.score); + } else { + sc.textContent = row.eliminated ? '—' : String(r + 1); + } + li.appendChild(av); + li.appendChild(mid); + li.appendChild(sc); + ul.appendChild(li); + } + } + + function draw() { + if (!mapData) return; + const w = mapData.width, h = mapData.height; + + const stackCam = isStack() ? getStackCameraCentersPx() : null; + let camX = stackCam ? stackCam.px : me.x * tileSize; + let camY = stackCam ? stackCam.py : me.y * tileSize; + if (isJumpSurvive()) { + camX = jumpSurviveCamCenterX; + camY = jumpSurviveCamCenterY; + } + let zDraw = zoom; + if (isSpaceShooter() || isBalloonBoss()) { + const mwPx = w * tileSize, mhPx = h * tileSize; + camX = mwPx * 0.5; + camY = mhPx * 0.5; + zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96; + } + const halfW = canvas.width / (2 * zDraw); + const halfH = canvas.height / (2 * zDraw); + + // world bounds ที่กล้องมองเห็น (เป็นพิกัดพิกเซลของ map) + const worldMinX = camX - halfW; + const worldMaxX = camX + halfW; + const worldMinY = camY - halfH; + const worldMaxY = camY + halfH; + + const mapWpx = w * tileSize, mapHpx = h * tileSize; + const visibleW = worldMaxX - worldMinX, visibleH = worldMaxY - worldMinY; + const showGrid = mapData.showMapInGame !== false && mapData.showMapInGame !== 'false'; + const timeMs = Date.now(); + + if (mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) { + /* กล้องเห็นพื้นที่นอก [0,map] ได้ — ห้ามสุ่มต้นทาง drawImage เกินขอบภาพ (จะเกิดซ้ำ/เส้นตัด/ซ้อนกันที่ขอบจอ) */ + ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + const natW = mapBackgroundImg.naturalWidth; + const natH = mapBackgroundImg.naturalHeight; + const bgMinX = Math.max(0, worldMinX); + const bgMaxX = Math.min(mapWpx, worldMaxX); + const bgMinY = Math.max(0, worldMinY); + const bgMaxY = Math.min(mapHpx, worldMaxY); + if (bgMaxX > bgMinX && bgMaxY > bgMinY) { + const srcX = (bgMinX / mapWpx) * natW; + const srcY = (bgMinY / mapHpx) * natH; + const srcW = ((bgMaxX - bgMinX) / mapWpx) * natW; + const srcH = ((bgMaxY - bgMinY) / mapHpx) * natH; + const destX = (bgMinX - camX) * zDraw + canvas.width / 2; + const destY = (bgMinY - camY) * zDraw + canvas.height / 2; + const destW = (bgMaxX - bgMinX) * zDraw; + const destH = (bgMaxY - bgMinY) * zDraw; + if (srcW > 0.25 && srcH > 0.25 && destW > 0.25 && destH > 0.25) { + ctx.drawImage(mapBackgroundImg, srcX, srcY, srcW, srcH, destX, destY, destW, destH); + } + } + } else { + ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + if (isSpaceShooter()) { + ctx.save(); + for (let s = 0; s < 120; s++) { + const sx = (Math.sin(s * 12.9898 + w) * 0.5 + 0.5) * canvas.width; + const sy = (Math.cos(s * 4.1415 + h) * 0.5 + 0.5) * canvas.height; + const br = (s % 4 === 0) ? 1.6 : 0.9; + ctx.fillStyle = s % 7 === 0 ? 'rgba(200, 240, 255, 0.35)' : 'rgba(255, 255, 255, 0.12)'; + ctx.beginPath(); + ctx.arc(sx, sy, br, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); + } else if (isBalloonBoss()) { + ctx.save(); + ctx.fillStyle = 'rgba(30, 45, 120, 0.25)'; + for (let col = 0; col < 24; col++) { + const x = (col + 0.5) * (canvas.width / 24); + for (let r = 0; r < 18; r++) { + const bit = (col * 17 + r * 31 + Math.floor(timeMs / 80)) % 2; + ctx.fillStyle = bit ? 'rgba(0, 255, 200, 0.14)' : 'rgba(180, 200, 255, 0.08)'; + ctx.fillRect(x - 1, (r / 18) * canvas.height + ((timeMs / 40 + col * 3) % 40), 2.2, 10); + } + } + ctx.fillStyle = 'rgba(240, 248, 255, 0.55)'; + ctx.beginPath(); + ctx.moveTo(0, canvas.height); + const baseY = canvas.height * 0.88; + for (let k = 0; k <= 40; k++) { + const bx = (k / 40) * canvas.width; + const by = baseY - Math.abs(Math.sin(k * 1.7 + w)) * canvas.height * 0.08 - (k % 3) * 4; + ctx.lineTo(bx, by); + } + ctx.lineTo(canvas.width, canvas.height); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + } + function worldToScreen(wx, wy) { + const sx = (wx - camX) * zDraw + canvas.width / 2; + const sy = (wy - camY) * zDraw + canvas.height / 2; + return [sx, sy]; + } + + function drawGridImagePlacementsLayer() { + if (!mapData.gridImagePlacements || !mapData.gridImagePlacements.length || !mapData.gridImageLibrary || !mapData.gridImageLibrary.length) return; + for (let gi = 0; gi < mapData.gridImagePlacements.length; gi++) { + const sp = mapData.gridImagePlacements[gi]; + const wx0 = sp.x * tileSize; + const wy0 = sp.y * tileSize; + const ww = sp.w * tileSize; + const wh = sp.h * tileSize; + if (wx0 + ww <= worldMinX || wx0 >= worldMaxX || wy0 + wh <= worldMinY || wy0 >= worldMaxY) continue; + let gim = mapGridImageImgs[sp.i]; + let carrySpriteAlpha = 1; + if (isQuizCarry()) { + const domOpt = quizCarryOptionIndexForGridSprite(mapData, sp); + const onHub = spriteOverlapsQuizCarryHubArea(mapData, sp); + const meHolding = me && me.quizCarryHeld != null; + let carryVisualActive = false; + if (domOpt != null && quizCarryOptionHeldByAnyone(domOpt)) carryVisualActive = true; + else if (domOpt == null && onHub && meHolding) carryVisualActive = true; + if (carryVisualActive) { + const gh = mapGridImageHeldImgs[sp.i]; + if (gh && gh.complete && gh.naturalWidth) gim = gh; + else carrySpriteAlpha = 0.32; + } + } + if (!gim || !gim.complete || !gim.naturalWidth) continue; + const sx0 = (wx0 - camX) * zDraw + canvas.width / 2; + const sy0 = (wy0 - camY) * zDraw + canvas.height / 2; + const sw = ww * zDraw; + const sh = wh * zDraw; + ctx.save(); + if (carrySpriteAlpha < 1) ctx.globalAlpha = carrySpriteAlpha; + ctx.beginPath(); + ctx.rect(sx0, sy0, sw, sh); + ctx.clip(); + ctx.drawImage(gim, sx0, sy0, sw, sh); + ctx.restore(); + ctx.strokeStyle = 'rgba(255,255,255,0.18)'; + ctx.lineWidth = 1; + ctx.strokeRect(sx0 + 0.5, sy0 + 0.5, sw - 1, sh - 1); + } + } + + const startTileX = Math.max(0, Math.floor(worldMinX / tileSize)); + const endTileX = Math.min(w - 1, Math.ceil(worldMaxX / tileSize)); + const startTileY = Math.max(0, Math.floor(worldMinY / tileSize)); + const endTileY = Math.min(h - 1, Math.ceil(worldMaxY / tileSize)); + if (showGrid) { + for (let y = startTileY; y <= endTileY; y++) { + const lane = isFrogger() ? getLane(y) : null; + let rowFill = null; + if (lane) { + if (lane.type === 'goal') rowFill = 'rgba(158,206,106,0.4)'; + else if (lane.type === 'spawn') rowFill = 'rgba(187,154,247,0.35)'; + else if (lane.type === 'road') rowFill = 'rgba(80,70,60,0.6)'; + else if (lane.type === 'water') rowFill = 'rgba(125,207,255,0.5)'; + } else if (isGauntlet()) { + rowFill = (y % 2 === 0) ? 'rgba(247,118,190,0.08)' : 'rgba(180,90,140,0.06)'; + } else if (isStack()) { + rowFill = (y % 2 === 0) ? 'rgba(122, 162, 247, 0.06)' : 'rgba(187, 154, 247, 0.05)'; + } else if (isJumpSurvive()) { + rowFill = (y % 2 === 0) ? 'rgba(90, 200, 255, 0.07)' : 'rgba(60, 140, 200, 0.06)'; + } else if (isSpaceShooter()) { + rowFill = (y % 2 === 0) ? 'rgba(30, 45, 110, 0.22)' : 'rgba(20, 32, 78, 0.28)'; + } else if (isBalloonBoss()) { + rowFill = (y % 2 === 0) ? 'rgba(40, 25, 80, 0.2)' : 'rgba(25, 18, 60, 0.24)'; + } else if (isQuizBattle()) { + rowFill = (y % 2 === 0) ? 'rgba(110, 175, 255, 0.06)' : 'rgba(230, 110, 140, 0.055)'; + } + for (let x = startTileX; x <= endTileX; x++) { + const wx = x * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw; + const ob = mapData.objects?.[y]?.[x] ?? 0; + if (ob === 1) { + ctx.fillStyle = 'rgba(65,72,104,0.92)'; + ctx.fillRect(sx, sy, size, size); + ctx.strokeStyle = '#565f89'; + ctx.strokeRect(sx, sy, size, size); + } else { + const cellColor = showGrid && mapData.cellColors && mapData.cellColors[y] && mapData.cellColors[y][x]; + if (cellColor) { ctx.fillStyle = cellColor; ctx.fillRect(sx, sy, size, size); } + else if (rowFill) { ctx.fillStyle = rowFill; ctx.fillRect(sx, sy, size, size); } + else if (!mapBackgroundImg || !mapBackgroundImg.complete) { + ctx.fillStyle = (x + y) % 2 === 0 ? '#24283b' : '#1f2335'; + ctx.fillRect(sx, sy, size, size); + } + } + } + } + } + drawGridImagePlacementsLayer(); + if (showGrid) { + for (let y = startTileY; y <= endTileY; y++) { + for (let x = startTileX; x <= endTileX; x++) { + const wx = x * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw; + const isInter = mapData.interactive && mapData.interactive[y] && mapData.interactive[y][x] === 1; + if (isInter) { + ctx.fillStyle = 'rgba(158,206,106,0.35)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(158,206,106,0.8)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + if (isQuiz()) { + const isQuizQ = mapData.quizQuestionArea && mapData.quizQuestionArea[y] && mapData.quizQuestionArea[y][x] === 1; + if (isQuizQ) { + ctx.fillStyle = 'rgba(255, 214, 102, 0.32)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(224, 185, 70, 0.78)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isQuizT = mapData.quizTrueArea && mapData.quizTrueArea[y] && mapData.quizTrueArea[y][x] === 1; + if (isQuizT) { + ctx.fillStyle = 'rgba(86, 202, 255, 0.38)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(122, 220, 255, 0.85)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isQuizF = mapData.quizFalseArea && mapData.quizFalseArea[y] && mapData.quizFalseArea[y][x] === 1; + if (isQuizF) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.38)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(255, 130, 200, 0.85)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + } + if (isQuizCarry()) { + const hub = mapData.quizCarryHubArea && mapData.quizCarryHubArea[y] && mapData.quizCarryHubArea[y][x] === 1; + if (hub) { + ctx.fillStyle = 'rgba(187, 154, 247, 0.35)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(200, 170, 255, 0.75)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isCarryQ = mapData.quizQuestionArea && mapData.quizQuestionArea[y] && mapData.quizQuestionArea[y][x] === 1; + if (isCarryQ) { + ctx.fillStyle = 'rgba(255, 214, 102, 0.3)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(224, 185, 70, 0.82)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const ov = mapData.quizCarryOptionArea && mapData.quizCarryOptionArea[y] && mapData.quizCarryOptionArea[y][x]; + if (ov >= 1 && ov <= QUIZ_CARRY_MAX_OPTION_SLOTS) { + ctx.fillStyle = quizCarryMinimapOptionFillCss(ov); + ctx.fillRect(sx + 3, sy + 3, size - 6, size - 6); + } + } + if (isQuizBattle() && showGrid && mapData.quizBattlePathArea && mapData.quizBattlePathArea[y] && mapData.quizBattlePathArea[y][x] === 1) { + ctx.fillStyle = 'rgba(186, 130, 255, 0.4)'; + ctx.fillRect(sx + 1, sy + 1, size - 2, size - 2); + ctx.strokeStyle = 'rgba(220, 170, 255, 0.55)'; + ctx.strokeRect(sx + 1, sy + 1, size - 2, size - 2); + } + if (isQuizBattle() && showGrid && mapData.quizBattleDomeArea && mapData.quizBattleDomeArea[y] && mapData.quizBattleDomeArea[y][x] === 1) { + const cid = mapData.quizBattleDomeComp && mapData.quizBattleDomeComp[y] && mapData.quizBattleDomeComp[y][x]; + const done = cid > 0 && quizBattleAnsweredComps.has(cid); + ctx.fillStyle = done ? 'rgba(90, 90, 110, 0.4)' : 'rgba(100, 200, 255, 0.36)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = done ? 'rgba(140, 140, 160, 0.65)' : 'rgba(255, 100, 130, 0.82)'; + ctx.lineWidth = 2; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.lineWidth = 1; + } + if (isStack()) { + const sr = mapData.stackReleaseArea, sl = mapData.stackLandArea; + if (sr && sr[y] && sr[y][x] === 1) { + ctx.fillStyle = 'rgba(125, 207, 255, 0.28)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(122, 220, 255, 0.55)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + if (sl && sl[y] && sl[y][x] === 1) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.26)'; + ctx.fillRect(sx + 3, sy + 3, size - 6, size - 6); + ctx.strokeStyle = 'rgba(255, 130, 200, 0.5)'; + ctx.strokeRect(sx + 3, sy + 3, size - 6, size - 6); + } + } + if (isBalloonBoss() && showGrid && mapData.balloonBossPlayerSlots) { + const sv = mapData.balloonBossPlayerSlots[y] && mapData.balloonBossPlayerSlots[y][x]; + if (sv >= 1 && sv <= 6) { + ctx.fillStyle = 'rgba(255, 120, 200, 0.12)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(255, 160, 220, 0.45)'; + ctx.lineWidth = 1.5; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.lineWidth = 1; + ctx.fillStyle = 'rgba(255, 228, 250, 0.92)'; + ctx.font = `bold ${Math.max(8, 10 * zDraw)}px sans-serif`; + ctx.textAlign = 'center'; + ctx.fillText('P' + sv, sx + size / 2, sy + size / 2 + 4); + ctx.textAlign = 'left'; + } + } + if (isSpaceShooter() && showGrid && mapData.shooterSpawnSlots) { + const sv = mapData.shooterSpawnSlots[y] && mapData.shooterSpawnSlots[y][x]; + if (sv >= 1 && sv <= 6) { + ctx.fillStyle = 'rgba(0, 255, 240, 0.14)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(120, 255, 255, 0.45)'; + ctx.lineWidth = 1.5; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.lineWidth = 1; + ctx.fillStyle = 'rgba(220, 250, 255, 0.9)'; + ctx.font = `bold ${Math.max(8, 10 * zDraw)}px sans-serif`; + ctx.textAlign = 'center'; + ctx.fillText('P' + sv, sx + size / 2, sy + size / 2 + 4); + ctx.textAlign = 'left'; + } + } + } + } + if (isJumpSurvive() && showGrid && mapData.jumpSurvivePlatformArea) { + const pa = mapData.jumpSurvivePlatformArea; + const sc = jumpSurvivePlatformScrollPx; + const tsz = tileSize; + const period = h * tsz; + const visTop = worldMinY; + const visBot = worldMaxY; + const eps = 1e-6; + for (let py = 0; py < h; py++) { + for (let px = 0; px < w; px++) { + if (!pa[py] || pa[py][px] !== 1) continue; + const tileWorldLeft = px * tsz; + const tileWorldRight = tileWorldLeft + tsz; + if (tileWorldRight <= worldMinX || tileWorldLeft >= worldMaxX) continue; + const rawTop = py * tsz - sc; + const kMin = Math.ceil((visTop - rawTop - tsz + eps) / period); + const kMax = Math.floor((visBot - rawTop - eps) / period); + if (kMax < kMin) continue; + for (let kk = kMin; kk <= kMax; kk++) { + const platTop = rawTop + kk * period; + if (platTop + tsz <= visTop || platTop >= visBot) continue; + const [psx, psy] = worldToScreen(tileWorldLeft, platTop); + const psz = tsz * zDraw; + ctx.fillStyle = 'rgba(0, 40, 52, 0.72)'; + ctx.fillRect(psx + 3, psy + 3, psz - 6, psz - 6); + ctx.strokeStyle = 'rgba(0, 255, 240, 0.55)'; + ctx.lineWidth = 2; + ctx.strokeRect(psx + 3, psy + 3, psz - 6, psz - 6); + ctx.strokeStyle = 'rgba(120, 255, 255, 0.35)'; + ctx.lineWidth = 1; + ctx.strokeRect(psx + 5, psy + 5, psz - 10, psz - 10); + const grad = ctx.createLinearGradient(psx, psy, psx, psy + psz); + grad.addColorStop(0, 'rgba(0, 255, 240, 0.12)'); + grad.addColorStop(0.5, 'rgba(0, 0, 0, 0)'); + grad.addColorStop(1, 'rgba(0, 180, 200, 0.08)'); + ctx.fillStyle = grad; + ctx.fillRect(psx + 4, psy + 4, psz - 8, psz - 8); + ctx.lineWidth = 1; + if (zDraw >= 1.15 && psz > 22) { + ctx.save(); + ctx.font = `600 ${Math.max(7, Math.min(11, psz * 0.14))}px "Share Tech Mono", ui-monospace, monospace`; + ctx.fillStyle = 'rgba(180, 255, 255, 0.5)'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('DATA', psx + psz * 0.5, psy + psz * 0.38); + ctx.font = `500 ${Math.max(5, Math.min(8, psz * 0.1))}px "Share Tech Mono", ui-monospace, monospace`; + ctx.fillStyle = 'rgba(120, 230, 255, 0.38)'; + ctx.fillText('/SEC/BLOCK', psx + psz * 0.5, psy + psz * 0.62); + ctx.restore(); + } + } + } + } + } + if (isFrogger() && mapData.lanes) { + for (let y = startTileY; y <= endTileY; y++) { + const lane = getLane(y); + if (!lane || (lane.type !== 'road' && lane.type !== 'water')) continue; + const positions = getVehiclePositions(lane, mapData.width, timeMs); + const isRoad = lane.type === 'road'; + for (let i = 0; i < positions.length; i++) { + const vx = positions[i]; + const wx = vx * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw * (isRoad ? 1.2 : 1.5); + ctx.fillStyle = isRoad ? '#e0a060' : '#8b7355'; + ctx.fillRect(sx, sy, size, size * 0.7); + ctx.strokeStyle = isRoad ? '#c0caf5' : '#9ece6a'; + ctx.strokeRect(sx, sy, size, size * 0.7); + } + } + } + } + + if (isQuizCarry()) { + drawQuizCarryChoiceLabels(ctx, worldToScreen, zDraw); + } + + if (isStack() && stackMini) { + drawStackMinigame(ctx, worldToScreen, zDraw); + } + + const gauntletObsDraw = isGauntlet() ? getGauntletObsDrawPositionsAt(performance.now()) : []; + if (isGauntlet() && gauntletObsDraw.length) { + const stx = Math.max(0, Math.floor(worldMinX / tileSize)); + const enx = Math.min(w - 1, Math.ceil(worldMaxX / tileSize)); + const sty = Math.max(0, Math.floor(worldMinY / tileSize)); + const eny = Math.min(h - 1, Math.ceil(worldMaxY / tileSize)); + for (let i = 0; i < gauntletObsDraw.length; i++) { + const o = gauntletObsDraw[i]; + if (!o) continue; + if (o.kind === 'lane' && typeof o.y === 'number') { + if (o.drawX < stx - 2 || o.drawX > enx + 2 || o.y < sty || o.y > eny) continue; + const size = tileSize * zDraw; + const inner = Math.max(2, size - 4); + const [sxC, syCellBottom] = worldToScreen((o.drawX + 0.5) * tileSize, (o.y + 1) * tileSize); + const dx = sxC - inner / 2; + const dy = syCellBottom - inner; + const laneRec = pickGauntletLaneImageRec(o.id); + if (laneRec && laneRec.img.complete && laneRec.img.naturalWidth > 0) { + try { + ctx.drawImage(laneRec.img, dx, dy, inner, inner); + } catch (e) { + ctx.fillStyle = '#f7768e'; + ctx.fillRect(dx, dy, inner, inner); + } + } else { + ctx.fillStyle = '#f7768e'; + ctx.fillRect(dx, dy, inner, inner); + } + } else if (o.kind === 'laser' && typeof o.drawX === 'number') { + if (o.drawX < stx - 2 || o.drawX > enx + 2) continue; + const { y0: laserRow0, y1: laserRow1 } = gauntletLaserRowHitRange(o, h); + const wx0 = o.drawX * tileSize; + const wx1 = (o.drawX + 1) * tileSize; + const wy0 = laserRow0 * tileSize; + const wy1 = (laserRow1 + 1) * tileSize; + const [sx0, syTop] = worldToScreen(wx0, wy0); + const [sx1, syBot] = worldToScreen(wx1, wy1); + const rx = Math.min(sx0, sx1); + const rw = Math.max(2, Math.abs(sx1 - sx0)); + const ry = Math.min(syTop, syBot); + const rh = Math.abs(syBot - syTop); + drawGauntletLaserColumnScreen(rx, ry, rw, rh); + } + } + } + + /** ป้ายติดตัว — กึ่งกลางลำตัว (ทับอก/เอว) แบบเดียวกับป้ายบนพื้น ไม่มีเส้นเชื่อม */ + function drawQuizCarryHeldSignBoard(sxFeet, feetSy, halfSpriteW, spriteTopY, signText, _faceDir, choiceIndex, imageUrl, imageElementOpt) { + const imgUrlSan = sanitizeQuizCarryImageUrlClient(imageUrl); + const gridHeldDraw = imageElementOpt && imageElementOpt.complete && imageElementOpt.naturalWidth > 0 ? imageElementOpt : null; + const raw = String(signText || '').trim(); + if (!raw && !imgUrlSan && !gridHeldDraw) return; + ctx.save(); + const heldPs = quizCarryPlaqueMapScaleClampedPlay(); + const bodyH = Math.max(18, feetSy - spriteTopY); + const midBodyY = spriteTopY + bodyH * 0.48; + const bodyCx = sxFeet; + const halfW = Math.max(10, halfSpriteW || 0); + const baseMaxW = Math.min(280, Math.max(100, halfW * 2.4 + tileSize * zDraw * 0.5)); + const maxPlaqueW = Math.min(340, Math.round(baseMaxW * heldPs)); + + const fontPx = Math.max(12, Math.min(22, tileSize * zDraw * 0.22 * heldPs)); + ctx.font = `600 ${fontPx}px system-ui, "Segoe UI", "Kanit", sans-serif`; + const padX = 12; + const padY = 9; + const maxInner = Math.min(maxPlaqueW - padX * 2, Math.max(72, tileSize * zDraw * 4.2)); + let lines; + if (!raw) { + lines = []; + } else { + lines = canvasWordWrapLines(ctx, raw, maxInner); + if (!lines.length) lines = [raw]; + if (lines.length > 3) lines = lines.slice(0, 3); + for (let i = 0; i < lines.length; i++) { + let s = lines[i]; + if (ctx.measureText(s).width <= maxInner) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxInner) s = s.slice(0, -1); + lines[i] = s + '…'; + } + } + let maxLineW = 0; + for (let i = 0; i < lines.length; i++) { + const lw = ctx.measureText(lines[i]).width; + if (lw > maxLineW) maxLineW = lw; + } + const lineH = fontPx * 1.22; + let w; + let h; + if (!lines.length && (imgUrlSan || gridHeldDraw)) { + w = Math.ceil(Math.min(maxPlaqueW, Math.max(104, maxInner + padX * 2))); + h = Math.ceil(Math.max(56, fontPx * 2.8 + padY * 2)); + } else { + w = Math.ceil(Math.max(maxLineW, 56) + padX * 2); + h = Math.ceil(lines.length * lineH + padY * 2); + w = Math.min(maxPlaqueW, Math.max(96, w)); + h = Math.max(40, h); + } + + let signX = bodyCx - w / 2; + let signY = midBodyY - h / 2; + + if (signX + w > canvas.width - 6) signX = canvas.width - w - 6; + if (signX < 6) signX = 6; + if (signY < 6) signY = 6; + if (signY + h > feetSy + 8) signY = feetSy + 8 - h; + + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, null, { imageUrl: imgUrlSan, imageElement: gridHeldDraw }); + ctx.restore(); + } + + function drawAvatar(ax, ay, isMe, name, characterId, direction, isWalking, playTint, gauntletAirTicks, gauntletScoreLabel, quizCarrySignPayload) { + const { cw, ch } = getCharacterFootprintWH(mapData); + const air = Number(gauntletAirTicks) || 0; + let liftWorldY = 0; + if (isGauntlet() && air > 0) { + liftWorldY = gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSize * 0.52; + } + const cxWorld = (ax + cw * 0.5) * tileSize; + /* ใช้ ay แบบต่อเนื่อง — ห้าม floor(ay) เดิมทำให้เท้ากระโดดทีละช่องขณะเดิน (กระตุกแนวตั้ง) */ + const cyBottomWorld = (ay + ch) * tileSize - liftWorldY; + const [sx, sy] = worldToScreen(cxWorld, cyBottomWorld); + const cellSpan = Math.max(cw, ch); + const r = Math.max(14, (tileSize * zDraw * cellSpan) / 2 - 2); + const size = r * 2.2; + const dir = direction || 'down'; + const rawImg = getAvatarImg(characterId, dir, timeMs, isWalking); + const charImg = playTint + ? getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, isWalking, playTint) + : rawImg; + /* รองรับทั้ง และ canvas จากย้อมสี (canvas ไม่มี naturalWidth → เดิมตกวงกลม) */ + let iw = 0, ih = 0; + if (charImg && charImg.tagName === 'CANVAS' && charImg.width > 0 && charImg.height > 0) { + iw = charImg.width; + ih = charImg.height; + } else if (charImg && charImg.complete && charImg.naturalWidth) { + iw = charImg.naturalWidth; + ih = charImg.naturalHeight; + } + let halfSpriteW = r; + let spriteTopY = sy - 2 * r; + if (iw > 0 && ih > 0) { + const scale = Math.min(size / iw, size / ih, 1); + const drawW = iw * scale; + const drawH = ih * scale; + halfSpriteW = drawW / 2; + spriteTopY = sy - drawH; + ctx.drawImage(charImg, 0, 0, iw, ih, sx - drawW / 2, sy - drawH, drawW, drawH); + } else { + const cy = sy - r; + ctx.fillStyle = isMe ? '#7aa2f7' : '#9ece6a'; + ctx.beginPath(); + ctx.arc(sx, cy, r, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = '#c0caf5'; + ctx.lineWidth = 2; + ctx.stroke(); + spriteTopY = cy - r; + } + ctx.fillStyle = '#c0caf5'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'center'; + const nameBase = name || ''; + const withScore = isGauntlet() && typeof gauntletScoreLabel === 'number' && gauntletScoreLabel >= 0 + ? (nameBase + ' · ' + gauntletScoreLabel) + : nameBase; + ctx.fillText(withScore, sx, sy + 10); + if (isQuizCarry() && quizCarrySignPayload) { + drawQuizCarryHeldSignBoard( + sx, sy, halfSpriteW, spriteTopY, + quizCarrySignPayload.text, dir, quizCarrySignPayload.choiceIndex, + quizCarrySignPayload.imageUrl, + quizCarrySignPayload.imageElement, + ); + } + } + function quizCarrySignForEntity(ent) { + if (!isQuizCarry() || !quizCarryCurrent || !ent || ent.quizCarryHeld == null) return null; + const idx = ent.quizCarryHeld; + const ch = quizCarryCurrent.choices; + if (!Array.isArray(ch) || idx < 0 || idx >= ch.length) return null; + const t = String(ch[idx] != null ? ch[idx] : '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(quizCarryCurrent, idx); + const gridHeldEl = findQuizCarryGridHeldImgForChoiceIndex(idx); + if (!t && !imgUrl && !gridHeldEl) return null; + return { text: t, choiceIndex: idx, imageUrl: imgUrl, imageElement: gridHeldEl || undefined }; + } + const safeX = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + const safeY = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + function peerVisualOffset(id) { + let h = 0; + for (let i = 0; i < (id || '').length; i++) h = (h * 31 + (id || '').charCodeAt(i)) >>> 0; + return { ax: ((h % 5) - 2) * 0.1, ay: ((Math.floor(h / 5) % 5) - 2) * 0.1 }; + } + const { ch: sortCh } = getCharacterFootprintWH(mapData); + /** ใช้แถวกริด (floor y) เป็นหลัก — ลดการสลับลำดับวาดทุกเฟรมเมื่อ y เป็น float เลอร์ป */ + function avatarDrawDepth(ent) { + return Math.floor(safeY(ent.y)) + sortCh; + } + if (!isStack() && !isSpaceShooter() && !isBalloonBoss()) { + let mt = me.playTint; + if (!mt || typeof mt.head !== 'string' || typeof mt.hair !== 'string' || typeof mt.body !== 'string') { + mt = playTintFromPeerId(String((myId != null && myId !== '') ? myId : (nick || 'local'))); + me.playTint = mt; + } + const meTag = isFrogger() ? (' (กบ) ' + froggerScore) : (isGauntlet() && meGauntletJumpTicks > 0 ? ' (กระโดด)' : (isJumpSurvive() && jumpSurviveEliminated ? ' (ตกรอบ)' : ' (คุณ)')); + const meWalking = isFrogger() ? false : (isGauntlet() ? (meGauntletJumpTicks > 0 || meGauntletJumpVis > 0.08) : !!me.isWalking); + const drawRows = []; + others.forEach((o, id) => { + if (!o) return; + drawRows.push({ isMe: false, id, o }); + }); + drawRows.push({ isMe: true, id: '__me', o: me }); + drawRows.sort((a, b) => { + const da = avatarDrawDepth(a.o); + const db = avatarDrawDepth(b.o); + if (da !== db) return da - db; + const xa = Math.floor(safeX(a.o.x) * 4); + const xb = Math.floor(safeX(b.o.x) * 4); + if (xa !== xb) return xa - xb; + const sa = a.isMe ? '\uFFFFme' : String(a.id); + const sb = b.isMe ? '\uFFFFme' : String(b.id); + return sa < sb ? -1 : sa > sb ? 1 : 0; + }); + drawRows.forEach((row) => { + if (!row.isMe) { + const id = row.id; + const o = row.o; + const off = isGauntlet() ? { ax: 0, ay: 0 } : peerVisualOffset(id); + const otherWalk = isGauntlet() + ? ((o.gauntletJumpTicks || 0) > 0 || (o.gauntletJumpVis || 0) > 0.08) + : (isPreviewBotId(id) + ? !!o.botIsWalking + : !!((o.tx != null && Math.abs((o.tx || o.x) - o.x) > 0.02) || (o.ty != null && Math.abs((o.ty || o.y) - o.y) > 0.02))); + const ot = o.playTint || playTintFromPeerId(id); + const faceDirOther = isGauntletFaceRightMapMno9kb07() ? 'right' : o.direction; + const botOut = isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated; + const peerName = botOut ? (o.nickname + ' (ตกรอบ)') : o.nickname; + if (botOut) ctx.save(); + if (botOut) ctx.globalAlpha = 0.4; + drawAvatar(safeX(o.x) + off.ax, safeY(o.y) + off.ay, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o)); + if (botOut) ctx.restore(); + } else { + if (isJumpSurvive() && jumpSurviveEliminated) ctx.save(); + if (isJumpSurvive() && jumpSurviveEliminated) ctx.globalAlpha = 0.4; + const faceDirMe = isGauntletFaceRightMapMno9kb07() ? 'right' : me.direction; + drawAvatar(safeX(me.x), safeY(me.y), true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me)); + if (isJumpSurvive() && jumpSurviveEliminated) ctx.restore(); + } + }); + } + if (isFrogger()) { + ctx.fillStyle = '#7aa2f7'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('โหมดกบ | คะแนน: ' + froggerScore, 10, 24); + } + if (isJumpSurvive() && !useCyberPlayHud()) { + ctx.fillStyle = 'rgba(148, 226, 213, 0.95)'; + ctx.font = '600 12px system-ui, "Segoe UI", "Kanit", sans-serif'; + ctx.textAlign = 'left'; + if (jumpSurviveEliminated) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.95)'; + ctx.fillText('คุณตกรอบแล้ว (ชนหรือหลุดเพดาน/พื้นจอ) · เล่นต่อไม่ได้ — ดูเพื่อนได้ · รอรอบใหม่หรือออกจากห้อง', 10, 22); + } else { + ctx.fillText('กระโดดให้รอด · Space / W / ↑ · A D / ← → · แพลตฟอร์มวน · ชนหรือหลุดบน/ล่างจอ = ตกรอบ (ดูเพื่อนต่อ)', 10, 22); + } + } + if (isGauntlet() && !useCyberPlayHud()) { + ctx.fillStyle = '#f7768e'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + if (isGauntletCrownHeistMapPlay()) { + ctx.fillText('พรมแดงสุดท้าย | คะแนน ' + (me.gauntletScore || 0) + ' · ชน -10 · ขอบซ้ายตาย · สูงสุด 6 คน', 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · จบเกมมีโบนัสอันดับ + เกรดทีม', 10, 42); + } else { + ctx.fillText('พรมแดงสุดท้าย | คะแนน: ' + (me.gauntletScore || 0) + ' (ข้าม 1 ชิ้น +1) · สูงสุด 6 คน', 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · ข้ามสำเร็จ → ขวา 1 + คะแนน · ชน → ซ้าย 1', 10, 42); + } + ctx.fillStyle = '#e0af68'; + if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) { + const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000)); + const mm = Math.floor(rem / 60); + const ss = rem % 60; + const clock = `${mm}:${String(ss).padStart(2, '0')}`; + ctx.fillText(`เหลือเวลา ${clock} · Time left ${clock}`, 10, 60); + } else if (gauntletRuntimeTimeLimitSec > 0) { + ctx.fillText('เวลา: กำลังซิงก์จากเซิร์ฟเวอร์... · Syncing timer...', 10, 60); + } else { + ctx.fillText('เวลา: ไม่จำกัด · No time limit (ตั้งได้ที่ Admin → เวลาเกม)', 10, 60); + } + } + if (isStack() && stackMini) { + if (previewFillBots) { + ctx.fillStyle = 'rgba(125, 220, 255, 0.9)'; + ctx.font = '600 12px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Stack · shared tower · Space / click = release on your turn', 10, 22); + drawStackPreviewCyberHud(ctx, canvas.width, canvas.height); + } else { + ctx.fillStyle = '#7dcfff'; + ctx.font = 'bold 15px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Stack | คะแนน ' + stackMini.score + ' · ชีวิต ' + stackMini.lives + ' · combo x' + (stackMini.combo || 0), 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / คลิก = ปล่อย', 10, 44); + } + } + if (isQuiz() || isQuizCarry() || isQuizBattle()) { + syncPlayQuizMapPanel(); + syncQuizCarryEmbedQuestionStrip(); + } + /* timeup ต้องซิงก์ทุกเฟรมแม้จบเซสชัน — อย่าให้อยู่ในเงื่อนไขด้านบนอย่างเดียว */ + syncQuizCarryTimeupDeskLayerPosition(); + if (previewFillBots && mapData && !useCyberPlayHud()) { + const human = countPlayHumans(); + const bots = [...others.keys()].filter(isPreviewBotId).length; + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = 'rgba(26,27,38,0.78)'; + const hudW = isStack() ? Math.min(canvas.width - 12, 520) : Math.min(canvas.width - 12, 320); + ctx.fillRect(6, canvas.height - 38, hudW, 30); + ctx.fillStyle = '#a9b1d6'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'left'; + let line = 'ทดสอบ: คนจริง ' + human + ' + บอท ' + bots + ' (เป้า ' + previewTargetHeadcount + ' คน)'; + if (isStack()) { + line += ' · ตึกร่วม + HUD ซ้าย/ขวา'; + } + ctx.fillText(line, 12, canvas.height - 18); + ctx.restore(); + } + if (isSpaceShooter()) { + drawSpaceShooterCombatLayer(ctx, worldToScreen, zDraw, timeMs); + } + if (isBalloonBoss()) { + drawBalloonBossCombatLayer(ctx, worldToScreen, zDraw, timeMs); + } + if (isQuizCarry()) { + drawQuizCarryEmbedCountdownHighlight(ctx, worldToScreen, zDraw, timeMs); + } + if (previewMode && editorEmbedReturn && isQuizCarry() && quizCarryEmbedCountdownEndAt > Date.now()) { + syncQuizCarryEmbedCountdownLayout(); + } + syncPlayCyberHud(); + syncQuizCarryGrabButton(); + } + + document.addEventListener('keydown', (e) => { + if (isMovementKey(e.code) && isChatFocused()) return; + if ((isSpaceShooter() || isBalloonBoss()) && mapData && !isChatFocused()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + } + if (isStack() && mapData && !isChatFocused()) { + if (e.code === 'Space') { + e.preventDefault(); + tryHumanStackDrop(); + return; + } + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + return; + } + } + if (isGauntlet() && mapData && !isChatFocused()) { + if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { + e.preventDefault(); + if (isGauntletCrownPregameBlockingPlay()) return; + if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return; + const now = Date.now(); + if (now - lastGauntletJumpKey < 200) return; + lastGauntletJumpKey = now; + socket.emit('gauntlet-jump'); + return; + } + if (['ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + return; + } + } + if (isJumpSurvive() && mapData && !isChatFocused()) { + if (isJumpSurviveMissionPregameBlockingPlay()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) e.preventDefault(); + return; + } + if (jumpSurviveEliminated) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) e.preventDefault(); + return; + } + if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { + e.preventDefault(); + if (!e.repeat) jumpSurviveJumpQueued = true; + return; + } + if (e.code === 'ArrowDown' || e.code === 'KeyS') { + e.preventDefault(); + return; + } + } + if (isQuizBattle() && mapData && !isChatFocused()) { + if (e.code === 'KeyE') { + e.preventDefault(); + if (!e.repeat && myId != null) tryOpenQuizBattleFromKey(); + return; + } + } + if (isQuizCarry() && mapData && !isChatFocused()) { + if (e.code === 'KeyF') { + e.preventDefault(); + if (!e.repeat && myId != null) tryQuizCarryInteractionForPlayer(myId, me.x, me.y, { fromKey: true }); + return; + } + } + keys[e.code] = true; + keys[e.key] = true; + if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault(); + if (isFrogger() && mapData && !isChatFocused()) { + const now = Date.now(); + if (now - lastFroggerKey < 280) return; + let dx = 0, dy = 0; + if (e.code === 'ArrowUp' || e.code === 'KeyW') { dy = -1; me.direction = 'up'; } + else if (e.code === 'ArrowDown' || e.code === 'KeyS') { dy = 1; me.direction = 'down'; } + else if (e.code === 'ArrowLeft' || e.code === 'KeyA') { dx = -1; me.direction = 'left'; } + else if (e.code === 'ArrowRight' || e.code === 'KeyD') { dx = 1; me.direction = 'right'; } + if (dx !== 0 || dy !== 0) { + const nx = Math.round(me.x) + dx, ny = Math.round(me.y) + dy; + if (nx >= 0 && nx < mapData.width && ny >= 0 && ny < mapData.height) { + const row = mapData.objects && mapData.objects[ny]; + if (!row || row[nx] !== 1) { + me.x = nx; me.y = ny; + lastFroggerKey = now; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + const lane = getLane(Math.floor(me.y)); + if (lane && lane.type === 'goal') { froggerScore++; respawnFrogger(); } + else if (checkFroggerCollision()) respawnFrogger(); + } + } + } + } + }); + document.addEventListener('keyup', (e) => { keys[e.code] = false; keys[e.key] = false; }); + + let lastSend = 0; + function tick() { + if (!mapData) { requestAnimationFrame(tick); return; } + if (isStack()) { + stackTickFrame(); + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isJumpSurvive()) { + if (isJumpSurviveMissionPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + jumpSurviveTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isSpaceShooter()) { + spaceShooterTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isBalloonBoss()) { + balloonBossTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isFrogger()) { + if (checkFroggerCollision()) respawnFrogger(); + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isGauntlet()) { + if (isGauntletCrownPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + me.isWalking = meGauntletJumpTicks > 0; + const stepGauntletJumpVis = (vis, tgt) => { + const t = Number(tgt) || 0; + let v = Number(vis) || 0; + const d = t - v; + const k = d < 0 ? 0.68 : 0.4; + return v + d * k; + }; + meGauntletJumpVis = stepGauntletJumpVis(meGauntletJumpVis, meGauntletJumpTicks); + const mp = lerpGauntletEntityPos(me.x, me.y, me.tx, me.ty); + me.x = mp.nx; + me.y = mp.ny; + others.forEach((o) => { + const op = lerpGauntletEntityPos(o.x, o.y, o.tx, o.ty); + o.x = op.nx; + o.y = op.ny; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks || 0; + o.gauntletJumpVis = stepGauntletJumpVis(o.gauntletJumpVis, o.gauntletJumpTicks || 0); + }); + draw(); + requestAnimationFrame(tick); + return; + } + /* บอท preview ขยับที่ o.x/o.y โดยตรง — ห้าม LERP กับ tx/ty เพราะ stepPreviewBots เคยตั้ง tx=ก่อนเดิน ทำให้โดนดึงถอย ~20%/เฟรม กระตุกและแทบไม่ไป */ + others.forEach((o, id) => { + if (previewFillBots && isPreviewBotId(id)) return; + if (o.tx != null) o.x += (o.tx - o.x) * LERP; + if (o.ty != null) o.y += (o.ty - o.y) * LERP; + }); + tickQuizCarryEmbedCountdown(); + tickQuizCarryRoundTimers(); + updateQuizCarryCarryPhaseHud(); + stepPreviewBots(); + if (isChatFocused()) { + me.isWalking = false; + enforceQuizBattleLaneOnMePlay(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isQuizCarryEmbedLobbyBlockingMovement() || isQuizCarryEmbedCountdownBlockingMovement()) { + me.isWalking = false; + playPath = []; + enforceQuizBattleLaneOnMePlay(); + draw(); + requestAnimationFrame(tick); + return; + } + const w = mapData.width || 20, h = mapData.height || 15; + let accX = 0, accY = 0; + let usePath = false; + const keyPressed = keys['ArrowUp'] || keys['KeyW'] || keys['ArrowDown'] || keys['KeyS'] || keys['ArrowLeft'] || keys['KeyA'] || keys['ArrowRight'] || keys['KeyD']; + if (playPath.length > 0 && keyPressed) playPath = []; + if (playPath.length > 0) { + const way = playPath[0]; + const dx = way.x - me.x, dy = way.y - me.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= PATH_ARRIVE_THRESH) { + playPath.shift(); + /* ข้าม waypoint ที่ซ้ำตำแหน่ง — กัน acc=0 ทั้งที่ยังมี path (เดินขึ้น/ลงแล้วแอนิเมชันค้าง) */ + while (playPath.length > 0) { + const w2 = playPath[0]; + const ux = w2.x - me.x, uy = w2.y - me.y; + if (Math.sqrt(ux * ux + uy * uy) > PATH_ARRIVE_THRESH) break; + playPath.shift(); + } + if (playPath.length === 0) { + me.isWalking = false; + me.x = Math.max(0, Math.min(w - 0.01, me.x)); + me.y = Math.max(0, Math.min(h - 0.01, me.y)); + enforceQuizBattleLaneOnMePlay(); + const t = Date.now(); + if (t - lastSend > 80) { lastSend = t; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } + maybeAutoOpenQuizBattle(); + draw(); + requestAnimationFrame(tick); + return; + } + usePath = true; + const next = playPath[0]; + accX = next.x - me.x; + accY = next.y - me.y; + } else { + usePath = true; + accX = dx; + accY = dy; + } + if (usePath && (accX !== 0 || accY !== 0)) { + if (Math.abs(accY) > Math.abs(accX)) me.direction = accY > 0 ? 'down' : 'up'; + else me.direction = accX > 0 ? 'right' : 'left'; + } + } + if (!usePath) { + if (keys['ArrowUp'] || keys['KeyW']) { accY = -1; me.direction = 'up'; } + 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'; } + } + const preWalkX = me.x, preWalkY = me.y; + if (accX !== 0 || accY !== 0) { + const len = Math.sqrt(accX * accX + accY * accY) || 1; + const step = Math.min(MOVE_SPEED, len); + const nx = me.x + (accX / len) * step; + const ny = me.y + (accY / len) * step; + /** Quiz Battle + เส้นทาง: ห้ามเลื่อนแกนเดียว (slide) — ไม่งั้นเดินทแยงหลุดออกนอกเลนได้ */ + const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData); + if (canWalkLikeLobby(nx, ny, me.x, me.y)) { + me.x = nx; + me.y = ny; + } else if (!pathStrict) { + if (canWalkLikeLobby(nx, me.y, me.x, me.y)) { + me.x = nx; + } else if (canWalkLikeLobby(me.x, ny, me.x, me.y)) { + me.y = ny; + } + } + } + me.x = Math.max(0, Math.min(w - 0.01, me.x)); + me.y = Math.max(0, Math.min(h - 0.01, me.y)); + enforceQuizBattleLaneOnMePlay(); + const movedThisTick = Math.abs(me.x - preWalkX) > 1e-5 || Math.abs(me.y - preWalkY) > 1e-5; + me.isWalking = !!(accX !== 0 || accY !== 0) || playPath.length > 0 || movedThisTick; + const now = Date.now(); + if (now - lastSend > 80) { lastSend = now; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } + maybeAutoOpenQuizBattle(); + draw(); + requestAnimationFrame(tick); + } + + const chatForm = document.getElementById('chat-form'); + if (chatForm) { + chatForm.addEventListener('submit', (e) => { + e.preventDefault(); + const input = document.getElementById('chat-input'); + const text = (input && input.value || '').trim(); + if (text) { socket.emit('chat', text); if (input) input.value = ''; } + }); + } + document.getElementById('btn-leave').addEventListener('click', () => { + socket.emit('leave-space'); + const mid = params.get('map'); + if (previewMode && mid) { + var edQ = '?id=' + encodeURIComponent(mid) + (editorEmbedReturn ? '&embed=1' : ''); + window.location.replace(BASE + '/editor.html' + edQ); + } else { + window.location.replace(BASE + '/lobby.html'); + } + }); + (function wireQuizBattleMcqModal() { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + if (!ov) return; + ov.addEventListener('click', (e) => { + const t = e.target; + if (!t) return; + if (t.classList && t.classList.contains('quiz-battle-choice')) { + const idx = parseInt(t.getAttribute('data-idx'), 10); + if (Number.isFinite(idx) && idx >= 0 && idx <= 2) trySubmitQuizBattleChoice(idx); + return; + } + if (t.id === 'quiz-battle-mcq-close') { + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + return; + } + if (t.classList && t.classList.contains('quiz-battle-mcq-backdrop')) { + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + } + }); + })(); + (function wireQuizCarryGrabButton() { + const btn = document.getElementById('quiz-carry-grab-btn'); + if (!btn) return; + btn.addEventListener('click', (ev) => { + if (!isQuizCarry() || !mapData || isChatFocused()) return; + if (!btn.classList.contains('quiz-carry-grab-btn--active')) return; + ev.preventDefault(); + if (myId == null) return; + tryQuizCarryInteractionForPlayer(myId, me.x, me.y, { fromKey: true }); + }); + })(); + + (function wireGauntletCrownHowtoPrimary() { + const btn = document.getElementById('btn-gch-ready'); + if (!btn) return; + btn.addEventListener('click', () => { + if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'howto') return; + const humans = quizCarryPregameHumanIds(); + if (!(humans.length === 1 || isMePlayHost())) return; + beginJumpSurviveMissionCountdownThenRun(); + return; + } + if (!isGauntletCrownHeistMapPlay()) return; + if (gauntletCrownPregamePhase !== 'howto') return; + if (myId == null || !isMePlayHost()) return; + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReady) { + if (humans.length === 1 && isMePlayHost()) { + beginGauntletCrownCountdownThenRun(); + return; + } + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { + if (!r || !r.ok) { /* ignore */ } + }); + return; + } + const sid = String(myId); + const next = !gauntletCrownLobbyReadyMap[sid]; + gauntletCrownLobbyReadyMap[sid] = next; + updateGauntletCrownHowtoHud(); + socket.emit('gauntlet-crown-lobby-ready', { ready: next }); + }); + })(); + + (function wireQuizCarryPregameOverlay() { + const primary = document.getElementById('quiz-carry-pregame-primary'); + if (!primary) return; + primary.addEventListener('click', () => { + if (!quizCarryPregameActive || myId == null) return; + if (!isMePlayHost()) return; + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]); + if (humansReady) { + /* ผู้เล่นจริงคนเดียว + พรีวิว: ไม่ต้องรอ ACK เซิร์ฟ (ห้องที่ map บนเซิร์ฟยังไม่ใช่ quiz_carry จะเคยล้มเหลว) */ + if (humans.length === 1 && isMePlayHost()) { + endQuizCarryEmbedPregameAndStart(); + return; + } + socket.emit('quiz-carry-lobby-start', {}, (r) => { + if (!r || !r.ok) showQuizCarryToast((r && r.error) || 'START ไม่ได้', false); + }); + return; + } + const sid = String(myId); + const next = !quizCarryLobbyReadyMap[sid]; + quizCarryLobbyReadyMap[sid] = next; + updateQuizCarryPregameHud(); + socket.emit('quiz-carry-lobby-ready', { ready: next }); + }); + })(); + window.addEventListener('beforeunload', () => socket.emit('leave-space')); + window.addEventListener('resize', () => { + if (!mapData || !canvas) return; + resizeCanvas(); + draw(); + }); +})(); diff --git a/www/html/Game/server.js b/www/html/Game/server.js index 618128b..eed3591 100644 --- a/www/html/Game/server.js +++ b/www/html/Game/server.js @@ -1031,13 +1031,8 @@ function sanitizeCarryEmbedCountdownTheme(raw) { /** หมวด Quiz Battle (อ้างอิงธีมหน้า Quiz-Battle) — id ต้องตรงกับแอดมิน */ const QUIZ_BATTLE_CATEGORY_IDS = new Set([ - 'cybercrime', - 'cyber_law', - 'privacy', - 'digital_security', - 'social_media', - 'ethics', - 'ai_data', + 'topic1', 'topic2', 'topic3', 'topic4', 'topic5', + 'topic6', 'topic7', 'topic8', 'topic9', 'topic10', ]); /** คำถาม 3 ตัวเลือก A/B/C แยกหมวด — สำหรับโหมด Quiz Battle / โดม */ @@ -1048,7 +1043,7 @@ function sanitizeBattleQuizMcq(arr) { if (!q || typeof q !== 'object') continue; let categoryId = String(q.categoryId || '').trim(); if (!categoryId || !QUIZ_BATTLE_CATEGORY_IDS.has(categoryId)) { - categoryId = 'cybercrime'; + categoryId = 'topic1'; } const text = String(q.text || '').trim().slice(0, 500); if (!text) continue; diff --git a/www/html/Game/server.js.bak-20260607-114128 b/www/html/Game/server.js.bak-20260607-114128 new file mode 100644 index 0000000..618128b --- /dev/null +++ b/www/html/Game/server.js.bak-20260607-114128 @@ -0,0 +1,7701 @@ +const http = require('http'); +const path = require('path'); +const fs = require('fs'); +const crypto = require('crypto'); +const { Server } = require('socket.io'); + +const PORT = parseInt(process.env.PORT, 10) || 3001; +/** Windows dev: bind 127.0.0.1 (0.0.0.0 มัก EACCES ถ้าพอร์ตอยู่ใน excluded range ของ Hyper-V) */ +const HOST = process.env.HOST || process.env.GAME_HOST || '0.0.0.0'; +const BASE_PATH = '/Game'; +const AI_SETTINGS_PATH = path.join(__dirname, 'data', 'ai-settings.json'); +const ADMIN_PASSWORD = process.env.GAME_AI_ADMIN_PASSWORD || 'game-ai-admin'; +const adminTokens = new Set(); +/** Lobby หลังคดี — ไม่ลบออกจาก spaces ตอน prune รายการห้องว่าง */ +const POST_CASE_LOBBY_SPACE_ID = 'mn8nx46h'; +/** หลัง host เลือกผู้ต้องสงสัยแล้วกด «เริ่มสืบสวน» — ฉากควิซเดิม (fallback) */ +const SUSPECT_INVESTIGATION_QUIZ_MAP_ID = 'mng8a80o'; +/** 7 Minigame จาก Admin — สุ่ม 3 แบบไม่ซ้ำผูกการ์ดผู้ต้องสงสัย (ล็อกจนกว่าสร้างห้องใหม่) */ +/** ตรงแท็บ Admin Minigame 1–7 (ไม่รวม Quiz Battle / กบ frogger เก่า) */ +const DETECTIVE_MINIGAME_POOL = [ + { key: 'quiz', mapId: 'mng8a80o', labelTh: 'Minigame-1 จริงหรือไม่' }, + { key: 'gauntlet', mapId: 'mno9kb07', labelTh: 'Minigame-2 วิ่งหลบสิ่งกีดขวาง' }, + { key: 'stack', mapId: 'mnn93hpi', labelTh: 'Minigame-3 Tower block' }, + { key: 'quiz_carry', mapId: 'mnorwqx1', labelTh: 'Minigame-4 คำศัพท์ในกระบวนการยุติธรรม' }, + { key: 'jump_survive', mapId: 'mnptfts2', labelTh: 'Minigame-5 กระโดดขึ้นแท่น' }, + { key: 'space_shooter', mapId: 'mnpz6rkp', labelTh: 'Minigame-6 ยิงอุกาบาต Violent Crime' }, + { key: 'balloon_boss', mapId: 'mnq1eml7', labelTh: 'Minigame-7 ยิง Mega Virus' }, +]; +const LOBBY_SLOT_TOTAL = 6; +const LOBBY_THEME_COUNT = 8; +const LOBBY_SKIN_COUNT = 3; + +/** ธีมสีเสื้อที่ถูกใช้แล้วในห้อง (ผู้เล่น + bot) */ +function getTakenLobbyThemeIndices(space, excludePeerId) { + const taken = new Set(); + if (!space) return taken; + space.peers.forEach((p, id) => { + if (excludePeerId && id === excludePeerId) return; + const ti = parseInt(p.lobbyColorThemeIndex, 10); + if (ti >= 1 && ti <= LOBBY_THEME_COUNT) taken.add(ti); + }); + const bots = space.lobbyBotThemeBySlot; + if (bots && typeof bots === 'object') { + Object.keys(bots).forEach((k) => { + const ti = parseInt(bots[k] && bots[k].themeIndex, 10); + if (ti >= 1 && ti <= LOBBY_THEME_COUNT) taken.add(ti); + }); + } + return taken; +} + +function pickFreeLobbyThemeIndex(space, excludePeerId) { + const taken = getTakenLobbyThemeIndices(space, excludePeerId); + for (let i = 1; i <= LOBBY_THEME_COUNT; i++) { + if (!taken.has(i)) return i; + } + const n = (space && space.peers ? space.peers.size : 0) + effectiveBotSlotCount(space); + return ((n - 1) % LOBBY_THEME_COUNT) + 1; +} + +function pickLobbySkinIndexForSlot(slotIndex) { + const s = Number(slotIndex); + if (!Number.isFinite(s)) return 1; + return (Math.abs(Math.floor(s)) % LOBBY_SKIN_COUNT) + 1; +} + +/** จัดสี bot ตามลำดับช่อง — ไม่ซ้ำกับผู้เล่น */ +function syncLobbyBotThemeSlots(space) { + if (!space) return []; + const botCount = effectiveBotSlotCount(space); + if (!space.lobbyBotThemeBySlot || typeof space.lobbyBotThemeBySlot !== 'object') { + space.lobbyBotThemeBySlot = {}; + } + const taken = getTakenLobbyThemeIndices(space); + const out = []; + for (let i = 0; i < botCount; i++) { + let slot = space.lobbyBotThemeBySlot[i]; + let themeIndex = parseInt(slot && slot.themeIndex, 10); + if (!(themeIndex >= 1 && themeIndex <= LOBBY_THEME_COUNT) || taken.has(themeIndex)) { + themeIndex = pickFreeLobbyThemeIndex(space); + } + taken.add(themeIndex); + const skinToneIndex = (() => { + const si = parseInt(slot && slot.skinToneIndex, 10); + return (si >= 1 && si <= LOBBY_SKIN_COUNT) ? si : pickLobbySkinIndexForSlot(i); + })(); + space.lobbyBotThemeBySlot[i] = { themeIndex, skinToneIndex }; + out.push({ themeIndex, skinToneIndex }); + } + Object.keys(space.lobbyBotThemeBySlot).forEach((k) => { + if (parseInt(k, 10) >= botCount) delete space.lobbyBotThemeBySlot[k]; + }); + return out; +} + +function emitLobbyTintSync(spaceId, space) { + const peersTint = []; + space.peers.forEach((p, id) => { + peersTint.push({ + id, + lobbyColorThemeIndex: p.lobbyColorThemeIndex || null, + lobbySkinToneIndex: p.lobbySkinToneIndex || null, + }); + }); + io.to(spaceId).emit('lobby-tint-sync', { + peers: peersTint, + lobbyBotThemes: syncLobbyBotThemeSlots(space), + }); +} + +/** จำนวน Bot จาก Create Room — ถ้าไม่เคยบันทึก ใช้ 6 − maxPlayers (เช่น 3 คน → 3 Bot) */ +function effectiveBotSlotCount(space) { + let n = parseInt(space && space.botSlotCount, 10); + if (isNaN(n) || n < 0) n = 0; + const maxP = space && space.maxPlayers ? parseInt(space.maxPlayers, 10) : 0; + if (n === 0 && maxP > 0 && maxP < LOBBY_SLOT_TOTAL) { + n = LOBBY_SLOT_TOTAL - maxP; + if (space) space.botSlotCount = n; + } + return Math.min(LOBBY_SLOT_TOTAL, Math.max(0, n)); +} +/** Stack Tower ภารกิจ — lobby READY / START เดียวกับ quiz mission (mng8a80o) */ +const STACK_TOWER_MISSION_MAP_ID = 'mnn93hpi'; +/** Jump Survival ภารกิจ Jumper — lobby READY / START เดียวกับ Stack Tower */ +const JUMP_SURVIVE_MISSION_MAP_ID = 'mnptfts2'; +/** ห้องสาธารณะที่สร้างแล้วไม่มีคนเข้าเกินนี้ → ลบออกจาก memory (Join Room ไม่โชว์ห้องผี) */ +const SPACE_EMPTY_TTL_MS = 45000; +// Chat completion models from https://developers.openai.com/api/docs/models (Feb 2025) +const AI_MODELS = [ + 'gpt-5.2', 'gpt-5.2-pro', 'gpt-5.1', 'gpt-5', 'gpt-5-pro', 'gpt-5-mini', 'gpt-5-nano', + 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', + 'o3', 'o3-mini', 'o3-pro', 'o4-mini', 'o3-deep-research', 'o4-mini-deep-research', + 'o1', 'o1-mini', 'o1-pro', + 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', + 'gpt-3.5-turbo', +]; + +function loadAiSettings() { + try { + if (fs.existsSync(AI_SETTINGS_PATH)) { + const raw = fs.readFileSync(AI_SETTINGS_PATH, 'utf8'); + return JSON.parse(raw); + } + } catch (e) { console.error('loadAiSettings', e.message); } + return { openai_api_key: '', model: 'gpt-4o-mini', intent: '', rag_enabled: false, rag_context: '' }; +} + +function saveAiSettings(settings) { + try { + const dir = path.dirname(AI_SETTINGS_PATH); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(AI_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8'); + } catch (e) { console.error('saveAiSettings', e.message); } +} + +function getAdminToken(cookieHeader) { + if (!cookieHeader) return null; + const m = cookieHeader.match(/game_ai_admin=([^;\s]+)/); + return m && adminTokens.has(m[1]) ? m[1] : null; +} + +const MAPS_DIR = path.join(__dirname, 'data', 'maps'); +const CHARACTERS_DIR = path.join(__dirname, 'data', 'characters'); +/** ชื่อเลเยอร์ตรงกับ character.html / อัปโหลด (ลำดับวาดใน client) */ +const CHAR_LAYER_MANIFEST_KEYS = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face']; +const CHAR_MANIFEST_DIRS = ['up', 'down', 'left', 'right']; + +/** รายการไฟล์เลเยอร์ต่อทิศ/เฟรม — ให้หน้า character แก้ไขโหลด preview ได้ */ +function buildCharacterLayerManifest(id, files) { + if (!id || typeof id !== 'string') return { ok: false, error: 'bad id', byDir: {}, byDirIdle: {} }; + const esc = id.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); + const ext = /\.(png|jpg|jpeg|gif|webp)$/i; + const layerAlt = CHAR_LAYER_MANIFEST_KEYS.join('|'); + const byDir = {}; + for (const dir of CHAR_MANIFEST_DIRS) { + const multiRe = new RegExp('^' + esc + '_' + dir + '_(\\d+)_layer_(' + layerAlt + ')\\.(png|jpg|jpeg|gif|webp)$', 'i'); + const singleRe = new RegExp('^' + esc + '_' + dir + '_layer_(' + layerAlt + ')\\.(png|jpg|jpeg|gif|webp)$', 'i'); + const multiHits = files.filter((f) => multiRe.test(f)); + if (multiHits.length) { + let maxFi = -1; + const perFrame = new Map(); + multiHits.forEach((f) => { + const m = f.match(multiRe); + if (!m) return; + const fi = parseInt(m[1], 10); + if (!Number.isFinite(fi) || fi < 0 || fi > 200) return; + const layer = m[2]; + if (fi > maxFi) maxFi = fi; + if (!perFrame.has(fi)) perFrame.set(fi, {}); + perFrame.get(fi)[layer] = f; + }); + const frames = []; + for (let i = 0; i <= maxFi; i++) frames.push(perFrame.get(i) || {}); + byDir[dir] = { multi: true, frames }; + } else { + const frame0 = {}; + files.forEach((f) => { + if (!f.startsWith(id + '_' + dir + '_layer_') || !ext.test(f)) return; + const m = f.match(singleRe); + if (m) frame0[m[1]] = f; + }); + if (Object.keys(frame0).length) byDir[dir] = { multi: false, frames: [frame0] }; + } + } + const byDirIdle = {}; + for (const dir of CHAR_MANIFEST_DIRS) { + const multiIdleRe = new RegExp('^' + esc + '_' + dir + '_idle_(\\d+)_layer_(' + layerAlt + ')\\.(png|jpg|jpeg|gif|webp)$', 'i'); + const singleIdleRe = new RegExp('^' + esc + '_' + dir + '_idle_layer_(' + layerAlt + ')\\.(png|jpg|jpeg|gif|webp)$', 'i'); + const multiIdleHits = files.filter((f) => multiIdleRe.test(f)); + if (multiIdleHits.length) { + let maxFi = -1; + const perFrame = new Map(); + multiIdleHits.forEach((f) => { + const m = f.match(multiIdleRe); + if (!m) return; + const fi = parseInt(m[1], 10); + if (!Number.isFinite(fi) || fi < 0 || fi > 200) return; + const layer = m[2]; + if (fi > maxFi) maxFi = fi; + if (!perFrame.has(fi)) perFrame.set(fi, {}); + perFrame.get(fi)[layer] = f; + }); + const frames = []; + for (let i = 0; i <= maxFi; i++) frames.push(perFrame.get(i) || {}); + byDirIdle[dir] = { multi: true, frames }; + } else { + const frame0 = {}; + files.forEach((f) => { + if (!f.startsWith(id + '_' + dir + '_idle_layer_') || !ext.test(f)) return; + const m = f.match(singleIdleRe); + if (m) frame0[m[1]] = f; + }); + if (Object.keys(frame0).length) byDirIdle[dir] = { multi: false, frames: [frame0] }; + } + } + return { ok: true, id, byDir, byDirIdle }; +} + +/** ต้องอยู่ใต้ public/img/ เพื่อให้ nginx (alias /Game/ → Game/public) เสิร์ฟ GET /Game/img/gauntlet-assets/* ได้ — เดิมเก็บใน data/ จะถูกคัดลอกมาครั้งแรก */ +const GAUNTLET_ASSETS_DIR = path.join(__dirname, 'public', 'img', 'gauntlet-assets'); +const GAUNTLET_ASSETS_DIR_LEGACY = path.join(__dirname, 'data', 'gauntlet-assets'); +const GAUNTLET_ASSETS_META_PATH = path.join(__dirname, 'data', 'gauntlet-assets-meta.json'); +/** ต้องอยู่ใต้ public/ เพื่อให้ nginx (alias /Game/ → Game/public) เสิร์ฟรูปได้ — อย่าใช้ data/ */ +const QUIZ_CARRY_PLAQUE_ASSETS_DIR = path.join(__dirname, 'public', 'img', 'quiz-carry-plaque-assets'); +const QUIZ_SETTINGS_PATH = path.join(__dirname, 'data', 'quiz-settings.json'); +/** ถ้า Admin (PHP) merge ลงไฟล์ใต้ docroot แต่ Node รันจากโฟลเดอร์อื่น ให้ตั้ง absolute path ที่นี่ — carry* / ธีมแผงจะอ่านทับจาก mirror ทุกครั้งที่ loadQuizSettings */ +const QUIZ_SETTINGS_MIRROR_PATH = process.env.GAME_QUIZ_SETTINGS_MIRROR_PATH + ? path.resolve(String(process.env.GAME_QUIZ_SETTINGS_MIRROR_PATH).trim()) + : ''; +const GAME_TIMING_PATH = path.join(__dirname, 'data', 'game-timing.json'); +/** ช่องตัวเลือกบนกริด quiz_carry เก็บเลข 1..N (ไม่ใช่แค่ 0/1 เหมือน hub) */ +const QUIZ_CARRY_MAX_OPTION_SLOTS = 16; +const maps = new Map(); +const defaultMap = () => ({ + width: 20, height: 15, tileSize: 32, gameType: 'zep', + characterCells: 1, characterCellsW: 1, characterCellsH: 1, + /** กล่องตรวจทับโซน «ห้ามซ้อน» (ชิดเท้า+กลางแนวนอน เหมือนชนกำแพง) · 0 = เต็มความกว้าง/สูงตัว */ + blockPlayerSeparationW: 0, + blockPlayerSeparationH: 0, + ground: [], objects: [], blockPlayer: [], interactive: [], startGameArea: [], spawnArea: [], spawn: { x: 1, y: 1 }, + gridImageLibrary: [], + gridImageSprites: [], + gridImageCells: [], + quizTrueArea: [], quizFalseArea: [], quizQuestionArea: [], quizQuestions: [], + quizCarryHubArea: [], quizCarryOptionArea: [], + /** quiz_carry embed: โซนวาง UI นับ 3-2-1 (0/1) — ใช้เมื่อ carryEmbedCountdownAnchor === 'grid' */ + carryEmbedCountdownArea: [], + /** Quiz Battle (MCQ โดม) — ช่องที่ 1 = จุดกด E เปิดคำถามจาก battleQuizMcq */ + quizBattleDomeArea: [], + /** Quiz Battle — ถ้าวาดอย่างน้อย 1 ช่อง = จำกัดเดินเฉพาะเส้นทาง (เหมือนแผนที่อ้างอิง) · ไม่วาด = เดินอิสระเหมือนเดิม */ + quizBattlePathArea: [], + stackReleaseArea: [], stackLandArea: [], + /** กระโดดให้รอด — แพลตฟอร์มในระบบ tile (ร่างสำหรับ side-view ภายหลัง) */ + jumpSurvivePlatforms: [], + jumpSurvivePlatformArea: [], + /** กริดช่องละ 0 หรือ 1–3 = รูปแพลตฟอร์มจาก jumpSurvivePlatformTiles[index-1] */ + jumpSurvivePlatformVariantArea: [], + jumpSurviveHazardArea: [], + jumpSurviveRisePxPerSec: 42, + jumpSurviveGravity: 3200, + /** 0 = ให้ไคลเอนต์ใช้ค่าเริ่มต้นกระโดดสูง; ตั้งเป็นพิกเซล/วิ (เช่น 980) เพื่อกำหนดเอง */ + jumpSurviveJumpImpulse: 0, + jumpSurviveMovePxPerSec: 200, +}); + +/* ===== Special Quiz Sets (Level 1-5 × Case 1-3 = 15 ชุด) — คำถามรับการ์ดพิเศษ ใช้ได้หลายเกม ===== */ +const SPECIAL_QUIZ_LEVELS = [1, 2, 3, 4, 5]; +const SPECIAL_QUIZ_CASES = [1, 2, 3]; +const SPECIAL_QUIZ_CARD_RARITIES = ['common', 'rare', 'legendary']; +const SPECIAL_CARD_ASSET_BASE = '/Game/img/special-cards/'; + +function specialCardTxtFilename(id) { + return id === 7 ? 'card--7txt.png' : `card-${id}-txt.png`; +} + +function specialCardImageUrl(id) { + return `${SPECIAL_CARD_ASSET_BASE}card-${id}.png`; +} + +function specialCardTxtImageUrl(id) { + return `${SPECIAL_CARD_ASSET_BASE}${specialCardTxtFilename(id)}`; +} + +function specialQuizSetKey(level, caseId) { + return 'L' + level + '_C' + caseId; +} + +/** คำถาม 1 ข้อ — รองรับทั้งจริง/เท็จ (tf) และตัวเลือก A/B/C (mcq) เพื่อขยายภายหลัง */ +function sanitizeSpecialQuizQuestion(q) { + if (!q || typeof q !== 'object') return null; + const text = String(q.text == null ? '' : q.text).trim(); + if (!text) return null; + const type = q.type === 'mcq' ? 'mcq' : 'tf'; + if (type === 'mcq') { + /* รักษาตำแหน่งตัวเลือก (A=0,B=1,...) — ตัดเฉพาะช่องว่างท้ายสุด ไม่ขยับ index คำตอบ */ + const choices = (Array.isArray(q.choices) ? q.choices : []) + .map((c) => String(c == null ? '' : c).trim()) + .slice(0, 8); + while (choices.length && choices[choices.length - 1] === '') choices.pop(); + if (choices.filter((c) => c).length < 2) return null; + let ci = Number(q.correctIndex); + if (!Number.isInteger(ci) || ci < 0 || ci >= choices.length || choices[ci] === '') { + ci = choices.findIndex((c) => c); + if (ci < 0) ci = 0; + } + return { type: 'mcq', text: text.slice(0, 500), choices, correctIndex: ci }; + } + return { type: 'tf', text: text.slice(0, 500), answerTrue: !!q.answerTrue }; +} + +function sanitizeSpecialCard(c) { + const src = (c && typeof c === 'object') ? c : {}; + const rarity = SPECIAL_QUIZ_CARD_RARITIES.indexOf(src.rarity) >= 0 ? src.rarity : 'rare'; + let cardId = Number(src.cardId); + if (!(cardId >= 1 && cardId <= 7)) cardId = null; + let imageUrl = String(src.imageUrl == null ? '' : src.imageUrl).trim().slice(0, 600); + let txtImageUrl = String(src.txtImageUrl == null ? '' : src.txtImageUrl).trim().slice(0, 600); + if (cardId) { + if (!imageUrl) imageUrl = specialCardImageUrl(cardId); + if (!txtImageUrl) txtImageUrl = specialCardTxtImageUrl(cardId); + } else if (imageUrl) { + const m = imageUrl.match(/\/card-(\d)\.png/i); + if (m) { + cardId = Number(m[1]); + imageUrl = specialCardImageUrl(cardId); + if (!txtImageUrl) txtImageUrl = specialCardTxtImageUrl(cardId); + } + } + return { + cardId: cardId || null, + th: String(src.th == null ? '' : src.th).trim().slice(0, 160), + en: String(src.en == null ? '' : src.en).trim().slice(0, 160), + rarity, + imageUrl, + txtImageUrl, + }; +} + +/** คืน object 15 ชุดเสมอ (ชุดที่ยังไม่ตั้ง = ว่าง) */ +function sanitizeSpecialQuizSets(obj) { + const out = {}; + SPECIAL_QUIZ_LEVELS.forEach((L) => SPECIAL_QUIZ_CASES.forEach((C) => { + const key = specialQuizSetKey(L, C); + const src = (obj && typeof obj === 'object' && obj[key] && typeof obj[key] === 'object') ? obj[key] : {}; + const questions = (Array.isArray(src.questions) ? src.questions : []) + .map(sanitizeSpecialQuizQuestion) + .filter(Boolean) + .slice(0, 50); + out[key] = { questions, specialCard: sanitizeSpecialCard(src.specialCard) }; + })); + return out; +} + +/** เลือกชุดคำถามตาม level+case (fallback ชุด L1_C1) */ +function getSpecialQuizSet(settings, level, caseId) { + const sets = (settings && settings.specialQuizSets) || sanitizeSpecialQuizSets({}); + const L = SPECIAL_QUIZ_LEVELS.indexOf(Number(level)) >= 0 ? Number(level) : 1; + /* caseId เป็นเลขคดีรวม 1-15 (5 ระดับ × 3 คดี) → แปลงเป็นคดีสัมพัทธ์ในระดับ 1-3 */ + const rel = ((Math.max(1, Number(caseId) || 1) - 1) % 3) + 1; + const C = SPECIAL_QUIZ_CASES.indexOf(rel) >= 0 ? rel : 1; + return sets[specialQuizSetKey(L, C)] || sets[specialQuizSetKey(1, 1)] || { questions: [], specialCard: sanitizeSpecialCard({}) }; +} + +/* ===== Special Quiz ในเกม — ไอคอนสุ่มในฉาก + เซสชันถาม-ตอบ multiplayer (ทุกคนต้องตอบถูก) ===== */ +/** เฉพาะ 4 มินิเกมนี้ (ตอนสืบสวน) ที่ไอคอนพิเศษจะสุ่มโผล่ */ +const SPECIAL_QUIZ_ELIGIBLE_MAP_IDS = ['mng8a80o', 'mnorwqx1', 'mnptfts2', 'mnpz6rkp']; +const SPECIAL_QUIZ_ICON_TYPES = ['lawyer', 'police']; +/** โอกาสโผล่ = 1 ใน 3 ของการเล่นแต่ละรอบ */ +const SPECIAL_QUIZ_SPAWN_CHANCE = 1 / 3; +/** เวลาให้ตอบต่อข้อ (มิลลิวิ) — ค่าคงที่เริ่มต้น (ย้ายไป Admin ได้ภายหลัง) */ +const SPECIAL_QUIZ_ANSWER_MS = 20000; +/** หน่วงโชว์เฉลยก่อนข้อต่อไป */ +const SPECIAL_QUIZ_REVEAL_MS = 2600; + +/** แปลงคำถาม 1 ข้อให้อยู่ในรูป {text, choices[], correctIndex} เสมอ (tf -> จริง/เท็จ) */ +function specialQuizNormalizeQuestion(q) { + if (!q || typeof q !== 'object') return null; + const text = String(q.text == null ? '' : q.text).trim(); + if (!text) return null; + if (q.type === 'mcq') { + const choices = (Array.isArray(q.choices) ? q.choices : []).map((c) => String(c == null ? '' : c)).filter((c) => c.trim() !== ''); + if (choices.length < 2) return null; + let ci = Number(q.correctIndex); + if (!Number.isInteger(ci) || ci < 0 || ci >= choices.length) ci = 0; + return { text, choices, correctIndex: ci }; + } + return { text, choices: ['จริง', 'เท็จ'], correctIndex: q.answerTrue ? 0 : 1 }; +} + +function specialQuizShuffle(arr) { + const a = arr.slice(); + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; +} + +function specialQuizTileWalkable(md, x, y) { + const w = md.width || 20; + const h = md.height || 15; + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + return !!(row && row[x] !== 1); +} + +/** หลีกเลี่ยงโซน gameplay (คำตอบจริง/ผิด, hub, ตัวเลือกหยิบมาวาง) เพื่อไม่ชนกลไกเกม */ +function specialQuizTileInGameZone(md, x, y) { + if (quizCellOn(md.quizTrueArea || [], x, y)) return true; + if (quizCellOn(md.quizFalseArea || [], x, y)) return true; + if (quizCellOn(md.quizCarryHubArea || [], x, y)) return true; + const oa = md.quizCarryOptionArea; + if (oa && oa[y] && oa[y][x]) return true; + return false; +} + +/** หา tile ที่เดินถึงได้ (BFS) และไม่อยู่ในโซน gameplay — คืนพิกัดกึ่งกลาง tile */ +function pickSpecialQuizSpawnTile(md) { + const w = md.width || 20; + const h = md.height || 15; + /* seed BFS จากพื้นที่ที่ผู้เล่นเกิดจริง (spawnArea) เพื่อรับประกันว่าไอคอนเดินไปถึงได้ + fallback: md.spawn (ถ้าเดินได้) แล้วค่อยสแกนหา walkable tile แรก */ + let sx = -1; + let sy = -1; + const spawnGrid = md.spawnArea; + if (Array.isArray(spawnGrid)) { + for (let y = 0; y < h && sx < 0; y++) { + const row = spawnGrid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && specialQuizTileWalkable(md, x, y)) { sx = x; sy = y; break; } + } + } + } + if (sx < 0) { + const sp = md.spawn || { x: 1, y: 1 }; + const fx = Math.floor(Number(sp.x)); + const fy = Math.floor(Number(sp.y)); + if (specialQuizTileWalkable(md, fx, fy)) { sx = fx; sy = fy; } + } + if (sx < 0) { + for (let y = 0; y < h && sx < 0; y++) { + for (let x = 0; x < w; x++) { + if (specialQuizTileWalkable(md, x, y)) { sx = x; sy = y; break; } + } + } + } + if (sx < 0) return null; + const q = [[sx, sy]]; + const seen = new Set([sx + ',' + sy]); + const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; + const cands = []; + while (q.length) { + const [cx, cy] = q.shift(); + for (let di = 0; di < dirs.length; di++) { + const nx = cx + dirs[di][0]; + const ny = cy + dirs[di][1]; + const k = nx + ',' + ny; + if (seen.has(k)) continue; + if (!specialQuizTileWalkable(md, nx, ny)) continue; + seen.add(k); + q.push([nx, ny]); + if (!specialQuizTileInGameZone(md, nx, ny)) { + const dist = Math.abs(nx - sx) + Math.abs(ny - sy); + if (dist >= 2) cands.push({ x: nx, y: ny }); + } + } + } + if (!cands.length) return null; + const pick = cands[Math.floor(Math.random() * cands.length)]; + return { x: pick.x + 0.5, y: pick.y + 0.5 }; +} + +function clearSpecialQuizTimers(space) { + if (Array.isArray(space.specialQuizTimers)) { + space.specialQuizTimers.forEach((t) => clearTimeout(t)); + } + space.specialQuizTimers = []; + if (space.specialQuizContinueTimer) { + clearTimeout(space.specialQuizContinueTimer); + space.specialQuizContinueTimer = null; + } + space.specialQuizAwaitContinue = false; +} + +/** payload ไอคอนสำหรับ client (null = ไม่มี/ถูกทริกแล้ว) */ +function specialQuizIconPayload(space) { + const sq = space && space.specialQuiz; + if (!sq || sq.triggered || sq.done) return null; + return { iconType: sq.iconType, x: sq.x, y: sq.y }; +} + +/** เริ่มมินิเกมสืบสวน: สุ่ม 1/3 ว่ารอบนี้จะมีไอคอนพิเศษไหม + เลือกตำแหน่ง */ +function maybeSpawnSpecialQuizForRun(space) { + clearSpecialQuizTimers(space); + space.specialQuiz = null; + space.specialCardAwardedThisRun = null; + space.timeDilationPendingSec = 0; + /* หมายเหตุ: ไม่ล้าง pendingSpecialCard ที่นี่ — การ์ดที่ใช้ใน trial/lobby ต้องคงอยู่จนกว่าจะถูกใช้ + หรือถูกแทนด้วยการ์ดใบใหม่ (endSpecialQuizSession เขียนทับเมื่อได้ใบใหม่) */ + if (!space || !space.detectiveMinigameActive) return; + const mapId = String(space.mapId || '').trim(); + if (SPECIAL_QUIZ_ELIGIBLE_MAP_IDS.indexOf(mapId) < 0) return; + const level = Number(space.detectiveLobbyLevel) || 1; + const caseId = Number(space.detectiveLobbyCaseId) || 1; + const settings = loadQuizSettings(); + const set = getSpecialQuizSet(settings, level, caseId); + const questions = (set && Array.isArray(set.questions) ? set.questions : []) + .map(specialQuizNormalizeQuestion) + .filter(Boolean); + const card = set && set.specialCard; + const hasCard = !!(card && (card.cardId || card.imageUrl)); + if (!questions.length || !hasCard) { console.log('[special-quiz] skip:no-questions-or-card', mapId, 'L' + level + '_C' + caseId, 'q=' + questions.length, 'card=' + hasCard); return; } + const forced = !!space.forceSpecialQuiz; + const roll = Math.random(); + if (!forced && roll > SPECIAL_QUIZ_SPAWN_CHANCE) { console.log('[special-quiz] skip:roll', mapId, roll.toFixed(3), '>', SPECIAL_QUIZ_SPAWN_CHANCE.toFixed(3)); return; } + if (forced) console.log('[special-quiz] FORCE-spawn (debug)', mapId); + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md) { console.log('[special-quiz] skip:no-map', mapId); return; } + const tile = pickSpecialQuizSpawnTile(md); + if (!tile) { console.log('[special-quiz] skip:no-tile', mapId); return; } + const iconType = SPECIAL_QUIZ_ICON_TYPES[Math.floor(Math.random() * SPECIAL_QUIZ_ICON_TYPES.length)]; + console.log('[special-quiz] SPAWN', mapId, 'L' + level + '_C' + caseId, iconType, 'tile', tile.x, tile.y); + space.specialQuiz = { + iconType, + x: tile.x, + y: tile.y, + level, + caseId, + card, + triggered: false, + done: false, + session: null, + }; +} + +function emitSpecialQuizIcon(sid, space) { + const payload = specialQuizIconPayload(space); + if (payload) io.to(sid).emit('special-quiz-icon', payload); +} + +/** roster ของเซสชัน: ผู้เล่นจริง + บอทเติมช่อง (บอทตอบถูกเสมอ) — โชว์ในแผง "ใครตอบอะไร" */ +function buildSpecialQuizRoster(space) { + const roster = []; + space.peers.forEach((p, id) => { + roster.push({ id, nickname: (p && p.nickname) ? p.nickname : String(id).slice(0, 6), isBot: false }); + }); + const botN = effectiveBotSlotCount(space); + for (let i = 0; i < botN; i++) { + roster.push({ id: 'sqbot-' + i, nickname: 'บอท ' + (i + 1), isBot: true }); + } + return roster; +} + +function specialQuizRosterPayload(sess) { + return (sess && Array.isArray(sess.roster) ? sess.roster : []) + .map((m) => ({ id: m.id, nickname: m.nickname, isBot: !!m.isBot })); +} + +/** id ที่ต้องตอบในข้อนี้ = ผู้เล่นจริงที่ยังอยู่ + บอททุกตัวใน roster (ใช้ตัดสิน "ทุกคนตอบถูก") */ +function specialQuizExpectedIds(space, sess) { + const realAlive = new Set(space.peers.keys()); + const out = []; + (sess && Array.isArray(sess.roster) ? sess.roster : []).forEach((m) => { + if (m.isBot) out.push(m.id); + else if (realAlive.has(m.id)) out.push(m.id); + }); + return out; +} + +/** ตอบครบทุกคน (จริง+บอท) แล้วตัดสินทันที ไม่ต้องรอหมดเวลา */ +function maybeResolveSpecialQuizAllAnswered(sid, space) { + const sess = space.specialQuiz && space.specialQuiz.session; + if (!sess || sess.resolving) return; + const expected = specialQuizExpectedIds(space, sess); + const allAnswered = expected.length > 0 && expected.every((pid) => Number.isInteger(sess.answers[pid])); + if (allAnswered) resolveSpecialQuizQuestion(sid, space, false); +} + +/** บอททยอยตอบถูกภายในเวลาตอบ (ตอบถูกเสมอเพื่อให้ทีมมีโอกาสได้การ์ด) */ +function scheduleSpecialQuizBotAnswers(sid, space, sess, q) { + const bots = (Array.isArray(sess.roster) ? sess.roster : []).filter((m) => m.isBot); + if (!bots.length) return; + const windowMs = Math.max(1200, SPECIAL_QUIZ_ANSWER_MS - 1500); + bots.forEach((b) => { + const delay = 900 + Math.floor(Math.random() * windowMs); + const t = setTimeout(() => { + const spNow = spaces.get(sid); + const sNow = spNow && spNow.specialQuiz && spNow.specialQuiz.session; + if (!spNow || sNow !== sess || sess.resolving) return; + if (Number.isInteger(sess.answers[b.id])) return; + sess.answers[b.id] = q.correctIndex; + io.to(sid).emit('special-quiz-answer-update', { id: b.id, answered: true }); + maybeResolveSpecialQuizAllAnswered(sid, spNow); + }, delay); + space.specialQuizTimers.push(t); + }); +} + +function sendSpecialQuizQuestion(sid, space) { + const sq = space.specialQuiz; + const sess = sq && sq.session; + if (!sess) return; + const q = sess.questions[sess.qIndex]; + if (!q) { endSpecialQuizSession(sid, space, false); return; } + sess.answers = {}; + sess.phaseEndsAt = Date.now() + SPECIAL_QUIZ_ANSWER_MS; + io.to(sid).emit('special-quiz-question', { + index: sess.qIndex, + total: sess.questions.length, + text: q.text, + choices: q.choices, + endsAt: sess.phaseEndsAt, + iconType: sq.iconType, + roster: specialQuizRosterPayload(sess), + }); + scheduleSpecialQuizBotAnswers(sid, space, sess, q); + const t = setTimeout(() => { + const spNow = spaces.get(sid); + if (!spNow || spNow.specialQuiz !== sq || sq.session !== sess) return; + resolveSpecialQuizQuestion(sid, spNow, true); + }, SPECIAL_QUIZ_ANSWER_MS + 80); + space.specialQuizTimers.push(t); +} + +/** ตัดสินข้อปัจจุบัน: ต้องตอบครบทุกคน และถูกทุกคน จึงผ่าน */ +function resolveSpecialQuizQuestion(sid, space, fromTimeout) { + const sq = space.specialQuiz; + const sess = sq && sq.session; + if (!sess || sess.resolving) return; + sess.resolving = true; + clearSpecialQuizTimers(space); + const q = sess.questions[sess.qIndex]; + const expected = specialQuizExpectedIds(space, sess); + let allCorrect = expected.length > 0; + expected.forEach((pid) => { + const a = sess.answers[pid]; + if (!(Number.isInteger(a) && a === q.correctIndex)) allCorrect = false; + }); + io.to(sid).emit('special-quiz-result', { + index: sess.qIndex, + correctIndex: q.correctIndex, + answers: { ...sess.answers }, + roster: specialQuizRosterPayload(sess), + allCorrect, + timeout: !!fromTimeout, + }); + if (!allCorrect) { + const t = setTimeout(() => { + const spNow = spaces.get(sid); + if (!spNow || spNow.specialQuiz !== sq) return; + endSpecialQuizSession(sid, spNow, false); + }, SPECIAL_QUIZ_REVEAL_MS); + space.specialQuizTimers.push(t); + return; + } + sess.qIndex++; + if (sess.qIndex >= sess.questions.length) { + const t = setTimeout(() => { + const spNow = spaces.get(sid); + if (!spNow || spNow.specialQuiz !== sq) return; + endSpecialQuizSession(sid, spNow, true); + }, SPECIAL_QUIZ_REVEAL_MS); + space.specialQuizTimers.push(t); + return; + } + const t = setTimeout(() => { + const spNow = spaces.get(sid); + if (!spNow || spNow.specialQuiz !== sq || sq.session !== sess) return; + sess.resolving = false; + sendSpecialQuizQuestion(sid, spNow); + }, SPECIAL_QUIZ_REVEAL_MS); + space.specialQuizTimers.push(t); +} + +/** + * นิยามการ์ดพิเศษ 7 ใบ — when = ช่วงที่การ์ดทำงาน: + * minigame = ใช้ทันทีในมินิเกมที่เพิ่งเล่น (Time Dilation) + * now = ใช้ทันทีตอนได้รับ (Fund coins) + * after_game = ตอนจบมินิเกมก่อนกลับ LobbyB (Free Evidence) + * pre_trial = ก่อนโหวตพิจารณาคดี (Silence) + * trial_vote = ระหว่างโหวตพิจารณาคดี (Extension) + * trial_revote= หลังเฉลยถ้าจับผิดตัว (Bail Coin) + * lobby_next = ใน LobbyB ก่อนเลือกมินิเกมรอบหน้า (Ban) + */ +const SPECIAL_CARD_DEFS = { + 1: { key: 'time_dilation', when: 'minigame', addSec: 10, th: 'เพิ่มเวลาเล่นมินิเกม +10 วินาที' }, + 2: { key: 'extension', when: 'trial_vote', addSec: 10, th: 'เพิ่มเวลาโหวตพิจารณาคดี +10 วินาที' }, + 3: { key: 'ban', when: 'lobby_next', th: 'โหวตแบนผู้เล่น 1 คน ไม่ให้เล่นมินิเกมรอบหน้า' }, + 4: { key: 'silence', when: 'pre_trial', th: 'โหวตปิดปากผู้เล่น 1 คน ห้ามโหวตในพิจารณาคดี' }, + 5: { key: 'bail', when: 'trial_revote', th: 'จับผิดตัว ให้โหวตใหม่ได้ 1 ครั้ง' }, + 6: { key: 'fund', when: 'now', coins: 10, th: 'ทุกคนรับ +10 COINS' }, + 7: { key: 'free_evidence', when: 'after_game', evidence: 2, th: 'ทุกคนรับหลักฐานฟรี +2 ใบ' }, +}; + +function specialCardDef(cardId) { + return SPECIAL_CARD_DEFS[Number(cardId)] || null; +} + +/** payload การ์ดสำหรับ client (รวม metadata การใช้งาน) */ +function specialCardClientPayload(card) { + if (!card) return null; + const def = specialCardDef(card.cardId) || {}; + return { + cardId: card.cardId || null, + th: card.th || def.th || '', + en: card.en || '', + rarity: card.rarity || 'common', + imageUrl: card.imageUrl || (card.cardId ? specialCardImageUrl(card.cardId) : ''), + txtImageUrl: card.txtImageUrl || (card.cardId ? specialCardTxtImageUrl(card.cardId) : ''), + effectKey: def.key || '', + effectWhen: def.when || '', + addSec: def.addSec || 0, + desc: def.th || '', + }; +} + +/** ใช้การ์ดที่ทำงาน "ทันที" (minigame / now) — ส่วนที่เหลือเข้าคิวรอ flow ที่ถูก */ +function applySpecialCardImmediate(sid, space, card) { + const def = specialCardDef(card && card.cardId); + if (!def) return; + if (def.when === 'minigame' && def.key === 'time_dilation') { + /* +10 วิให้รอบ quiz (mng8a80o) ตอน resume — มินิเกมอื่นฝั่ง client ขยายเอง */ + space.timeDilationPendingSec = (space.timeDilationPendingSec || 0) + (def.addSec || 10); + io.to(sid).emit('special-card-applied', { + card: specialCardClientPayload(card), + addSec: def.addSec || 10, + }); + consumePendingSpecialCard(space, card.cardId); + return; + } + if (def.when === 'now' && def.key === 'fund') { + /* +COINS — แต่ละ client บวกเหรียญของตัวเองผ่าน PHP (server ไม่รู้ playerKey) */ + io.to(sid).emit('special-card-applied', { + card: specialCardClientPayload(card), + coins: def.coins || 10, + }); + consumePendingSpecialCard(space, card.cardId); + return; + } + /* การ์ดที่เหลือ (after_game / pre_trial / trial_vote / trial_revote / lobby_next) + — คงไว้ใน pendingSpecialCard เพื่อใช้ภายหลัง */ +} + +function consumePendingSpecialCard(space, cardId) { + if (space.pendingSpecialCard && space.pendingSpecialCard.cardId === Number(cardId)) { + space.pendingSpecialCard.consumed = true; + } +} + +function endSpecialQuizSession(sid, space, success) { + const sq = space.specialQuiz; + if (!sq) return; + clearSpecialQuizTimers(space); + sq.done = true; + sq.session = null; + let cardOut = null; + if (success && sq.card && (sq.card.cardId || sq.card.imageUrl)) { + space.specialCardAwardedThisRun = sq.card; + cardOut = specialCardClientPayload(sq.card); + space.pendingSpecialCard = { cardId: Number(sq.card.cardId) || null, card: sq.card, consumed: false }; + applySpecialCardImmediate(sid, space, sq.card); + } + io.to(sid).emit('special-quiz-ended', { success: !!success, card: cardOut }); + /* รอ client กด «เล่นต่อ» ก่อนค่อยเล่นรอบ quiz ต่อ (กันเวลาเดินตอนค้างอ่านการ์ด) + — มี safety timeout กันค้างถ้าไม่มีใครกด */ + space.specialQuizAwaitContinue = true; + if (space.specialQuizContinueTimer) clearTimeout(space.specialQuizContinueTimer); + space.specialQuizContinueTimer = setTimeout(() => { + space.specialQuizContinueTimer = null; + finishSpecialQuizContinue(sid, space); + }, 32000); +} + +/** เล่นรอบ quiz ต่อหลังผู้เล่นกด «เล่นต่อ» (เรียกครั้งเดียว / หรือ safety timeout) */ +function finishSpecialQuizContinue(sid, space) { + if (!space || !space.specialQuizAwaitContinue) return; + space.specialQuizAwaitContinue = false; + if (space.specialQuizContinueTimer) { + clearTimeout(space.specialQuizContinueTimer); + space.specialQuizContinueTimer = null; + } + resumeQuizAfterSpecialQuiz(sid, space); +} + +function startSpecialQuizSession(sid, space) { + const sq = space.specialQuiz; + if (!sq || sq.triggered || sq.done) return false; + const settings = loadQuizSettings(); + const set = getSpecialQuizSet(settings, sq.level, sq.caseId); + const all = (set && Array.isArray(set.questions) ? set.questions : []) + .map(specialQuizNormalizeQuestion) + .filter(Boolean); + if (!all.length) return false; + /* ควิซพิเศษ = สุ่มถามแค่ 1 ข้อ (รับการ์ดพิเศษ) */ + const picked = specialQuizShuffle(all).slice(0, 1); + sq.triggered = true; + sq.session = { + questions: picked, + qIndex: 0, + answers: {}, + phaseEndsAt: 0, + resolving: false, + roster: buildSpecialQuizRoster(space), + }; + clearSpecialQuizTimers(space); + pauseQuizForSpecialQuiz(sid, space); + io.to(sid).emit('special-quiz-icon-clear', {}); + sendSpecialQuizQuestion(sid, space); + return true; +} + +function defaultQuizSettings() { + return { + readMs: 10000, + answerMs: 5000, + betweenMs: 3500, + /** quiz_carry: โชว์เฉพาะคำถามก่อน (มิลลิวิ) แล้วค่อยให้หยิบตัวเลือก — 0 = ไม่รอ */ + carryReadMs: 3000, + /** quiz_carry: หลังตัวเลือกขึ้น ให้เวลาตอบ (มิลลิวิ) */ + carryAnswerMs: 5000, + /** quiz_carry: จำนวนข้อต่อเซสชันก่อนจบเกม (พรีวิว) — 0 = ไม่จบอัตโนมัติ */ + carrySessionLength: 0, + /** quiz_carry: สีแผง #quiz-map-question-panel ในเกม/พรีวิว (จัดใน Admin แท็บหยิบมาวาง) */ + carryMapPanelTheme: defaultCarryMapPanelTheme(), + /** เกมตอบคำถามถูก/ผิด: สีแผงคำถามบนแผนที่ (#quiz-map-question-panel) — Admin แท็บคำถามเกม */ + quizMapPanelTheme: defaultCarryMapPanelTheme(), + /** quiz_carry: สี/ขอบ/ขนาดตัวเลขนับ 3-2-1 (embed พรีวิว) — Admin แท็บหยิบมาวาง */ + carryEmbedCountdownTheme: defaultCarryEmbedCountdownTheme(), + /** quiz_carry: สีป้ายตัวเลือกบนแผนที่ (canvas) ต่อช่อง 1–16 */ + carryChoicePlaqueThemes: defaultCarryChoicePlaqueThemes(), + /** legacy: ช่องแรกเท่ากับ carryChoicePlaqueThemes[0] */ + carryChoicePlaqueTheme: defaultCarryChoicePlaqueTheme(), + /** quiz_carry: ขยายป้ายตัวเลือกบนแมป (กว้าง/สูง/ฟอนต์) — 0.85–2.5 */ + carryChoicePlaqueMapScale: 1.25, + /** quiz_carry: ถ้าใส่รหัสฉาก + คูณ — เดินเร็วเฉพาะแมปนั้น (เทียบกับค่าเริ่มในไคลเอนต์) */ + carryWalkSpeedMultForMapId: '', + carryWalkSpeedMult: null, + /** ถูก/ผิด: สุ่มกี่ข้อต่อรอบจากชุดคำถาม (สูงสุดเท่าที่มีในชุด) */ + quizRoundQuestionCount: 10, + questions: [], + carryQuestions: [], + battleQuizMcq: [], + /** 15 ชุดคำถามพิเศษ (Level×Case) สำหรับรับการ์ดพิเศษ */ + specialQuizSets: sanitizeSpecialQuizSets({}), + }; +} + +function defaultCarryMapPanelTheme() { + return { + panelBg: 'rgba(12, 14, 28, 0.88)', + panelBorder: 'rgba(255, 214, 102, 0.7)', + borderWidthPx: 2, + textColor: '#f1f5f9', + questionFontMinPx: 10, + questionFontMaxPx: 24, + }; +} + +function sanitizeCssColorToken(input, fallback) { + const t = String(input == null ? '' : input).trim().slice(0, 120); + if (!t) return fallback; + if (/url\s*\(|expression\s*\(|@import|\/\*|javascript|<|>|\\0/i.test(t)) return fallback; + if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(t)) return t; + if (/^rgba?\(\s*[^)]+\)$/i.test(t)) { + const inner = t.replace(/^rgba?\(\s*/i, '').replace(/\)\s*$/, ''); + if (inner.length <= 96 && /^[0-9\s.,%+-]+$/.test(inner)) return t.replace(/\s+/g, ' ').trim(); + } + return fallback; +} + +function sanitizeCarryMapPanelTheme(raw) { + const d = defaultCarryMapPanelTheme(); + if (!raw || typeof raw !== 'object') return d; + let qMin = Number(raw.questionFontMinPx); + let qMax = Number(raw.questionFontMaxPx); + if (!Number.isFinite(qMin)) qMin = d.questionFontMinPx; + if (!Number.isFinite(qMax)) qMax = d.questionFontMaxPx; + qMin = Math.round(Math.max(10, Math.min(40, qMin))); + qMax = Math.round(Math.max(14, Math.min(56, qMax))); + if (qMax < qMin) { + const t = qMin; + qMin = qMax; + qMax = t; + } + return { + panelBg: sanitizeCssColorToken(raw.panelBg, d.panelBg), + panelBorder: sanitizeCssColorToken(raw.panelBorder, d.panelBorder), + borderWidthPx: (() => { + let w = Number(raw.borderWidthPx); + if (!Number.isFinite(w)) w = d.borderWidthPx; + return Math.max(0, Math.min(12, Math.round(w))); + })(), + textColor: sanitizeCssColorToken(raw.textColor, d.textColor), + questionFontMinPx: qMin, + questionFontMaxPx: qMax, + }; +} + +/** ป้ายตัวเลือกบนแผนที่ (canvas) — quiz_carry */ +function defaultCarryChoicePlaqueTheme() { + return { + borderMode: 'neon', + fixedBorder: 'rgba(122, 200, 255, 0.9)', + fillBg: 'rgba(12, 10, 20, 0.88)', + textColor: 'rgba(248, 249, 255, 1)', + borderWidthPx: 2.5, + }; +} + +function sanitizeCarryChoicePlaqueTheme(raw) { + const d = defaultCarryChoicePlaqueTheme(); + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return sanitizeCarryChoicePlaqueTheme({}); + const mode = String(raw.borderMode || '').toLowerCase() === 'fixed' ? 'fixed' : 'neon'; + let bw = Number(raw.borderWidthPx); + if (!Number.isFinite(bw)) bw = d.borderWidthPx; + bw = Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10; + return { + borderMode: mode, + fixedBorder: sanitizeCssColorToken(raw.fixedBorder, d.fixedBorder), + fillBg: sanitizeCssColorToken(raw.fillBg, d.fillBg), + textColor: sanitizeCssColorToken(raw.textColor, d.textColor), + borderWidthPx: bw, + plaqueImageUrl: sanitizeCarryChoiceImageUrl(raw.plaqueImageUrl), + }; +} + +const QUIZ_CARRY_PLAQUE_THEME_SLOTS = 16; + +function defaultCarryChoicePlaqueThemes() { + const out = []; + for (let i = 0; i < QUIZ_CARRY_PLAQUE_THEME_SLOTS; i++) { + out.push(sanitizeCarryChoicePlaqueTheme({})); + } + return out; +} + +/** อาร์เรย์ 16 ช่อง — รับได้ทั้ง carryChoicePlaqueThemes หรืออ็อบเจ็กต์เดียว (legacy) */ +function sanitizeCarryChoicePlaqueThemes(raw) { + if (Array.isArray(raw) && raw.length > 0) { + const out = []; + for (let i = 0; i < QUIZ_CARRY_PLAQUE_THEME_SLOTS; i++) { + out.push(sanitizeCarryChoicePlaqueTheme(raw[i] || {})); + } + return out; + } + if (raw != null && typeof raw === 'object' && !Array.isArray(raw)) { + const one = sanitizeCarryChoicePlaqueTheme(raw); + return Array.from({ length: QUIZ_CARRY_PLAQUE_THEME_SLOTS }, () => ({ ...one })); + } + return defaultCarryChoicePlaqueThemes(); +} + +function sanitizeCarryChoiceImageUrl(input) { + const s = String(input == null ? '' : input).trim().slice(0, 512); + if (!s) return ''; + if (/[\s<>'"`]/.test(s)) return ''; + if (s.startsWith('/')) { + if (!/^\/[\w\-./?#=&%+]+$/i.test(s)) return ''; + return s; + } + const low = s.toLowerCase(); + if (!low.startsWith('https://') && !low.startsWith('http://')) return ''; + try { + const u = new URL(s); + if (u.protocol !== 'http:' && u.protocol !== 'https:') return ''; + return s; + } catch (e) { + return ''; + } +} + +function defaultCarryEmbedCountdownTheme() { + return { + overlayBackdrop: 'rgba(6, 8, 14, 0.52)', + innerBg: 'rgba(0, 0, 0, 0.78)', + innerBorder: 'rgba(94, 234, 212, 0.82)', + innerBorderWpx: 1, + innerRadiusPx: 14, + digitColor: '#ffe066', + mapDigitCqmin: 78, + mapDigitCqh: 82, + mapDigitMaxPx: 220, + screenDigitVw: 32, + screenDigitMaxPx: 200, + }; +} + +function sanitizeCarryEmbedCountdownTheme(raw) { + const d = defaultCarryEmbedCountdownTheme(); + if (!raw || typeof raw !== 'object') return d; + const clampN = (v, lo, hi, def) => { + const n = Number(v); + if (!Number.isFinite(n)) return def; + return Math.max(lo, Math.min(hi, Math.round(n))); + }; + return { + overlayBackdrop: sanitizeCssColorToken(raw.overlayBackdrop, d.overlayBackdrop), + innerBg: sanitizeCssColorToken(raw.innerBg, d.innerBg), + innerBorder: sanitizeCssColorToken(raw.innerBorder, d.innerBorder), + innerBorderWpx: clampN(raw.innerBorderWpx, 0, 12, d.innerBorderWpx), + innerRadiusPx: clampN(raw.innerRadiusPx, 0, 32, d.innerRadiusPx), + digitColor: sanitizeCssColorToken(raw.digitColor, d.digitColor), + mapDigitCqmin: clampN(raw.mapDigitCqmin, 35, 100, d.mapDigitCqmin), + mapDigitCqh: clampN(raw.mapDigitCqh, 35, 100, d.mapDigitCqh), + mapDigitMaxPx: clampN(raw.mapDigitMaxPx, 48, 400, d.mapDigitMaxPx), + screenDigitVw: clampN(raw.screenDigitVw, 6, 44, d.screenDigitVw), + screenDigitMaxPx: clampN(raw.screenDigitMaxPx, 48, 220, d.screenDigitMaxPx), + }; +} + +/** หมวด Quiz Battle (อ้างอิงธีมหน้า Quiz-Battle) — id ต้องตรงกับแอดมิน */ +const QUIZ_BATTLE_CATEGORY_IDS = new Set([ + 'cybercrime', + 'cyber_law', + 'privacy', + 'digital_security', + 'social_media', + 'ethics', + 'ai_data', +]); + +/** คำถาม 3 ตัวเลือก A/B/C แยกหมวด — สำหรับโหมด Quiz Battle / โดม */ +function sanitizeBattleQuizMcq(arr) { + if (!Array.isArray(arr)) return []; + const out = []; + for (const q of arr) { + if (!q || typeof q !== 'object') continue; + let categoryId = String(q.categoryId || '').trim(); + if (!categoryId || !QUIZ_BATTLE_CATEGORY_IDS.has(categoryId)) { + categoryId = 'cybercrime'; + } + const text = String(q.text || '').trim().slice(0, 500); + if (!text) continue; + const rawChoices = Array.isArray(q.choices) ? q.choices : []; + const choices = rawChoices.map((c) => String(c || '').trim().slice(0, 160)); + if (choices.length !== 3 || !choices.every(Boolean)) continue; + let ci = Number(q.correctIndex); + if (!Number.isFinite(ci)) ci = 0; + ci = Math.max(0, Math.min(2, Math.floor(ci))); + out.push({ categoryId, text, choices, correctIndex: ci }); + if (out.length >= 200) break; + } + return out; +} + +/** คำถามแบบหลายตัวเลือก (หยิบมาวาง / quiz_carry) — เก็บใน quiz-settings.json */ +function sanitizeCarryQuestions(arr) { + if (!Array.isArray(arr)) return []; + const out = []; + for (const q of arr) { + if (!q || typeof q !== 'object') continue; + const text = String(q.text || '').trim().slice(0, 500); + if (!text) continue; + let choices = Array.isArray(q.choices) + ? q.choices.map((c) => String(c || '').trim().slice(0, 160)).filter(Boolean) + : []; + if (choices.length < 2) continue; + if (choices.length > 16) choices = choices.slice(0, 16); + let ci = Number(q.correctIndex); + if (!Number.isFinite(ci)) ci = 0; + ci = Math.max(0, Math.min(choices.length - 1, Math.floor(ci))); + const row = { text, choices, correctIndex: ci }; + if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) { + const urls = choices.map((_, idx) => sanitizeCarryChoiceImageUrl(q.choiceImageUrls[idx])); + if (urls.some((u) => u)) row.choiceImageUrls = urls; + } + out.push(row); + if (out.length >= 80) break; + } + return out; +} + +function clampQuizMs(n, def) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(1000, Math.min(300000, Math.floor(v))); +} + +function clampQuizBetweenMs(n, def) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(0, Math.min(300000, Math.floor(v))); +} + +function clampCarrySessionLength(n, def) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(0, Math.min(500, Math.floor(v))); +} + +/** เกมตอบคำถามถูก/ผิด: จำนวนข้อที่สุ่มจากชุดต่อรอบ (เซสชัน) — 1–50 */ +function clampQuizRoundQuestionCount(n, def) { + const v = Number(n); + if (!Number.isFinite(v)) { + const d = Number(def); + if (!Number.isFinite(d)) return 10; + return Math.max(1, Math.min(50, Math.floor(d))); + } + return Math.max(1, Math.min(50, Math.floor(v))); +} + +function clampCarryChoicePlaqueMapScale(n, def) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.round(Math.max(0.85, Math.min(2.5, v)) * 100) / 100; +} + +/** รหัสฉาก (เช่น mnorwqx1) — ว่าง = ไม่ใช้คูณเดินแยกแมป */ +function sanitizeCarryWalkSpeedMultForMapId(raw) { + const t = String(raw == null ? '' : raw).trim().slice(0, 64); + if (!t) return ''; + if (!/^[a-zA-Z0-9_-]+$/.test(t)) return ''; + return t; +} + +/** quiz_carry: คูณความเร็วเดินเมื่อ map id ตรง — null = ไม่ตั้ง */ +function clampCarryWalkSpeedMultSetting(n) { + if (n === null || n === undefined || n === '') return null; + const v = Number(n); + if (Number.isNaN(v)) return null; + return Math.round(Math.max(0.5, Math.min(3, v)) * 100) / 100; +} + +function mergeQuizCarryFromMirrorIfSet(base) { + if (!QUIZ_SETTINGS_MIRROR_PATH) return base; + try { + if (!fs.existsSync(QUIZ_SETTINGS_MIRROR_PATH)) return base; + const raw = fs.readFileSync(QUIZ_SETTINGS_MIRROR_PATH, 'utf8'); + const j = JSON.parse(raw); + if (!j || typeof j !== 'object') return base; + if (j.carryMapPanelTheme != null && typeof j.carryMapPanelTheme === 'object') { + base.carryMapPanelTheme = sanitizeCarryMapPanelTheme(j.carryMapPanelTheme); + } + if (j.quizMapPanelTheme != null && typeof j.quizMapPanelTheme === 'object') { + base.quizMapPanelTheme = sanitizeCarryMapPanelTheme(j.quizMapPanelTheme); + } + if (j.carryReadMs != null) { + base.carryReadMs = clampQuizBetweenMs(j.carryReadMs, base.carryReadMs); + } + if (j.carryAnswerMs != null) { + base.carryAnswerMs = clampQuizMs(j.carryAnswerMs, base.carryAnswerMs); + } + if (j.carrySessionLength != null) { + base.carrySessionLength = clampCarrySessionLength(j.carrySessionLength, base.carrySessionLength); + } + if (j.carryWalkSpeedMultForMapId != null) { + base.carryWalkSpeedMultForMapId = sanitizeCarryWalkSpeedMultForMapId(j.carryWalkSpeedMultForMapId); + } + if (j.carryWalkSpeedMult != null) { + const wm = clampCarryWalkSpeedMultSetting(j.carryWalkSpeedMult); + if (wm != null) base.carryWalkSpeedMult = wm; + } + if (j.carryEmbedCountdownTheme != null && typeof j.carryEmbedCountdownTheme === 'object') { + base.carryEmbedCountdownTheme = sanitizeCarryEmbedCountdownTheme(j.carryEmbedCountdownTheme); + } + if (Array.isArray(j.carryChoicePlaqueThemes) && j.carryChoicePlaqueThemes.length) { + base.carryChoicePlaqueThemes = sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueThemes); + base.carryChoicePlaqueTheme = base.carryChoicePlaqueThemes[0]; + } else if (j.carryChoicePlaqueTheme != null && typeof j.carryChoicePlaqueTheme === 'object') { + base.carryChoicePlaqueThemes = sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueTheme); + base.carryChoicePlaqueTheme = base.carryChoicePlaqueThemes[0]; + } + if (Array.isArray(j.carryQuestions) && j.carryQuestions.length) { + base.carryQuestions = sanitizeCarryQuestions(j.carryQuestions); + } + if (j.quizRoundQuestionCount != null) { + base.quizRoundQuestionCount = clampQuizRoundQuestionCount(j.quizRoundQuestionCount, base.quizRoundQuestionCount); + } + return base; + } catch (e) { + console.error('mergeQuizCarryFromMirrorIfSet', e.message); + return base; + } +} + +/* ===== การ์ดหลักฐาน (Evidence Cards) — 3 คดี × ผู้ต้องสงสัย 3 × การ์ด 3 ใบ จัดการผ่าน Admin ===== */ +const EVIDENCE_CARDS_PATH = path.join(__dirname, 'data', 'evidence-cards.json'); +const EVIDENCE_RARITIES = ['common', 'rare', 'legendary']; +const EVIDENCE_CASE_COUNT = 15; + +/* ===== รูปภาพคดี + Cutscene (LobbyA → LobbyB) — จัดการผ่าน Admin ===== */ +const CASE_MEDIA_PATH = path.join(__dirname, 'data', 'case-media.json'); +const CASE_MEDIA_COUNT = 15; +/* Main-Lobby อยู่นอกโฟลเดอร์ Game (เสิร์ฟจาก /var/www/html) — ขึ้นไป 1 ระดับ */ +const MAIN_LOBBY_IMAGE_ROOT = path.join(__dirname, '..', 'Main-Lobby', 'IMAGE'); + +function caseMediaDefault(n) { + const suspects = [1, 2, 3].map((s) => '/Main-Lobby/IMAGE/suspect-pick/case-' + n + '-suspect-' + s + '.png'); + const culprits = [1, 2, 3].map((s) => '/Main-Lobby/IMAGE/culprit-prison/case-' + n + '-suspect-' + s + '.png'); + return { + name: 'คดีที่ ' + n, + art: '/Main-Lobby/IMAGE/case/case-' + n + '.png', + detail: '/Main-Lobby/IMAGE/case/case-detail-' + n + '.png', + story: [ + '/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-1.png', + '/Main-Lobby/IMAGE/cutscene/case-' + n + '-story-2.png', + ], + suspects, + culprits, + }; +} + +function sanitizeCaseMediaUrlList(arr, fallbackArr) { + const fb = Array.isArray(fallbackArr) ? fallbackArr : []; + const src = Array.isArray(arr) ? arr : []; + return [0, 1, 2].map((i) => sanitizeCaseMediaUrl(src[i], fb[i] || fb[0] || '')); +} + +/** อนุญาตเฉพาะ path รูปภายในเว็บ (กัน URL ภายนอก/สคริปต์) — ไม่ผ่าน → คืนค่า fallback */ +function sanitizeCaseMediaUrl(v, fallback) { + if (typeof v !== 'string') return fallback; + const s = v.trim(); + if (!s) return fallback; + if (!/^\/[\w./-]*\.(png|jpe?g|webp)$/i.test(s)) return fallback; + return s; +} + +function sanitizeCaseMedia(obj) { + const src = (obj && typeof obj === 'object' && obj.cases && typeof obj.cases === 'object') ? obj.cases : {}; + const cases = {}; + for (let n = 1; n <= CASE_MEDIA_COUNT; n++) { + const d = caseMediaDefault(n); + const c = src[n] || src[String(n)] || {}; + const story = Array.isArray(c.story) ? c.story : []; + cases[n] = { + name: (typeof c.name === 'string' && c.name.trim()) ? c.name.trim().slice(0, 80) : d.name, + art: sanitizeCaseMediaUrl(c.art, d.art), + detail: sanitizeCaseMediaUrl(c.detail, d.detail), + story: [sanitizeCaseMediaUrl(story[0], d.story[0]), sanitizeCaseMediaUrl(story[1], d.story[1])], + suspects: sanitizeCaseMediaUrlList(c.suspects, d.suspects), + culprits: sanitizeCaseMediaUrlList(c.culprits, d.culprits), + }; + } + return { cases }; +} + +function loadCaseMedia() { + try { + if (fs.existsSync(CASE_MEDIA_PATH)) { + return sanitizeCaseMedia(JSON.parse(fs.readFileSync(CASE_MEDIA_PATH, 'utf8'))); + } + } catch (e) { /* ignore — คืนค่า default */ } + return sanitizeCaseMedia({}); +} + +function saveCaseMedia(d) { + try { + const clean = sanitizeCaseMedia(d); + fs.writeFileSync(CASE_MEDIA_PATH, JSON.stringify(clean, null, 2), 'utf8'); + return { ok: true, cases: clean.cases }; + } catch (e) { + let err = 'บันทึกไม่ได้'; + if (e && e.code === 'EACCES') err = 'ไม่มีสิทธิ์เขียนไฟล์ case-media.json (chown เป็น user ที่รันเกม)'; + else if (e && e.message) err = 'บันทึกไม่ได้: ' + e.message; + return { ok: false, error: err }; + } +} + +/** รายการรูปที่มีจริงในโฟลเดอร์ case / cutscene (ไว้ให้ Admin เลือก) */ +function listCaseMediaImages() { + const out = { case: [], cutscene: [], 'suspect-pick': [], 'culprit-prison': [] }; + ['case', 'cutscene', 'suspect-pick', 'culprit-prison'].forEach((sub) => { + try { + const p = path.join(MAIN_LOBBY_IMAGE_ROOT, sub); + out[sub] = fs.readdirSync(p) + .filter((f) => /\.(png|jpe?g|webp)$/i.test(f)) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) + .map((f) => '/Main-Lobby/IMAGE/' + sub + '/' + f); + } catch (e) { /* โฟลเดอร์อาจไม่มี */ } + }); + return out; +} +const EVIDENCE_IMAGE_DIRS = ['suspect-1', 'suspect-2', 'suspect-3']; +/* rarity คิดจากเลขไฟล์การ์ด: 01-22 = common, 23-44 = rare, 45-54 = legendary (ชี้คนร้าย) */ +const EVIDENCE_RARITY_RANGES = [ + { rarity: 'common', min: 1, max: 22, stars: 1 }, + { rarity: 'rare', min: 23, max: 44, stars: 2 }, + { rarity: 'legendary', min: 45, max: 54, stars: 3 }, +]; + +function evidenceRarityForNum(n) { + for (const r of EVIDENCE_RARITY_RANGES) if (n >= r.min && n <= r.max) return r; + return EVIDENCE_RARITY_RANGES[0]; +} + +function evidenceCardImageDefault(suspectIdx, cardIdx) { + return '/Game/img/evidence-cards/suspect-' + (suspectIdx + 1) + '/Card-0' + (cardIdx + 1) + '.png'; +} + +/* แคชพูลรูปการ์ดต่อโฟลเดอร์ผู้ต้องสงสัย แยกตาม rarity (สแกนไฟล์จริง) */ +let _evidencePoolCache = null; +let _evidencePoolCacheAt = 0; +function evidenceImagePools() { + const now = Date.now(); + if (_evidencePoolCache && (now - _evidencePoolCacheAt) < 30000) return _evidencePoolCache; + const pools = {}; + EVIDENCE_IMAGE_DIRS.forEach((dir) => { + const buckets = { common: [], rare: [], legendary: [] }; + try { + const p = path.join(__dirname, 'public', 'img', 'evidence-cards', dir); + fs.readdirSync(p).forEach((f) => { + const m = /^Card-0*(\d+)\.(png|jpe?g|webp)$/i.exec(f); + if (!m) return; + const num = parseInt(m[1], 10); + const rr = evidenceRarityForNum(num); + buckets[rr.rarity].push({ num, rarity: rr.rarity, stars: rr.stars, imageUrl: '/Game/img/evidence-cards/' + dir + '/' + f }); + }); + } catch (e) { /* โฟลเดอร์อาจไม่มี */ } + Object.keys(buckets).forEach((k) => buckets[k].sort((a, b) => a.num - b.num)); + pools[dir] = buckets; + }); + _evidencePoolCache = pools; + _evidencePoolCacheAt = now; + return pools; +} + +function evidencePoolCounts() { + const pools = evidenceImagePools(); + const out = {}; + EVIDENCE_IMAGE_DIRS.forEach((dir) => { + const b = pools[dir] || { common: [], rare: [], legendary: [] }; + out[dir] = { common: b.common.length, rare: b.rare.length, legendary: b.legendary.length }; + }); + return out; +} + +/* แปลงเกรด -> rarity key ('legendary'=ชี้คนร้าย/'rare'/'common') ตามเปอร์เซ็นต์ที่กำหนด */ +function rollEvidenceRarityKey(grade) { + 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'); + return r < 0.20 ? 'rare' : 'common'; // C ลงไป: ไม่มีชี้คนร้าย +} + +/* โฟลเดอร์รูปของผู้ต้องสงสัยตาม config คดี (fallback = suspect-(idx+1)) */ +function evidenceImageDirForSuspect(space, suspectIndex) { + const def = EVIDENCE_IMAGE_DIRS[suspectIndex] || EVIDENCE_IMAGE_DIRS[0]; + try { + const cid = String((space && space.caseId) || '1'); + const cfg = loadEvidenceCards(); + const cse = cfg.cases && cfg.cases[cid]; + const s = cse && cse.suspects && cse.suspects[suspectIndex]; + if (s && s.imageDir && EVIDENCE_IMAGE_DIRS.indexOf(s.imageDir) >= 0) return s.imageDir; + } catch (e) { /* ใช้ default */ } + return def; +} + +/* เลือกการ์ด 1 ใบจากพูลของผู้ต้องสงสัยตามเกรด (มี fallback ถ้า rarity นั้นไม่มีรูป) */ +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'); + let pool = buckets[want]; + if (!pool || !pool.length) { + const tryOrder = ['common', 'rare', 'legendary']; + for (const k of tryOrder) { if (buckets[k] && buckets[k].length) { pool = buckets[k]; want = k; break; } } + } + if (!pool || !pool.length) return null; + const pick = pool[Math.floor(Math.random() * pool.length)]; + return { rarity: pick.rarity, stars: pick.stars, num: pick.num, imageUrl: pick.imageUrl }; +} + +function evidenceGradeFromPct(avgPct) { + return avgPct >= 80 ? 'A' : avgPct >= 60 ? 'B' : 'C'; +} + +/* คะแนนดิบของผู้เล่นต่อ gameType (ใช้กรณี normalize เทียบ best) */ +function evidencePlayerRawScore(space, p, gameType) { + switch (gameType) { + case 'gauntlet': + case 'jump_survive': return Math.max(0, p.gauntletScore | 0); + case 'space_shooter': return Math.max(0, p.spaceShooterScore | 0); + case 'balloon_boss': return Math.max(0, p.balloonBossScore | 0); + default: + return Math.max(0, p.gauntletScore | 0) || Math.max(0, p.spaceShooterScore | 0) || Math.max(0, p.balloonBossScore | 0); + } +} + +/* เกรดของรอบ (A/B/C) — quiz ใช้ % สัมบูรณ์, เกมอื่น normalize เทียบ best แล้วเฉลี่ยทั้งห้อง */ +function computeRunGradeForEvidence(space) { + try { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + const gameType = md && md.gameType; + if (gameType === 'gauntlet' || gameType === 'jump_survive') { + return buildGauntletCrownMissionPayload(space).grade || 'C'; + } + /* เกมจริงหรือไม่ (quiz TF): เฉลี่ย % ของคะแนนเทียบเต็ม (จำนวนข้อ × แต้มต่อข้อ) */ + if (gameType === 'quiz' && space.quizSession && space.quizSession.players) { + const total = (space.quizSession.questions || []).length * QUIZ_TF_POINTS_PER_CORRECT; + const ids = Object.keys(space.quizSession.players); + if (total > 0 && ids.length) { + const avgPct = ids.reduce((s, id) => { + const sc = Math.max(0, Number(space.quizSession.players[id].score) || 0); + return s + Math.min(100, (sc / total) * 100); + }, 0) / ids.length; + return evidenceGradeFromPct(avgPct); + } + } + const peers = [...space.peers.values()]; + if (!peers.length) return 'C'; + const raw = peers.map((p) => evidencePlayerRawScore(space, p, gameType)); + const max = Math.max(1, ...raw); + const avgPct = raw.reduce((s, v) => s + (v / max) * 100, 0) / raw.length; + return evidenceGradeFromPct(avgPct); + } catch (e) { return 'C'; } +} + +/* card object ที่เก็บในแฟ้มหลักฐาน */ +function sanitizeStoredEvidenceCard(c) { + if (!c || typeof c !== 'object') return null; + let rar = String(c.rarity || 'common').toLowerCase(); + if (EVIDENCE_RARITIES.indexOf(rar) < 0) rar = 'common'; + const img = String(c.imageUrl || '').slice(0, 400); + if (!img) return null; + let stars = Math.floor(Number(c.stars)); + if (!(stars >= 1 && stars <= 3)) stars = rar === 'legendary' ? 3 : rar === 'rare' ? 2 : 1; + let num = Math.floor(Number(c.num)); + if (!(num >= 1)) num = 0; + return { rarity: rar, stars, num, imageUrl: img }; +} + +function sanitizeEvidenceCases(obj) { + const src = (obj && obj.cases) ? obj.cases : {}; + const out = {}; + for (let i = 1; i <= EVIDENCE_CASE_COUNT; i++) { + const cid = String(i); + const cse = src[cid] || {}; + const suspects = Array.isArray(cse.suspects) ? cse.suspects : []; + const outSus = []; + for (let si = 0; si < 3; si++) { + const s = suspects[si] || {}; + let dir = String(s.imageDir || EVIDENCE_IMAGE_DIRS[si] || EVIDENCE_IMAGE_DIRS[0]); + if (EVIDENCE_IMAGE_DIRS.indexOf(dir) < 0) dir = EVIDENCE_IMAGE_DIRS[si] || EVIDENCE_IMAGE_DIRS[0]; + outSus.push({ + linkName: String(s.linkName || ('ผู้ต้องสงสัย ' + (si + 1))).slice(0, 60), + imageDir: dir, + }); + } + out[cid] = { suspects: outSus }; + } + return { cases: out }; +} + +function loadEvidenceCards() { + try { + if (fs.existsSync(EVIDENCE_CARDS_PATH)) { + return sanitizeEvidenceCases(JSON.parse(fs.readFileSync(EVIDENCE_CARDS_PATH, 'utf8'))); + } + } catch (e) { /* ignore — คืนค่า default */ } + return sanitizeEvidenceCases({}); +} + +/** รายการรูปการ์ดที่มีจริงในแต่ละโฟลเดอร์ผู้ต้องสงสัย (ไว้ให้ Admin เลือกรูป) */ +function listEvidenceCardImages() { + const out = { 'suspect-1': [], 'suspect-2': [], 'suspect-3': [] }; + Object.keys(out).forEach((dir) => { + try { + const p = path.join(__dirname, 'public', 'img', 'evidence-cards', dir); + const files = fs.readdirSync(p) + .filter((f) => /\.(png|jpe?g|webp)$/i.test(f)) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); + out[dir] = files.map((f) => '/Game/img/evidence-cards/' + dir + '/' + f); + } catch (e) { /* โฟลเดอร์อาจไม่มี */ } + }); + return out; +} + +function saveEvidenceCards(d) { + try { + const clean = sanitizeEvidenceCases(d); + fs.writeFileSync(EVIDENCE_CARDS_PATH, JSON.stringify(clean, null, 2), 'utf8'); + return { ok: true, cases: clean.cases }; + } catch (e) { + let err = 'บันทึกไม่ได้'; + if (e && e.code === 'EACCES') err = 'ไม่มีสิทธิ์เขียนไฟล์ evidence-cards.json (chown เป็น user ที่รันเกม)'; + else if (e && e.message) err = 'บันทึกไม่ได้: ' + e.message; + return { ok: false, error: err }; + } +} + +function loadQuizSettings() { + try { + if (fs.existsSync(QUIZ_SETTINGS_PATH)) { + const j = JSON.parse(fs.readFileSync(QUIZ_SETTINGS_PATH, 'utf8')); + const d = defaultQuizSettings(); + const questions = Array.isArray(j.questions) ? j.questions : []; + const carryQuestions = sanitizeCarryQuestions(j.carryQuestions); + const battleQuizMcq = sanitizeBattleQuizMcq(j.battleQuizMcq); + const carryMapPanelTheme = sanitizeCarryMapPanelTheme(j.carryMapPanelTheme); + const quizMapPanelTheme = sanitizeCarryMapPanelTheme(j.quizMapPanelTheme); + const carryEmbedCountdownTheme = sanitizeCarryEmbedCountdownTheme(j.carryEmbedCountdownTheme); + const carryChoicePlaqueThemes = Array.isArray(j.carryChoicePlaqueThemes) && j.carryChoicePlaqueThemes.length + ? sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueThemes) + : sanitizeCarryChoicePlaqueThemes(j.carryChoicePlaqueTheme); + const carryChoicePlaqueTheme = carryChoicePlaqueThemes[0]; + const carryChoicePlaqueMapScale = clampCarryChoicePlaqueMapScale(j.carryChoicePlaqueMapScale, d.carryChoicePlaqueMapScale); + const carryWalkSpeedMultForMapId = sanitizeCarryWalkSpeedMultForMapId(j.carryWalkSpeedMultForMapId); + let carryWalkSpeedMult = clampCarryWalkSpeedMultSetting(j.carryWalkSpeedMult); + if (!carryWalkSpeedMultForMapId) carryWalkSpeedMult = null; + else if (carryWalkSpeedMult == null) carryWalkSpeedMult = clampCarryWalkSpeedMultSetting(1.42); + const out = { + readMs: clampQuizMs(j.readMs, d.readMs), + answerMs: clampQuizMs(j.answerMs, d.answerMs), + betweenMs: clampQuizBetweenMs(j.betweenMs, d.betweenMs), + carryReadMs: clampQuizBetweenMs(j.carryReadMs, d.carryReadMs), + carryAnswerMs: clampQuizMs(j.carryAnswerMs, d.carryAnswerMs), + carrySessionLength: clampCarrySessionLength(j.carrySessionLength, d.carrySessionLength), + carryMapPanelTheme, + quizMapPanelTheme, + carryEmbedCountdownTheme, + carryChoicePlaqueThemes, + carryChoicePlaqueTheme, + carryChoicePlaqueMapScale, + carryWalkSpeedMultForMapId, + carryWalkSpeedMult, + quizRoundQuestionCount: clampQuizRoundQuestionCount(j.quizRoundQuestionCount, d.quizRoundQuestionCount), + questions, + carryQuestions, + battleQuizMcq, + specialQuizSets: sanitizeSpecialQuizSets(j.specialQuizSets), + }; + return mergeQuizCarryFromMirrorIfSet(out); + } + } catch (e) { console.error('loadQuizSettings', e.message); } + return mergeQuizCarryFromMirrorIfSet(defaultQuizSettings()); +} + +const GAUNTLET_DEFAULT_TICK_MS = 220; +const GAUNTLET_DEFAULT_JUMP_TICKS = 16; +const GAUNTLET_DEFAULT_TIME_LIMIT_SEC = 0; +/** รอบสวิงซ้าย-ขวา ต่อวินาที (Stack) — ค่าน้อย = ช้า · cycles per second of horizontal swing */ +const STACK_SWING_HZ_DEFAULT = 0.55; + +function defaultGameTiming() { + return { + gauntletTickMs: GAUNTLET_DEFAULT_TICK_MS, + gauntletJumpTicks: GAUNTLET_DEFAULT_JUMP_TICKS, + gauntletTimeLimitSec: GAUNTLET_DEFAULT_TIME_LIMIT_SEC, + ...defaultGauntletVisuals(), + stackSwingHz: STACK_SWING_HZ_DEFAULT, + stackBlockWidthTiles: null, + /** กระโดดให้รอด: ความสูงกระโดด = ทวีคูณ × ความสูงตัวละคร (tile) — 0.5–4 ค่าเริ่ม 1.5 */ + jumpSurviveJumpHeightMult: 1.5, + /** จำกัดเวลารอบภารกิจ/มินิเกมกระโดดขึ้นแท่น (วินาที) — 0 = ไคลเอนต์ใช้ 60 วิ; ไม่ใช่ค่าเดียวกับพรมแดง */ + jumpSurviveMissionTimeSec: 0, + /** รูปแพลตฟอร์ม jump_survive ช่อง 1–3 — ว่าง = ไคลเอนต์วาด cyan; w/h 0 = ใช้ขนาดไทล์ */ + jumpSurvivePlatformTiles: [ + { url: '', w: 0, h: 0 }, + { url: '', w: 0, h: 0 }, + { url: '', w: 0, h: 0 }, + ], + /** Stack ภารกิจ Tower (mnn93hpi): จำกัดเวลารอบ (วินาที) — ไคลเอนต์ใช้เมื่อเล่นภารกิจ */ + stackTowerMissionTimeSec: 90, + /** Stack ภารกิจ Tower: ทีมพลาดได้กี่ครั้งก่อนจบ (ไม่รีเซ็ตหอ) */ + stackTeamMissesMax: 3, + /** Stack ภารกิจ Tower: จำนวนชั้นสำเร็จ (ไม่คิดคอมโบ) ที่รวมแล้วได้ Progress 100% — แต่ละชั้น +100/N %; คอมโบ ×2 ของชั้นนั้น */ + stackTowerProgressBlocks: 50, + stackBlockNormalImageUrls: ['', '', '', '', '', ''], + stackBlockHeavyImageUrls: ['', '', '', '', '', ''], + /** โอกาสใช้รูป “ใหญ่” ต่อการปล่อยหนึ่งครั้ง (0–100) — ถ้าไม่มี URL ใหญ่ช่องนั้น = ใช้ปกติ */ + stackHeavyBlockPercent: 35, + /** ยิงยานอวกาศ / space_shooter: จำกัดเวลารอบ (วินาที) — 0 = ไคลเอนต์ใช้ค่าเริ่ม 90; แมป spaceShooterTimeSec > 0 ทับ */ + spaceShooterMissionTimeSec: 0, + spaceShooterShipImageUrls: ['', '', '', '', '', ''], + spaceShooterAsteroidSpriteUrls: [], + spaceShooterAsteroidExplodeFrameMs: 70, + /** ระยะห่างเกิดอุกาบาต (ms) — ค่าน้อย = ถี่ขึ้น; แมป spaceShooterAsteroidIntervalMs ≥200 ทับ */ + spaceShooterAsteroidIntervalMs: 1040, + spaceShooterShipDamageOverlayUrls: ['', '', ''], + /** balloon_boss / Mega Virus — 0 = ไคลเอนต์ใช้ 120 วิเมื่อแมปไม่ตั้ง balloonBossTimeSec */ + balloonBossMissionTimeSec: 0, + balloonBossBossImageUrl: '', + balloonBossPlayerBalloonImageUrls: ['', '', '', '', '', ''], + /** กรอบฟองรอบผู้เล่น / ลูกโป่ง fallback เริ่มต้น — Artboard 9 (วง cyan +หาง) */ + balloonBossPlayerBalloonFallbackUrl: '/Game/img/MegaVirus/Artboard 9.png', + }; +} + +function clampStackSwingHz(n) { + const v = Number(n); + if (Number.isNaN(v)) return STACK_SWING_HZ_DEFAULT; + return Math.max(0.08, Math.min(2.8, v)); +} + +/** ความกว้างบล็อก Stack เป็น tile — null = ให้ client คำนวณจากพื้นที่ลงบนแผนที่ */ +function clampStackBlockWidthTiles(n) { + if (n === null || n === undefined || n === '') return null; + const v = Number(n); + if (Number.isNaN(v) || v <= 0) return null; + return Math.round(Math.max(0.85, Math.min(3.2, v)) * 100) / 100; +} + +function clampStackTowerMissionTimeSec(n) { + const v = Number(n); + if (Number.isNaN(v) || v <= 0) return 90; + return Math.max(10, Math.min(7200, Math.floor(v))); +} + +function clampStackTeamMissesMax(n) { + const v = Number(n); + if (Number.isNaN(v)) return 3; + return Math.max(1, Math.min(20, Math.floor(v))); +} + +function clampStackTowerProgressBlocks(n) { + const v = Number(n); + if (Number.isNaN(v)) return 50; + return Math.max(1, Math.min(500, Math.floor(v))); +} + +/** Stack: รูปบล็อกต่อที่นั่ง 1–6 (ปกติ / ใหญ่) — ว่าง = ไคลเอนต์วาดสีเดิม */ +function normalizeStackBlockSixImageUrls(val) { + let arr = val; + if (typeof arr === 'string') { + try { + const p = JSON.parse(arr); + if (Array.isArray(p)) arr = p; + } catch (e) { + arr = arr.split(/[\n\r]+/); + } + } + if (!Array.isArray(arr)) arr = []; + const out = []; + for (let i = 0; i < 6; i++) { + out.push(sanitizeGauntletAssetUrl(arr[i])); + } + return out; +} + +function clampStackHeavyBlockPercent(n) { + const v = Number(n); + if (Number.isNaN(v)) return 35; + return Math.max(0, Math.min(100, Math.floor(v))); +} + +function clampJumpSurviveJumpHeightMult(n) { + const v = Number(n); + if (Number.isNaN(v)) return 1.5; + return Math.round(Math.max(0.5, Math.min(4, v)) * 100) / 100; +} + +/** กระโดดขึ้นแท่น / ภารกิจ Jumper: จำกัดเวลารอบ (วินาที) — 0 = ไม่ตั้งในไฟล์ ให้ไคลเอนต์ใช้ค่าเริ่ม 60; ค่าอื่น = 10–7200 */ +function clampJumpSurviveMissionTimeSec(n) { + const v = Number(n); + if (Number.isNaN(v) || v <= 0) return 0; + return Math.max(10, Math.min(7200, Math.floor(v))); +} + +/** กระโดดให้รอด: รูปแพลตฟอร์ม 3 ช่อง — url ว่าง = ไคลเอนต์วาด cyan; w/h ≤0 = ใช้ขนาดไทล์ตอนรัน */ +function normalizeJumpSurvivePlatformTilesFromTiming(val, legacyTileUrl) { + let arr = val; + if (typeof arr === 'string') { + try { + const p = JSON.parse(arr); + if (Array.isArray(p)) arr = p; + } catch (e) { + arr = []; + } + } + if (!Array.isArray(arr)) arr = []; + const legacy = typeof legacyTileUrl === 'string' ? sanitizeGauntletAssetUrl(legacyTileUrl) : ''; + const out = []; + for (let i = 0; i < 3; i++) { + const o = arr[i]; + const obj = o && typeof o === 'object' ? o : {}; + let url = sanitizeGauntletAssetUrl(typeof obj.url === 'string' ? obj.url : ''); + if (i === 0 && !url && legacy) url = legacy; + let ww = Number(obj.w); + let hh = Number(obj.h); + if (!Number.isFinite(ww) || ww <= 0) ww = 0; + else ww = Math.max(8, Math.min(4096, Math.round(ww))); + if (!Number.isFinite(hh) || hh <= 0) hh = 0; + else hh = Math.max(8, Math.min(4096, Math.round(hh))); + out.push({ url, w: ww, h: hh }); + } + return out; +} + +function clampSpaceShooterMissionTimeSec(n) { + const v = Number(n); + if (Number.isNaN(v) || v <= 0) return 0; + return Math.max(10, Math.min(7200, Math.floor(v))); +} + +/** รูปยาน space_shooter ช่อง 1–6 (ตรงกับ spawn slot บนแมป) — ว่าง = ไคลเอนต์วาดยานเวกเตอร์ */ +function normalizeSpaceShooterShipImageUrls(val) { + let arr = val; + if (typeof arr === 'string') { + try { + const p = JSON.parse(arr); + if (Array.isArray(p)) arr = p; + } catch (e) { + arr = arr.split(/[\n\r]+/); + } + } + if (!Array.isArray(arr)) arr = []; + const out = []; + for (let i = 0; i < 6; i++) { + out.push(sanitizeGauntletAssetUrl(arr[i])); + } + return out; +} + +/** อุกาบาต space_shooter: บรรทัดแรก = ตอนตก, ถัดไป = เฟรมแตก (สูงสุด 32 URL) */ +function normalizeSpaceShooterAsteroidSpriteUrls(val) { + let arr = val; + if (typeof arr === 'string') { + try { + const p = JSON.parse(arr); + if (Array.isArray(p)) arr = p; + } catch (e) { + arr = arr.split(/[\n\r]+/); + } + } + if (!Array.isArray(arr)) arr = []; + const out = []; + for (let i = 0; i < arr.length && out.length < 32; i++) { + const u = sanitizeGauntletAssetUrl(arr[i]); + if (u) out.push(u); + } + return out; +} + +function clampSpaceShooterAsteroidExplodeFrameMs(n) { + const v = Number(n); + if (Number.isNaN(v)) return 70; + return Math.max(30, Math.min(500, Math.round(v))); +} + +const SPACE_SHOOTER_ASTEROID_INTERVAL_MS_DEFAULT = 1040; +function clampSpaceShooterAsteroidIntervalMs(n) { + const v = Number(n); + if (Number.isNaN(v) || v <= 0) return SPACE_SHOOTER_ASTEROID_INTERVAL_MS_DEFAULT; + return Math.max(200, Math.min(10000, Math.floor(v))); +} + +/** รูปทับยานเมื่อชนอุกาบาต ครั้งที่ 1–3 (ภารกิจ Violent Crime) */ +function normalizeSpaceShooterShipDamageOverlayUrls(val) { + let arr = val; + if (typeof arr === 'string') { + try { + const p = JSON.parse(arr); + if (Array.isArray(p)) arr = p; + } catch (e) { + arr = arr.split(/[\n\r]+/); + } + } + if (!Array.isArray(arr)) arr = []; + const out = ['', '', '']; + for (let i = 0; i < 3; i++) { + out[i] = sanitizeGauntletAssetUrl(arr[i]); + } + return out; +} + +function clampGauntletTickMs(n) { + const v = Number(n); + if (Number.isNaN(v)) return GAUNTLET_DEFAULT_TICK_MS; + return Math.max(80, Math.min(800, Math.floor(v))); +} + +function clampGauntletJumpTicks(n) { + const v = Number(n); + if (Number.isNaN(v)) return GAUNTLET_DEFAULT_JUMP_TICKS; + return Math.max(4, Math.min(40, Math.floor(v))); +} + +/** 0 = ไม่จำกัดเวลา · ค่าอื่น = วินาที (สูงสุด 2 ชม.) */ +function clampGauntletTimeLimitSec(n) { + const v = Number(n); + if (Number.isNaN(v) || v <= 0) return 0; + return Math.max(10, Math.min(7200, Math.floor(v))); +} + +function defaultGauntletVisuals() { + return { + gauntletLaneImageUrls: [], + gauntletLaserTopUrl: '', + gauntletLaserBottomUrl: '', + gauntletLaserLineUrl: '', + gauntletLaserFillColor: 'rgba(140,230,255,0.42)', + gauntletLaserStrokeColor: 'rgba(255,220,255,0.9)', + gauntletLaserLineWidthPx: 2, + }; +} + +/** ช่องว่างใน path (เช่น Artboard 9.png) — encode เป็น %20 ให้ nginx/เบราว์เซอร์โหลดได้ */ +function encodeSpacesInAssetUrlPath(t) { + const s = String(t || ''); + const q = s.indexOf('?'); + const pathPart = q === -1 ? s : s.slice(0, q); + const query = q === -1 ? '' : s.slice(q); + return pathPart.replace(/ /g, '%20') + query; +} + +/** รูปเสิร์ฟที่ `/Game/img/...` (alias ไป `public/img`) — อย่าใส่ `/Game/public/img/` จาก path บนดิสก์ */ +function normalizeGameAssetUrlForWeb(t) { + let s = String(t || ''); + s = s.replace(/^\/Game\/public\/img\//i, '/Game/img/'); + s = s.replace(/^Game\/public\/img\//i, 'Game/img/'); + return s; +} + +/** ลด %2520 → %20 → space (สูงสุด 2 รอบ) — กันเข้ารหัสซ้ำจากบันทึก/คัดลอก URL */ +function decodeAssetUrlPercentRuns(t) { + let s = String(t || ''); + const q = s.indexOf('?'); + let base = q === -1 ? s : s.slice(0, q); + const query = q === -1 ? '' : s.slice(q); + for (let i = 0; i < 2; i += 1) { + try { + const d = decodeURIComponent(base); + if (d === base) break; + base = d; + } catch (e) { + break; + } + } + return base + query; +} + +/** ช่อง Admin มักใส่แค่ …/Artboard ลืม 9.png — ชี้ไป Artboard%209.png */ +function fixBareMegaVirusArtboardUrl(t) { + const s = String(t || '').trim().replace(/\/+$/, ''); + if (/^\/Game\/img\/MegaVirus\/Artboard$/i.test(s)) return '/Game/img/MegaVirus/Artboard%209.png'; + if (/^Game\/img\/MegaVirus\/Artboard$/i.test(s)) return '/Game/img/MegaVirus/Artboard%209.png'; + return t; +} + +function sanitizeGauntletAssetUrl(s) { + const t = fixBareMegaVirusArtboardUrl( + normalizeGameAssetUrlForWeb(decodeAssetUrlPercentRuns(String(s || '').trim())).replace(/\/+$/, '') + ); + if (!t || t.length > 500) return ''; + /** ห้ามแท็บ/บรรทัดใหม่/ตัวอักษรอันตราย — อนุญาตช่องว่าง (U+0020) ในชื่อไฟล์ */ + if (/[\t\n\r\x00-\x08\x0b\x0c\x0e-\x1f<>"'`]/.test(t)) return ''; + /** placeholder จาก Admin เช่น /Game/img/....png — ไม่ใช่ path จริง */ + if (/\.{4,}/.test(t)) return ''; + if (/^https?:\/\//i.test(t)) return encodeSpacesInAssetUrlPath(t); + if (t.startsWith('/')) return encodeSpacesInAssetUrlPath(t); + /** Admin มักใส่แบบไม่มี slash นำหน้า — ให้เทียบเท่า /Game/... บน nginx */ + if (/^Game\//i.test(t)) return encodeSpacesInAssetUrlPath(`/${t.replace(/^\/+/, '')}`); + return ''; +} + +function clampGauntletLaserColor(s, def) { + const t = String(s || '').trim().slice(0, 100); + if (!t) return def; + if (/[<>"'`]/.test(t)) return def; + return t; +} + +function clampGauntletLaserLineWidthPx(n) { + const v = Number(n); + if (Number.isNaN(v)) return 2; + return Math.max(0, Math.min(24, Math.round(v))); +} + +function normalizeGauntletVisualsFromRequest(d) { + const def = defaultGauntletVisuals(); + if (!d || typeof d !== 'object') return { ...def }; + let laneArr = d.gauntletLaneImageUrls; + if (typeof laneArr === 'string') laneArr = laneArr.split(/[\n\r]+/); + if (!Array.isArray(laneArr)) laneArr = []; + const gauntletLaneImageUrls = []; + for (let i = 0; i < laneArr.length && gauntletLaneImageUrls.length < 24; i++) { + const u = sanitizeGauntletAssetUrl(laneArr[i]); + if (u) gauntletLaneImageUrls.push(u); + } + return { + gauntletLaneImageUrls, + gauntletLaserTopUrl: sanitizeGauntletAssetUrl(d.gauntletLaserTopUrl), + gauntletLaserBottomUrl: sanitizeGauntletAssetUrl(d.gauntletLaserBottomUrl), + gauntletLaserLineUrl: sanitizeGauntletAssetUrl(d.gauntletLaserLineUrl), + gauntletLaserFillColor: clampGauntletLaserColor(d.gauntletLaserFillColor, def.gauntletLaserFillColor), + gauntletLaserStrokeColor: clampGauntletLaserColor(d.gauntletLaserStrokeColor, def.gauntletLaserStrokeColor), + gauntletLaserLineWidthPx: clampGauntletLaserLineWidthPx(d.gauntletLaserLineWidthPx), + }; +} + +function getGauntletVisualsForClient() { + const r = runtimeGameTiming; + const d = defaultGauntletVisuals(); + return { + gauntletLaneImageUrls: Array.isArray(r.gauntletLaneImageUrls) ? r.gauntletLaneImageUrls : d.gauntletLaneImageUrls, + gauntletLaserTopUrl: r.gauntletLaserTopUrl || '', + gauntletLaserBottomUrl: r.gauntletLaserBottomUrl || '', + gauntletLaserLineUrl: r.gauntletLaserLineUrl || '', + gauntletLaserFillColor: r.gauntletLaserFillColor != null ? r.gauntletLaserFillColor : d.gauntletLaserFillColor, + gauntletLaserStrokeColor: r.gauntletLaserStrokeColor != null ? r.gauntletLaserStrokeColor : d.gauntletLaserStrokeColor, + gauntletLaserLineWidthPx: r.gauntletLaserLineWidthPx != null ? r.gauntletLaserLineWidthPx : d.gauntletLaserLineWidthPx, + }; +} + +function loadGameTiming() { + try { + if (fs.existsSync(GAME_TIMING_PATH)) { + const j = JSON.parse(fs.readFileSync(GAME_TIMING_PATH, 'utf8')); + const vis = normalizeGauntletVisualsFromRequest(j); + return { + gauntletTickMs: clampGauntletTickMs(j.gauntletTickMs), + gauntletJumpTicks: clampGauntletJumpTicks(j.gauntletJumpTicks), + gauntletTimeLimitSec: clampGauntletTimeLimitSec(j.gauntletTimeLimitSec), + ...vis, + stackSwingHz: Object.prototype.hasOwnProperty.call(j, 'stackSwingHz') + ? clampStackSwingHz(j.stackSwingHz) + : STACK_SWING_HZ_DEFAULT, + stackBlockWidthTiles: Object.prototype.hasOwnProperty.call(j, 'stackBlockWidthTiles') + ? clampStackBlockWidthTiles(j.stackBlockWidthTiles) + : null, + stackTowerMissionTimeSec: Object.prototype.hasOwnProperty.call(j, 'stackTowerMissionTimeSec') + ? clampStackTowerMissionTimeSec(j.stackTowerMissionTimeSec) + : 90, + stackTeamMissesMax: Object.prototype.hasOwnProperty.call(j, 'stackTeamMissesMax') + ? clampStackTeamMissesMax(j.stackTeamMissesMax) + : 3, + stackTowerProgressBlocks: Object.prototype.hasOwnProperty.call(j, 'stackTowerProgressBlocks') + ? clampStackTowerProgressBlocks(j.stackTowerProgressBlocks) + : 50, + stackBlockNormalImageUrls: normalizeStackBlockSixImageUrls(j.stackBlockNormalImageUrls), + stackBlockHeavyImageUrls: normalizeStackBlockSixImageUrls(j.stackBlockHeavyImageUrls), + stackHeavyBlockPercent: Object.prototype.hasOwnProperty.call(j, 'stackHeavyBlockPercent') + ? clampStackHeavyBlockPercent(j.stackHeavyBlockPercent) + : 35, + jumpSurviveJumpHeightMult: Object.prototype.hasOwnProperty.call(j, 'jumpSurviveJumpHeightMult') + ? clampJumpSurviveJumpHeightMult(j.jumpSurviveJumpHeightMult) + : 1.5, + jumpSurviveMissionTimeSec: Object.prototype.hasOwnProperty.call(j, 'jumpSurviveMissionTimeSec') + ? clampJumpSurviveMissionTimeSec(j.jumpSurviveMissionTimeSec) + : 0, + jumpSurvivePlatformTiles: normalizeJumpSurvivePlatformTilesFromTiming( + j.jumpSurvivePlatformTiles, + j.jumpSurvivePlatformTileUrl, + ), + spaceShooterMissionTimeSec: Object.prototype.hasOwnProperty.call(j, 'spaceShooterMissionTimeSec') + ? clampSpaceShooterMissionTimeSec(j.spaceShooterMissionTimeSec) + : 0, + spaceShooterShipImageUrls: normalizeSpaceShooterShipImageUrls(j.spaceShooterShipImageUrls), + spaceShooterAsteroidSpriteUrls: normalizeSpaceShooterAsteroidSpriteUrls(j.spaceShooterAsteroidSpriteUrls), + spaceShooterAsteroidExplodeFrameMs: Object.prototype.hasOwnProperty.call(j, 'spaceShooterAsteroidExplodeFrameMs') + ? clampSpaceShooterAsteroidExplodeFrameMs(j.spaceShooterAsteroidExplodeFrameMs) + : 70, + spaceShooterAsteroidIntervalMs: Object.prototype.hasOwnProperty.call(j, 'spaceShooterAsteroidIntervalMs') + ? clampSpaceShooterAsteroidIntervalMs(j.spaceShooterAsteroidIntervalMs) + : SPACE_SHOOTER_ASTEROID_INTERVAL_MS_DEFAULT, + spaceShooterShipDamageOverlayUrls: normalizeSpaceShooterShipDamageOverlayUrls(j.spaceShooterShipDamageOverlayUrls), + balloonBossMissionTimeSec: Object.prototype.hasOwnProperty.call(j, 'balloonBossMissionTimeSec') + ? clampSpaceShooterMissionTimeSec(j.balloonBossMissionTimeSec) + : 0, + balloonBossBossImageUrl: Object.prototype.hasOwnProperty.call(j, 'balloonBossBossImageUrl') + ? sanitizeGauntletAssetUrl(j.balloonBossBossImageUrl) + : '', + balloonBossPlayerBalloonImageUrls: normalizeStackBlockSixImageUrls(j.balloonBossPlayerBalloonImageUrls), + balloonBossPlayerBalloonFallbackUrl: Object.prototype.hasOwnProperty.call(j, 'balloonBossPlayerBalloonFallbackUrl') + ? sanitizeGauntletAssetUrl(j.balloonBossPlayerBalloonFallbackUrl) + : '', + }; + } + } catch (e) { console.error('loadGameTiming', e.message); } + return defaultGameTiming(); +} + +let runtimeGameTiming = loadGameTiming(); + +function getGauntletTickMs() { + return runtimeGameTiming.gauntletTickMs; +} + +function getGauntletJumpTicks() { + return runtimeGameTiming.gauntletJumpTicks; +} + +function getGauntletTimeLimitSec() { + return runtimeGameTiming.gauntletTimeLimitSec || 0; +} + +function saveGameTimingToFile(d) { + try { + const dir = path.dirname(GAME_TIMING_PATH); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + const prev = { ...runtimeGameTiming }; + const vis = normalizeGauntletVisualsFromRequest(d); + const stackHz = Object.prototype.hasOwnProperty.call(d, 'stackSwingHz') + ? clampStackSwingHz(d.stackSwingHz) + : clampStackSwingHz(prev.stackSwingHz != null ? prev.stackSwingHz : STACK_SWING_HZ_DEFAULT); + const stackBlockW = Object.prototype.hasOwnProperty.call(d, 'stackBlockWidthTiles') + ? clampStackBlockWidthTiles(d.stackBlockWidthTiles) + : (Object.prototype.hasOwnProperty.call(prev, 'stackBlockWidthTiles') + ? prev.stackBlockWidthTiles + : null); + const prevMult = Object.prototype.hasOwnProperty.call(prev, 'jumpSurviveJumpHeightMult') + ? prev.jumpSurviveJumpHeightMult + : 1.5; + const jumpMult = Object.prototype.hasOwnProperty.call(d, 'jumpSurviveJumpHeightMult') + ? clampJumpSurviveJumpHeightMult(d.jumpSurviveJumpHeightMult) + : clampJumpSurviveJumpHeightMult(prevMult); + const prevJumpMissionSec = Object.prototype.hasOwnProperty.call(prev, 'jumpSurviveMissionTimeSec') + ? prev.jumpSurviveMissionTimeSec + : 0; + const jumpMissionSec = Object.prototype.hasOwnProperty.call(d, 'jumpSurviveMissionTimeSec') + ? clampJumpSurviveMissionTimeSec(d.jumpSurviveMissionTimeSec) + : clampJumpSurviveMissionTimeSec(prevJumpMissionSec); + const prevJumpTiles = normalizeJumpSurvivePlatformTilesFromTiming( + prev.jumpSurvivePlatformTiles, + prev.jumpSurvivePlatformTileUrl, + ); + let jumpSurvivePlatformTiles; + if (Object.prototype.hasOwnProperty.call(d, 'jumpSurvivePlatformTiles')) { + jumpSurvivePlatformTiles = normalizeJumpSurvivePlatformTilesFromTiming( + d.jumpSurvivePlatformTiles, + d.jumpSurvivePlatformTileUrl, + ); + } else if (Object.prototype.hasOwnProperty.call(d, 'jumpSurvivePlatformTileUrl')) { + jumpSurvivePlatformTiles = normalizeJumpSurvivePlatformTilesFromTiming( + [ + { url: d.jumpSurvivePlatformTileUrl, w: 0, h: 0 }, + prevJumpTiles[1], + prevJumpTiles[2], + ], + '', + ); + } else { + jumpSurvivePlatformTiles = prevJumpTiles; + } + const prevSpaceShooterMissionSec = Object.prototype.hasOwnProperty.call(prev, 'spaceShooterMissionTimeSec') + ? prev.spaceShooterMissionTimeSec + : 0; + const spaceShooterMissionSec = Object.prototype.hasOwnProperty.call(d, 'spaceShooterMissionTimeSec') + ? clampSpaceShooterMissionTimeSec(d.spaceShooterMissionTimeSec) + : clampSpaceShooterMissionTimeSec(prevSpaceShooterMissionSec); + const prevShipUrls = Array.isArray(prev.spaceShooterShipImageUrls) + ? normalizeSpaceShooterShipImageUrls(prev.spaceShooterShipImageUrls) + : normalizeSpaceShooterShipImageUrls([]); + const spaceShooterShipImageUrls = Object.prototype.hasOwnProperty.call(d, 'spaceShooterShipImageUrls') + ? normalizeSpaceShooterShipImageUrls(d.spaceShooterShipImageUrls) + : prevShipUrls; + const prevAstUrls = Array.isArray(prev.spaceShooterAsteroidSpriteUrls) + ? normalizeSpaceShooterAsteroidSpriteUrls(prev.spaceShooterAsteroidSpriteUrls) + : []; + const spaceShooterAsteroidSpriteUrls = Object.prototype.hasOwnProperty.call(d, 'spaceShooterAsteroidSpriteUrls') + ? normalizeSpaceShooterAsteroidSpriteUrls(d.spaceShooterAsteroidSpriteUrls) + : prevAstUrls; + const prevAstFms = Object.prototype.hasOwnProperty.call(prev, 'spaceShooterAsteroidExplodeFrameMs') + ? prev.spaceShooterAsteroidExplodeFrameMs + : 70; + const spaceShooterAsteroidExplodeFrameMs = Object.prototype.hasOwnProperty.call(d, 'spaceShooterAsteroidExplodeFrameMs') + ? clampSpaceShooterAsteroidExplodeFrameMs(d.spaceShooterAsteroidExplodeFrameMs) + : clampSpaceShooterAsteroidExplodeFrameMs(prevAstFms); + const prevAstIv = Object.prototype.hasOwnProperty.call(prev, 'spaceShooterAsteroidIntervalMs') + ? prev.spaceShooterAsteroidIntervalMs + : SPACE_SHOOTER_ASTEROID_INTERVAL_MS_DEFAULT; + const spaceShooterAsteroidIntervalMs = Object.prototype.hasOwnProperty.call(d, 'spaceShooterAsteroidIntervalMs') + ? clampSpaceShooterAsteroidIntervalMs(d.spaceShooterAsteroidIntervalMs) + : clampSpaceShooterAsteroidIntervalMs(prevAstIv); + const prevDmg = Array.isArray(prev.spaceShooterShipDamageOverlayUrls) + ? normalizeSpaceShooterShipDamageOverlayUrls(prev.spaceShooterShipDamageOverlayUrls) + : normalizeSpaceShooterShipDamageOverlayUrls([]); + const spaceShooterShipDamageOverlayUrls = Object.prototype.hasOwnProperty.call(d, 'spaceShooterShipDamageOverlayUrls') + ? normalizeSpaceShooterShipDamageOverlayUrls(d.spaceShooterShipDamageOverlayUrls) + : prevDmg; + const prevBbMission = Object.prototype.hasOwnProperty.call(prev, 'balloonBossMissionTimeSec') + ? prev.balloonBossMissionTimeSec + : 0; + const balloonBossMissionTimeSec = Object.prototype.hasOwnProperty.call(d, 'balloonBossMissionTimeSec') + ? clampSpaceShooterMissionTimeSec(d.balloonBossMissionTimeSec) + : clampSpaceShooterMissionTimeSec(prevBbMission); + const prevBbBoss = Object.prototype.hasOwnProperty.call(prev, 'balloonBossBossImageUrl') + ? sanitizeGauntletAssetUrl(prev.balloonBossBossImageUrl) + : ''; + const balloonBossBossImageUrl = Object.prototype.hasOwnProperty.call(d, 'balloonBossBossImageUrl') + ? sanitizeGauntletAssetUrl(d.balloonBossBossImageUrl) + : prevBbBoss; + const prevBbBalloons = Array.isArray(prev.balloonBossPlayerBalloonImageUrls) + ? normalizeStackBlockSixImageUrls(prev.balloonBossPlayerBalloonImageUrls) + : normalizeStackBlockSixImageUrls([]); + const balloonBossPlayerBalloonImageUrls = Object.prototype.hasOwnProperty.call(d, 'balloonBossPlayerBalloonImageUrls') + ? normalizeStackBlockSixImageUrls(d.balloonBossPlayerBalloonImageUrls) + : prevBbBalloons; + const prevBbFb = Object.prototype.hasOwnProperty.call(prev, 'balloonBossPlayerBalloonFallbackUrl') + ? sanitizeGauntletAssetUrl(prev.balloonBossPlayerBalloonFallbackUrl) + : ''; + const balloonBossPlayerBalloonFallbackUrl = Object.prototype.hasOwnProperty.call(d, 'balloonBossPlayerBalloonFallbackUrl') + ? sanitizeGauntletAssetUrl(d.balloonBossPlayerBalloonFallbackUrl) + : prevBbFb; + const prevStackTowerSec = Object.prototype.hasOwnProperty.call(prev, 'stackTowerMissionTimeSec') + ? prev.stackTowerMissionTimeSec + : 90; + const stackTowerMissionTimeSec = Object.prototype.hasOwnProperty.call(d, 'stackTowerMissionTimeSec') + ? clampStackTowerMissionTimeSec(d.stackTowerMissionTimeSec) + : clampStackTowerMissionTimeSec(prevStackTowerSec); + const prevStackMisses = Object.prototype.hasOwnProperty.call(prev, 'stackTeamMissesMax') + ? prev.stackTeamMissesMax + : 3; + const stackTeamMissesMax = Object.prototype.hasOwnProperty.call(d, 'stackTeamMissesMax') + ? clampStackTeamMissesMax(d.stackTeamMissesMax) + : clampStackTeamMissesMax(prevStackMisses); + const prevStackProgBlocks = Object.prototype.hasOwnProperty.call(prev, 'stackTowerProgressBlocks') + ? prev.stackTowerProgressBlocks + : 50; + const stackTowerProgressBlocks = Object.prototype.hasOwnProperty.call(d, 'stackTowerProgressBlocks') + ? clampStackTowerProgressBlocks(d.stackTowerProgressBlocks) + : clampStackTowerProgressBlocks(prevStackProgBlocks); + const prevStackNorm = Array.isArray(prev.stackBlockNormalImageUrls) + ? normalizeStackBlockSixImageUrls(prev.stackBlockNormalImageUrls) + : normalizeStackBlockSixImageUrls([]); + const prevStackHeavy = Array.isArray(prev.stackBlockHeavyImageUrls) + ? normalizeStackBlockSixImageUrls(prev.stackBlockHeavyImageUrls) + : normalizeStackBlockSixImageUrls([]); + const stackBlockNormalImageUrls = Object.prototype.hasOwnProperty.call(d, 'stackBlockNormalImageUrls') + ? normalizeStackBlockSixImageUrls(d.stackBlockNormalImageUrls) + : prevStackNorm; + const stackBlockHeavyImageUrls = Object.prototype.hasOwnProperty.call(d, 'stackBlockHeavyImageUrls') + ? normalizeStackBlockSixImageUrls(d.stackBlockHeavyImageUrls) + : prevStackHeavy; + const prevHeavyPct = Object.prototype.hasOwnProperty.call(prev, 'stackHeavyBlockPercent') + ? clampStackHeavyBlockPercent(prev.stackHeavyBlockPercent) + : 35; + const stackHeavyBlockPercent = Object.prototype.hasOwnProperty.call(d, 'stackHeavyBlockPercent') + ? clampStackHeavyBlockPercent(d.stackHeavyBlockPercent) + : prevHeavyPct; + const out = { + gauntletTickMs: clampGauntletTickMs(d.gauntletTickMs), + gauntletJumpTicks: clampGauntletJumpTicks(d.gauntletJumpTicks), + gauntletTimeLimitSec: clampGauntletTimeLimitSec(d.gauntletTimeLimitSec), + ...vis, + stackSwingHz: stackHz, + stackBlockWidthTiles: stackBlockW, + stackTowerMissionTimeSec, + stackTeamMissesMax, + stackTowerProgressBlocks, + stackBlockNormalImageUrls, + stackBlockHeavyImageUrls, + stackHeavyBlockPercent, + jumpSurviveJumpHeightMult: jumpMult, + jumpSurviveMissionTimeSec: jumpMissionSec, + jumpSurvivePlatformTiles, + spaceShooterMissionTimeSec: spaceShooterMissionSec, + spaceShooterShipImageUrls, + spaceShooterAsteroidSpriteUrls, + spaceShooterAsteroidExplodeFrameMs, + spaceShooterAsteroidIntervalMs, + spaceShooterShipDamageOverlayUrls, + balloonBossMissionTimeSec, + balloonBossBossImageUrl, + balloonBossPlayerBalloonImageUrls, + balloonBossPlayerBalloonFallbackUrl, + }; + fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8'); + runtimeGameTiming = out; + rescheduleAllGauntletTickers(); + return { ok: true, ...out }; + } catch (e) { + console.error('saveGameTimingToFile', e.message); + let err = 'บันทึกไม่ได้'; + if (e && e.code === 'EACCES') { + err = 'ไม่มีสิทธิ์เขียนไฟล์ game-timing.json (ให้ chown เป็น user ที่รันเกม เช่น www-data)'; + } else if (e && e.message) err = 'บันทึกไม่ได้: ' + e.message; + return { ok: false, error: err }; + } +} + +function migrateGauntletAssetsFromLegacyIfNeeded() { + try { + if (!fs.existsSync(GAUNTLET_ASSETS_DIR_LEGACY)) return; + if (!fs.existsSync(GAUNTLET_ASSETS_DIR)) fs.mkdirSync(GAUNTLET_ASSETS_DIR, { recursive: true }); + const names = fs.readdirSync(GAUNTLET_ASSETS_DIR_LEGACY); + names.forEach((f) => { + if (!safeGauntletStoredFilename(f)) return; + const src = path.join(GAUNTLET_ASSETS_DIR_LEGACY, f); + const dest = path.join(GAUNTLET_ASSETS_DIR, f); + if (fs.existsSync(dest)) return; + try { + fs.copyFileSync(src, dest); + console.log('[gauntlet-assets] migrated', f, 'from data/ to public/img/gauntlet-assets/'); + } catch (e) { + console.error('[gauntlet-assets] migrate failed', f, e && e.message); + } + }); + } catch (e) { + console.error('migrateGauntletAssetsFromLegacyIfNeeded', e && e.message); + } +} + +function ensureGauntletAssetsDir() { + if (!fs.existsSync(GAUNTLET_ASSETS_DIR)) fs.mkdirSync(GAUNTLET_ASSETS_DIR, { recursive: true }); + migrateGauntletAssetsFromLegacyIfNeeded(); +} + +function loadGauntletAssetsMeta() { + try { + if (fs.existsSync(GAUNTLET_ASSETS_META_PATH)) { + const j = JSON.parse(fs.readFileSync(GAUNTLET_ASSETS_META_PATH, 'utf8')); + return j && typeof j === 'object' ? j : {}; + } + } catch (e) { console.error('loadGauntletAssetsMeta', e.message); } + return {}; +} + +function saveGauntletAssetsMeta(meta) { + const dir = path.dirname(GAUNTLET_ASSETS_META_PATH); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(GAUNTLET_ASSETS_META_PATH, JSON.stringify(meta, null, 2), 'utf8'); +} + +function safeGauntletStoredFilename(name) { + const base = path.basename(String(name || '')); + if (!/^gauntlet-[a-f0-9]{16}\.(png|jpg|jpeg|gif|webp)$/i.test(base)) return null; + return base; +} + +function safeQuizCarryPlaqueStoredFilename(name) { + const base = path.basename(String(name || '')); + if (!/^qcarry-[a-f0-9]{16}\.(png|jpg|jpeg|gif|webp)$/i.test(base)) return null; + return base; +} + +function ensureQuizCarryPlaqueAssetsDir() { + if (!fs.existsSync(QUIZ_CARRY_PLAQUE_ASSETS_DIR)) fs.mkdirSync(QUIZ_CARRY_PLAQUE_ASSETS_DIR, { recursive: true }); +} + +function gauntletAssetMimeByExt(ext) { + const e = String(ext || '').toLowerCase(); + if (e === '.png') return 'image/png'; + if (e === '.jpg' || e === '.jpeg') return 'image/jpeg'; + if (e === '.gif') return 'image/gif'; + if (e === '.webp') return 'image/webp'; + return 'application/octet-stream'; +} + +function listGauntletAssetsApi() { + ensureGauntletAssetsDir(); + const meta = loadGauntletAssetsMeta(); + let files = []; + try { + files = fs.readdirSync(GAUNTLET_ASSETS_DIR); + } catch (e) { + return []; + } + return files + .filter((f) => safeGauntletStoredFilename(f)) + .map((f) => { + const fp = path.join(GAUNTLET_ASSETS_DIR, f); + let st; + try { + st = fs.statSync(fp); + } catch (e) { + return null; + } + const entry = meta[f]; + const label = entry && typeof entry.label === 'string' ? entry.label.slice(0, 120) : ''; + return { + filename: f, + url: BASE_PATH + '/img/gauntlet-assets/' + f, + label: label || f, + bytes: st.size, + mtime: st.mtimeMs, + }; + }) + .filter(Boolean) + .sort((a, b) => b.mtime - a.mtime); +} + +function saveQuizSettings(d) { + try { + const prev = loadQuizSettings(); + const dir = path.dirname(QUIZ_SETTINGS_PATH); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + const readMs = d.readMs != null ? clampQuizMs(d.readMs, prev.readMs) : prev.readMs; + const answerMs = d.answerMs != null ? clampQuizMs(d.answerMs, prev.answerMs) : prev.answerMs; + const betweenMs = d.betweenMs != null ? clampQuizBetweenMs(d.betweenMs, prev.betweenMs) : prev.betweenMs; + const carryReadMs = d.carryReadMs != null ? clampQuizBetweenMs(d.carryReadMs, prev.carryReadMs) : prev.carryReadMs; + const carryAnswerMs = d.carryAnswerMs != null ? clampQuizMs(d.carryAnswerMs, prev.carryAnswerMs) : prev.carryAnswerMs; + const carrySessionLength = d.carrySessionLength != null + ? clampCarrySessionLength(d.carrySessionLength, prev.carrySessionLength) + : prev.carrySessionLength; + const questions = Array.isArray(d.questions) + ? (d.questions || []) + .filter((q) => q && String(q.text || '').trim()) + .map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue })) + : prev.questions; + const carryQuestions = Array.isArray(d.carryQuestions) + ? sanitizeCarryQuestions(d.carryQuestions) + : prev.carryQuestions; + const battleQuizMcq = Array.isArray(d.battleQuizMcq) + ? sanitizeBattleQuizMcq(d.battleQuizMcq) + : (prev.battleQuizMcq || []); + const carryMapPanelTheme = d.carryMapPanelTheme != null && typeof d.carryMapPanelTheme === 'object' + ? sanitizeCarryMapPanelTheme(d.carryMapPanelTheme) + : prev.carryMapPanelTheme; + const quizMapPanelTheme = d.quizMapPanelTheme != null && typeof d.quizMapPanelTheme === 'object' + ? sanitizeCarryMapPanelTheme(d.quizMapPanelTheme) + : prev.quizMapPanelTheme; + const carryEmbedCountdownTheme = d.carryEmbedCountdownTheme != null && typeof d.carryEmbedCountdownTheme === 'object' + ? sanitizeCarryEmbedCountdownTheme(d.carryEmbedCountdownTheme) + : prev.carryEmbedCountdownTheme; + let carryChoicePlaqueThemes = prev.carryChoicePlaqueThemes; + if (Array.isArray(d.carryChoicePlaqueThemes) && d.carryChoicePlaqueThemes.length) { + carryChoicePlaqueThemes = sanitizeCarryChoicePlaqueThemes(d.carryChoicePlaqueThemes); + } else if (d.carryChoicePlaqueTheme != null && typeof d.carryChoicePlaqueTheme === 'object') { + carryChoicePlaqueThemes = sanitizeCarryChoicePlaqueThemes(d.carryChoicePlaqueTheme); + } + if (!Array.isArray(carryChoicePlaqueThemes) || !carryChoicePlaqueThemes.length) { + carryChoicePlaqueThemes = sanitizeCarryChoicePlaqueThemes(prev.carryChoicePlaqueTheme); + } + const carryChoicePlaqueTheme = carryChoicePlaqueThemes[0]; + const prevScale = prev.carryChoicePlaqueMapScale != null + ? clampCarryChoicePlaqueMapScale(prev.carryChoicePlaqueMapScale, 1.25) + : 1.25; + const carryChoicePlaqueMapScale = d.carryChoicePlaqueMapScale != null + ? clampCarryChoicePlaqueMapScale(d.carryChoicePlaqueMapScale, prevScale) + : prevScale; + const prevWalkId = sanitizeCarryWalkSpeedMultForMapId(prev.carryWalkSpeedMultForMapId); + const prevWalkMult = prev.carryWalkSpeedMult != null ? clampCarryWalkSpeedMultSetting(prev.carryWalkSpeedMult) : null; + let carryWalkSpeedMultForMapId = d.carryWalkSpeedMultForMapId !== undefined + ? sanitizeCarryWalkSpeedMultForMapId(d.carryWalkSpeedMultForMapId) + : prevWalkId; + let carryWalkSpeedMult = d.carryWalkSpeedMult !== undefined ? clampCarryWalkSpeedMultSetting(d.carryWalkSpeedMult) : prevWalkMult; + if (!carryWalkSpeedMultForMapId) { + carryWalkSpeedMult = null; + } else if (carryWalkSpeedMult == null) { + carryWalkSpeedMult = clampCarryWalkSpeedMultSetting(1.42); + } + const quizRoundQuestionCount = d.quizRoundQuestionCount != null + ? clampQuizRoundQuestionCount(d.quizRoundQuestionCount, prev.quizRoundQuestionCount) + : prev.quizRoundQuestionCount; + const specialQuizSets = d.specialQuizSets != null + ? sanitizeSpecialQuizSets(d.specialQuizSets) + : sanitizeSpecialQuizSets(prev.specialQuizSets); + const out = { + readMs, + answerMs, + betweenMs, + carryReadMs, + carryAnswerMs, + carrySessionLength, + carryMapPanelTheme, + quizMapPanelTheme, + carryEmbedCountdownTheme, + carryChoicePlaqueThemes, + carryChoicePlaqueTheme, + carryChoicePlaqueMapScale, + carryWalkSpeedMultForMapId, + carryWalkSpeedMult, + quizRoundQuestionCount, + questions, + carryQuestions, + battleQuizMcq, + specialQuizSets, + }; + fs.writeFileSync(QUIZ_SETTINGS_PATH, JSON.stringify(out, null, 2), 'utf8'); + return { + ok: true, + battleQuizMcqSaved: (out.battleQuizMcq || []).length, + carryQuestionsSaved: (out.carryQuestions || []).length, + specialQuizSets: out.specialQuizSets, + carryMapPanelTheme: out.carryMapPanelTheme, + quizMapPanelTheme: out.quizMapPanelTheme, + carryEmbedCountdownTheme: out.carryEmbedCountdownTheme, + carryChoicePlaqueThemes: out.carryChoicePlaqueThemes, + carryChoicePlaqueTheme: out.carryChoicePlaqueTheme, + carryChoicePlaqueMapScale: out.carryChoicePlaqueMapScale, + }; + } catch (e) { + console.error('saveQuizSettings', e.message); + let err = 'บันทึกไม่ได้'; + if (e && e.code === 'EACCES') { + err = 'ไม่มีสิทธิ์เขียนไฟล์ quiz-settings.json (ให้ chown เป็น user ที่รันเกม เช่น www-data)'; + } else if (e && e.message) err = 'บันทึกไม่ได้: ' + e.message; + return { ok: false, error: err }; + } +} + +function getQuizQuestionPool(md) { + const g = loadQuizSettings(); + const globalQ = (g.questions || []).filter((q) => q && String(q.text || '').trim()); + if (globalQ.length) return globalQ; + return (md.quizQuestions || []).filter((q) => q && String(q.text || '').trim()); +} + +function quizCellOn(grid, tx, ty) { + return !!(grid && grid[ty] && grid[ty][tx] === 1); +} + +function getCharacterFootprintWHForMove(md) { + let cw = Math.floor(Number(md.characterCellsW)); + let ch = Math.floor(Number(md.characterCellsH)); + const hasW = Number.isFinite(cw) && cw >= 1; + const hasH = Number.isFinite(ch) && ch >= 1; + if (hasW && hasH) { + return { cw: Math.max(1, Math.min(4, cw)), ch: Math.max(1, Math.min(4, ch)) }; + } + const leg = Math.max(1, Math.min(4, Math.floor(Number(md.characterCells)) || 1)); + return { cw: leg, ch: leg }; +} + +function getCharacterCollisionFootprintWHForMove(md) { + const { cw, ch } = getCharacterFootprintWHForMove(md); + let colW = Math.floor(Number(md.characterCollisionW)); + let colH = Math.floor(Number(md.characterCollisionH)); + if (!Number.isFinite(colW) || colW < 1) colW = cw; + if (!Number.isFinite(colH) || colH < 1) colH = ch; + colW = Math.max(1, Math.min(cw, colW)); + colH = Math.max(1, Math.min(ch, colH)); + return { cw, ch, colW, colH }; +} + +const QUIZ_BATTLE_SPRITE_COL_SCALE_W = 1.15; +const QUIZ_BATTLE_SPRITE_COL_SCALE_H = 1.35; + +function quizBattleSpriteBoundsTileKeys(md, px, py) { + const out = []; + if (!md || md.gameType !== 'quiz_battle') return out; + const x = Number(px), y = Number(py); + if (!Number.isFinite(x) || !Number.isFinite(y)) return out; + const w = md.width || 20, h = md.height || 15; + const { cw, ch } = getCharacterFootprintWHForMove(md); + const cx = x + cw * 0.5; + const feetY = y + ch; + const left = cx - (cw * QUIZ_BATTLE_SPRITE_COL_SCALE_W) / 2; + const right = cx + (cw * QUIZ_BATTLE_SPRITE_COL_SCALE_W) / 2; + const top = feetY - ch * QUIZ_BATTLE_SPRITE_COL_SCALE_H; + const bottom = feetY; + if (left < 0 || right > w || top < 0 || bottom > h) return out; + const minTx = Math.floor(left); + const maxTx = Math.min(w - 1, Math.floor(right - 1e-6)); + const minTy = Math.floor(top); + const maxTy = Math.min(h - 1, Math.floor(bottom - 1e-6)); + for (let ty = minTy; ty <= maxTy; ty++) { + for (let tx = minTx; tx <= maxTx; tx++) out.push(`${tx},${ty}`); + } + return out; +} + +function serverWallCollisionTileKeys(md, px, py) { + if (md && md.gameType === 'quiz_battle') return quizBattleSpriteBoundsTileKeys(md, px, py); + const w = md.width || 20; + const h = md.height || 15; + const { cw, ch, colW, colH } = getCharacterCollisionFootprintWHForMove(md); + const minTx = Math.floor(Number(px)); + const minTy = Math.floor(Number(py)); + const offX = minTx + Math.floor((cw - colW) / 2); + const offY = minTy + (ch - colH); + const out = []; + for (let ty = offY; ty < offY + colH; ty++) { + for (let tx = offX; tx < offX + colW; tx++) { + if (tx >= 0 && ty >= 0 && tx < w && ty < h) out.push(`${tx},${ty}`); + } + } + return out; +} + +function quizCarryFootprintTileKeys(md, px, py) { + const w = md.width || 20; + const h = md.height || 15; + const { cw, ch } = getCharacterFootprintWHForMove(md); + const minTx = Math.floor(Number(px)); + const minTy = Math.floor(Number(py)); + const maxTx = Math.min(w - 1, minTx + cw - 1); + const maxTy = Math.min(h - 1, minTy + ch - 1); + const out = []; + for (let ty = minTy; ty <= maxTy; ty++) { + for (let tx = minTx; tx <= maxTx; tx++) { + if (tx >= 0 && ty >= 0) out.push(`${tx},${ty}`); + } + } + return out; +} + +function quizCarryFootprintOverlapsHubServer(md, px, py) { + if (!md || !md.quizCarryHubArea || md.gameType !== 'quiz_carry') return false; + for (const k of quizCarryFootprintTileKeys(md, px, py)) { + const [tx, ty] = k.split(',').map(Number); + if (quizCellOn(md.quizCarryHubArea, tx, ty)) return true; + } + return false; +} + +/** Nearest walkable tile center not in true/false answer zones (BFS). */ +function findNearestOutsideQuizAnswerZones(md, sx, sy) { + const w = md.width || 20; + const h = md.height || 15; + const tGrid = md.quizTrueArea || []; + const fGrid = md.quizFalseArea || []; + const inAnswer = (x, y) => quizCellOn(tGrid, x, y) || quizCellOn(fGrid, x, y); + const walkable = (x, y) => { + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + return row && row[x] !== 1; + }; + const fx = Math.floor(Number(sx)); + const fy = Math.floor(Number(sy)); + if (!walkable(fx, fy)) { + const sp = md.spawn || { x: 1, y: 1 }; + return { x: (typeof sp.x === 'number' ? sp.x : 1) + 0.5, y: (typeof sp.y === 'number' ? sp.y : 1) + 0.5 }; + } + if (!inAnswer(fx, fy)) return { x: sx, y: sy }; + const q = [[fx, fy]]; + const seen = new Set([`${fx},${fy}`]); + const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; + while (q.length) { + const [cx, cy] = q.shift(); + for (let di = 0; di < dirs.length; di++) { + const nx = cx + dirs[di][0]; + const ny = cy + dirs[di][1]; + const k = `${nx},${ny}`; + if (seen.has(k)) continue; + if (!walkable(nx, ny)) continue; + seen.add(k); + if (!inAnswer(nx, ny)) { + return { x: nx + 0.5, y: ny + 0.5 }; + } + q.push([nx, ny]); + } + } + const sp = md.spawn || { x: 1, y: 1 }; + return { x: (typeof sp.x === 'number' ? sp.x : 1) + 0.5, y: (typeof sp.y === 'number' ? sp.y : 1) + 0.5 }; +} + +function validateQuizMap(md) { + const pool = getQuizQuestionPool(md); + if (pool.length < 1) { + return 'เพิ่มคำถามใน Admin แท็บ «คำถามเกม» (หรือบันทึกคำถามในฉากแบบเก่าเป็นสำรอง)'; + } + const w = md.width || 20; + const h = md.height || 15; + const tGrid = md.quizTrueArea || []; + const fGrid = md.quizFalseArea || []; + let anyT = false; + let anyF = false; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (quizCellOn(tGrid, x, y)) anyT = true; + if (quizCellOn(fGrid, x, y)) anyF = true; + } + } + if (!anyT || !anyF) return 'วาดโซนคำตอบ ถูก และ ผิด อย่างน้อยช่องละ 1'; + return null; +} + +function clearSpaceQuizTimers(space) { + if (!space.quizTimers || !Array.isArray(space.quizTimers)) { + space.quizTimers = []; + return; + } + space.quizTimers.forEach((t) => clearTimeout(t)); + space.quizTimers = []; +} + +/** หยุดรอบ quiz (mng8a80o) ชั่วคราวระหว่างตอบคำถามพิเศษ — เก็บเวลาที่เหลือไว้ */ +function pauseQuizForSpecialQuiz(sid, space) { + const sess = space.quizSession; + if (!sess || !sess.active || space.quizFrozenForSpecial) return; + const phase = sess.phase; + if (phase !== 'read' && phase !== 'answer') return; + const remaining = Math.max(0, (Number(sess.phaseEndsAt) || 0) - Date.now()); + space.quizFrozenForSpecial = true; + space.quizFreezePhase = phase; + space.quizFreezeRemainingMs = remaining; + clearSpaceQuizTimers(space); + io.to(sid).emit('quiz-paused', { phase }); +} + +/** เล่นรอบ quiz ต่อหลังตอบคำถามพิเศษเสร็จ — ตั้ง timer ใหม่ด้วยเวลาที่เหลือ */ +function resumeQuizAfterSpecialQuiz(sid, space) { + if (!space.quizFrozenForSpecial) return; + space.quizFrozenForSpecial = false; + const sess = space.quizSession; + const phase = space.quizFreezePhase; + const remaining = Math.max(1200, Number(space.quizFreezeRemainingMs) || 0); + space.quizFreezePhase = null; + space.quizFreezeRemainingMs = 0; + if (!sess || !sess.active) return; + const md = sess.quizMapMd || getLobbyLayoutMapForSpace(space); + if (!md) return; + /* Time Dilation: +N วิให้รอบ quiz (mng8a80o) ตอน resume */ + let extraMs = 0; + if (space.timeDilationPendingSec > 0) { + extraMs = space.timeDilationPendingSec * 1000; + space.timeDilationPendingSec = 0; + } + sess.phaseEndsAt = Date.now() + remaining + extraMs; + if (phase === 'read') { + sess.phase = 'read'; + const q = sess.questions[sess.qIndex]; + io.to(sid).emit('quiz-phase', { + phase: 'read', + questionIndex: sess.qIndex + 1, + questionTotal: sess.questions.length, + text: String((q && q.text) || '').trim(), + endsAt: sess.phaseEndsAt, + resumed: true, + }); + const t = setTimeout(() => scheduleQuizAnswerPhase(sid, space, md), remaining); + space.quizTimers.push(t); + } else { + sess.phase = 'answer'; + io.to(sid).emit('quiz-phase', { + phase: 'answer', + questionIndex: sess.qIndex + 1, + questionTotal: sess.questions.length, + endsAt: sess.phaseEndsAt, + resumed: true, + }); + const t = setTimeout(() => resolveQuizRound(sid, space, md), remaining); + space.quizTimers.push(t); + } +} + +/** เกมถูก/ผิด: คะแนนต่อข้อที่ตอบถูก (ตรงกับ client + เอฟเฟกต์ score+.png) */ +const QUIZ_TF_POINTS_PER_CORRECT = 10; + +function shuffleQuizQuestions(arr) { + const a = arr.slice(); + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; +} + +function ensurePeerLobbyThemes(space) { + if (!space || !space.peers) return; + const seen = new Set(); + space.peers.forEach((p, id) => { + let ti = parseInt(p.lobbyColorThemeIndex, 10); + if (!(ti >= 1 && ti <= LOBBY_THEME_COUNT) || seen.has(ti)) { + ti = pickFreeLobbyThemeIndex(space, id); + p.lobbyColorThemeIndex = ti; + } + seen.add(ti); + if (!parseInt(p.lobbySkinToneIndex, 10) || p.lobbySkinToneIndex < 1 || p.lobbySkinToneIndex > LOBBY_SKIN_COUNT) { + p.lobbySkinToneIndex = pickLobbySkinIndexForSlot(p.spawnJoinOrder || 0); + } + }); +} + +function buildPeersSnapForSpace(space, extraFields) { + return [...space.peers.values()].map((p) => { + const row = { + id: p.id, + x: p.x, + y: p.y, + direction: p.direction || 'down', + nickname: p.nickname, + ready: !!p.ready, + characterId: p.characterId ?? null, + lobbyColorThemeIndex: p.lobbyColorThemeIndex || null, + lobbySkinToneIndex: p.lobbySkinToneIndex || null, + }; + if (extraFields && extraFields.gauntlet) { + row.gauntletJumpTicks = p.gauntletJumpTicks || 0; + row.gauntletScore = p.gauntletScore || 0; + row.gauntletEliminated = !!p.gauntletEliminated; + } + return row; + }); +} + +/** สุ่ม 3 minigame จาก pool 7 แบบ — เรียกครั้งเดียวตอนเข้า LobbyB (ล็อกบน space) */ +/** แมปเก่า (กบ frogger) → Minigame-2 Gauntlet ตาม Admin */ +function normalizeDetectiveCardEntry(entry) { + if (!entry) return entry; + const mid = entry.mapId != null ? String(entry.mapId).trim() : ''; + if (mid === 'mlpecp2j' || entry.key === 'frogger') { + const gaunt = DETECTIVE_MINIGAME_POOL.find((e) => e.key === 'gauntlet'); + if (gaunt && maps.has(gaunt.mapId)) { + const md = maps.get(gaunt.mapId); + return { + ...entry, + key: gaunt.key, + mapId: gaunt.mapId, + labelTh: gaunt.labelTh, + mapName: (md && md.name) || gaunt.mapId, + gameType: (md && md.gameType) || 'gauntlet', + }; + } + } + return entry; +} + +function assignDetectiveSuspectCardMinigames(space) { + const available = DETECTIVE_MINIGAME_POOL.filter((e) => maps.has(e.mapId)); + const shuffled = shuffleQuizQuestions(available.length ? available : [{ key: 'quiz', mapId: SUSPECT_INVESTIGATION_QUIZ_MAP_ID, labelTh: 'QuestionGame' }]); + space.suspectCardMinigames = shuffled.slice(0, 3).map((entry, i) => { + const md = maps.get(entry.mapId); + return { + cardIndex: i, + key: entry.key, + mapId: entry.mapId, + labelTh: entry.labelTh, + mapName: (md && md.name) || entry.mapId, + gameType: (md && md.gameType) || '', + }; + }); + // ชุดการ์ดใหม่ = ล้างหลักฐานที่ผู้เล่นเก็บได้ + space.playerEvidence = {}; + // สุ่มคนร้ายตัวจริง 1 ใน 3 ใบ (เก็บฝั่ง server ไม่ส่งให้ client จนกว่าจะเปิดเผย) + space.culpritIndex = Math.floor(Math.random() * 3); + space.trialPhase = null; // null | 'voting' | 'revealed' + space.trialVotes = {}; // socketId -> suspectIndex + space.trialScores = space.trialScores || {}; +} + +function ensureSpacePlayerEvidence(space) { + if (!space.playerEvidence || typeof space.playerEvidence !== 'object') space.playerEvidence = {}; + if (!space.evidenceByNick || typeof space.evidenceByNick !== 'object') space.evidenceByNick = {}; +} + +/* แถวหลักฐานต่อผู้ต้องสงสัย = array ของ card object (ไม่จำกัดจำนวน) — กรอง element ที่ไม่ใช่การ์ดทิ้ง */ +function normalizeEvidenceSlotList(cards) { + if (!Array.isArray(cards)) return []; + const out = []; + for (let i = 0; i < cards.length; i++) { + const c = sanitizeStoredEvidenceCard(cards[i]); + if (c) out.push(c); + } + return out; +} + +/** ดึง nickname normalized ของผู้เล่นบน space (จากแถว peers) — ใช้คีย์เก็บหลักฐานข้ามการ reconnect */ +function peerNickKey(space, playerId) { + if (!space || !playerId) return ''; + const p = space.peers && space.peers.get ? space.peers.get(playerId) : null; + if (!p || !p.nickname) return ''; + return normalizeLobbyNickname(p.nickname); +} + +/** สำเนาหลักฐาน — fallback ไป evidenceByNick ถ้าไม่มีตาม socket.id (กรณี reconnect socket ใหม่) */ +function clonePlayerEvidence(space, playerId) { + ensureSpacePlayerEvidence(space); + let row = space.playerEvidence[playerId]; + if (!row || !row.length) { + const nk = peerNickKey(space, playerId); + if (nk && space.evidenceByNick[nk]) { + row = space.evidenceByNick[nk]; + space.playerEvidence[playerId] = row; + } + } + if (!row) row = [[], [], []]; + return [0, 1, 2].map((i) => normalizeEvidenceSlotList(row[i])); +} + +/** ซิงค์หลักฐานของผู้เล่นปัจจุบันลง evidenceByNick — เรียกหลังการ์ดเปลี่ยน */ +function syncEvidenceByNick(space, playerId) { + ensureSpacePlayerEvidence(space); + const nk = peerNickKey(space, playerId); + if (!nk) return; + const row = space.playerEvidence[playerId]; + if (Array.isArray(row)) space.evidenceByNick[nk] = row; +} + +/** จำนวนหลักฐานที่เก็บได้ต่อผู้ต้องสงสัย (ไม่จำกัด) — ใช้แสดงติก/จำนวนบนการ์ด suspect */ +function playerSuspectProgressFromEvidence(space, playerId) { + const row = clonePlayerEvidence(space, playerId); + return row.map((arr) => arr.length); +} + +/** มอบการ์ดหลักฐาน 1 ใบ (สุ่ม rarity ตามเกรด → สุ่มรูปจากพูล) เก็บเป็น card object ไม่จำกัดจำนวน */ +function awardRandomEvidenceCardToPlayer(space, playerId, suspectIndex, grade) { + if (suspectIndex < 0 || suspectIndex > 2) return null; + ensureSpacePlayerEvidence(space); + /* เผื่อ rejoin: ดึงหลักฐานเก่าจาก nickname ถ้ามี ก่อนเพิ่มใบใหม่ */ + if (!space.playerEvidence[playerId]) { + const nk = peerNickKey(space, playerId); + space.playerEvidence[playerId] = (nk && space.evidenceByNick[nk]) + ? space.evidenceByNick[nk] + : [[], [], []]; + } + const list = normalizeEvidenceSlotList(space.playerEvidence[playerId][suspectIndex]); + const card = pickEvidenceCardForSuspect(space, suspectIndex, grade); + if (!card) { + space.playerEvidence[playerId][suspectIndex] = list; + syncEvidenceByNick(space, playerId); + return null; + } + list.push(card); + space.playerEvidence[playerId][suspectIndex] = list; + syncEvidenceByNick(space, playerId); + return card; +} + +function awardDetectiveEvidenceForMinigameEnd(space, suspectIndex, playerIds, grade) { + const out = {}; + (playerIds || []).forEach((pid) => { + if (!space.peers.has(pid)) return; + const card = awardRandomEvidenceCardToPlayer(space, pid, suspectIndex, grade); + if (card != null) out[pid] = card; + }); + return out; +} + +function resolveDetectiveSuspectAwardIndex(space) { + if (typeof space.suspectActiveIndex === 'number' && space.suspectActiveIndex >= 0 && space.suspectActiveIndex <= 2) { + return space.suspectActiveIndex; + } + if (typeof space.lastDetectiveInvestigatedSuspect === 'number' + && space.lastDetectiveInvestigatedSuspect >= 0 && space.lastDetectiveInvestigatedSuspect <= 2) { + return space.lastDetectiveInvestigatedSuspect; + } + if (typeof space.suspectPickIndex === 'number' && space.suspectPickIndex >= 0 && space.suspectPickIndex <= 2) { + return space.suspectPickIndex; + } + return -1; +} + +/** มอบการ์ดหลักฐาน 1 ใบต่อผู้เล่นเมื่อจบมินิเกม (ครั้งเดียวต่อรอบ — ไม่ขึ้นเกรด) */ +function grantDetectiveEvidenceForCurrentRun(sid, space, playerIds) { + const suspectIdx = resolveDetectiveSuspectAwardIndex(space); + if (suspectIdx < 0) return { suspectIdx: -1, awarded: {} }; + if (space.detectiveAwardDoneForRun) { + return { suspectIdx, awarded: {}, alreadyGranted: true }; + } + const ids = (playerIds && playerIds.length) ? playerIds : [...space.peers.keys()]; + const grade = computeRunGradeForEvidence(space); + const awardedList = {}; + ids.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) */ + let freeEvidenceCount = 0; + const pend = space.pendingSpecialCard; + if (pend && Number(pend.cardId) === 7 && !pend.consumed) { + const def = specialCardDef(7); + freeEvidenceCount = (def && def.evidence) || 2; + for (let n = 0; n < freeEvidenceCount; n++) { + ids.forEach((pid) => { + if (!space.peers.has(pid)) return; + const extra = awardRandomEvidenceCardToPlayer(space, pid, suspectIdx, grade); + if (extra != null) { awardedList[pid].push(extra); if (awarded[pid] == null) awarded[pid] = extra; } + }); + } + pend.consumed = true; + } + space.detectiveAwardDoneForRun = true; + space.lastDetectiveInvestigatedSuspect = suspectIdx; + space.lastEvidenceGrant = { suspectIdx, awarded, awardedList, grade, freeEvidenceCount }; + emitLobbyEvidenceSync(sid, space); + return { suspectIdx, awarded, awardedList, grade, alreadyGranted: false, freeEvidenceCount }; +} + +function emitLobbyEvidenceSync(sid, space) { + if (!sid || !space) return; + space.peers.forEach((_p, pid) => { + const sock = io.sockets.sockets.get(pid); + if (!sock) return; + sock.emit('lobby-evidence-sync', { + myPlayerEvidence: clonePlayerEvidence(space, pid), + suspectProgress: playerSuspectProgressFromEvidence(space, pid), + }); + }); +} + +function emitDetectiveLobbyReturnToPeers(sid, space, message, basePayload) { + const msg = message || 'จบมินิเกม — กลับ LobbyB'; + space.peers.forEach((_p, pid) => { + const sock = io.sockets.sockets.get(pid); + if (!sock) return; + const personal = { + ...basePayload, + myPlayerEvidence: clonePlayerEvidence(space, pid), + suspectProgress: playerSuspectProgressFromEvidence(space, pid), + }; + sock.emit('quiz-ended', { message: msg, returnToLobbyB: true }); + sock.emit('detective-minigame-ended', personal); + sock.emit('game-start', personal); + }); +} + +/** เริ่มมินิเกมตามการ์ดที่เลือก (หลังกดเริ่มสืบสวน) */ +function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) { + const mapId = cardEntry && cardEntry.mapId ? String(cardEntry.mapId).trim() : ''; + const md = mapId && maps.has(mapId) ? maps.get(mapId) : null; + if (!md) return { ok: false, error: 'ไม่พบฉากเกมบนเซิร์ฟเวอร์' }; + + space.suspectPhaseActive = false; + space.detectiveMinigameActive = true; + space.detectiveAwardDoneForRun = false; + /* Card 3 Ban — ผู้ที่ถูกแบนไม่ได้เล่นมินิเกมรอบนี้ (1 รอบ) */ + space.bannedThisRunPlayerId = space.bannedPlayerId || null; + space.bannedPlayerId = null; + space.detectiveMinigameCardIndex = cardEntry.cardIndex; + // จำว่ากำลังสืบผู้ต้องสงสัยคนไหน เพื่อเติมหลักฐาน (ติกถูก) ตอนเล่นจบ + space.suspectActiveIndex = (typeof selectedIndex === 'number' && selectedIndex >= 0 && selectedIndex <= 2) + ? selectedIndex + : (cardEntry && typeof cardEntry.cardIndex === 'number' ? cardEntry.cardIndex : 0); + space.gauntletReturnMapId = POST_CASE_LOBBY_SPACE_ID; + clearSpaceQuizTimers(space); + if (space.quizSession) space.quizSession.active = false; + space.quizSession = null; + space.mapId = mapId; + space.mapData = md; + /* สุ่มไอคอนคำถามพิเศษสำหรับรอบนี้ (1 ใน 3) — ส่งจริงผ่าน joinCb เมื่อ client เข้า play.html */ + maybeSpawnSpecialQuizForRun(space); + emitSpecialQuizIcon(sid, space); + + let si = 0; + space.peers.forEach((p) => { + const sp = pickSpawnForJoin(md, si++); + p.x = sp.x; + p.y = sp.y; + p.direction = 'down'; + p.gauntletJumpTicks = 0; + p.gauntletScore = 0; + p.gauntletJumpPending = false; + p.gauntletEliminated = false; + }); + + const peersSnap = buildPeersSnapForSpace(space, { gauntlet: md.gameType === 'gauntlet' }); + const invPayload = { + selectedIndex, + cardIndex: cardEntry.cardIndex, + minigameKey: cardEntry.key, + minigameLabel: cardEntry.labelTh, + mapId, + }; + io.to(sid).emit('suspect-investigation-start', invPayload); + + if (md.gameType === 'quiz') { + const qErr = validateQuizMap(md); + if (qErr) { + space.detectiveMinigameActive = false; + space.gauntletReturnMapId = null; + return { ok: false, error: qErr }; + } + /** ภารกิจ mng8a80o — HOW TO + READY + นับ 3-2-1 เหมือน preview (อย่า startQuizGame ทันที) */ + const isQuizMissionShell = String(mapId).trim() === SUSPECT_INVESTIGATION_QUIZ_MAP_ID; + if (!isQuizMissionShell) { + io.to(sid).emit('game-start', { + quizMode: true, + stayInRoomLobby: true, + mapId, + peersSnap, + detectiveMinigame: true, + minigameLabel: cardEntry.labelTh, + bannedPlayerId: space.bannedThisRunPlayerId || null, + }); + setTimeout(() => { + const spNow = spaces.get(sid); + if (!spNow || !spNow.peers.size) return; + const mdLive = maps.get(mapId); + if (!mdLive || mdLive.gameType !== 'quiz') return; + startQuizGame(sid, spNow, mdLive); + }, 900); + return { ok: true, mapId, gameType: md.gameType }; + } + /* mng8a80o: ต่อ gauntletRun shell ด้านล่าง */ + } + + stopGauntletTicker(space); + space.gauntletRun = null; + if (md.gameType === 'gauntlet') { + initGauntletPeersAll(space, md); + ensureGauntletEndsAtIfNeeded(space); + startGauntletTicker(sid, space); + } else if (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space) || isJumpSurviveMissionShellSpace(space) || isSpaceShooterMissionShellSpace(space) || md.gameType === 'quiz_carry') { + if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {}; + space.peers.forEach((_p, id) => { space.gauntletCrownLobbyReady[id] = false; }); + space.gauntletRun = newBalloonBossShellGauntletRunState(); + } + + const gameStartPayload = { + mapId, + peersSnap, + detectiveMinigame: true, + detectiveReturnLobbyB: true, + minigameLabel: cardEntry.labelTh, + bannedPlayerId: space.bannedThisRunPlayerId || null, + }; + if (md.gameType === 'gauntlet' && space.gauntletRun) { + gameStartPayload.gauntletEndsAt = space.gauntletRun.endsAt != null ? space.gauntletRun.endsAt : null; + } + if (space.gauntletRun && space.gauntletRun.crownRunHeld) { + gameStartPayload.gauntletCrownRunHeld = true; + } + io.to(sid).emit('game-start', gameStartPayload); + return { ok: true, mapId, gameType: md.gameType }; +} + +function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) { + ensurePostCaseLobbyMapLoaded(); + clearSpaceQuizTimers(space); + clearSpecialQuizTimers(space); + space.specialQuiz = null; + if (space.quizSession) space.quizSession.active = false; + space.quizSession = null; + space.detectiveMinigameActive = false; + space.gauntletReturnMapId = null; + stopGauntletTicker(space); + space.gauntletRun = null; + const suspectIdx = resolveDetectiveSuspectAwardIndex(space); + if (suspectIdx >= 0) { + space.lastDetectiveInvestigatedSuspect = suspectIdx; + let awardIds = []; + if (awardOptions && awardOptions.allPlayers) { + awardIds = [...space.peers.keys()]; + } else if (awardOptions && awardOptions.playerId && space.peers.has(awardOptions.playerId)) { + awardIds = [awardOptions.playerId]; + } else if (awardOptions && Array.isArray(awardOptions.playerIds) && awardOptions.playerIds.length) { + awardIds = awardOptions.playerIds.filter((id) => space.peers.has(id)); + } + /* Card 3 Ban — ผู้ที่ถูกแบนรอบนี้ไม่ได้เล่น จึงไม่ได้รับหลักฐาน */ + if (space.bannedThisRunPlayerId) { + awardIds = awardIds.filter((id) => id !== space.bannedThisRunPlayerId); + } + /* ไม่มอบการ์ดเมื่อกลับ LobbyB โดยไม่ระบุ (เช่น refresh room-lobby ระหว่างมินิเกม) */ + if (!space.detectiveAwardDoneForRun && awardIds.length) { + /* ผ่าน grant…CurrentRun เพื่อให้ set lastEvidenceGrant + ใช้ Card 7 Free Evidence + sync แฟ้ม (สำหรับหน้าเปิดเผยหลักฐาน) */ + grantDetectiveEvidenceForCurrentRun(sid, space, awardIds); + } + } + space.suspectActiveIndex = null; + space.bannedThisRunPlayerId = null; + const lb = maps.get(POST_CASE_LOBBY_SPACE_ID); + if (!lb) { + io.to(sid).emit('quiz-ended', { message: message || 'จบมินิเกม — ไม่พบ LobbyB', returnToLobbyB: true }); + return; + } + space.mapId = POST_CASE_LOBBY_SPACE_ID; + space.mapData = lb; + space.suspectPhaseActive = true; + let gi = 0; + space.peers.forEach((p) => { + const sp = pickSpawnForJoin(lb, gi++); + p.x = sp.x; + p.y = sp.y; + p.direction = 'down'; + p.gauntletJumpTicks = 0; + p.gauntletScore = 0; + p.gauntletJumpPending = false; + p.gauntletEliminated = false; + }); + const peersSnap = buildPeersSnapForSpace(space); + const basePayload = { + stayInRoomLobby: true, + mapId: POST_CASE_LOBBY_SPACE_ID, + peersSnap, + suspectPhaseActive: true, + suspectPickIndex: typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0, + cardMinigames: space.suspectCardMinigames || [], + detectiveReturn: true, + }; + emitDetectiveLobbyReturnToPeers(sid, space, message, basePayload); +} + +/* ===== ขั้นพิจารณาคดี — โหวตชี้ตัวคนร้าย ===== */ +function emitTrialVoteUpdate(sid, space) { + const counts = [0, 0, 0]; + const votes = space.trialVotes || {}; + Object.keys(votes).forEach((id) => { const v = votes[id]; if (v >= 0 && v <= 2) counts[v]++; }); + io.to(sid).emit('trial-vote-update', { + counts, + voted: Object.keys(votes).length, + totalPlayers: trialEligibleVoterTotal(space), + }); +} + +function computeAndEmitTrialResult(sid, space) { + /* กันบอทบางตัวยังไม่โหวต — โหวตให้หมดก่อนรวมผล */ + autoVoteBotsTrial(space); + clearTrialVoteTimer(space); + const counts = [0, 0, 0]; + const votes = space.trialVotes || {}; + const culprit = (typeof space.culpritIndex === 'number') ? space.culpritIndex : 0; + /* Card 5 Bail Coin — จับผิดตัวให้โหวตใหม่ได้ 1 ครั้ง */ + { + const c2 = [0, 0, 0]; + Object.keys(votes).forEach((id) => { const v = votes[id]; if (v >= 0 && v <= 2) c2[v]++; }); + let mostVoted = 0; for (let k = 1; k <= 2; k++) { if (c2[k] > c2[mostVoted]) mostVoted = k; } + const anyVotes = (c2[0] + c2[1] + c2[2]) > 0; + const wrong = anyVotes && mostVoted !== culprit; + const pend = space.pendingSpecialCard; + if (wrong && pend && Number(pend.cardId) === 5 && !pend.consumed && !space.trialBailUsed) { + pend.consumed = true; + space.trialBailUsed = true; + io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'bail' }); + openTrialVotingPhase(sid, space, { revote: true }); + return; + } + } + space.trialPhase = 'revealed'; + const winners = []; + if (!space.trialScores) space.trialScores = {}; + Object.keys(votes).forEach((id) => { + const v = votes[id]; + if (v >= 0 && v <= 2) counts[v]++; + if (v === culprit) { + winners.push(id); + space.trialScores[id] = (space.trialScores[id] || 0) + 100; + } + }); + const winnerNames = winners.map((id) => { + const p = space.peers.get(id); + if (p && p.nickname) return p.nickname; + if (typeof id === 'string' && id.indexOf(TESTIMONY_BOT_PREFIX) === 0) { + const slot = parseInt(id.slice(TESTIMONY_BOT_PREFIX.length), 10); + return Number.isFinite(slot) ? ('บอท ' + (slot + 1)) : 'บอท'; + } + return String(id).slice(0, 6); + }); + io.to(sid).emit('trial-result', { + culpritIndex: culprit, + counts, + winners, + winnerNames, + cardMinigames: space.suspectCardMinigames || [], + }); + space.silencedPlayerId = null; +} + +/* ===== ห้องสรุปหลักฐาน — การไต่สวน (ปากคำ 3 รอบ ก่อนโหวต) ===== */ +/** id บอทใน testimony/trial — สอดคล้องกับ slot บน lobby (__lobby_bot_0..N) */ +const TESTIMONY_BOT_PREFIX = '__lobby_bot_'; + +function testimonyBotIds(space) { + const n = effectiveBotSlotCount(space); + const out = []; + for (let i = 0; i < n; i++) out.push(TESTIMONY_BOT_PREFIX + i); + return out; +} + +function buildTestimonyMembers(space) { + const arr = []; + space.peers.forEach((p, id) => { + arr.push({ id, nickname: (p && p.nickname) ? p.nickname : String(id).slice(0, 6), isHost: id === space.hostId, isBot: false }); + }); + testimonyBotIds(space).forEach((bid, i) => { + arr.push({ id: bid, nickname: 'บอท ' + (i + 1), isHost: false, isBot: true }); + }); + return arr; +} + +/** จำนวนผู้เล่นทั้งหมดที่ต้อง submit/vote (รวมบอท) */ +function testimonyTotalParticipants(space) { + return (space.peers ? space.peers.size : 0) + effectiveBotSlotCount(space); +} + +/** จำนวนการ์ดที่บอทควรเลือก — เท่ากับ max ของ "ผู้เล่นจริง" ในรอบ (กันบอทเลือก 2 ขณะคนเลือก 1) */ +function botTestimonyPickCount(space) { + const round = typeof space.testimonyRound === 'number' ? space.testimonyRound : 0; + let maxOwned = 0; + space.peers.forEach((_p, pid) => { + const ev = clonePlayerEvidence(space, pid); + const len = Array.isArray(ev[round]) ? ev[round].length : 0; + if (len > maxOwned) maxOwned = len; + }); + /* ถ้าผู้เล่นจริงไม่มีหลักฐานเลย — ให้บอทเลือก 1 ใบ (กันรอบติด) */ + if (maxOwned === 0) return 1; + return Math.max(1, Math.min(2, maxOwned)); +} + +/** สุ่ม picks ให้บอทในรอบ testimony ปัจจุบัน — เก็บเป็น card object (สุ่มจากพูล rarity 'C') */ +function autoSubmitBotTestimonyPicks(space) { + if (!space.testimonySubmitted) space.testimonySubmitted = {}; + const round = typeof space.testimonyRound === 'number' ? space.testimonyRound : 0; + const want = botTestimonyPickCount(space); + testimonyBotIds(space).forEach((bid) => { + if (space.testimonySubmitted[bid] != null) return; + const picks = []; + for (let n = 0; n < want; n++) { + const c = pickEvidenceCardForSuspect(space, round, 'C'); + if (c) picks.push(c); + } + space.testimonySubmitted[bid] = picks; + }); +} + +function emitTestimonyOpen(sid, space) { + space.testimonySubmitted = {}; + space.testimonyReadyNext = {}; + space.testimonyRevealed = false; + /* บอทส่งหลักฐานทันทีเมื่อเปิด round */ + autoSubmitBotTestimonyPicks(space); + const submittedIds = Object.keys(space.testimonySubmitted || {}); + const base = { + round: space.testimonyRound, + suspectIndex: space.testimonyRound, + members: buildTestimonyMembers(space), + totalPlayers: testimonyTotalParticipants(space), + hostId: space.hostId, + cardMinigames: space.suspectCardMinigames || [], + submitted: submittedIds, + }; + space.peers.forEach((_p, pid) => { + const sock = io.sockets.sockets.get(pid); + if (!sock) return; + sock.emit('testimony-open', { + ...base, + myPlayerEvidence: clonePlayerEvidence(space, pid), + suspectProgress: playerSuspectProgressFromEvidence(space, pid), + }); + }); + /* sync สถานะปุ่ม (เผื่อบอทส่งครบแล้ว — host ต้องเห็นปุ่ม "เปิดหลักฐานทั้งหมด" ทันที) */ + emitTestimonyStatus(sid, space); +} + +function emitTestimonyStatus(sid, space) { + io.to(sid).emit('testimony-status', { + round: space.testimonyRound, + submitted: Object.keys(space.testimonySubmitted || {}), + totalPlayers: testimonyTotalParticipants(space), + }); +} + +function doTestimonyReveal(sid, space) { + space.testimonyRevealed = true; + /* กันกรณีบอทยังไม่ submit (เผื่อ edge case) */ + autoSubmitBotTestimonyPicks(space); + /* บอทพร้อมไปรอบถัดไปทันที (auto-ready) */ + if (!space.testimonyReadyNext) space.testimonyReadyNext = {}; + testimonyBotIds(space).forEach((bid) => { space.testimonyReadyNext[bid] = true; }); + io.to(sid).emit('testimony-reveal', { + round: space.testimonyRound, + picks: space.testimonySubmitted || {}, + members: buildTestimonyMembers(space), + ready: Object.keys(space.testimonyReadyNext), + totalPlayers: testimonyTotalParticipants(space), + }); +} + +/** สุ่มโหวตชี้คนร้ายให้บอท — สมจริงๆ ก็คือเลือกใครก็ได้ใน 3 คน (มี bias ให้ตัวร้ายจริง 40% เพื่อให้สนุก) */ +/** เวลาโหวตพิจารณาคดี (มิลลิวิ) + เวลาที่การ์ด Extension เพิ่มให้ */ +const TRIAL_VOTE_MS = 30000; +const TRIAL_VOTE_EXTENSION_MS = 10000; + +function clearTrialVoteTimer(space) { + if (space.trialVoteTimer) { clearTimeout(space.trialVoteTimer); space.trialVoteTimer = null; } +} + +/** เปิดเฟสโหวต — ตั้งตัวจับเวลา + ส่ง trial-open (ใช้ทั้งโหวตครั้งแรกและโหวตใหม่จาก Bail) */ +function openTrialVotingPhase(sid, space, opts) { + opts = opts || {}; + space.testimonyActive = false; + space.trialPhase = 'voting'; + space.trialVotes = {}; + autoVoteBotsTrial(space); + clearTrialVoteTimer(space); + /* Card 2 Extension — เพิ่มเวลาโหวต +10 วิ (ใช้ครั้งเดียว) */ + let extraMs = 0; + let extensionCard = false; + const pend = space.pendingSpecialCard; + if (pend && Number(pend.cardId) === 2 && !pend.consumed) { + extraMs = TRIAL_VOTE_EXTENSION_MS; + extensionCard = true; + pend.consumed = true; + } + const durMs = TRIAL_VOTE_MS + extraMs; + space.trialVoteEndsAt = Date.now() + durMs; + space.peers.forEach((_p, pid) => { + const sock = io.sockets.sockets.get(pid); + if (!sock) return; + sock.emit('trial-open', { + hostId: space.hostId, + cardMinigames: space.suspectCardMinigames || [], + myPlayerEvidence: clonePlayerEvidence(space, pid), + suspectProgress: playerSuspectProgressFromEvidence(space, pid), + voteEndsAt: space.trialVoteEndsAt, + voteDurationMs: durMs, + extensionCard, + extensionSec: extensionCard ? (TRIAL_VOTE_EXTENSION_MS / 1000) : 0, + revote: !!opts.revote, + }); + }); + if (extensionCard) { + io.to(sid).emit('special-card-applied', { + card: specialCardClientPayload(pend.card), + addSec: TRIAL_VOTE_EXTENSION_MS / 1000, + context: 'trial', + }); + } + emitTrialVoteUpdate(sid, space); + space.trialVoteTimer = setTimeout(() => { + const spNow = spaces.get(sid); + if (!spNow || spNow.trialPhase !== 'voting') return; + computeAndEmitTrialResult(sid, spNow); + }, durMs + 120); +} + +/* ===== โหวตเลือกผู้เล่น 1 คน (ใช้ร่วม: Silence ปิดปาก / Ban ห้ามเล่น) ===== */ +const PICK_VOTE_MS = 15000; + +function clearPickVoteTimer(space) { + if (space.pickVoteTimer) { clearTimeout(space.pickVoteTimer); space.pickVoteTimer = null; } +} + +function pickVoteCandidates(space) { + const arr = []; + space.peers.forEach((p, id) => arr.push({ id, nickname: (p && p.nickname) ? p.nickname : String(id).slice(0, 6), isBot: false })); + testimonyBotIds(space).forEach((bid, i) => arr.push({ id: bid, nickname: 'บอท ' + (i + 1), isBot: true })); + return arr; +} + +function autoVotePickBots(space) { + const pv = space.pickVote; + if (!pv) return; + testimonyBotIds(space).forEach((bid) => { + if (pv.votes[bid] != null) return; + const choices = pv.candidates.filter((c) => c.id !== bid); + if (!choices.length) return; + pv.votes[bid] = choices[Math.floor(Math.random() * choices.length)].id; + }); +} + +function emitPickVoteUpdate(sid, space) { + const pv = space.pickVote; + if (!pv) return; + const counts = {}; + Object.keys(pv.votes).forEach((vid) => { const t = pv.votes[vid]; counts[t] = (counts[t] || 0) + 1; }); + io.to(sid).emit('player-pick-vote-update', { + voted: Object.keys(pv.votes).length, + total: pv.candidates.length, + counts, + }); +} + +function startPlayerPickVote(sid, space, purpose, onResolve) { + clearPickVoteTimer(space); + const candidates = pickVoteCandidates(space); + space.pickVote = { purpose, votes: {}, candidates, endsAt: Date.now() + PICK_VOTE_MS, resolved: false, onResolve }; + autoVotePickBots(space); + space.peers.forEach((_p, pid) => { + const sock = io.sockets.sockets.get(pid); + if (!sock) return; + sock.emit('player-pick-vote-open', { + purpose, + candidates: candidates.map((c) => ({ id: c.id, nickname: c.nickname, isBot: c.isBot })), + endsAt: space.pickVote.endsAt, + durationMs: PICK_VOTE_MS, + }); + }); + emitPickVoteUpdate(sid, space); + space.pickVoteTimer = setTimeout(() => { + const sp = spaces.get(sid); + if (!sp || !sp.pickVote || sp.pickVote.resolved) return; + resolvePlayerPickVote(sid, sp); + }, PICK_VOTE_MS + 120); + maybeResolvePickVote(sid, space); +} + +function maybeResolvePickVote(sid, space) { + const pv = space.pickVote; + if (!pv || pv.resolved) return; + const ids = pv.candidates.map((c) => c.id); + if (ids.length > 0 && ids.every((id) => pv.votes[id] != null)) resolvePlayerPickVote(sid, space); +} + +function resolvePlayerPickVote(sid, space) { + const pv = space.pickVote; + if (!pv || pv.resolved) return; + pv.resolved = true; + clearPickVoteTimer(space); + const counts = {}; + Object.keys(pv.votes).forEach((vid) => { const t = pv.votes[vid]; counts[t] = (counts[t] || 0) + 1; }); + let max = -1; + Object.keys(counts).forEach((tid) => { if (counts[tid] > max) max = counts[tid]; }); + const maxIds = Object.keys(counts).filter((tid) => counts[tid] === max); + const target = maxIds.length ? maxIds[Math.floor(Math.random() * maxIds.length)] : null; + const tc = pv.candidates.find((c) => c.id === target); + const targetName = tc ? tc.nickname : ''; + io.to(sid).emit('player-pick-vote-result', { purpose: pv.purpose, targetId: target, targetName, counts }); + const cb = pv.onResolve; + space.pickVote = null; + if (typeof cb === 'function') cb(target, targetName); +} + +function trialSilencedIsParticipant(space) { + const s = space.silencedPlayerId; + if (!s) return false; + if (space.peers.has(s)) return true; + return (typeof s === 'string' && s.indexOf(TESTIMONY_BOT_PREFIX) === 0); +} + +function trialEligibleVoterTotal(space) { + let t = testimonyTotalParticipants(space); + if (trialSilencedIsParticipant(space)) t = Math.max(0, t - 1); + return t; +} + +function autoVoteBotsTrial(space) { + if (!space.trialVotes) space.trialVotes = {}; + const culprit = (typeof space.culpritIndex === 'number') ? space.culpritIndex : Math.floor(Math.random() * 3); + testimonyBotIds(space).forEach((bid) => { + if (bid === space.silencedPlayerId) return; /* บอทที่ถูกปิดปากไม่โหวต */ + if (space.trialVotes[bid] != null) return; + /* 40% โหวตถูก, 60% โหวตสุ่ม */ + let pick; + if (Math.random() < 0.4) { + pick = culprit; + } else { + pick = Math.floor(Math.random() * 3); + } + space.trialVotes[bid] = pick; + }); +} + +function advanceTestimony(sid, space) { + space.testimonyRound = (typeof space.testimonyRound === 'number' ? space.testimonyRound : 0) + 1; + if (space.testimonyRound >= 3) { + space.trialBailUsed = false; + space.silencedPlayerId = null; + /* Card 4 Silence — โหวตเลือกผู้เล่น 1 คนให้ห้ามโหวต ก่อนเข้าโหวตจริง */ + const pend = space.pendingSpecialCard; + if (pend && Number(pend.cardId) === 4 && !pend.consumed) { + pend.consumed = true; + io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'silence' }); + startPlayerPickVote(sid, space, 'silence', (targetId) => { + const sp = spaces.get(sid); + if (!sp) return; + sp.silencedPlayerId = targetId || null; + openTrialVotingPhase(sid, sp, { revote: false }); + }); + return; + } + openTrialVotingPhase(sid, space, { revote: false }); + } else { + emitTestimonyOpen(sid, space); + } +} + +function endQuizGame(sid, space, message) { + clearSpaceQuizTimers(space); + if (space.quizSession) space.quizSession.active = false; + if (space.detectiveMinigameActive) { + returnDetectiveSpaceToLobbyB(sid, space, message, { allPlayers: true }); + space.quizSession = null; + return; + } + io.to(sid).emit('quiz-ended', { message: message || 'จบเกม' }); + space.quizSession = null; +} + +function emitQuizPlayerStates(sid, space) { + const sess = space.quizSession; + if (!sess || !sess.players) return; + space.peers.forEach((_, peerId) => { + const st = sess.players[peerId]; + if (st) io.to(peerId).emit('quiz-player-state', { ...st }); + }); +} + +function scheduleQuizReadPhase(sid, space, md) { + const sess = space.quizSession; + if (!sess || !sess.active) return; + const idx = sess.qIndex; + const qList = sess.questions; + if (idx >= qList.length) { + endQuizGame(sid, space, 'จบเกม — ครบทุกข้อแล้ว'); + return; + } + const q = qList[idx]; + const readMs = (Number.isFinite(sess.readMs) && sess.readMs >= 1000) ? sess.readMs : 10000; + sess.phase = 'read'; + sess.phaseEndsAt = Date.now() + readMs; + io.to(sid).emit('quiz-phase', { + phase: 'read', + questionIndex: idx + 1, + questionTotal: qList.length, + text: String(q.text || '').trim(), + endsAt: sess.phaseEndsAt, + }); + const t = setTimeout(() => scheduleQuizAnswerPhase(sid, space, md), readMs); + space.quizTimers.push(t); +} + +function scheduleQuizAnswerPhase(sid, space, md) { + const sess = space.quizSession; + if (!sess || !sess.active) return; + const answerMs = (Number.isFinite(sess.answerMs) && sess.answerMs >= 1000) ? sess.answerMs : 5000; + sess.phase = 'answer'; + sess.phaseEndsAt = Date.now() + answerMs; + io.to(sid).emit('quiz-phase', { + phase: 'answer', + questionIndex: sess.qIndex + 1, + questionTotal: sess.questions.length, + endsAt: sess.phaseEndsAt, + }); + emitQuizPlayerStates(sid, space); + const t = setTimeout(() => resolveQuizRound(sid, space, md), answerMs); + space.quizTimers.push(t); +} + +function resolveQuizRound(sid, space, md) { + const sess = space.quizSession; + if (!sess || !sess.active) return; + /* ใช้แผนที่ตอนเริ่มเกมเป็นหลัก — กัน getLobbyLayoutMapForSpace คืน lobby/zep แล้ว return ทิ้งทำให้ไม่มีคะแนน/ไม่เตะ */ + const mdLive = sess.quizMapMd || getLobbyLayoutMapForSpace(space) || md; + if (!mdLive) return; + const idx = sess.qIndex; + const q = sess.questions[idx]; + const correctTrue = !!q.answerTrue; + const tGrid = mdLive.quizTrueArea || []; + const fGrid = mdLive.quizFalseArea || []; + + let eligibleCount = 0; + let anyRight = false; + const results = []; + + const ejectMoves = []; + + space.peers.forEach((p, peerId) => { + const st = sess.players[peerId]; + if (!st) return; + eligibleCount++; + const tx = Math.floor(Number(p.x)); + const ty = Math.floor(Number(p.y)); + const inT = quizCellOn(tGrid, tx, ty); + const inF = quizCellOn(fGrid, tx, ty); + let choice = null; + if (inT && !inF) choice = true; + else if (inF && !inT) choice = false; + else if (inT && inF) choice = true; + + let right = false; + if (choice === null) right = false; + else right = (choice === correctTrue); + + if (right) { + st.score = (typeof st.score === 'number' ? st.score : 0) + QUIZ_TF_POINTS_PER_CORRECT; + anyRight = true; + } + /* ผิดทุกกรณี = ล็อกโซนจริงเท็จ + กลับจุดเกิด (สุ่มใน spawnArea ถ้ามี) */ + if (!right) { + st.cannotTrue = true; + st.cannotFalse = true; + const joinOrd = typeof p.spawnJoinOrder === 'number' && Number.isFinite(p.spawnJoinOrder) + ? Math.max(0, Math.floor(p.spawnJoinOrder)) + : [...space.peers.keys()].indexOf(peerId); + const pos = quizWrongAnswerRespawnPosition(mdLive, joinOrd); + p.x = pos.x; + p.y = pos.y; + ejectMoves.push({ + id: peerId, + x: p.x, + y: p.y, + direction: p.direction || 'down', + characterId: p.characterId ?? null, + }); + } + + results.push({ + id: peerId, + nickname: p.nickname || '', + right, + choice, + eliminated: !!st.eliminated, + score: typeof st.score === 'number' ? st.score : 0, + }); + }); + + ejectMoves.forEach((ev) => { + io.to(sid).emit('user-move', ev); + try { + const sock = io.of('/').sockets && io.of('/').sockets.get(ev.id); + if (sock) sock.emit('user-move', ev); + } catch (e) { /* ignore */ } + }); + + const allWrong = eligibleCount > 0 && !anyRight; + + const scores = {}; + space.peers.forEach((_, peerId) => { + const st = sess.players[peerId]; + scores[peerId] = st && typeof st.score === 'number' ? st.score : 0; + }); + + io.to(sid).emit('quiz-result', { + questionIndex: idx + 1, + correctTrue, + results, + allWrong, + scores, + }); + emitQuizPlayerStates(sid, space); + + /** ภารกิจสืบสวน / mng8a80o — เล่นครบ quizRoundQuestionCount แม้ทุกคนผิดข้อนี้ (เหมือน preview ไม่จบกลางรอบ) */ + const playFullQuizRound = + !!space.detectiveMinigameActive || isQuizQuestionMissionShellSpace(space); + if (allWrong && !playFullQuizRound) { + endQuizGame(sid, space, 'จบเกม — ทุกคนตอบผิดข้อนี้'); + return; + } + + sess.qIndex++; + if (sess.qIndex >= sess.questions.length) { + endQuizGame(sid, space, 'จบเกม — ครบทุกข้อแล้ว'); + return; + } + + const betweenMs = (Number.isFinite(sess.betweenMs) && sess.betweenMs >= 0) ? sess.betweenMs : 3500; + const mapRef = sess.quizMapMd || md; + const t = setTimeout(() => scheduleQuizReadPhase(sid, space, mapRef), betweenMs); + space.quizTimers.push(t); +} + +function startQuizGame(sid, space, md) { + clearSpaceQuizTimers(space); + const settings = loadQuizSettings(); + const pool = getQuizQuestionPool(md); + const shuffled = shuffleQuizQuestions(pool); + const cap = clampQuizRoundQuestionCount(settings.quizRoundQuestionCount, 10); + const picked = shuffled.slice(0, Math.min(cap, shuffled.length)); + const players = {}; + space.peers.forEach((_, peerId) => { + players[peerId] = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + }); + space.quizSession = { + active: true, + questions: picked, + qIndex: 0, + phase: 'idle', + phaseEndsAt: 0, + readMs: settings.readMs, + answerMs: settings.answerMs, + betweenMs: settings.betweenMs, + players, + quizMapMd: md, + }; + scheduleQuizReadPhase(sid, space, md); +} + +function safeMapId(id) { + return (id || '').replace(/[^a-z0-9_-]/gi, '') || null; +} + +/** กริด hub = 0/1 · option = 0 หรือเลขช่อง 1..QUIZ_CARRY_MAX_OPTION_SLOTS (ห้ามใช้กฎ === 1 กับ option — เลข 2–16หาย) */ +function normalizeQuizCarryLayersOnMap(m) { + if (!m || m.gameType !== 'quiz_carry') return; + const w = m.width || 20, h = m.height || 15; + const normHub = () => { + const src = m.quizCarryHubArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + m.quizCarryHubArea = rows; + }; + const normOptions = () => { + const src = m.quizCarryOptionArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= QUIZ_CARRY_MAX_OPTION_SLOTS ? Math.floor(n) : 0); + } + rows.push(row); + } + m.quizCarryOptionArea = rows; + }; + const normCountdown = () => { + const src = m.carryEmbedCountdownArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + row.push(Number(v) === 1 ? 1 : 0); + } + rows.push(row); + } + m.carryEmbedCountdownArea = rows; + }; + normHub(); + normOptions(); + normCountdown(); +} + +/** กริดแพลตฟอร์มกระโดดให้รอด — 0/1 ต่อช่อง */ +function normalizeQuizBattleDomeAreaOnMap(m) { + if (!m || m.gameType !== 'quiz_battle') return; + const w = m.width || 20, h = m.height || 15; + const src = m.quizBattleDomeArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + m.quizBattleDomeArea = rows; +} + +function normalizeQuizBattlePathAreaOnMap(m) { + if (!m || m.gameType !== 'quiz_battle') return; + const w = m.width || 20, h = m.height || 15; + const src = m.quizBattlePathArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + m.quizBattlePathArea = rows; +} + +function quizBattlePathModeActiveServer(m) { + if (!m || m.gameType !== 'quiz_battle' || !m.quizBattlePathArea) return false; + const g = m.quizBattlePathArea; + for (let y = 0; y < g.length; y++) { + const row = g[y]; + if (!row) continue; + for (let x = 0; x < row.length; x++) if (row[x] === 1) return true; + } + return false; +} + +function quizBattleFootprintFullyOnPathServer(m, px, py) { + if (!quizBattlePathModeActiveServer(m)) return true; + const g = m.quizBattlePathArea; + const keys = quizBattleSpriteBoundsTileKeys(m, px, py); + if (!keys.length) return false; + for (const k of keys) { + const [tx, ty] = k.split(',').map(Number); + if (!g[ty] || g[ty][tx] !== 1) return false; + } + return true; +} + +/** ยืน/เดินได้บนเลนม่วง หรือทุกช่องสไปรต์อยู่ใน spawnArea (ฟ้า) */ +function quizBattlePositionAllowedServer(m, px, py) { + if (!quizBattlePathModeActiveServer(m)) return true; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return false; + if (quizBattleFootprintFullyOnPathServer(m, px, py)) return true; + const sa = m.spawnArea; + if (!sa || !Array.isArray(sa)) return false; + const keys = quizBattleSpriteBoundsTileKeys(m, px, py); + if (!keys.length) return false; + for (const k of keys) { + const [tx, ty] = k.split(',').map(Number); + if (!sa[ty] || sa[ty][tx] !== 1) return false; + } + return true; +} + +function serverFootprintClearOfWalls(md, px, py) { + const w = md.width || 20, h = md.height || 15; + const keys = serverWallCollisionTileKeys(md, px, py); + if (!keys.length) return false; + for (const k of keys) { + const [tx, ty] = k.split(',').map(Number); + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = md.objects && md.objects[ty]; + if (!row || row[tx] === 1) return false; + } + return true; +} + +/** จุดใกล้สุดบนเส้นทาง ( footprint อยู่ใน path + ไม่ทับกำแพง ) — ใช้ตอน join / เริ่มเกม */ +function snapPositionOntoQuizBattlePathServer(md, px, py) { + if (!md || md.gameType !== 'quiz_battle' || !quizBattlePathModeActiveServer(md)) return { x: px, y: py }; + if (quizBattleSnapTargetValidServer(md, px, py)) return { x: px, y: py }; + const w = md.width || 20, h = md.height || 15; + const g = md.quizBattlePathArea; + let bestX = null, bestY = null, bestD = Infinity; + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + const nx = tx + 0.01; + const ny = ty + 0.01; + if (!quizBattleFootprintFullyOnPathServer(md, nx, ny)) continue; + if (!serverFootprintClearOfWalls(md, nx, ny)) continue; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + const nx = tx + 0.5; + const ny = ty + 0.5; + if (!quizBattleFootprintFullyOnPathServer(md, nx, ny)) continue; + if (!serverFootprintClearOfWalls(md, nx, ny)) continue; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + const sa = md.spawnArea; + if (sa && Array.isArray(sa)) { + for (let ty = 0; ty < h; ty++) { + const row = sa[ty]; + if (!row) continue; + for (let tx = 0; tx < w; tx++) { + if (row[tx] !== 1) continue; + for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) { + if (!quizBattleSnapTargetValidServer(md, nx, ny)) continue; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + return { x: px, y: py }; +} + +function collectQuizBattleValidSpawnCentersServer(md) { + const out = []; + if (!md || md.gameType !== 'quiz_battle' || !quizBattlePathModeActiveServer(md)) return out; + const w = md.width || 20, h = md.height || 15; + const g = md.quizBattlePathArea; + const seen = new Set(); + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) { + const key = nx.toFixed(3) + ',' + ny.toFixed(3); + if (seen.has(key)) continue; + if (!quizBattleFootprintFullyOnPathServer(md, nx, ny)) continue; + if (!serverFootprintClearOfWalls(md, nx, ny)) continue; + seen.add(key); + out.push({ tx, ty, x: nx, y: ny }); + } + } + } + out.sort((a, b) => a.ty - b.ty || a.tx - b.tx || a.x - b.x); + return out; +} + +function quizBattleSnapTargetValidServer(md, x, y) { + if (!md || !md.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + if (!serverFootprintClearOfWalls(md, x, y)) return false; + if (quizBattlePathModeActiveServer(md) && !quizBattlePositionAllowedServer(md, x, y)) return false; + return true; +} + +function quizBattleNearestValidSpawnWorldServer(md, prefTx, prefTy) { + const ptx = Math.floor(Number(prefTx)) || 0; + const pty = Math.floor(Number(prefTy)) || 0; + for (const [nx, ny] of [[ptx + 0.5, pty + 0.5], [ptx + 0.01, pty + 0.01]]) { + if (serverFootprintClearOfWalls(md, nx, ny)) return { x: nx, y: ny }; + } + const pool = collectQuizBattleSpawnPoolServer(md); + if (pool.length) { + let best = pool[0], bestD = Infinity; + for (const c of pool) { + const d = Math.abs(c.tx - ptx) + Math.abs(c.ty - pty); + if (d < bestD) { bestD = d; best = c; } + } + return { x: best.x, y: best.y }; + } + const valid = collectQuizBattleValidSpawnCentersServer(md); + if (!valid.length) return { x: ptx + 0.5, y: pty + 0.5 }; + let best = valid[0], bestD = Infinity; + for (const c of valid) { + const d = Math.abs(c.tx - ptx) + Math.abs(c.ty - pty); + if (d < bestD) { bestD = d; best = c; } + } + return { x: best.x, y: best.y }; +} + +function collectQuizBattleSpawnPoolServer(md) { + const out = []; + if (!md) return out; + const w = md.width || 20, h = md.height || 15; + const grid = md.spawnArea; + const seen = new Set(); + const cells = []; + if (grid && Array.isArray(grid)) { + for (let ty = 0; ty < h; ty++) { + const row = grid[ty]; + if (!row) continue; + for (let tx = 0; tx < w; tx++) { + if (Number(row[tx]) === 1) cells.push({ tx, ty }); + } + } + } + if (!cells.length && md.spawn) { + cells.push({ + tx: Math.max(0, Math.min(w - 1, Math.floor(Number(md.spawn.x)) || 1)), + ty: Math.max(0, Math.min(h - 1, Math.floor(Number(md.spawn.y)) || 1)), + }); + } + for (const { tx, ty } of cells) { + for (const [nx, ny] of [[tx + 0.5, ty + 0.5], [tx + 0.01, ty + 0.01]]) { + const key = nx.toFixed(3) + ',' + ny.toFixed(3); + if (seen.has(key)) continue; + if (quizBattlePathModeActiveServer(md)) { + if (!serverFootprintClearOfWalls(md, nx, ny)) continue; + } else if (!isMapTileWalkableForSpawn(md, tx, ty)) { + continue; + } + seen.add(key); + out.push({ tx, ty, x: nx, y: ny }); + } + } + if (!out.length && quizBattlePathModeActiveServer(md)) { + return collectQuizBattleValidSpawnCentersServer(md); + } + return out; +} + +function pickQuizBattleRandomSpawnWorldServer(md) { + const pool = collectQuizBattleSpawnPoolServer(md); + const fb = md.spawn || { x: 1, y: 1 }; + if (!pool.length) { + return quizBattleNearestValidSpawnWorldServer(md, fb.x, fb.y); + } + const idx = typeof crypto.randomInt === 'function' + ? crypto.randomInt(0, pool.length) + : Math.floor(Math.random() * pool.length); + const pick = pool[idx]; + return { x: pick.x, y: pick.y }; +} + +function quizBattleSpawnWorldFromJoinOrderServer(md, joinOrderIndex) { + if (!md) return { x: 1.5, y: 1.5 }; + const ord = joinOrderIndex | 0; + const mode = md.lobbySpawnMode; + if (mode === 'slots6') { + const slots = parseLobbyPlayerSpawnsFromMap(md); + const j = Math.min(Math.max(0, ord), 5); + const slot = slots[j]; + if (slot) return quizBattleNearestValidSpawnWorldServer(md, slot.x, slot.y); + return pickQuizBattleRandomSpawnWorldServer(md); + } + if (mode === 'fixed' && md.spawn) { + const sx = Number(md.spawn.x) || 1, sy = Number(md.spawn.y) || 1; + return quizBattleNearestValidSpawnWorldServer(md, sx, sy); + } + return pickQuizBattleRandomSpawnWorldServer(md); +} + +function pickQuizBattleSpawnFromMap(md, joinOrderIndex) { + const world = quizBattleSpawnWorldFromJoinOrderServer(md, joinOrderIndex); + return { x: Math.floor(world.x), y: Math.floor(world.y) }; +} + +function normalizeJumpSurvivePlatformAreaOnMap(m) { + if (!m || m.gameType !== 'jump_survive') return; + const w = m.width || 20, h = m.height || 15; + const src = m.jumpSurvivePlatformArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + m.jumpSurvivePlatformArea = rows; +} + +function normalizeJumpSurvivePlatformVariantAreaOnMap(m) { + if (!m || m.gameType !== 'jump_survive') return; + const w = m.width || 20, h = m.height || 15; + const pa = m.jumpSurvivePlatformArea || []; + const src = m.jumpSurvivePlatformVariantArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const row = []; + for (let x = 0; x < w; x++) { + const has = pa[y] && pa[y][x] === 1; + let v = has ? Math.floor(Number(src[y] && src[y][x])) : 0; + if (has && (!Number.isFinite(v) || v < 1)) v = 1; + if (v > 3) v = 3; + if (!has) v = 0; + row.push(v); + } + rows.push(row); + } + m.jumpSurvivePlatformVariantArea = rows; +} + +function normalizeJumpSurviveHazardAreaOnMap(m) { + if (!m || m.gameType !== 'jump_survive') return; + const w = m.width || 20, h = m.height || 15; + const src = m.jumpSurviveHazardArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + m.jumpSurviveHazardArea = rows; +} + +/** + * แมปภารกิจ Stack Tower (`mnn93hpi`): ปิดกล้องตามหอเสมอ · คลีน/จำกัดค่า stackTowerBgScroll (intro+loop แนวตั้ง) จาก JSON + */ +function applyStackTowerMissionMapPlayPolicy(safeId, m) { + if (safeId !== STACK_TOWER_MISSION_MAP_ID || !m || m.gameType !== 'stack') return; + const raw = m.stackTowerBgScroll && typeof m.stackTowerBgScroll === 'object' ? m.stackTowerBgScroll : {}; + const dirRaw = String(raw.scrollDirection || raw.direction || 'down').toLowerCase(); + const scrollDirection = (dirRaw === 'down' || dirRaw === 'top' || dirRaw === 'toptobottom') ? 'down' : 'up'; + const spNum = Number(raw.speedPxPerSec); + const speedPxPerSec = Number.isFinite(spNum) + ? Math.max(0, Math.min(400, Math.floor(spNum))) + : 0; + const sel = Math.floor(Number(raw.stepEveryLayers)); + const stepEveryLayers = Number.isFinite(sel) ? Math.max(0, Math.min(200, sel)) : 0; + const ssp = Math.floor(Number(raw.stepScrollPx)); + const stepScrollPx = Number.isFinite(ssp) ? Math.max(0, Math.min(8000, ssp)) : 0; + const sam = Math.floor(Number(raw.stepAnimMs)); + const stepAnimMs = Number.isFinite(sam) ? Math.max(120, Math.min(4000, sam)) : 520; + const st = { + enabled: raw.enabled === true, + speedPxPerSec, + scrollDirection, + stepEveryLayers, + stepScrollPx, + stepAnimMs, + }; + if (typeof raw.introImage === 'string' && raw.introImage.length) st.introImage = raw.introImage; + if (typeof raw.loopImage === 'string' && raw.loopImage.length) st.loopImage = raw.loopImage; + const rg = Number(raw.releaseGapWorldPx); + if (Number.isFinite(rg) && rg >= 0) st.releaseGapWorldPx = Math.min(800, Math.floor(rg)); + m.stackTowerBgScroll = st; + const rawCf = m.stackTowerCameraFollow && typeof m.stackTowerCameraFollow === 'object' ? m.stackTowerCameraFollow : {}; + const pxPerLayer = Math.max(0, Math.min(80, Math.floor(Number(rawCf.pxPerLayer)) || 12)); + const maxPx = Math.max(0, Math.min(800, Math.floor(Number(rawCf.maxPx)) || 260)); + m.stackTowerCameraFollow = { enabled: false, pxPerLayer, maxPx }; +} + +function loadMaps() { + try { + if (!fs.existsSync(path.join(__dirname, 'data'))) fs.mkdirSync(path.join(__dirname, 'data'), { recursive: true }); + if (!fs.existsSync(MAPS_DIR)) fs.mkdirSync(MAPS_DIR, { recursive: true }); + if (!fs.existsSync(CHARACTERS_DIR)) fs.mkdirSync(CHARACTERS_DIR, { recursive: true }); + const files = fs.readdirSync(MAPS_DIR); + for (const f of files) { + if (!f.endsWith('.json')) continue; + const id = f.slice(0, -5); + if (!safeMapId(id)) continue; + try { + const data = JSON.parse(fs.readFileSync(path.join(MAPS_DIR, f), 'utf8')); + normalizeQuizCarryLayersOnMap(data); + normalizeJumpSurvivePlatformAreaOnMap(data); + normalizeJumpSurvivePlatformVariantAreaOnMap(data); + normalizeJumpSurviveHazardAreaOnMap(data); + normalizeQuizBattleDomeAreaOnMap(data); + normalizeQuizBattlePathAreaOnMap(data); + applyStackTowerMissionMapPlayPolicy(id, data); + maps.set(id, data); + } catch (e) { console.error('load map', f, e.message); } + } + } catch (e) { console.error('loadMaps', e.message); } +} + +/** + * อ่านแมปหนึ่งไฟล์จากดิสก์เข้า `maps` — ใช้ตอน GET /api/maps/:id เพื่อให้ deploy แก้ JSON แล้วมีผลทันที + * (เดิมโหลดครั้งเดียวตอนบูต · ถ้าแก้ไฟล์แล้วไม่รีสตาร์ท Node ลูกค้าได้ข้อมูลเก่า) + */ +function rehydrateSingleMapFromDisk(safeId) { + if (!safeId) return false; + const fp = path.join(MAPS_DIR, safeId + '.json'); + if (!fs.existsSync(fp)) return false; + try { + const data = JSON.parse(fs.readFileSync(fp, 'utf8')); + normalizeQuizCarryLayersOnMap(data); + normalizeJumpSurvivePlatformAreaOnMap(data); + normalizeJumpSurvivePlatformVariantAreaOnMap(data); + normalizeJumpSurviveHazardAreaOnMap(data); + normalizeQuizBattleDomeAreaOnMap(data); + normalizeQuizBattlePathAreaOnMap(data); + applyStackTowerMissionMapPlayPolicy(safeId, data); + maps.set(safeId, data); + return true; + } catch (e) { + console.error('rehydrate map', safeId, e.message); + return false; + } +} + +/** โหลด LobbyB จากไฟล์อีกครั้งถ้ายังไม่อยู่ใน memory (หลังรีสตาร์ท / ไฟล์เพิ่งวาง) */ +function ensurePostCaseLobbyMapLoaded() { + if (maps.has(POST_CASE_LOBBY_SPACE_ID)) return true; + try { + const fp = path.join(MAPS_DIR, POST_CASE_LOBBY_SPACE_ID + '.json'); + if (!fs.existsSync(fp)) return false; + const data = JSON.parse(fs.readFileSync(fp, 'utf8')); + normalizeQuizCarryLayersOnMap(data); + maps.set(POST_CASE_LOBBY_SPACE_ID, data); + return true; + } catch (e) { + console.error('ensurePostCaseLobbyMapLoaded', e.message); + return false; + } +} + +function saveMap(id, m) { + const safe = safeMapId(id); + if (!safe) return; + try { + if (!fs.existsSync(MAPS_DIR)) fs.mkdirSync(MAPS_DIR, { recursive: true }); + fs.writeFileSync(path.join(MAPS_DIR, safe + '.json'), JSON.stringify(m, null, 0), 'utf8'); + } catch (e) { console.error('saveMap', id, e.message); } +} + +function isMapNameTaken(name, excludeId) { + const n = (name || '').trim().toLowerCase(); + for (const [id, m] of maps) if (id !== excludeId && (m.name || '').trim().toLowerCase() === n) return true; + return false; +} + +function applyDevCors(req, res) { + const origin = req.headers.origin; + if (origin && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + res.setHeader('Vary', 'Origin'); + } +} + +const server = http.createServer((req, res) => { + applyDevCors(req, res); + if (req.method === 'OPTIONS' && req.url && req.url.indexOf(BASE_PATH + '/api') === 0) { + res.writeHead(204); + res.end(); + return; + } + let url = req.url; + if (url === BASE_PATH || url === BASE_PATH + '/') url = BASE_PATH + '/index.html'; + if (url.startsWith(BASE_PATH + '/api/maps')) { + let id = url.replace(BASE_PATH + '/api/maps/', '').replace(BASE_PATH + '/api/maps', '').replace(/^\//, '').split('/')[0]; + if (id.includes('?')) id = id.split('?')[0]; + if (req.method === 'GET' && id) { + const sid = safeMapId(decodeURIComponent(id)); + if (!sid) return res.writeHead(404), res.end(JSON.stringify({ error: 'ไม่พบฉาก' })); + rehydrateSingleMapFromDisk(sid); + const m = maps.get(sid); + if (!m) return res.writeHead(404), res.end(JSON.stringify({ error: 'ไม่พบฉาก' })); + return res.writeHead(200, { 'Content-Type': 'application/json' }), res.end(JSON.stringify(m)); + } + if (req.method === 'GET' && !id) { + const list = [...maps].map(([id, m]) => ({ id, name: m.name })); + return res.writeHead(200, { 'Content-Type': 'application/json' }), res.end(JSON.stringify(list)); + } + if (req.method === 'POST' && url === BASE_PATH + '/api/maps' || url === BASE_PATH + '/api/maps/') { + let body = ''; + req.on('data', c => body += c); + req.on('end', () => { + try { + const d = JSON.parse(body); + const name = (d.name || '').trim() || 'ฉากใหม่'; + if (isMapNameTaken(name)) return res.writeHead(400), res.end(JSON.stringify({ ok: false, error: 'ชื่อฉากซ้ำ กรุณาตั้งชื่ออื่น' })); + const id = Date.now().toString(36); + const m = { ...defaultMap(), ...d, name }; + if (!m.blockPlayer) m.blockPlayer = []; + if (!m.interactive) m.interactive = []; + if (!m.startGameArea) m.startGameArea = []; + if (!m.spawnArea) m.spawnArea = []; + if (!m.quizTrueArea) m.quizTrueArea = []; + if (!m.quizFalseArea) m.quizFalseArea = []; + if (!m.quizQuestionArea) m.quizQuestionArea = []; + if (!m.quizQuestions) m.quizQuestions = []; + if (!m.quizCarryHubArea) m.quizCarryHubArea = []; + if (!m.quizCarryOptionArea) m.quizCarryOptionArea = []; + if (!m.carryEmbedCountdownArea) m.carryEmbedCountdownArea = []; + if (!m.quizBattleDomeArea) m.quizBattleDomeArea = []; + if (!m.quizBattlePathArea) m.quizBattlePathArea = []; + if (!m.stackReleaseArea) m.stackReleaseArea = []; + if (!m.stackLandArea) m.stackLandArea = []; + if (!Array.isArray(m.jumpSurvivePlatforms)) m.jumpSurvivePlatforms = []; + if (!m.jumpSurvivePlatformArea) m.jumpSurvivePlatformArea = []; + if (!m.jumpSurvivePlatformVariantArea) m.jumpSurvivePlatformVariantArea = []; + if (!m.jumpSurviveHazardArea) m.jumpSurviveHazardArea = []; + normalizeQuizCarryLayersOnMap(m); + normalizeJumpSurvivePlatformAreaOnMap(m); + normalizeJumpSurvivePlatformVariantAreaOnMap(m); + normalizeJumpSurviveHazardAreaOnMap(m); + normalizeQuizBattleDomeAreaOnMap(m); + normalizeQuizBattlePathAreaOnMap(m); + maps.set(id, m); + saveMap(id, m); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, mapId: id })); + } catch (e) { + res.writeHead(400), res.end(JSON.stringify({ ok: false, error: 'ข้อมูลไม่ถูกต้อง' })); + } + }); + return; + } + if (req.method === 'PUT' && id && maps.has(id)) { + let body = ''; + req.on('data', c => body += c); + req.on('end', () => { + try { + const d = JSON.parse(body); + const name = (d.name || '').trim() || maps.get(id).name; + if (isMapNameTaken(name, id)) return res.writeHead(400), res.end(JSON.stringify({ ok: false, error: 'ชื่อฉากซ้ำ กรุณาตั้งชื่ออื่น' })); + const m = { ...maps.get(id), ...d, name }; + if (!m.blockPlayer) m.blockPlayer = []; + if (!m.interactive) m.interactive = []; + if (!m.startGameArea) m.startGameArea = []; + if (!m.spawnArea) m.spawnArea = []; + if (!m.quizTrueArea) m.quizTrueArea = []; + if (!m.quizFalseArea) m.quizFalseArea = []; + if (!m.quizQuestionArea) m.quizQuestionArea = []; + if (!m.quizQuestions) m.quizQuestions = []; + if (!m.quizCarryHubArea) m.quizCarryHubArea = []; + if (!m.quizCarryOptionArea) m.quizCarryOptionArea = []; + if (!m.carryEmbedCountdownArea) m.carryEmbedCountdownArea = []; + if (!m.quizBattleDomeArea) m.quizBattleDomeArea = []; + if (!m.quizBattlePathArea) m.quizBattlePathArea = []; + if (!m.stackReleaseArea) m.stackReleaseArea = []; + if (!m.stackLandArea) m.stackLandArea = []; + if (!Array.isArray(m.jumpSurvivePlatforms)) m.jumpSurvivePlatforms = []; + if (!m.jumpSurvivePlatformArea) m.jumpSurvivePlatformArea = []; + if (!m.jumpSurvivePlatformVariantArea) m.jumpSurvivePlatformVariantArea = []; + if (!m.jumpSurviveHazardArea) m.jumpSurviveHazardArea = []; + normalizeQuizCarryLayersOnMap(m); + normalizeJumpSurvivePlatformAreaOnMap(m); + normalizeJumpSurvivePlatformVariantAreaOnMap(m); + normalizeJumpSurviveHazardAreaOnMap(m); + normalizeQuizBattleDomeAreaOnMap(m); + normalizeQuizBattlePathAreaOnMap(m); + applyStackTowerMissionMapPlayPolicy(id, m); + maps.set(id, m); + saveMap(id, m); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, mapId: id })); + } catch (e) { + res.writeHead(400), res.end(JSON.stringify({ ok: false, error: 'ข้อมูลไม่ถูกต้อง' })); + } + }); + return; + } + } + const quizCarryDiskUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/'; + const quizCarryDiskPaths = new Set([ + BASE_PATH + '/api/quiz-carry-from-disk', + BASE_PATH + '/api-quiz-carry-from-disk.php', + ]); + if (quizCarryDiskPaths.has(quizCarryDiskUrlPath)) { + if (req.method === 'GET') { + const g = loadQuizSettings(); + const slice = { + carryMapPanelTheme: g.carryMapPanelTheme, + carryEmbedCountdownTheme: g.carryEmbedCountdownTheme, + carryChoicePlaqueThemes: g.carryChoicePlaqueThemes, + carryChoicePlaqueTheme: g.carryChoicePlaqueTheme, + carryChoicePlaqueMapScale: g.carryChoicePlaqueMapScale, + carryReadMs: g.carryReadMs, + carryAnswerMs: g.carryAnswerMs, + carrySessionLength: g.carrySessionLength, + carryWalkSpeedMultForMapId: g.carryWalkSpeedMultForMapId, + carryWalkSpeedMult: g.carryWalkSpeedMult, + carryQuestions: g.carryQuestions, + }; + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store, no-cache, must-revalidate', + Pragma: 'no-cache', + }); + return res.end(JSON.stringify(slice)); + } + res.writeHead(405); + return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' })); + } + const quizSettingsUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/'; + if (quizSettingsUrlPath === BASE_PATH + '/api/quiz-settings') { + if (req.method === 'GET') { + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store, no-cache, must-revalidate', + Pragma: 'no-cache', + }); + return res.end(JSON.stringify(loadQuizSettings())); + } + if (req.method === 'PUT') { + let body = ''; + req.on('data', (c) => { body += c; }); + req.on('end', () => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + try { + const d = JSON.parse(body || '{}'); + const saved = saveQuizSettings(d); + if (!saved.ok) { + res.writeHead(500); + return res.end(JSON.stringify({ ok: false, error: saved.error || 'บันทึกไม่ได้' })); + } + res.writeHead(200); + return res.end(JSON.stringify({ + ok: true, + battleQuizMcqSaved: saved.battleQuizMcqSaved, + carryQuestionsSaved: saved.carryQuestionsSaved, + specialQuizSets: saved.specialQuizSets, + carryMapPanelTheme: saved.carryMapPanelTheme, + carryEmbedCountdownTheme: saved.carryEmbedCountdownTheme, + carryChoicePlaqueThemes: saved.carryChoicePlaqueThemes, + carryChoicePlaqueTheme: saved.carryChoicePlaqueTheme, + carryChoicePlaqueMapScale: saved.carryChoicePlaqueMapScale, + })); + } catch (e) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'ข้อมูลไม่ถูกต้อง' })); + } + }); + return; + } + res.writeHead(405); + return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' })); + } + const evidenceCardsUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/'; + if (evidenceCardsUrlPath === BASE_PATH + '/api/evidence-cards') { + if (req.method === 'GET') { + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store, no-cache, must-revalidate', + Pragma: 'no-cache', + }); + return res.end(JSON.stringify(loadEvidenceCards())); + } + if (req.method === 'PUT') { + let body = ''; + req.on('data', (c) => { body += c; }); + req.on('end', () => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + try { + const d = JSON.parse(body || '{}'); + const saved = saveEvidenceCards(d); + if (!saved.ok) { + res.writeHead(500); + return res.end(JSON.stringify({ ok: false, error: saved.error || 'บันทึกไม่ได้' })); + } + res.writeHead(200); + return res.end(JSON.stringify({ ok: true, cases: saved.cases })); + } catch (e) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'ข้อมูลไม่ถูกต้อง' })); + } + }); + return; + } + res.writeHead(405); + return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' })); + } + const evidenceImagesUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/'; + if (evidenceImagesUrlPath === BASE_PATH + '/api/evidence-card-images') { + if (req.method === 'GET') { + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store, no-cache, must-revalidate', + Pragma: 'no-cache', + }); + return res.end(JSON.stringify({ images: listEvidenceCardImages(), pools: evidencePoolCounts(), dirs: EVIDENCE_IMAGE_DIRS })); + } + res.writeHead(405); + return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' })); + } + const caseMediaUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/'; + if (caseMediaUrlPath === BASE_PATH + '/api/case-media') { + if (req.method === 'GET') { + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store, no-cache, must-revalidate', + Pragma: 'no-cache', + }); + return res.end(JSON.stringify(loadCaseMedia())); + } + if (req.method === 'PUT') { + let body = ''; + req.on('data', (c) => { body += c; }); + req.on('end', () => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + try { + const d = JSON.parse(body || '{}'); + const saved = saveCaseMedia(d); + if (!saved.ok) { + res.writeHead(500); + return res.end(JSON.stringify({ ok: false, error: saved.error || 'บันทึกไม่ได้' })); + } + res.writeHead(200); + return res.end(JSON.stringify({ ok: true, cases: saved.cases })); + } catch (e) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'ข้อมูลไม่ถูกต้อง' })); + } + }); + return; + } + res.writeHead(405); + return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' })); + } + const caseMediaImagesUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/'; + if (caseMediaImagesUrlPath === BASE_PATH + '/api/case-media-images') { + if (req.method === 'GET') { + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store, no-cache, must-revalidate', + Pragma: 'no-cache', + }); + return res.end(JSON.stringify({ images: listCaseMediaImages(), count: CASE_MEDIA_COUNT })); + } + res.writeHead(405); + return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' })); + } + const gameTimingUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/'; + if (gameTimingUrlPath === BASE_PATH + '/api/game-timing') { + if (req.method === 'GET') { + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store, no-cache, must-revalidate', + 'Pragma': 'no-cache', + }); + return res.end(JSON.stringify({ ...runtimeGameTiming })); + } + if (req.method === 'PUT') { + let body = ''; + req.on('data', (c) => { body += c; }); + req.on('end', () => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.setHeader('Cache-Control', 'no-store'); + try { + const d = JSON.parse(body || '{}'); + const saved = saveGameTimingToFile(d); + if (!saved.ok) { + res.writeHead(500); + return res.end(JSON.stringify({ ok: false, error: saved.error || 'บันทึกไม่ได้' })); + } + res.writeHead(200); + const { ok: _ok, error: _err, ...timingRest } = saved; + return res.end(JSON.stringify({ ok: true, ...timingRest })); + } catch (e) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'ข้อมูลไม่ถูกต้อง' })); + } + }); + return; + } + res.writeHead(405); + return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' })); + } + const gaAssetsApiPath = url.split('?')[0].replace(/\/+$/, '') || '/'; + if (gaAssetsApiPath === BASE_PATH + '/api/gauntlet-assets' && req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify({ ok: true, items: listGauntletAssetsApi() })); + } + if (gaAssetsApiPath === BASE_PATH + '/api/gauntlet-assets/upload' && req.method === 'POST') { + let body = ''; + req.on('data', (c) => { body += c; }); + req.on('end', () => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + try { + const d = JSON.parse(body || '{}'); + const dataUrl = d.imageDataUrl; + if (!dataUrl || typeof dataUrl !== 'string') { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'ต้องส่ง imageDataUrl (data URL รูป)' })); + } + const m = dataUrl.match(/^data:image\/(png|jpeg|jpg|gif|webp);base64,([\s\S]+)$/i); + if (!m) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'รองรับเฉพาะ png / jpg / gif / webp' })); + } + const extRaw = m[1].toLowerCase(); + const ext = extRaw === 'jpeg' ? 'jpg' : extRaw; + const b64 = m[2].replace(/\s/g, ''); + let buf; + try { + buf = Buffer.from(b64, 'base64'); + } catch (e) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'base64 ไม่ถูกต้อง' })); + } + if (buf.length < 16 || buf.length > 4 * 1024 * 1024) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'ขนาดไฟล์ต้องอยู่ระหว่าง 16 ไบต์ ถึง 4 MB' })); + } + ensureGauntletAssetsDir(); + const labelIn = d.label != null ? String(d.label).trim().slice(0, 120) : ''; + const replaceFilename = d.replaceFilename != null ? String(d.replaceFilename) : ''; + let fname; + if (replaceFilename) { + const s = safeGauntletStoredFilename(replaceFilename); + if (!s) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'ชื่อไฟล์แทนที่ไม่ถูกต้อง' })); + } + const cur = path.join(GAUNTLET_ASSETS_DIR, s); + if (!fs.existsSync(cur)) { + res.writeHead(404); + return res.end(JSON.stringify({ ok: false, error: 'ไม่พบไฟล์เดิม' })); + } + const norm = (e) => { + let x = String(e || '').toLowerCase(); + if (x.startsWith('.')) x = x.slice(1); + return x === 'jpeg' ? 'jpg' : x; + }; + if (norm(path.extname(s)) !== norm(ext)) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'นามสกุลรูปใหม่ต้องตรงกับไฟล์เดิม' })); + } + fname = s; + } else { + fname = `gauntlet-${crypto.randomBytes(8).toString('hex')}.${ext}`; + } + fs.writeFileSync(path.join(GAUNTLET_ASSETS_DIR, fname), buf); + const meta = loadGauntletAssetsMeta(); + const prev = meta[fname] || {}; + meta[fname] = { + ...prev, + label: labelIn || prev.label || fname, + updatedAt: Date.now(), + }; + saveGauntletAssetsMeta(meta); + res.writeHead(200); + return res.end(JSON.stringify({ + ok: true, + filename: fname, + url: BASE_PATH + '/img/gauntlet-assets/' + fname, + })); + } catch (e) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'อัปโหลดไม่สำเร็จ: ' + (e.message || '') })); + } + }); + return; + } + const qCarryPlaqueUploadPath = url.split('?')[0].replace(/\/+$/, '') || '/'; + if (qCarryPlaqueUploadPath === BASE_PATH + '/api/quiz-carry-plaque-upload' && req.method === 'POST') { + let body = ''; + req.on('data', (c) => { body += c; }); + req.on('end', () => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + try { + const d = JSON.parse(body || '{}'); + const dataUrl = d.imageDataUrl; + if (!dataUrl || typeof dataUrl !== 'string') { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'ต้องส่ง imageDataUrl (data URL รูป)' })); + } + const m = dataUrl.match(/^data:image\/(png|jpeg|jpg|gif|webp);base64,([\s\S]+)$/i); + if (!m) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'รองรับเฉพาะ png / jpg / gif / webp' })); + } + const extRaw = m[1].toLowerCase(); + const ext = extRaw === 'jpeg' ? 'jpg' : extRaw; + const b64 = m[2].replace(/\s/g, ''); + let buf; + try { + buf = Buffer.from(b64, 'base64'); + } catch (e) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'base64 ไม่ถูกต้อง' })); + } + if (buf.length < 16 || buf.length > 4 * 1024 * 1024) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'ขนาดไฟล์ต้องอยู่ระหว่าง 16 ไบต์ ถึง 4 MB' })); + } + ensureQuizCarryPlaqueAssetsDir(); + const fname = `qcarry-${crypto.randomBytes(8).toString('hex')}.${ext}`; + fs.writeFileSync(path.join(QUIZ_CARRY_PLAQUE_ASSETS_DIR, fname), buf); + res.writeHead(200); + return res.end(JSON.stringify({ + ok: true, + filename: fname, + url: BASE_PATH + '/img/quiz-carry-plaque-assets/' + fname, + })); + } catch (e) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'อัปโหลดไม่สำเร็จ: ' + (e.message || '') })); + } + }); + return; + } + if (gaAssetsApiPath === BASE_PATH + '/api/gauntlet-assets' && req.method === 'PATCH') { + let body = ''; + req.on('data', (c) => { body += c; }); + req.on('end', () => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + try { + const d = JSON.parse(body || '{}'); + const fname = safeGauntletStoredFilename(d.filename); + if (!fname) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'filename ไม่ถูกต้อง' })); + } + const fp = path.join(GAUNTLET_ASSETS_DIR, fname); + if (!fs.existsSync(fp)) { + res.writeHead(404); + return res.end(JSON.stringify({ ok: false, error: 'ไม่พบไฟล์' })); + } + const meta = loadGauntletAssetsMeta(); + const label = d.label != null ? String(d.label).trim().slice(0, 120) : ''; + meta[fname] = { ...(meta[fname] || {}), label: label || fname, updatedAt: Date.now() }; + saveGauntletAssetsMeta(meta); + res.writeHead(200); + return res.end(JSON.stringify({ ok: true, filename: fname, label: meta[fname].label })); + } catch (e) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: e.message || 'บันทึกไม่ได้' })); + } + }); + return; + } + if (gaAssetsApiPath === BASE_PATH + '/api/gauntlet-assets' && req.method === 'DELETE') { + try { + const q = url.includes('?') ? url.split('?')[1] : ''; + const sp = new URLSearchParams(q); + const rawFn = sp.get('file'); + const fname = safeGauntletStoredFilename(rawFn); + if (!fname) { + res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify({ ok: false, error: 'ระบุ file ไม่ถูกต้อง' })); + } + const fp = path.join(GAUNTLET_ASSETS_DIR, fname); + if (fs.existsSync(fp)) fs.unlinkSync(fp); + const meta = loadGauntletAssetsMeta(); + delete meta[fname]; + saveGauntletAssetsMeta(meta); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify({ ok: true })); + } catch (e) { + res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify({ ok: false, error: 'ลบไม่สำเร็จ' })); + } + } + if (url === BASE_PATH + '/api/characters' && req.method === 'GET') { + try { + if (!fs.existsSync(CHARACTERS_DIR)) fs.mkdirSync(CHARACTERS_DIR, { recursive: true }); + const files = fs.readdirSync(CHARACTERS_DIR); + const ids = new Set(); + const ext = /\.(png|jpg|jpeg|gif|webp)$/i; + files.forEach(f => { + let m = f.match(/^(.+)_(up|down|left|right)(?:_\d+)?\.(png|jpg|jpeg|gif|webp)$/i); + if (m) ids.add(m[1]); + m = f.match(/^(.+)_(up|down|left|right)(?:_\d+)?_layer_[a-zA-Z]+\.(png|jpg|jpeg|gif|webp)$/i); + if (m) ids.add(m[1]); + m = f.match(/^(.+)_(up|down|left|right)_idle(?:_\d+)?_layer_[a-zA-Z]+\.(png|jpg|jpeg|gif|webp)$/i); + if (m) ids.add(m[1]); + m = f.match(/^(.+)_(up|down|left|right)_idle(?:_\d+)?\.(png|jpg|jpeg|gif|webp)$/i); + if (m) ids.add(m[1]); + }); + const idArr = [...ids]; + const oldestMsById = {}; + idArr.forEach((id) => { + let oldest = Infinity; + const prefix = id + '_'; + files.forEach((f) => { + if (!f.startsWith(prefix) || !ext.test(f)) return; + try { + const st = fs.statSync(path.join(CHARACTERS_DIR, f)); + const t = st.mtimeMs != null ? st.mtimeMs : st.mtime.getTime(); + if (t < oldest) oldest = t; + } catch (e) { /* ignore */ } + }); + oldestMsById[id] = oldest === Infinity ? 0 : oldest; + }); + idArr.sort((a, b) => { + const ta = oldestMsById[a] || 0; + const tb = oldestMsById[b] || 0; + if (ta !== tb) return ta - tb; + return String(a).localeCompare(String(b)); + }); + const list = idArr.map((id) => { + const prefix = id + '_'; + const hasLayerFiles = files.some((f) => f.startsWith(prefix) && f.includes('_layer_') && ext.test(f)); + const layerNameSet = new Set(); + if (hasLayerFiles) { + files.forEach((f) => { + if (!f.startsWith(prefix) || !ext.test(f)) return; + let m = f.match(/_(?:up|down|left|right)_idle(?:_\d+)?_layer_([a-zA-Z]+)\.[^.]+$/i); + if (m) { + layerNameSet.add(m[1]); + return; + } + m = f.match(/_(?:up|down|left|right)(?:_\d+)?_layer_([a-zA-Z]+)\.[^.]+$/i); + if (m) layerNameSet.add(m[1]); + }); + } + const layers = layerNameSet.size ? [...layerNameSet].sort() : []; + const layerManifest = hasLayerFiles ? buildCharacterLayerManifest(id, files) : null; + return { id, name: id, hasLayerFiles, layers, layerManifest }; + }); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify(list)); + } catch (e) { + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify([])); + } + } + const urlPath = url.split('?')[0]; + { + const lm = urlPath.match(new RegExp('^' + String(BASE_PATH).replace(/\//g, '\\/') + '\\/api\\/characters\\/([^/]+)\\/layer-manifest$')); + if (lm && req.method === 'GET') { + const id = decodeURIComponent(lm[1] || '').replace(/[^a-z0-9_-]/gi, ''); + if (!id) { + res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify({ ok: false, error: 'id ไม่ถูกต้อง', byDir: {}, byDirIdle: {} })); + } + try { + if (!fs.existsSync(CHARACTERS_DIR)) fs.mkdirSync(CHARACTERS_DIR, { recursive: true }); + const files = fs.readdirSync(CHARACTERS_DIR); + const manifest = buildCharacterLayerManifest(id, files); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify(manifest)); + } catch (e) { + res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify({ ok: false, error: e.message || 'manifest fail', byDir: {}, byDirIdle: {} })); + } + } + } + if (urlPath.startsWith(BASE_PATH + '/api/characters/') && req.method === 'DELETE') { + if (/\/layer-manifest$/i.test(urlPath)) { + res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' }); + return res.end(JSON.stringify({ ok: false, error: 'ใช้ GET เท่านั้น' })); + } + const idRaw = decodeURIComponent(urlPath.slice((BASE_PATH + '/api/characters/').length)); + const id = (idRaw || '').replace(/[^a-z0-9_-]/gi, ''); + if (!id) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ ok: false, error: 'id ไม่ถูกต้อง' })); + } + try { + if (!fs.existsSync(CHARACTERS_DIR)) fs.mkdirSync(CHARACTERS_DIR, { recursive: true }); + const files = fs.readdirSync(CHARACTERS_DIR); + const esc = id.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); + const re = new RegExp('^' + esc + '_(up|down|left|right)(?:_\\d+)?\\.(png|jpg|jpeg|gif|webp)$', 'i'); + const idleRe = new RegExp('^' + esc + '_(up|down|left|right)_idle(?:_\\d+)?\\.(png|jpg|jpeg|gif|webp)$', 'i'); + const layerRe = new RegExp('^' + esc + '_(up|down|left|right)(?:_\\d+)?_layer_[a-zA-Z]+\\.(png|jpg|jpeg|gif|webp)$', 'i'); + const layerIdleRe = new RegExp('^' + esc + '_(up|down|left|right)_idle(?:_\\d+)?_layer_[a-zA-Z]+\\.(png|jpg|jpeg|gif|webp)$', 'i'); + let removed = 0; + files.forEach(f => { + if (re.test(f) || idleRe.test(f) || layerRe.test(f) || layerIdleRe.test(f)) { + try { + fs.unlinkSync(path.join(CHARACTERS_DIR, f)); + removed++; + } catch (e) { + // ignore single file errors + } + } + }); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, removed })); + } catch (e) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: false, error: 'ลบไม่สำเร็จ: ' + (e.message || '') })); + } + return; + } + if (urlPath === BASE_PATH + '/api/characters/upload' && req.method === 'POST') { + let body = ''; + req.on('data', c => body += c); + req.on('end', () => { + try { + if (!body || body.length < 2) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ ok: false, error: 'ไม่มีข้อมูล (เลือกรูปแล้วกดอัปโหลด)' })); + } + const d = JSON.parse(body); + const dirs = ['up', 'down', 'left', 'right']; + const id = (d.name || '').trim().replace(/[^a-z0-9_-]/gi, '') || 'char-' + Date.now(); + if (!fs.existsSync(CHARACTERS_DIR)) fs.mkdirSync(CHARACTERS_DIR, { recursive: true }); + const ALLOWED_LAYER = { shadow: 1, bodyColor: 1, bodyStroke: 1, headColor: 1, headStroke: 1, hairColor: 1, hairStroke: 1, face: 1 }; + let walkWritten = 0; + for (const dir of dirs) { + let data = d[dir]; + if (!data) continue; + const list = Array.isArray(data) ? data : [data]; + let frameIndex = 0; + for (const dataUrl of list) { + if (!dataUrl || typeof dataUrl !== 'string') continue; + const m = dataUrl.match(/^data:image\/[\w+]+;base64,([\s\S]+)$/); + if (!m) continue; + const b64 = m[1].replace(/\s/g, ''); + if (!b64.length) continue; + let buf; + try { + buf = Buffer.from(b64, 'base64'); + } catch (err) { + continue; + } + if (buf.length === 0) continue; + // เขียนไฟล์เฟรม (สำหรับ animation) + const fnameFrame = list.length > 1 ? (id + '_' + dir + '_' + frameIndex + '.png') : (id + '_' + dir + '.png'); + fs.writeFileSync(path.join(CHARACTERS_DIR, fnameFrame), buf); + // ถ้ามีหลายเฟรม ให้เฟรมแรกซ้ำไปเป็นไฟล์หลัก _.png เพื่อใช้เป็น preview / fallback + if (list.length > 1 && frameIndex === 0) { + const baseName = id + '_' + dir + '.png'; + fs.writeFileSync(path.join(CHARACTERS_DIR, baseName), buf); + } + walkWritten++; + frameIndex++; + } + } + let layerWritten = 0; + // ไฟล์เลเยอร์แยก (ให้หน้า play ย้อม bodyColor / hairColor / headColor ตามส่วน — ตรงกับ character.html) + const lf = d.layerFrames; + if (lf && typeof lf === 'object') { + for (const dir of dirs) { + const arr = lf[dir]; + if (!Array.isArray(arr)) continue; + const multi = arr.length > 1; + arr.forEach((frameObj, frameIndex) => { + if (!frameObj || typeof frameObj !== 'object') return; + for (const layerName of Object.keys(frameObj)) { + if (!ALLOWED_LAYER[layerName]) continue; + const dataUrl = frameObj[layerName]; + if (!dataUrl || typeof dataUrl !== 'string') continue; + const m = dataUrl.match(/^data:image\/[\w+]+;base64,([\s\S]+)$/); + if (!m) continue; + const b64 = m[1].replace(/\s/g, ''); + if (!b64.length) continue; + let buf; + try { + buf = Buffer.from(b64, 'base64'); + } catch (err) { + continue; + } + if (buf.length === 0) continue; + const fnameLayer = multi + ? (id + '_' + dir + '_' + frameIndex + '_layer_' + layerName + '.png') + : (id + '_' + dir + '_layer_' + layerName + '.png'); + fs.writeFileSync(path.join(CHARACTERS_DIR, fnameLayer), buf); + layerWritten++; + } + }); + } + } + const lfIdle = d.layerFramesIdle; + if (lfIdle && typeof lfIdle === 'object') { + for (const dir of dirs) { + const arr = lfIdle[dir]; + if (!Array.isArray(arr)) continue; + const multi = arr.length > 1; + arr.forEach((frameObj, frameIndex) => { + if (!frameObj || typeof frameObj !== 'object') return; + for (const layerName of Object.keys(frameObj)) { + if (!ALLOWED_LAYER[layerName]) continue; + const dataUrl = frameObj[layerName]; + if (!dataUrl || typeof dataUrl !== 'string') continue; + const m = dataUrl.match(/^data:image\/[\w+]+;base64,([\s\S]+)$/); + if (!m) continue; + const b64 = m[1].replace(/\s/g, ''); + if (!b64.length) continue; + let buf; + try { + buf = Buffer.from(b64, 'base64'); + } catch (err) { + continue; + } + if (buf.length === 0) continue; + const fnameLayer = multi + ? (id + '_' + dir + '_idle_' + frameIndex + '_layer_' + layerName + '.png') + : (id + '_' + dir + '_idle_layer_' + layerName + '.png'); + fs.writeFileSync(path.join(CHARACTERS_DIR, fnameLayer), buf); + layerWritten++; + } + }); + } + } + const idleObj = d.idle && typeof d.idle === 'object' ? d.idle : null; + if (idleObj) { + for (const dir of dirs) { + let data = idleObj[dir]; + if (!data) continue; + const list = Array.isArray(data) ? data : [data]; + let frameIndex = 0; + for (const dataUrl of list) { + if (!dataUrl || typeof dataUrl !== 'string') continue; + const m = dataUrl.match(/^data:image\/[\w+]+;base64,([\s\S]+)$/); + if (!m) continue; + const b64 = m[1].replace(/\s/g, ''); + if (!b64.length) continue; + let buf; + try { + buf = Buffer.from(b64, 'base64'); + } catch (err) { + continue; + } + if (buf.length === 0) continue; + const fnameFrame = list.length > 1 + ? (id + '_' + dir + '_idle_' + frameIndex + '.png') + : (id + '_' + dir + '_idle.png'); + fs.writeFileSync(path.join(CHARACTERS_DIR, fnameFrame), buf); + if (list.length > 1 && frameIndex === 0) { + const baseName = id + '_' + dir + '_idle.png'; + fs.writeFileSync(path.join(CHARACTERS_DIR, baseName), buf); + } + frameIndex++; + } + } + } + if (walkWritten === 0 && layerWritten === 0) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ ok: false, error: 'ไม่มีรูปที่อัปโหลดได้ (ต้องมีรูปเดินหรือเลเยอร์อย่างน้อยหนึ่งทิศ)' })); + } + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, characterId: id })); + } catch (e) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: false, error: 'อัปโหลดไม่สำเร็จ: ' + (e.message || '') })); + } + }); + return; + } + if (url.startsWith(BASE_PATH + '/api/spaces')) { + const parts = url.slice((BASE_PATH + '/api/spaces').length).split('/').filter(Boolean); + const spaceId = parts[0]; + if (req.method === 'GET' && !spaceId) { + const now = Date.now(); + for (const [id, s] of [...spaces.entries()]) { + if (s.isPrivate || id === POST_CASE_LOBBY_SPACE_ID) continue; + const n = s.peers ? s.peers.size : 0; + if (n !== 0) continue; + const created = s.createdAt; + const stale = created == null || now - created > SPACE_EMPTY_TTL_MS; + if (stale) spaces.delete(id); + } + const list = [...spaces] + .filter(([, s]) => !s.isPrivate) + .filter(([, s]) => (s.peers && s.peers.size > 0)) + .map(([id, s]) => ({ + spaceId: id, + spaceName: s.spaceName || id, + mapName: (s.mapData && s.mapData.name) || '—', + peerCount: s.peers ? s.peers.size : 0, + maxPlayers: s.maxPlayers || 10 + })); + return res.writeHead(200, { 'Content-Type': 'application/json' }), res.end(JSON.stringify(list)); + } + if (req.method === 'GET' && spaceId) { + const s = spaces.get(spaceId); + if (!s) return res.writeHead(404), res.end(JSON.stringify({ error: 'ไม่พบห้อง' })); + return res.writeHead(200, { 'Content-Type': 'application/json' }), res.end(JSON.stringify({ ok: true, mapName: s.mapData?.name })); + } + if (req.method === 'POST' && !spaceId) { + let body = ''; + req.on('data', c => body += c); + req.on('end', () => { + try { + const d = JSON.parse(body || '{}'); + const mapData = d.mapId ? maps.get(d.mapId) : null; + if (!mapData) return res.writeHead(400), res.end(JSON.stringify({ ok: false, error: 'ไม่พบฉาก' })); + const isPrivate = !!d.isPrivate; + let sid; + if (isPrivate) { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + let code; + for (let i = 0; i < 20; i++) { + code = ''; + for (let j = 0; j < 6; j++) code += chars[Math.floor(Math.random() * chars.length)]; + if (!spaces.has(code)) { sid = code; break; } + } + if (!sid) sid = 'priv-' + Date.now(); + } else { + const base = (d.name || '').trim() || 'space'; + sid = base + '-' + Date.now(); + } + const spaceName = (d.name || '').trim() || (isPrivate ? 'ห้องส่วนตัว' : sid); + let maxPlayers = parseInt(d.maxPlayers, 10); + if (isNaN(maxPlayers) || maxPlayers < 1) maxPlayers = 10; + /* Quiz Battle (ฉากเดินตอบ แบบ ZEP) รองรับได้ถึง 50 คน · โหมดอื่นคงเพดานเดิม 10 */ + const mpCap = mapData.gameType === 'quiz_battle' ? 50 : 10; + maxPlayers = Math.min(mpCap, Math.max(1, maxPlayers)); + if (mapData.gameType === 'gauntlet' || mapData.gameType === 'space_shooter' || mapData.gameType === 'balloon_boss') maxPlayers = Math.min(maxPlayers, 6); + const mapId = d.mapId ? String(d.mapId).trim() : null; + const previewEditorTest = isPrivate && String(spaceName).toLowerCase() === 'preview'; + let botSlotCount = parseInt(d.botSlotCount, 10); + if (isNaN(botSlotCount) || botSlotCount < 0) botSlotCount = 0; + botSlotCount = effectiveBotSlotCount({ botSlotCount, maxPlayers }); + spaces.set(sid, { + mapData, mapId, peers: new Map(), spaceName, hostId: null, isPrivate, maxPlayers, createdAt: Date.now(), + previewEditorTest, + botSlotCount, + suspectCardMinigames: null, + suspectPhaseActive: false, + suspectPickIndex: 0, + detectiveMinigameActive: false, + }); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, spaceId: sid, roomCode: isPrivate ? sid : null })); + } catch (e) { + res.writeHead(400), res.end(JSON.stringify({ ok: false, error: 'ข้อมูลไม่ถูกต้อง' })); + } + }); + return; + } + } + if (url === BASE_PATH + '/api/ice-servers' && req.method === 'GET') { + function sendIceServers(iceServers) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ iceServers: iceServers || [] })); + } + function fallback() { + const iceServers = [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }]; + const turnUrl = process.env.TURN_SERVER || process.env.TURN_URL; + const turnUser = process.env.TURN_USER; + const turnCred = process.env.TURN_CRED; + if (turnUrl && turnUser && turnCred) iceServers.push({ urls: turnUrl, username: turnUser, credential: turnCred }); + sendIceServers(iceServers); + } + const meteredKey = process.env.METERED_TURN_API_KEY || process.env.METERED_TURN_SECRET_KEY; + const meteredAppId = process.env.METERED_TURN_APP_ID; + let meteredBase = process.env.METERED_TURN_URL; + if (!meteredBase) { + let meteredHost = process.env.METERED_TURN_SUBDOMAIN || 'openrelayprojectsecret'; + if (meteredHost.indexOf('metered.live') === -1) meteredHost = meteredHost + '.metered.live'; + meteredBase = 'https://' + meteredHost; + } + meteredBase = meteredBase.replace(/\/$/, ''); + if (meteredKey) { + function tryNext(credUrl) { + return fetch(credUrl).then(function (r) { return r.json(); }).then(function (data) { + if (data && (data.error || data.message)) throw new Error(data.error || data.message); + const list = Array.isArray(data) ? data : (data && data.iceServers); + if (list && list.length) return sendIceServers(list); + if (data && data.data && Array.isArray(data.data) && data.data.length) { + const turnHost = process.env.METERED_TURN_HOST || 'global.relay.metered.ca'; + const built = data.data.map(function (c) { + return { urls: 'turn:' + turnHost + ':443', username: c.username, credential: c.password }; + }); + return sendIceServers(built); + } + throw new Error('No iceServers'); + }); + } + const urlsToTry = [ + meteredBase + '/api/v1/turn/credentials?apiKey=' + encodeURIComponent(meteredKey), + meteredBase + '/api/v1/turn/credentials?apiKey=' + encodeURIComponent(meteredAppId || meteredKey), + meteredBase + '/api/v2/turn/credentials?secretKey=' + encodeURIComponent(meteredKey) + ]; + let p = tryNext(urlsToTry[0]); + for (let i = 1; i < urlsToTry.length; i++) { + p = p.catch(function () { return tryNext(urlsToTry[i]); }); + } + p.catch(fallback); + return; + } + const iceServers = [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }]; + const turnUrl = process.env.TURN_SERVER || process.env.TURN_URL; + const turnUser = process.env.TURN_USER; + const turnCred = process.env.TURN_CRED; + if (turnUrl && turnUser && turnCred) iceServers.push({ urls: turnUrl, username: turnUser, credential: turnCred }); + sendIceServers(iceServers); + return; + } + if (url === BASE_PATH + '/api/ai-admin-login' && req.method === 'POST') { + let body = ''; + req.on('data', c => body += c); + req.on('end', () => { + res.setHeader('Content-Type', 'application/json'); + try { + const d = JSON.parse(body || '{}'); + const password = (d.password || '').trim(); + if (password !== ADMIN_PASSWORD) { + return res.writeHead(401), res.end(JSON.stringify({ ok: false, error: 'รหัสผ่านไม่ถูกต้อง' })); + } + const token = require('crypto').randomBytes(24).toString('hex'); + adminTokens.add(token); + res.setHeader('Set-Cookie', 'game_ai_admin=' + token + '; Path=' + BASE_PATH + '; HttpOnly; Max-Age=86400; SameSite=Lax'); + res.writeHead(200); + res.end(JSON.stringify({ ok: true })); + } catch (e) { + res.writeHead(400), res.end(JSON.stringify({ ok: false, error: 'ข้อมูลไม่ถูกต้อง' })); + } + }); + return; + } + if (url === BASE_PATH + '/api/ai-settings' && req.method === 'GET') { + const token = getAdminToken(req.headers.cookie); + if (!token) return res.writeHead(401), res.end(JSON.stringify({ error: 'ต้องล็อกอินก่อน' })); + const s = loadAiSettings(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + model: s.model || 'gpt-4o-mini', + models: AI_MODELS, + hasKey: !!(s.openai_api_key && s.openai_api_key.length > 0), + intent: s.intent != null ? s.intent : '', + rag_enabled: !!s.rag_enabled, + rag_context: s.rag_context != null ? s.rag_context : '', + })); + return; + } + if (url === BASE_PATH + '/api/ai-settings' && req.method === 'PUT') { + const token = getAdminToken(req.headers.cookie); + if (!token) return res.writeHead(401), res.end(JSON.stringify({ error: 'ต้องล็อกอินก่อน' })); + let body = ''; + req.on('data', c => body += c); + req.on('end', () => { + res.setHeader('Content-Type', 'application/json'); + try { + const d = JSON.parse(body || '{}'); + const s = loadAiSettings(); + if (d.openai_api_key !== undefined) s.openai_api_key = String(d.openai_api_key).trim(); + if (d.model !== undefined) s.model = String(d.model).trim() || 'gpt-4o-mini'; + if (d.intent !== undefined) s.intent = String(d.intent); + if (d.rag_enabled !== undefined) s.rag_enabled = !!d.rag_enabled; + if (d.rag_context !== undefined) s.rag_context = String(d.rag_context); + saveAiSettings(s); + res.writeHead(200); + res.end(JSON.stringify({ ok: true })); + } catch (e) { + res.writeHead(400), res.end(JSON.stringify({ ok: false, error: e.message })); + } + }); + return; + } + if (url === BASE_PATH + '/api/ai-chat' && req.method === 'POST') { + let body = ''; + req.on('data', c => body += c); + req.on('end', () => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + try { + const d = JSON.parse(body || '{}'); + const message = (d.message || d.text || '').trim(); + if (!message) return res.writeHead(400), res.end(JSON.stringify({ error: 'ไม่มีข้อความ' })); + const s = loadAiSettings(); + if (!s.openai_api_key) return res.writeHead(503), res.end(JSON.stringify({ error: 'ยังไม่ได้ตั้งค่า OpenAI API Key ในหน้า Admin' })); + const model = s.model || 'gpt-4o-mini'; + const messages = []; + const systemParts = []; + if (s.intent && String(s.intent).trim()) systemParts.push(String(s.intent).trim()); + if (s.rag_enabled && s.rag_context && String(s.rag_context).trim()) { + systemParts.push('ความรู้/บริบทเพิ่มเติม (ใช้ประกอบการตอบ):\n' + String(s.rag_context).trim()); + } + if (systemParts.length) messages.push({ role: 'system', content: systemParts.join('\n\n') }); + messages.push({ role: 'user', content: message }); + const payload = JSON.stringify({ + model: model, + messages: messages, + max_tokens: 500, + }); + const urlObj = new URL('https://api.openai.com/v1/chat/completions'); + const opts = { + hostname: urlObj.hostname, + path: urlObj.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + s.openai_api_key, + 'Content-Length': Buffer.byteLength(payload, 'utf8'), + }, + }; + const reqOut = require('https').request(opts, (resOut) => { + let data = ''; + resOut.on('data', chunk => data += chunk); + resOut.on('end', () => { + try { + const parsed = JSON.parse(data); + const text = parsed.choices && parsed.choices[0] && parsed.choices[0].message && parsed.choices[0].message.content; + if (text != null) { + res.writeHead(200); + res.end(JSON.stringify({ response: String(text).trim() })); + } else { + res.writeHead(502); + res.end(JSON.stringify({ error: parsed.error && parsed.error.message ? parsed.error.message : 'OpenAI error' })); + } + } catch (e) { + res.writeHead(502), res.end(JSON.stringify({ error: 'Invalid response from OpenAI' })); + } + }); + }); + reqOut.on('error', (e) => { res.writeHead(502), res.end(JSON.stringify({ error: e.message })); }); + reqOut.write(payload); + reqOut.end(); + } catch (e) { + res.writeHead(400), res.end(JSON.stringify({ error: e.message })); + } + }); + return; + } + if (url.startsWith(BASE_PATH + '/img/characters/') && (req.method === 'GET' || req.method === 'HEAD')) { + const sub = url.slice((BASE_PATH + '/img/characters/').length).split('?')[0]; + const fname = path.basename(sub).replace(/[^a-z0-9_.-]/gi, ''); + if (fname && fname.length > 0) { + const fp = path.join(CHARACTERS_DIR, fname); + if (fs.existsSync(fp)) { + return fs.readFile(fp, (err, data) => { + if (err) return res.writeHead(500), res.end(); + res.writeHead(200, { 'Content-Type': 'image/png' }); + if (req.method === 'HEAD') return res.end(); + res.end(data); + }); + } + } + } + if (url.startsWith(BASE_PATH + '/img/gauntlet-assets/') && req.method === 'GET') { + const sub = decodeURIComponent(url.slice((BASE_PATH + '/img/gauntlet-assets/').length).split('?')[0]); + const fname = safeGauntletStoredFilename(sub); + if (!fname) { + res.writeHead(400); + return res.end(); + } + const fp = path.join(GAUNTLET_ASSETS_DIR, fname); + if (!fs.existsSync(fp)) { + res.writeHead(404); + return res.end(); + } + const ext = path.extname(fname); + return fs.readFile(fp, (err, data) => { + if (err) return res.writeHead(500), res.end(); + res.writeHead(200, { 'Content-Type': gauntletAssetMimeByExt(ext) }); + res.end(data); + }); + } + if (url.indexOf(BASE_PATH) === 0) url = url.slice(BASE_PATH.length) || '/index.html'; + const filePath = path.join(__dirname, 'public', url); + const ext = path.extname(filePath); + const types = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css' }; + fs.readFile(filePath, (err, data) => { + if (err) return res.writeHead(404), res.end('Not Found'); + res.writeHead(200, { + 'Content-Type': types[ext] || 'application/octet-stream', + 'Cache-Control': 'no-store, no-cache, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }); + res.end(data); + }); +}); + +const io = new Server(server, { path: BASE_PATH + '/socket.io', cors: { origin: '*' } }); +const spaces = new Map(); + +const GAUNTLET_MAX_PLAYERS = 6; +/** ระยะอยู่กลางอากาศ (tick) — ปรับได้ที่ Admin / data/game-timing.json */ +/** สุ่มเป็นคอลัมน์เลเซอร์ (ทุกแถว) ต่อหนึ่งรอบเกิด */ +const GAUNTLET_LASER_SPAWN_CHANCE = 0.14; +/** เฉพาะแถวที่มีผู้เล่น — แถวละโอกาสเกิด lane obstacle */ +const GAUNTLET_LANE_ROW_SPAWN_CHANCE = 0.34; +/** Last Light / museum gauntlet — scoring, mission UI, how-to flow */ +const GAUNTLET_CROWN_HEIST_MAP_ID = 'mno9kb07'; +/** Mega Virus — balloon_boss ฉากภารกิจ (flow เดียวกับ crown) */ +const BALLOON_BOSS_MISSION_MAP_ID = 'mnq1eml7'; +/** Space Shooter ภารกิจ Violent Crime — lobby READY / START เดียวกับ Jumper */ +const SPACE_SHOOTER_MISSION_MAP_ID = 'mnpz6rkp'; + +function isGauntletCrownHeistMapBySpace(space) { + return !!(space && space.mapId && String(space.mapId) === GAUNTLET_CROWN_HEIST_MAP_ID); +} + +function isBalloonBossMissionShellSpace(space) { + if (!space || !space.mapId || String(space.mapId) !== BALLOON_BOSS_MISSION_MAP_ID) return false; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + return !!(md && md.gameType === 'balloon_boss'); +} + +/** ภารกิจคำถาม quiz + แมป mng8a80o — lobby READY เดียวกับ Mega Virus */ +function isQuizQuestionMissionShellSpace(space) { + if (!space || !space.mapId || String(space.mapId) !== SUSPECT_INVESTIGATION_QUIZ_MAP_ID) return false; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + return !!(md && md.gameType === 'quiz'); +} + +function isStackTowerMissionShellSpace(space) { + if (!space || !space.mapId || String(space.mapId) !== STACK_TOWER_MISSION_MAP_ID) return false; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + return !!(md && md.gameType === 'stack'); +} + +function isJumpSurviveMissionShellSpace(space) { + if (!space || !space.mapId || String(space.mapId) !== JUMP_SURVIVE_MISSION_MAP_ID) return false; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + return !!(md && md.gameType === 'jump_survive'); +} + +function isSpaceShooterMissionShellSpace(space) { + if (!space || !space.mapId || String(space.mapId) !== SPACE_SHOOTER_MISSION_MAP_ID) return false; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + return !!(md && md.gameType === 'space_shooter'); +} + +function isCrownLobbyShellSpace(space) { + return isGauntletCrownHeistMapBySpace(space) || isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space) || isJumpSurviveMissionShellSpace(space) || isSpaceShooterMissionShellSpace(space); +} + +function newBalloonBossShellGauntletRunState() { + return { + obstacles: [], + nextObsId: 1, + spawnAcc: 0, + nextSpawnIn: 999, + crownRunHeld: true, + timerId: null, + endsAt: null, + }; +} + +function emitBalloonBossMissionShellGauntletSync(sid, space) { + if (!isBalloonBossMissionShellSpace(space)) return; + const gr = space.gauntletRun; + io.to(sid).emit('gauntlet-sync', { + gauntletCrownRunHeld: !!(gr && gr.crownRunHeld), + }); +} + +function rollGauntletCrownRewardCard(grade) { + const r = Math.random(); + if (grade === 'A') { + if (r < 0.3) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' }; + if (r < 0.8) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'B') { + if (r < 0.2) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' }; + if (r < 0.5) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (r < 0.2) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; +} + +function gauntletCrownRankBonus(rankOrdinal) { + if (rankOrdinal === 1) return 100; + if (rankOrdinal === 2) return 80; + if (rankOrdinal === 3) return 60; + return 0; +} + +function buildGauntletCrownMissionPayload(space) { + const peersArr = [...space.peers.values()]; + const rows = peersArr.map((p) => ({ + id: p.id, + nickname: (p.nickname || '').trim() || 'ผู้เล่น', + characterId: p.characterId ?? null, + baseScore: p.gauntletEliminated ? 0 : Math.max(0, p.gauntletScore | 0), + eliminated: !!p.gauntletEliminated, + })); + rows.sort((a, b) => { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th'); + }); + let lastScore = null; + let rankOrdinal = 0; + const ranked = []; + for (let i = 0; i < rows.length; i++) { + const pos = i + 1; + if (lastScore !== rows[i].baseScore) { + rankOrdinal = pos; + lastScore = rows[i].baseScore; + } + const bonus = gauntletCrownRankBonus(rankOrdinal); + const finalScore = rows[i].baseScore + bonus; + const rankLabel = rankOrdinal === 1 ? '1st' : rankOrdinal === 2 ? '2nd' : rankOrdinal === 3 ? '3rd' : String(rankOrdinal); + ranked.push({ + id: rows[i].id, + nickname: rows[i].nickname, + characterId: rows[i].characterId, + baseScore: rows[i].baseScore, + eliminated: rows[i].eliminated, + rank: rankOrdinal, + rankLabel, + rankBonus: bonus, + finalScore, + }); + } + const totalSum = ranked.reduce((s, r) => s + r.finalScore, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + const rewardCard = rollGauntletCrownRewardCard(grade); + return { + ranked, + totalSum, + averageScore, + grade, + rewardCard, + totalParts: ranked.map((r) => r.finalScore), + }; +} + +function gauntletSpawnYs(md, playerCount) { + const h = md.height || 15; + const lo = 1; + const hi = Math.max(lo, h - 2); + const n = Math.min(GAUNTLET_MAX_PLAYERS, Math.max(1, playerCount)); + if (hi <= lo) return Array.from({ length: n }, () => lo); + const ys = []; + for (let i = 0; i < n; i++) { + ys.push(Math.round(lo + (i * (hi - lo)) / Math.max(1, n - 1))); + } + return ys; +} + +/** ช่อง spawnArea=1 ที่เดินได้ เรียงจากบนลงล่าง — ใช้เมื่อไม่มี gauntletPlayerSpawns */ +function collectGauntletSpawnSlotsFromSpawnArea(md) { + const grid = md.spawnArea; + if (!grid || !Array.isArray(grid)) return []; + const w = md.width || 20; + const h = md.height || 15; + const slots = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && isMapTileWalkableForSpawn(md, x, y)) slots.push({ x, y }); + } + } + slots.sort((a, b) => a.y - b.y || a.x - b.x); + return slots; +} + +/** ลำดับผู้เล่น 1..6: ใช้ gauntletPlayerSpawns ตามลำดับใน JSON ก่อน ถ้าไม่มี/ว่างค่อยใช้ spawnArea */ +function collectGauntletSpawnSlotsFromMap(md) { + const explicit = md.gauntletPlayerSpawns; + if (Array.isArray(explicit) && explicit.length > 0) { + const w = md.width || 20; + const h = md.height || 15; + const slots = []; + for (const raw of explicit) { + if (slots.length >= GAUNTLET_MAX_PLAYERS) break; + const x = Math.floor(Number(raw && raw.x)); + const y = Math.floor(Number(raw && raw.y)); + if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x >= w || y < 0 || y >= h) continue; + if (!isMapTileWalkableForSpawn(md, x, y)) continue; + slots.push({ x, y }); + } + if (slots.length > 0) return slots; + } + return collectGauntletSpawnSlotsFromSpawnArea(md); +} + +function gauntletResolveSpawnXForRow(md, spawnColX, y, slotXFallback) { + const w = md.width || 20; + if (isMapTileWalkableForSpawn(md, spawnColX, y)) return spawnColX; + if (slotXFallback != null && isMapTileWalkableForSpawn(md, slotXFallback, y)) return slotXFallback; + for (let d = 0; d < w; d++) { + if (spawnColX + d < w && isMapTileWalkableForSpawn(md, spawnColX + d, y)) return spawnColX + d; + if (spawnColX - d >= 0 && isMapTileWalkableForSpawn(md, spawnColX - d, y)) return spawnColX - d; + } + return Math.max(0, Math.min(w - 1, spawnColX)); +} + +/** แนว Y ตามลำดับจุดเกิด แต่ทุกคนใช้คอลัมน์ x เดียวกัน (ซ้ายสุดจากชุดจุดที่กำหนด) — กันยืนเฉียง */ +function gauntletSpawnPositions(md, playerCount) { + const n = Math.min(GAUNTLET_MAX_PLAYERS, Math.max(1, playerCount)); + const slots = collectGauntletSpawnSlotsFromMap(md); + const fallbackYs = gauntletSpawnYs(md, n); + const spawnColX = slots.length ? Math.min(...slots.map((s) => s.x)) : 1; + const out = []; + for (let i = 0; i < n; i++) { + const y = i < slots.length + ? slots[i].y + : (fallbackYs[i] != null ? fallbackYs[i] : fallbackYs[fallbackYs.length - 1]); + const slotX = i < slots.length ? slots[i].x : null; + const x = gauntletResolveSpawnXForRow(md, spawnColX, y, slotX); + out.push({ x, y }); + } + return out; +} + +function stopGauntletTicker(space) { + if (space.gauntletRun && space.gauntletRun.timerId) { + clearInterval(space.gauntletRun.timerId); + space.gauntletRun.timerId = null; + } +} + +/** ถ้าตั้งจำกัดเวลาใน Admin แต่ยังไม่มี endsAt (เช่น join ห้อง Gauntlet โดยตรง) — เริ่มนับตอนนี้ */ +function ensureGauntletEndsAtIfNeeded(space) { + const gr = space.gauntletRun; + if (!gr) return; + if (gr.crownRunHeld) return; + const lim = getGauntletTimeLimitSec(); + if (lim > 0 && gr.endsAt == null) { + gr.endsAt = Date.now() + lim * 1000; + } +} + +function newGauntletRunState(space) { + const crown = !!(space && isGauntletCrownHeistMapBySpace(space)); + return { + obstacles: [], + nextObsId: 1, + spawnAcc: 0, + nextSpawnIn: 3, + crownRunHeld: crown, + }; +} + +function emitGauntletSync(sid, space) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'gauntlet') return; + const gr = space.gauntletRun; + if (!gr) return; + const h = md.height || 15; + const obsOut = gr.obstacles.map((o) => { + if (o.kind === 'laser') { + const y0 = o.y0 != null && Number.isFinite(Number(o.y0)) ? Math.floor(Number(o.y0)) : 0; + const y1 = o.y1 != null && Number.isFinite(Number(o.y1)) ? Math.floor(Number(o.y1)) : h - 1; + return { id: o.id, kind: 'laser', x: o.x, y: null, y0, y1 }; + } + return { id: o.id, kind: o.kind, x: o.x, y: o.y }; + }); + const playersOut = [...space.peers.values()].map((p) => ({ + id: p.id, + x: p.x, + y: p.y, + direction: p.direction || 'down', + characterId: p.characterId ?? null, + gauntletJumpTicks: p.gauntletJumpTicks || 0, + gauntletScore: p.gauntletScore || 0, + gauntletEliminated: !!p.gauntletEliminated, + })); + io.to(sid).emit('gauntlet-sync', { + obstacles: obsOut, + players: playersOut, + gauntletTickMs: getGauntletTickMs(), + gauntletJumpTicks: getGauntletJumpTicks(), + gauntletTimeLimitSec: getGauntletTimeLimitSec(), + gauntletEndsAt: gr.endsAt != null ? gr.endsAt : null, + gauntletCrownRunHeld: !!gr.crownRunHeld, + ...getGauntletVisualsForClient(), + }); +} + +function startGauntletTicker(sid, space) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'gauntlet') return; + if (space.gauntletRun && space.gauntletRun.timerId) return; + if (!space.gauntletRun) { + space.gauntletRun = newGauntletRunState(space); + } + ensureGauntletEndsAtIfNeeded(space); + space.gauntletRun.timerId = setInterval(() => { + runGauntletTick(sid, space); + }, getGauntletTickMs()); +} + +function initGauntletPeersAll(space, md) { + const list = [...space.peers.values()]; + const pos = gauntletSpawnPositions(md, list.length); + const crown = isGauntletCrownHeistMapBySpace(space); + list.forEach((p, i) => { + const pt = pos[i] || pos[pos.length - 1]; + p.x = pt.x; + p.y = pt.y; + p.gauntletJumpTicks = 0; + p.gauntletScore = crown ? 100 : 0; + p.gauntletJumpPending = false; + p.gauntletEliminated = false; + }); + space.gauntletRun = newGauntletRunState(space); + if (crown) { + if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {}; + space.peers.forEach((_p, id) => { + space.gauntletCrownLobbyReady[id] = false; + }); + } + space.gauntletPreviewRowsBySocket = new Map(); +} + +function findFallbackLobbyMapForGauntletEnd() { + for (const [id, m] of maps) { + if (m && m.gameType === 'lobby') return { id, m }; + } + return null; +} + +/** หมดเวลา Gauntlet — หยุด tick, คืนแผนที่ lobby, แจ้งไคลเอนต์ */ +function endGauntletGame(sid, space, reason) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'gauntlet') return; + if (space.detectiveMinigameActive) { + const msg = reason === 'time' ? 'หมดเวลา — จบมินิเกม' : 'จบมินิเกม'; + returnDetectiveSpaceToLobbyB(sid, space, msg, { allPlayers: true }); + return; + } + stopGauntletTicker(space); + const wasCrown = isGauntletCrownHeistMapBySpace(space); + const crownMission = wasCrown ? buildGauntletCrownMissionPayload(space) : null; + const rankings = crownMission + ? crownMission.ranked.map((r) => ({ + id: r.id, + nickname: r.nickname, + score: r.finalScore, + })) + : [...space.peers.values()].map((p) => ({ + id: p.id, + nickname: (p.nickname || '').trim() || 'ผู้เล่น', + score: Math.max(0, p.gauntletScore | 0), + })).sort((a, b) => b.score - a.score); + + let retId = space.gauntletReturnMapId; + let retMap = retId && maps.has(retId) ? maps.get(retId) : null; + if (!retMap || retMap.gameType === 'gauntlet') { + const fb = findFallbackLobbyMapForGauntletEnd(); + if (fb) { + retId = fb.id; + retMap = fb.m; + } + } + if (retMap && retMap.gameType !== 'gauntlet') { + space.mapId = retId; + space.mapData = retMap; + let gi = 0; + space.peers.forEach((p) => { + const sp = pickSpawnForJoin(retMap, gi++); + p.x = sp.x; + p.y = sp.y; + p.gauntletJumpTicks = 0; + p.gauntletScore = 0; + p.gauntletJumpPending = false; + p.gauntletEliminated = false; + }); + } else { + console.warn('endGauntletGame: no lobby map to return to', sid); + } + space.gauntletReturnMapId = null; + if (space.gauntletPreviewRowsBySocket) space.gauntletPreviewRowsBySocket.clear(); + space.gauntletRun = null; + const msg = reason === 'time' ? 'หมดเวลา — เกมพรมแดงจบแล้ว' : 'เกมพรมแดงจบแล้ว'; + io.to(sid).emit('gauntlet-ended', { + reason: reason || 'time', + message: msg, + rankings, + crownMission: crownMission || undefined, + }); +} + +/** แถว y บน–ล่างของเลเซอร์จากแมป (null ทั้งคู่ = เต็มความสูง) */ +function gauntletLaserVerticalSpanFromMap(md) { + const hRaw = md && md.height != null ? Number(md.height) : 15; + const h = Math.max(1, Math.floor(Number.isFinite(hRaw) ? hRaw : 15)); + const rs = md && md.gauntletLaserRowStart; + const re = md && md.gauntletLaserRowEnd; + if (rs == null || re == null || !Number.isFinite(Number(rs)) || !Number.isFinite(Number(re))) { + return { y0: 0, y1: h - 1 }; + } + let y0 = Math.floor(Number(rs)); + let y1 = Math.floor(Number(re)); + y0 = Math.max(0, Math.min(h - 1, y0)); + y1 = Math.max(0, Math.min(h - 1, y1)); + if (y1 < y0) { + const t = y0; + y0 = y1; + y1 = t; + } + return { y0, y1 }; +} + +/** + * แถว grid ของ lane obstacle: แถวเท้า (ขอบล่าง footprint; แถวบนของตัว = player y) + * — สูงกว่าแบบ top+ch หนึ่งช่อง (ไม่ให้ลงเกินพรม) + */ +function gauntletLaneGridRowFromPlayerTop(md, topY, h) { + const { ch } = getCharacterFootprintWHForMove(md); + const top = Math.floor(Number(topY)); + if (!Number.isFinite(top) || top < 0 || top >= h) return null; + const lr = top + ch - 1; + if (lr < 0 || lr >= h) return null; + return lr; +} + +function runGauntletTick(sid, space) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'gauntlet') { + stopGauntletTicker(space); + return; + } + const w = md.width || 20; + const h = md.height || 15; + const gr = space.gauntletRun; + if (!gr) return; + ensureGauntletEndsAtIfNeeded(space); + if (gr.endsAt != null && Date.now() >= gr.endsAt) { + endGauntletGame(sid, space, 'time'); + return; + } + + if (gr.crownRunHeld) { + space.peers.forEach((p) => { + p.gauntletJumpPending = false; + }); + emitGauntletSync(sid, space); + return; + } + + /* ใช้ pending ให้ jump กับ collision อยู่รอบ tick เดียวกัน (กัน race ระหว่าง setInterval กับ socket) */ + space.peers.forEach((p) => { + if (p.gauntletJumpPending) { + if ((p.gauntletJumpTicks || 0) === 0) { + p.gauntletJumpTicks = getGauntletJumpTicks(); + } + p.gauntletJumpPending = false; + } + }); + + for (let i = 0; i < gr.obstacles.length; i++) gr.obstacles[i].x -= 1; + gr.obstacles = gr.obstacles.filter((o) => o.x >= 0); + + const laneSpawnRows = new Set(); + space.peers.forEach((peer) => { + const lr = gauntletLaneGridRowFromPlayerTop(md, peer.y, h); + if (lr != null) laneSpawnRows.add(lr); + }); + /* แถวบนที่ client แจ้ง (บอท preview) — แปลงเป็นแถว lane เดียวกับ peer */ + const previewRows = space.gauntletPreviewRowsBySocket; + if (previewRows && previewRows.size) { + previewRows.forEach((set) => { + set.forEach((y) => { + const lr = gauntletLaneGridRowFromPlayerTop(md, y, h); + if (lr != null) laneSpawnRows.add(lr); + }); + }); + } + /* lane obstacle เฉพาะแถวที่ยังมีผู้เล่น/บอทในเลนนั้น — ลบชิ้นที่ไม่มีใครแล้ว */ + gr.obstacles = gr.obstacles.filter((o) => o.kind !== 'lane' || laneSpawnRows.has(o.y)); + + gr.spawnAcc = (gr.spawnAcc || 0) + 1; + if (gr.spawnAcc >= (gr.nextSpawnIn || 3)) { + gr.spawnAcc = 0; + gr.nextSpawnIn = 2 + Math.floor(Math.random() * 6); + /* ประเภท 2: เลเซอร์ — ทุกแถว */ + if (Math.random() < GAUNTLET_LASER_SPAWN_CHANCE) { + const span = gauntletLaserVerticalSpanFromMap(md); + gr.obstacles.push({ id: ++gr.nextObsId, kind: 'laser', x: w - 1, y0: span.y0, y1: span.y1 }); + } + /* ประเภท 1: แยกเลน — แถวเท้า (ไม่ใช่แถวหัว; ไม่ใช่แถวใต้เท้าอีกช่อง) */ + laneSpawnRows.forEach((ly) => { + if (Math.random() < GAUNTLET_LANE_ROW_SPAWN_CHANCE) { + gr.obstacles.push({ id: ++gr.nextObsId, kind: 'lane', x: w - 1, y: ly }); + } + }); + } + + /* กระโดดข้ามสำเร็จ → เลื่อนผู้เล่นไปขวา แต่ไม่ลบ obstacle (ยังไหลต่อให้คนอื่น/รอบถัดไป) */ + const crownHeist = isGauntletCrownHeistMapBySpace(space); + const { ch: gauntletCh } = getCharacterFootprintWHForMove(md); + space.peers.forEach((p) => { + if (crownHeist && p.gauntletEliminated) { + return; + } + let px = Math.floor(Number(p.x)) || 0; + let py = Math.floor(Number(p.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + const air = (p.gauntletJumpTicks || 0) > 0; + let advanceX = false; + let hitBack = false; + for (const o of gr.obstacles) { + if (o.kind === 'lane' && o.x === px && o.y >= py && o.y < py + gauntletCh) { + if (air) advanceX = true; + else hitBack = true; + } + if (o.kind === 'laser' && o.x === px) { + const y0 = o.y0 != null && Number.isFinite(Number(o.y0)) ? Math.max(0, Math.min(h - 1, Math.floor(Number(o.y0)))) : 0; + const y1 = o.y1 != null && Number.isFinite(Number(o.y1)) ? Math.max(0, Math.min(h - 1, Math.floor(Number(o.y1)))) : h - 1; + const lo = Math.min(y0, y1); + const hi = Math.max(y0, y1); + if (py < lo || py > hi) { + /* เลเซอร์ไม่ครอบแถวนี้ */ + } else if (air) { + advanceX = true; + } else { + hitBack = true; + } + } + } + if (advanceX) { + px = Math.min(w - 2, px + 1); + p.gauntletJumpTicks = 0; + if (!crownHeist) { + p.gauntletScore = (p.gauntletScore || 0) + 1; + } + } else if (hitBack) { + if (crownHeist) { + if (px <= 0) { + p.gauntletEliminated = true; + p.gauntletScore = 0; + } else { + p.gauntletScore = Math.max(0, (p.gauntletScore || 0) - 10); + px = Math.max(0, px - 1); + } + } else { + px = Math.max(0, px - 1); + } + } + if ((p.gauntletJumpTicks || 0) > 0) p.gauntletJumpTicks--; + p.x = px; + p.y = py; + }); + + emitGauntletSync(sid, space); +} + +function rescheduleAllGauntletTickers() { + for (const [sid, space] of spaces) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'gauntlet') continue; + if (space.gauntletRun && space.gauntletRun.timerId) { + clearInterval(space.gauntletRun.timerId); + space.gauntletRun.timerId = null; + } + if (space.peers.size === 0) continue; + if (!space.gauntletRun) { + space.gauntletRun = newGauntletRunState(space); + } + ensureGauntletEndsAtIfNeeded(space); + space.gauntletRun.timerId = setInterval(() => { + runGauntletTick(sid, space); + }, getGauntletTickMs()); + } +} + +function getLobbyLayoutMapForSpace(space) { + return (space.mapId && maps.get(space.mapId)) || space.mapData; +} + +/** ห้องกำลังใช้เลย์เอาต์ LobbyB (mn8nx46h หรือแมป lobby ชื่อ LobbyB) — กัน mapId บน space ค้างค่าอื่นแต่ mapData เป็น LobbyB */ +function serverMapIsPostCaseLobbyB(space) { + if (!space) return false; + if (space.mapId === POST_CASE_LOBBY_SPACE_ID) return true; + const md = getLobbyLayoutMapForSpace(space); + if (!md || md.gameType !== 'lobby') return false; + const nm = String(md.name || '').trim().toLowerCase(); + return nm === 'lobbyb'; +} + +/** ให้ชื่อใน whitelist ตรงกับตอน join หลังเริ่มคดี (กันช่องว่าง/ยูนิโค้ดต่างรูปแบบ) */ +function normalizeLobbyNickname(nickname) { + const raw = String(nickname || '').trim(); + if (!raw) return 'ผู้เล่น'; + try { + return raw.normalize('NFKC').toLowerCase(); + } catch (e) { + return raw.toLowerCase(); + } +} + +/** ห้องนี้เป็น LobbyB (โถงหลังคดี) แล้ว — ไม่ต้องย้ายซ้ำ */ +function lobbySpaceAlreadyLobbyB(space, md) { + if (!md || md.gameType !== 'lobby') return false; + if (space.mapId === POST_CASE_LOBBY_SPACE_ID) return true; + if (String(md.name || '').trim() === 'LobbyB') return true; + return false; +} + +function lobbyMapHasStartGameArea(md) { + if (!md) return false; + const namedLobbyA = String(md.name || '').trim().toLowerCase() === 'lobbya'; + if (md.gameType !== 'lobby' && !namedLobbyA) return false; + const area = md.startGameArea; + if (!area || !Array.isArray(area)) return false; + for (let y = 0; y < area.length; y++) { + const row = area[y]; + if (!row) continue; + for (let x = 0; x < row.length; x++) if (row[x] === 1) return true; + } + return false; +} + +/** ถ้าไม่ได้วาดพื้นที่เริ่มเกมในเอดิเตอร์ = ไม่บังคับ */ +function lobbyHostStandingInStartArea(space, hostId) { + const md = getLobbyLayoutMapForSpace(space); + if (!lobbyMapHasStartGameArea(md)) return true; + const p = space.peers.get(hostId); + if (!p || p.x == null || p.y == null) return false; + const tx = Math.floor(p.x); + const ty = Math.floor(p.y); + const row = md.startGameArea[ty]; + return !!(row && row[tx] === 1); +} + +function isMapTileWalkableForSpawn(md, x, y) { + const w = md.width || 20; + const h = md.height || 15; + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + if (row && row[x] === 1) return false; + return true; +} + +function mapCharacterFootprintWH(md) { + const cw = Math.max(1, Math.min(4, Math.floor(Number(md && md.characterCellsW) || Number(md && md.characterCells) || 1))); + const ch = Math.max(1, Math.min(4, Math.floor(Number(md && md.characterCellsH) || Number(md && md.characterCells) || 1))); + return { cw, ch }; +} + +/** จุดเกิด = มุมซ้ายบนของ footprint — ต้องเดินได้ทั้งกล่อง (ตรง editor / room-lobby) */ +function isMapSpawnAnchorWalkable(md, x, y) { + if (!isMapTileWalkableForSpawn(md, x, y)) return false; + const { cw, ch } = mapCharacterFootprintWH(md); + const w = md.width || 20; + const h = md.height || 15; + const maxX = Math.min(w, x + cw); + const maxY = Math.min(h, y + ch); + for (let ty = y; ty < maxY; ty++) { + for (let tx = x; tx < maxX; tx++) { + if (!isMapTileWalkableForSpawn(md, tx, ty)) return false; + } + } + return true; +} + +/** แปลง lobbyPlayerSpawns จากแมปเป็นอาร์เรย์ยาว 6 ช่อง (null หรือ {x,y}) */ +function parseLobbyPlayerSpawnsFromMap(md) { + const w = md.width || 20; + const h = md.height || 15; + const out = [null, null, null, null, null, null]; + const raw = md && md.lobbyPlayerSpawns; + if (!Array.isArray(raw)) return out; + for (let i = 0; i < 6 && i < raw.length; i++) { + const cell = raw[i]; + if (!cell || typeof cell !== 'object') continue; + const x = Math.floor(Number(cell.x)); + const y = Math.floor(Number(cell.y)); + if (!Number.isFinite(x) || !Number.isFinite(y)) continue; + if (x < 0 || x >= w || y < 0 || y >= h) continue; + out[i] = { x, y }; + } + return out; +} + +/** jump_survive: ช่อง P บนกริด (shooterSpawnSlots) เติม slots6 เมื่อ lobbyPlayerSpawns ว่าง */ +function augmentLobbySlotsFromShooterPaintJumpSurvive(md, slots6) { + if (!md || md.gameType !== 'jump_survive' || !Array.isArray(slots6)) return; + const g = md.shooterSpawnSlots; + if (!g) return; + const w = md.width || 20, h = md.height || 15; + for (let slot = 1; slot <= 6; slot++) { + const idx = slot - 1; + if (slots6[idx]) continue; + let found = false; + for (let yy = 0; yy < h && !found; yy++) { + const row = g[yy]; + if (!row) continue; + for (let xx = 0; xx < w; xx++) { + if (row[xx] === slot) { + slots6[idx] = { x: xx, y: yy }; + found = true; + break; + } + } + } + } +} + +/** + * จุดเกิดตอน join — random = สุ่มใน spawnArea / fixed = ปุ่มตั้งจุดเกิด / slots6 = P ตามลำดับเข้า (0=คนแรก) + * โหมดพรมแดงยังถูกทับด้วย gauntletSpawnPositions หลัง join ตามเดิม + */ +function pickSpawnForJoin(md, joinOrderIndex) { + if (!md) return { x: 1, y: 1 }; + if (md.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(md)) { + const qb = pickQuizBattleSpawnFromMap(md, joinOrderIndex); + if (qb) return qb; + } + const mode = md.lobbySpawnMode; + const ord = joinOrderIndex | 0; + if (mode === 'slots6' && ord >= 6) return pickRandomSpawnFromMap(md); + const j = Math.min(Math.max(0, ord), 5); + if (mode === 'fixed' && md.spawn) { + const fx = Number.isFinite(Number(md.spawn.x)) ? Math.floor(Number(md.spawn.x)) : 1; + const fy = Number.isFinite(Number(md.spawn.y)) ? Math.floor(Number(md.spawn.y)) : 1; + const w = md.width || 20; + const h = md.height || 15; + const x = Math.max(0, Math.min(w - 1, fx)); + const y = Math.max(0, Math.min(h - 1, fy)); + if (isMapSpawnAnchorWalkable(md, x, y)) return { x, y }; + return pickRandomSpawnFromMap(md); + } + if (mode === 'slots6') { + const slots = parseLobbyPlayerSpawnsFromMap(md); + augmentLobbySlotsFromShooterPaintJumpSurvive(md, slots); + const pick = slots[j]; + if (pick && isMapSpawnAnchorWalkable(md, pick.x, pick.y)) return { x: pick.x, y: pick.y }; + return pickRandomSpawnFromMap(md); + } + return pickRandomSpawnFromMap(md); +} + +/** สุ่มจุดเกิดในช่อง spawnArea=1 ที่เดินได้ — ไม่มีช่องว่าง = ใช้ spawn ค่าเดิมจากเอดิเตอร์ */ +function pickRandomSpawnFromMap(md) { + const fallback = md.spawn || { x: 1, y: 1 }; + const fx = Number.isFinite(Number(fallback.x)) ? Number(fallback.x) : 1; + const fy = Number.isFinite(Number(fallback.y)) ? Number(fallback.y) : 1; + const grid = md.spawnArea; + if (!grid || !Array.isArray(grid)) return { x: fx, y: fy }; + const w = md.width || 20; + const h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && isMapTileWalkableForSpawn(md, x, y)) pool.push({ x, y }); + } + } + if (!pool.length) return { x: fx, y: fy }; + const idx = typeof crypto.randomInt === 'function' + ? crypto.randomInt(0, pool.length) + : Math.floor(Math.random() * pool.length); + const pick = pool[idx]; + return { x: pick.x, y: pick.y }; +} + +/** ตอบผิดในเกมคำถาม — กลับจุดเกิดตามลำดับเข้า (เดียวกับ pickSpawnForJoin) แล้วดีดออกนอกโซนถ้าทับโซนตอบ */ +function quizWrongAnswerRespawnPosition(md, joinOrderIndex) { + const ord = joinOrderIndex | 0; + const sp = pickSpawnForJoin(md, ord); + return findNearestOutsideQuizAnswerZones(md, Number(sp.x) + 0.5, Number(sp.y) + 0.5); +} + +function initTroublesomeState(space) { + if (!space.troublesomeEligible) space.troublesomeEligible = new Set(); + if (space.troublesomeOfferSent == null) space.troublesomeOfferSent = false; + if (space.suspectPickIndex == null) space.suspectPickIndex = 0; + if (space.suspectPhaseActive == null) space.suspectPhaseActive = false; +} + +function attemptTroublesomeRoll(sid, force) { + const space = spaces.get(sid); + if (!space || space.troublesomeOfferSent) return; + const peerIds = [...space.peers.keys()]; + if (peerIds.length === 0) return; + if (!force && space.troublesomeEligible.size < peerIds.length) return; + space.troublesomeOfferSent = true; + if (space.troublesomeDebTimer) clearTimeout(space.troublesomeDebTimer); + if (space.troublesomeMaxTimer) clearTimeout(space.troublesomeMaxTimer); + space.troublesomeDebTimer = null; + space.troublesomeMaxTimer = null; + let pool = peerIds.filter((id) => space.troublesomeEligible.has(id)); + if (pool.length === 0) pool = peerIds; + const chosen = pool[Math.floor(Math.random() * pool.length)]; + space.troublesomeTargetId = chosen; + space.troublesomeResponseReceived = false; + io.to(chosen).emit('troublesome-offer', { seconds: 15 }); +} + +function scheduleTroublesomeRoll(sid) { + const space = spaces.get(sid); + if (!space || space.troublesomeOfferSent) return; + if (space.troublesomeDebTimer) clearTimeout(space.troublesomeDebTimer); + space.troublesomeDebTimer = setTimeout(() => attemptTroublesomeRoll(sid, false), 450); + if (!space.troublesomeMaxTimer) { + space.troublesomeMaxTimer = setTimeout(() => attemptTroublesomeRoll(sid, true), 8000); + } +} + +/** + * ห้องทดสอบเอดิเตอร์ / join ด้วย mapId: mapData บน space อาจยังเป็น lobby แต่ไคลเอนต์เล่น quiz_carry จากพารามิเตอร์ + * — ให้ lobby Ready/START ทำงานถ้า mapId ชี้แมป quiz_carry หรือเป็นห้อง preview + */ +function spaceAllowsQuizCarryLobbyRelaxed(space) { + if (!space) return false; + if (space.mapId) { + const byId = maps.get(space.mapId); + if (byId && byId.gameType === 'quiz_carry') return true; + } + const md = space.mapData; + if (md && md.gameType === 'quiz_carry') return true; + return !!(space.previewEditorTest + || (space.isPrivate && String(space.spaceName || '').toLowerCase() === 'preview')); +} + +/* ============================================================ + * Quiz Battle (Kahoot/ZEP-style) — ห้องตอบคำถามพร้อมกัน รับได้สูงสุด 50 คน + * 10 ห้อง · auto-start (กด Start แล้วนับถอยหลังเริ่มเอง) · คำถามจาก battleQuizMcq + * แยกจากระบบ spaces (โลกเดิน) ใช้ io เดียวกัน · event ขึ้นต้น qb-* + * ============================================================ */ +const QB_ROOM_COUNT = 10; +const QB_MAX_PLAYERS = 50; +const QB_QUESTIONS_PER_ROUND = 10; // จำนวนข้อต่อรอบ (สุ่มจาก pool battleQuizMcq) +const QB_ANSWER_MS = 20000; // เวลาตอบต่อข้อ +const QB_REVEAL_MS = 4000; // โชว์เฉลย + leaderboard ก่อนข้อต่อไป +const QB_COUNTDOWN_MS = 5000; // นับถอยหลังก่อนเริ่ม (auto-start) +const QB_BASE_POINTS = 1000; // คะแนนเต็มต่อข้อ (ตอบถูก + เร็วสุด) +const QB_STREAK_BONUS = 100; // โบนัสต่อ streak ตอบถูกติดกัน +const QB_MIN_PLAYERS_TO_START = 1; // ขั้นต่ำที่กด Start ได้ +const QB_END_LINGER_MS = 12000; // โชว์อันดับจบเกมก่อนรีเซ็ตห้องกลับ lobby +const QB_LOBBY_WATCH_ROOM = 'qb-lobby-watchers'; + +const qbRooms = new Map(); // roomId -> room state + +function qbRoomId(n) { return 'qb-' + n; } + +function qbCreateRoom(n) { + return { + n, + id: qbRoomId(n), + status: 'lobby', // lobby | countdown | question | reveal | ended + players: new Map(), // socketId -> player + questions: [], + qIndex: -1, + qStartAt: 0, + qEndsAt: 0, + timers: [], + }; +} + +function qbGetRoom(n) { + const id = qbRoomId(n); + let r = qbRooms.get(id); + if (!r) { r = qbCreateRoom(n); qbRooms.set(id, r); } + return r; +} + +function qbClearTimers(room) { + if (Array.isArray(room.timers)) room.timers.forEach((t) => clearTimeout(t)); + room.timers = []; +} + +function qbCountsSnapshot() { + const rooms = []; + for (let n = 1; n <= QB_ROOM_COUNT; n++) { + const r = qbRooms.get(qbRoomId(n)); + rooms.push({ n, count: r ? r.players.size : 0, status: r ? r.status : 'lobby' }); + } + return rooms; +} + +function qbBroadcastCounts() { + const payload = { rooms: qbCountsSnapshot() }; + io.to(QB_LOBBY_WATCH_ROOM).emit('qb-counts', payload); + for (let n = 1; n <= QB_ROOM_COUNT; n++) { + const r = qbRooms.get(qbRoomId(n)); + if (r && r.players.size) io.to(r.id).emit('qb-counts', payload); + } +} + +function qbPlayerList(room) { + return [...room.players.values()] + .map((p) => ({ id: p.id, name: p.name, score: p.score })) + .sort((a, b) => b.score - a.score); +} + +function qbLeaderboard(room) { + return qbPlayerList(room).map((p, i) => ({ rank: i + 1, id: p.id, name: p.name, score: p.score })); +} + +function qbEmitRoomState(room) { + io.to(room.id).emit('qb-room-state', { + n: room.n, + status: room.status, + max: QB_MAX_PLAYERS, + players: qbPlayerList(room), + }); +} + +function qbShuffle(arr) { + const a = arr.slice(); + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; +} + +/** หยิบคำถามจาก battleQuizMcq (อ่านสด) → สุ่ม → ตัด N ข้อ · คืน [] ถ้าไม่มีคำถาม */ +function qbPickQuestions() { + let pool = []; + try { pool = loadQuizSettings().battleQuizMcq || []; } catch (e) { pool = []; } + pool = pool.filter((q) => q && q.text && Array.isArray(q.choices) && q.choices.length >= 2); + if (!pool.length) return []; + return qbShuffle(pool).slice(0, QB_QUESTIONS_PER_ROUND); +} + +function qbResetRoom(room) { + qbClearTimers(room); + room.status = 'lobby'; + room.questions = []; + room.qIndex = -1; + for (const p of room.players.values()) { + p.score = 0; + p.streak = 0; + p.answeredIndex = -1; + } + io.to(room.id).emit('qb-reset', {}); + qbEmitRoomState(room); + qbBroadcastCounts(); +} + +function qbStartCountdown(room) { + if (room.status !== 'lobby') return { ok: false, error: 'เกมเริ่มไปแล้ว' }; + if (room.players.size < QB_MIN_PLAYERS_TO_START) return { ok: false, error: 'ผู้เล่นไม่พอ' }; + const questions = qbPickQuestions(); + if (!questions.length) return { ok: false, error: 'ยังไม่มีคำถามในคลัง (เพิ่มที่หน้า Admin)' }; + qbClearTimers(room); + room.questions = questions; + room.qIndex = -1; + room.status = 'countdown'; + for (const p of room.players.values()) { p.score = 0; p.streak = 0; p.answeredIndex = -1; } + const endsAt = Date.now() + QB_COUNTDOWN_MS; + io.to(room.id).emit('qb-countdown', { endsAt, ms: QB_COUNTDOWN_MS, totalQuestions: questions.length }); + qbBroadcastCounts(); + room.timers.push(setTimeout(() => qbNextQuestion(room), QB_COUNTDOWN_MS)); + return { ok: true }; +} + +function qbNextQuestion(room) { + qbClearTimers(room); + room.qIndex += 1; + if (room.qIndex >= room.questions.length) { qbEndGame(room); return; } + const q = room.questions[room.qIndex]; + room.status = 'question'; + room.qStartAt = Date.now(); + room.qEndsAt = room.qStartAt + QB_ANSWER_MS; + for (const p of room.players.values()) { p.answeredIndex = -1; p.answeredAt = 0; p.lastDelta = 0; p.lastCorrect = false; } + io.to(room.id).emit('qb-question', { + index: room.qIndex, + total: room.questions.length, + text: q.text, + choices: q.choices, + endsAt: room.qEndsAt, + durationMs: QB_ANSWER_MS, + }); + io.to(room.id).emit('qb-answered-count', { answered: 0, total: room.players.size }); + room.timers.push(setTimeout(() => qbReveal(room), QB_ANSWER_MS)); +} + +function qbAllAnswered(room) { + for (const p of room.players.values()) { if (p.answeredIndex < 0) return false; } + return room.players.size > 0; +} + +function qbReveal(room) { + qbClearTimers(room); + if (room.status !== 'question') return; + room.status = 'reveal'; + const q = room.questions[room.qIndex]; + const correctIndex = q.correctIndex; + const results = [...room.players.values()].map((p) => ({ + id: p.id, name: p.name, correct: !!p.lastCorrect, delta: p.lastDelta || 0, score: p.score, choice: p.answeredIndex, + })); + const isLast = room.qIndex >= room.questions.length - 1; + io.to(room.id).emit('qb-reveal', { + index: room.qIndex, + correctIndex, + results, + leaderboard: qbLeaderboard(room), + isLast, + nextInMs: QB_REVEAL_MS, + }); + room.timers.push(setTimeout(() => qbNextQuestion(room), QB_REVEAL_MS)); +} + +function qbEndGame(room) { + qbClearTimers(room); + room.status = 'ended'; + io.to(room.id).emit('qb-ended', { leaderboard: qbLeaderboard(room), lingerMs: QB_END_LINGER_MS }); + qbBroadcastCounts(); + room.timers.push(setTimeout(() => qbResetRoom(room), QB_END_LINGER_MS)); +} + +/** ให้คะแนนแบบ Kahoot: ตอบถูก = ฐาน × (0.5 + 0.5×เศษเวลาที่เหลือ) + โบนัส streak */ +function qbScoreAnswer(room, p, choiceIndex) { + const q = room.questions[room.qIndex]; + const now = Date.now(); + p.answeredIndex = choiceIndex; + p.answeredAt = now; + const correct = Number(choiceIndex) === Number(q.correctIndex); + p.lastCorrect = correct; + if (correct) { + const remain = Math.max(0, room.qEndsAt - now); + const frac = QB_ANSWER_MS > 0 ? remain / QB_ANSWER_MS : 0; + p.streak = (p.streak || 0) + 1; + const base = Math.round(QB_BASE_POINTS * (0.5 + 0.5 * frac)); + const bonus = (p.streak - 1) * QB_STREAK_BONUS; + p.lastDelta = base + bonus; + p.score += p.lastDelta; + } else { + p.streak = 0; + p.lastDelta = 0; + } + return p.lastDelta; +} + +function qbLeaveRoom(socket) { + const roomId = socket.data.qbRoom; + if (!roomId) return; + const room = qbRooms.get(roomId); + socket.data.qbRoom = null; + socket.leave(roomId); + if (!room) return; + if (room.players.delete(socket.id)) { + if (room.players.size === 0) { + qbClearTimers(room); + room.status = 'lobby'; + room.questions = []; + room.qIndex = -1; + } else { + qbEmitRoomState(room); + if (room.status === 'question' && qbAllAnswered(room)) qbReveal(room); + } + qbBroadcastCounts(); + } +} + +/* ============================================================ + * Quiz Battle (ฉากเดินตอบ แบบ ZEP) — sync คะแนน/อันดับข้ามผู้เล่นแบบ authoritative + * เก็บบน space: qbwSolved = Map(socketId -> Set(compId ที่ตอบถูก)) · score = จำนวนโดมที่ปลด + * event: quizbattle-hello (ขอคะแนนปัจจุบัน) · quizbattle-solve {compId} · server ส่งกลับ quizbattle-scores + * ============================================================ */ +function qbwScoresSnapshot(space) { + const scores = {}; + if (space && space.qbwSolved) { + for (const [pid, set] of space.qbwSolved) scores[pid] = set ? set.size : 0; + } + if (space && space.peers) { + for (const id of space.peers.keys()) if (scores[id] == null) scores[id] = 0; + } + return scores; +} + +function qbwBroadcastScores(sid, space) { + if (!sid || !space) return; + io.to(sid).emit('quizbattle-scores', { scores: qbwScoresSnapshot(space) }); +} + +io.on('connection', (socket) => { + /* ===== Quiz Battle ฉากเดินตอบ (ZEP) — sync คะแนน ===== */ + socket.on('quizbattle-hello', (cb) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (typeof cb === 'function') cb({ ok: !!space, scores: space ? qbwScoresSnapshot(space) : {} }); + }); + + socket.on('quizbattle-solve', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false }); + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'quiz_battle') return reply({ ok: false }); + const compId = parseInt(data && data.compId, 10); + if (!(compId > 0)) return reply({ ok: false }); + if (!space.qbwSolved) space.qbwSolved = new Map(); + let set = space.qbwSolved.get(socket.id); + if (!set) { set = new Set(); space.qbwSolved.set(socket.id, set); } + set.add(compId); + qbwBroadcastScores(sid, space); + reply({ ok: true, solved: set.size }); + }); + + /* ===== Quiz Battle (Kahoot-style 50 คน) ===== */ + socket.on('qb-watch-counts', (cb) => { + socket.join(QB_LOBBY_WATCH_ROOM); + if (typeof cb === 'function') cb({ ok: true, rooms: qbCountsSnapshot() }); + }); + + socket.on('qb-join', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const n = parseInt(data && data.room, 10); + if (!(n >= 1 && n <= QB_ROOM_COUNT)) return reply({ ok: false, error: 'ห้องไม่ถูกต้อง' }); + const room = qbGetRoom(n); + if (socket.data.qbRoom && socket.data.qbRoom !== room.id) qbLeaveRoom(socket); + if (!room.players.has(socket.id) && room.players.size >= QB_MAX_PLAYERS) { + return reply({ ok: false, error: 'ห้องเต็ม (50 คน)' }); + } + const name = String((data && data.name) || 'ผู้เล่น').trim().slice(0, 24) || 'ผู้เล่น'; + const characterId = String((data && data.characterId) || '').trim().slice(0, 64); + let p = room.players.get(socket.id); + if (!p) { + p = { id: socket.id, name, characterId, score: 0, streak: 0, answeredIndex: -1, answeredAt: 0, lastDelta: 0, lastCorrect: false }; + room.players.set(socket.id, p); + } else { + p.name = name; p.characterId = characterId; + } + socket.data.qbRoom = room.id; + socket.join(room.id); + socket.leave(QB_LOBBY_WATCH_ROOM); + qbEmitRoomState(room); + qbBroadcastCounts(); + // ส่งสถานะปัจจุบันให้คนที่เพิ่งเข้า (เผื่อเกมกำลังเล่นอยู่) + const snapshot = { n: room.n, status: room.status, max: QB_MAX_PLAYERS }; + if (room.status === 'question') { + const q = room.questions[room.qIndex]; + snapshot.question = { index: room.qIndex, total: room.questions.length, text: q.text, choices: q.choices, endsAt: room.qEndsAt, durationMs: QB_ANSWER_MS }; + } else if (room.status === 'countdown') { + snapshot.totalQuestions = room.questions.length; + } + reply({ ok: true, ...snapshot, players: qbPlayerList(room) }); + }); + + socket.on('qb-leave', (cb) => { + qbLeaveRoom(socket); + if (typeof cb === 'function') cb({ ok: true }); + }); + + socket.on('qb-start', (cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const roomId = socket.data.qbRoom; + const room = roomId ? qbRooms.get(roomId) : null; + if (!room || !room.players.has(socket.id)) return reply({ ok: false, error: 'ยังไม่ได้เข้าห้อง' }); + reply(qbStartCountdown(room)); + }); + + socket.on('qb-answer', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const roomId = socket.data.qbRoom; + const room = roomId ? qbRooms.get(roomId) : null; + if (!room || room.status !== 'question') return reply({ ok: false }); + const p = room.players.get(socket.id); + if (!p || p.answeredIndex >= 0) return reply({ ok: false, already: true }); + const q = room.questions[room.qIndex]; + const ci = parseInt(data && data.choiceIndex, 10); + if (!(ci >= 0 && ci < q.choices.length)) return reply({ ok: false }); + const delta = qbScoreAnswer(room, p, ci); + let answered = 0; + for (const pp of room.players.values()) if (pp.answeredIndex >= 0) answered++; + io.to(room.id).emit('qb-answered-count', { answered, total: room.players.size }); + reply({ ok: true, accepted: true }); + if (qbAllAnswered(room)) qbReveal(room); + }); + + socket.on('join-space', ({ spaceId, nickname, characterId, playMapId, desiredLobbyColorThemeIndex, desiredLobbySkinToneIndex }, cb) => { + const space = spaces.get(spaceId); + if (!space || !space.mapData) return cb && cb({ ok: false, error: 'ไม่พบห้อง' }); + const maxPlayers = space.maxPlayers || 10; + const mdJoinPre = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (mdJoinPre && mdJoinPre.gameType === 'gauntlet' && space.peers.size >= GAUNTLET_MAX_PLAYERS) { + return cb && cb({ ok: false, error: 'เกมพรมแดงรับได้สูงสุด ' + GAUNTLET_MAX_PLAYERS + ' คน' }); + } + if (mdJoinPre && mdJoinPre.gameType === 'space_shooter' && space.peers.size >= GAUNTLET_MAX_PLAYERS) { + return cb && cb({ ok: false, error: 'ยิงยานอวกาศรับได้สูงสุด ' + GAUNTLET_MAX_PLAYERS + ' คน' }); + } + if (mdJoinPre && mdJoinPre.gameType === 'balloon_boss' && space.peers.size >= GAUNTLET_MAX_PLAYERS) { + return cb && cb({ ok: false, error: 'ลูกโป้งยิงบอสรับได้สูงสุด ' + GAUNTLET_MAX_PLAYERS + ' คน' }); + } + if (space.peers.size >= maxPlayers) return cb && cb({ ok: false, error: 'ห้องเต็มแล้ว (' + space.peers.size + '/' + maxPlayers + ')' }); + const joinNickNorm = normalizeLobbyNickname(nickname); + if (space.joinLocked) { + const wl = space.caseParticipantNicknames; + if (wl && wl.size > 0) { + if (!wl.has(joinNickNorm)) { + return cb && cb({ ok: false, error: 'ห้องนี้เริ่มคดีแล้ว ไม่รับผู้เล่นเพิ่ม' }); + } + } else { + return cb && cb({ ok: false, error: 'ห้องนี้เริ่มคดีแล้ว ไม่รับผู้เล่นเพิ่ม' }); + } + } + socket.join(spaceId); + socket.data.spaceId = spaceId; + if (!space.hostId) space.hostId = socket.id; + if (serverMapIsPostCaseLobbyB(space)) initTroublesomeState(space); + ensurePeerLobbyThemes(space); + const mdJoin = (space.mapId && maps.get(space.mapId)) || space.mapData; + const spawnJoinOrder = space.peers.size; + const spawnPt = pickSpawnForJoin(mdJoin, spawnJoinOrder); + const bbStartBalloons = Math.max(1, Math.min(12, Math.floor(Number(mdJoin.balloonBossBalloonsPerPlayer)) || 3)); + /* ใช้สีที่ client เลือกไว้ใน Main-Lobby — คนจริงสำคัญกว่าบอท */ + let chosenThemeIdx = parseInt(desiredLobbyColorThemeIndex, 10); + if (chosenThemeIdx >= 1 && chosenThemeIdx <= LOBBY_THEME_COUNT) { + /* ถ้ามี "ผู้เล่นจริงคนอื่น" ถือสีนี้แล้ว → fallback สุ่มใหม่ */ + let takenByHuman = false; + space.peers.forEach((p) => { + const ti = parseInt(p.lobbyColorThemeIndex, 10); + if (ti === chosenThemeIdx) takenByHuman = true; + }); + if (takenByHuman) { + chosenThemeIdx = pickFreeLobbyThemeIndex(space); + } else if (space.lobbyBotThemeBySlot && typeof space.lobbyBotThemeBySlot === 'object') { + /* ถ้าบอทใช้สีนี้ → ดันบอทไปสีอื่น (เคลียร์ slot.themeIndex; syncLobbyBotThemeSlots ใน joinCb จะหาสีใหม่ที่ไม่ชนกับ peer ใหม่) */ + Object.keys(space.lobbyBotThemeBySlot).forEach((k) => { + const slot = space.lobbyBotThemeBySlot[k]; + if (slot && parseInt(slot.themeIndex, 10) === chosenThemeIdx) slot.themeIndex = 0; + }); + } + } else { + chosenThemeIdx = pickFreeLobbyThemeIndex(space); + } + let chosenSkinIdx = parseInt(desiredLobbySkinToneIndex, 10); + if (!(chosenSkinIdx >= 1 && chosenSkinIdx <= LOBBY_SKIN_COUNT)) { + chosenSkinIdx = pickLobbySkinIndexForSlot(spawnJoinOrder); + } + const peer = { + id: socket.id, x: +spawnPt.x, y: +spawnPt.y, direction: 'down', nickname: nickname || 'ผู้เล่น', ready: false, characterId: characterId || null, voiceMicOn: true, + spawnJoinOrder, + lobbyColorThemeIndex: chosenThemeIdx, + lobbySkinToneIndex: chosenSkinIdx, + gauntletJumpTicks: 0, gauntletScore: 0, gauntletJumpPending: false, gauntletEliminated: false, spaceShooterScore: 0, + balloonBossScore: 0, balloonBossBossDmg: 0, balloonBossBalloons: mdJoin.gameType === 'balloon_boss' ? bbStartBalloons : 5, balloonBossEliminated: false, + }; + if (mdJoin.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(mdJoin)) { + const sn = quizBattleSpawnWorldFromJoinOrderServer(mdJoin, spawnJoinOrder); + peer.x = sn.x; + peer.y = sn.y; + } + space.peers.set(socket.id, peer); + /* กลับ LobbyB เฉพาะเมื่อไม่ได้เข้า play ฉากมินิเกมที่กำลังเล่น (เช่น refresh room-lobby) */ + if (space.detectiveMinigameActive) { + const clientMap = playMapId != null ? String(playMapId).trim() : ''; + const activeMap = space.mapId != null ? String(space.mapId).trim() : ''; + if (!clientMap || !activeMap || clientMap !== activeMap) { + returnDetectiveSpaceToLobbyB(spaceId, space, 'กลับ LobbyB'); + } + } + if (spaceAllowsQuizCarryLobbyRelaxed(space)) { + if (!space.quizCarryLobbyReady || typeof space.quizCarryLobbyReady !== 'object') space.quizCarryLobbyReady = {}; + space.quizCarryLobbyReady[socket.id] = false; + io.to(spaceId).emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } }); + } + if (mdJoin.gameType === 'gauntlet') { + const ord = [...space.peers.keys()].indexOf(socket.id); + const pos = gauntletSpawnPositions(mdJoin, space.peers.size); + const pt = pos[ord] != null ? pos[ord] : pos[pos.length - 1]; + peer.x = pt.x; + peer.y = pt.y; + peer.gauntletJumpTicks = 0; + if (isGauntletCrownHeistMapBySpace(space)) { + peer.gauntletScore = 100; + peer.gauntletEliminated = false; + if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {}; + space.gauntletCrownLobbyReady[socket.id] = false; + io.to(spaceId).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } }); + } + startGauntletTicker(spaceId, space); + } else if (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space) || isJumpSurviveMissionShellSpace(space) || isSpaceShooterMissionShellSpace(space)) { + if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {}; + space.gauntletCrownLobbyReady[socket.id] = false; + if (!space.gauntletRun) space.gauntletRun = newBalloonBossShellGauntletRunState(); + io.to(spaceId).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } }); + } + const peersList = [...space.peers.values()]; + const mapDataOut = (space.mapId && maps.get(space.mapId)) || space.mapData || mdJoin; + const joinCb = { + ok: true, + mapData: mapDataOut, + mapId: space.mapId || null, + peers: peersList, + hostId: space.hostId, + spaceName: space.spaceName || spaceId, + isPrivate: !!space.isPrivate, + roomCode: space.isPrivate ? spaceId : null, + maxPlayers: space.maxPlayers || 10, + suspectPhaseActive: !!space.suspectPhaseActive && serverMapIsPostCaseLobbyB(space), + suspectPickIndex: typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0, + cardMinigames: space.suspectCardMinigames || [], + myPlayerEvidence: clonePlayerEvidence(space, socket.id), + suspectProgress: playerSuspectProgressFromEvidence(space, socket.id), + botSlotCount: effectiveBotSlotCount(space), + lobbyBotThemes: syncLobbyBotThemeSlots(space), + joinLocked: !!space.joinLocked, + }; + if (serverMapIsPostCaseLobbyB(space)) { + joinCb.detectiveLobbyLevel = space.detectiveLobbyLevel != null ? space.detectiveLobbyLevel : null; + joinCb.detectiveLobbyCaseId = space.detectiveLobbyCaseId != null ? space.detectiveLobbyCaseId : null; + } + if (mdJoin.gameType === 'quiz_carry' || spaceAllowsQuizCarryLobbyRelaxed(space)) { + try { + const qs = loadQuizSettings(); + joinCb.quizCarrySettingsSnap = { + carryMapPanelTheme: qs.carryMapPanelTheme, + carryEmbedCountdownTheme: qs.carryEmbedCountdownTheme, + carryChoicePlaqueThemes: qs.carryChoicePlaqueThemes, + carryChoicePlaqueTheme: qs.carryChoicePlaqueTheme, + carryChoicePlaqueMapScale: qs.carryChoicePlaqueMapScale, + carryReadMs: qs.carryReadMs, + carryAnswerMs: qs.carryAnswerMs, + carrySessionLength: qs.carrySessionLength, + carryWalkSpeedMultForMapId: qs.carryWalkSpeedMultForMapId, + carryWalkSpeedMult: qs.carryWalkSpeedMult, + }; + } catch (e) { /* ignore */ } + } + if (mdJoin.gameType === 'quiz') { + try { + const qsQuiz = loadQuizSettings(); + joinCb.quizSettingsSnap = { + quizMapPanelTheme: qsQuiz.quizMapPanelTheme, + }; + } catch (e) { /* ignore */ } + } + if (mdJoin.gameType === 'gauntlet' && space.gauntletRun) { + joinCb.gauntletEndsAt = space.gauntletRun.endsAt != null ? space.gauntletRun.endsAt : null; + } + if ((isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space) || isJumpSurviveMissionShellSpace(space) || isSpaceShooterMissionShellSpace(space)) && space.gauntletRun) { + joinCb.gauntletCrownRunHeld = !!space.gauntletRun.crownRunHeld; + } + /* ไอคอนคำถามพิเศษของรอบนี้ (ถ้ามีและยังไม่ถูกทริก) */ + const sqIcon = specialQuizIconPayload(space); + if (sqIcon) joinCb.specialQuizIcon = sqIcon; + if (typeof cb === 'function') cb(joinCb); + emitLobbyTintSync(spaceId, space); + socket.to(spaceId).emit('user-joined', peer); + if (space.voiceInits) { + for (const [peerId, initData] of Object.entries(space.voiceInits)) { + if (space.peers.get(peerId)?.voiceMicOn) socket.emit('voice-init', { from: peerId, data: initData }); + } + } + }); + + /** ผู้เล่นเดินชนไอคอนคำถามพิเศษ — ใครชนก่อนเป็นคนเปิดเซสชันให้ทั้งห้อง */ + /** debug: บังคับ spawn ไอคอนทุกรอบในเซสชันนี้ (เปิดด้วย ?sqForce=1) */ + socket.on('special-quiz-force', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return; + space.forceSpecialQuiz = !!(data && data.on); + console.log('[special-quiz] force flag =', space.forceSpecialQuiz, 'by', socket.id); + }); + + /** Test Mode (จาก localStorage ฝั่ง client) — เปิด shortcut ทดสอบ เช่น Ctrl+Q ตอบถูก */ + socket.on('special-quiz-testmode', (data) => { + socket.data.testMode = !!(data && data.on); + }); + + socket.on('special-quiz-collide', (_data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false }); + const sq = space.specialQuiz; + if (!sq || sq.triggered || sq.done) return reply({ ok: false }); + const started = startSpecialQuizSession(sid, space); + reply({ ok: !!started }); + }); + + /** ผู้เล่นส่งคำตอบของข้อปัจจุบัน */ + socket.on('special-quiz-answer', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false }); + const sess = space.specialQuiz && space.specialQuiz.session; + if (!sess || sess.resolving) return reply({ ok: false }); + const q = sess.questions[sess.qIndex]; + if (!q) return reply({ ok: false }); + /* Test Mode: Ctrl+Q เติมคำตอบที่ถูกให้ (เฉพาะ socket ที่เปิด test mode) */ + let choice; + if (data && data.debugCorrect && socket.data.testMode) { + choice = q.correctIndex; + } else { + choice = Number(data && data.choiceIndex); + } + if (!Number.isInteger(choice) || choice < 0 || choice >= q.choices.length) { + return reply({ ok: false }); + } + if (Number.isInteger(sess.answers[socket.id])) return reply({ ok: false, error: 'ตอบไปแล้ว' }); + sess.answers[socket.id] = choice; + reply({ ok: true }); + /* แจ้งทุกคนว่าคนนี้ตอบแล้ว (ยังไม่เปิดเผยว่าตอบอะไรจนกว่าจะเฉลย) */ + io.to(sid).emit('special-quiz-answer-update', { id: socket.id, answered: true }); + /* ตอบครบทุกคน (จริง+บอท) แล้วตัดสินทันที ไม่ต้องรอหมดเวลา */ + maybeResolveSpecialQuizAllAnswered(sid, space); + }); + + /** ผู้เล่นกด «เล่นต่อ» หลังโชว์การ์ดพิเศษ — เล่นรอบ quiz ต่อ (คนแรกที่กดพอ) */ + socket.on('special-quiz-continue', (_data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false }); + finishSpecialQuizContinue(sid, space); + reply({ ok: true }); + }); + + /** เปลี่ยนสีเสื้อ/ผิวใน lobby — ห้ามเลือกสีที่คนอื่นใช้แล้ว */ + socket.on('lobby-tint-update', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + const peer = space && space.peers.get(socket.id); + if (!space || !peer) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + let themeIndex = Math.floor(Number(data && data.themeIndex)); + let skinIndex = Math.floor(Number(data && data.skinToneIndex)); + if (!Number.isFinite(themeIndex) || themeIndex < 1 || themeIndex > LOBBY_THEME_COUNT) { + return reply({ ok: false, error: 'ธีมสีไม่ถูกต้อง' }); + } + if (!Number.isFinite(skinIndex) || skinIndex < 1 || skinIndex > LOBBY_SKIN_COUNT) { + skinIndex = peer.lobbySkinToneIndex || 1; + } + const taken = getTakenLobbyThemeIndices(space, socket.id); + if (taken.has(themeIndex)) return reply({ ok: false, error: 'สีเสื้อนี้มีคนใช้แล้ว' }); + peer.lobbyColorThemeIndex = themeIndex; + peer.lobbySkinToneIndex = skinIndex; + emitLobbyTintSync(sid, space); + reply({ + ok: true, + lobbyColorThemeIndex: themeIndex, + lobbySkinToneIndex: skinIndex, + lobbyBotThemes: space.lobbyBotThemeBySlot ? syncLobbyBotThemeSlots(space) : [], + }); + }); + + /** เปลี่ยนชื่อที่แสดงใน lobby / เกม (ซิงก์ทุก client ในห้อง) */ + socket.on('update-display-name', ({ nickname }, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + const p = space.peers.get(socket.id); + if (!p) return reply({ ok: false, error: 'ไม่พบผู้เล่น' }); + let safe = String(nickname || '').trim(); + if (!safe) return reply({ ok: false, error: 'ชื่อว่าง' }); + try { + safe = safe.normalize('NFKC'); + } catch (e) { /* ignore */ } + if (safe.length > 32) safe = safe.slice(0, 32); + const oldNorm = normalizeLobbyNickname(p.nickname); + const newNorm = normalizeLobbyNickname(safe); + p.nickname = safe; + if (space.caseParticipantNicknames && space.caseParticipantNicknames.size > 0) { + if (space.caseParticipantNicknames.has(oldNorm)) { + space.caseParticipantNicknames.delete(oldNorm); + space.caseParticipantNicknames.add(newNorm); + } + } + io.to(sid).emit('peer-display-name', { id: socket.id, nickname: safe }); + reply({ ok: true, nickname: safe }); + }); + + /** ห้องทดสอบจากเอดิเตอร์ (ชื่อ preview) — สลับโฮสต์ให้เครื่องนี้กดเริ่มเกมได้ */ + socket.on('preview-claim-host', (cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + const isPreviewEditorRoom = !!(space.previewEditorTest + || (space.isPrivate && String(space.spaceName || '').toLowerCase() === 'preview')); + if (!isPreviewEditorRoom) return reply({ ok: false, error: 'ใช้ได้เฉพาะห้องทดสอบจากเอดิเตอร์' }); + space.hostId = socket.id; + io.to(sid).emit('host-changed', { hostId: socket.id }); + return reply({ ok: true }); + }); + + socket.on('troublesome-eligible', () => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !serverMapIsPostCaseLobbyB(space) || space.troublesomeOfferSent) return; + initTroublesomeState(space); + space.troublesomeEligible.add(socket.id); + scheduleTroublesomeRoll(sid); + }); + + socket.on('troublesome-response', (data) => { + const accept = !!(data && data.accept); + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !serverMapIsPostCaseLobbyB(space)) return; + if (space.troublesomeTargetId && socket.id !== space.troublesomeTargetId) return; + if (space.troublesomeResponseReceived) return; + space.troublesomeResponseReceived = true; + space.troublesomeAccept = accept; + let idx = Math.floor(Number(space.suspectPickIndex)); + if (Number.isNaN(idx) || idx < 0 || idx > 2) idx = 0; + space.suspectPickIndex = idx; + if (!space.suspectCardMinigames || space.suspectCardMinigames.length < 3) { + assignDetectiveSuspectCardMinigames(space); + } + space.suspectPhaseActive = true; + const phaseBase = { + hostId: space.hostId, + selectedIndex: space.suspectPickIndex, + disruptorAccepted: accept, + cardMinigames: space.suspectCardMinigames || [], + }; + space.peers.forEach((_p, pid) => { + const sock = io.sockets.sockets.get(pid); + if (!sock) return; + sock.emit('suspect-phase-open', { + ...phaseBase, + myPlayerEvidence: clonePlayerEvidence(space, pid), + suspectProgress: playerSuspectProgressFromEvidence(space, pid), + }); + }); + socket.emit('troublesome-ack', { ok: true, accept }); + }); + + socket.on('suspect-pick-select', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !serverMapIsPostCaseLobbyB(space) || !space.suspectPhaseActive) return; + if (space.hostId !== socket.id) return; + let idx = Math.floor(Number(data && data.index)); + if (Number.isNaN(idx) || idx < 0 || idx > 2) return; + space.suspectPickIndex = idx; + io.to(sid).emit('suspect-pick-update', { selectedIndex: idx }); + }); + + socket.on('suspect-pick-start', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (space && Array.isArray(space.suspectCardMinigames)) { + space.suspectCardMinigames = space.suspectCardMinigames.map(normalizeDetectiveCardEntry); + } + if (!space || !serverMapIsPostCaseLobbyB(space)) { + return reply({ ok: false, error: 'ไม่พร้อม — ต้องอยู่โถง LobbyB' }); + } + if (!space.suspectPhaseActive) { + return reply({ ok: false, error: 'ไม่อยู่ในขั้นเลือกผู้ต้องสงสัย' }); + } + if (space.hostId !== socket.id) { + return reply({ ok: false, error: 'เฉพาะ host กดเริ่มสืบสวนได้' }); + } + if (!space.suspectCardMinigames || space.suspectCardMinigames.length < 3) { + assignDetectiveSuspectCardMinigames(space); + } + const selectedIndex = typeof space.suspectPickIndex === 'number' ? space.suspectPickIndex : 0; + const cardEntry = space.suspectCardMinigames[selectedIndex]; + if (!cardEntry || !cardEntry.mapId) { + return reply({ ok: false, error: 'ยังไม่ได้สุ่มมินิเกมสำหรับการ์ดนี้' }); + } + /* Card 3 Ban — ก่อนเริ่มมินิเกมรอบหน้า โหวตเลือกผู้เล่น 1 คนที่ห้ามเล่น */ + const pend = space.pendingSpecialCard; + if (pend && Number(pend.cardId) === 3 && !pend.consumed && !space.pickVote) { + pend.consumed = true; + io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'ban' }); + startPlayerPickVote(sid, space, 'ban', (targetId) => { + const sp = spaces.get(sid); + if (!sp) return; + sp.bannedPlayerId = targetId || null; + const res = beginDetectiveSuspectMinigame(sid, sp, cardEntry, selectedIndex); + if (!res || !res.ok) { + io.to(sid).emit('quiz-ended', { message: (res && res.error) || 'เริ่มมินิเกมไม่สำเร็จ', returnToLobbyB: true }); + } + }); + return reply({ ok: true, banVote: true }); + } + const started = beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex); + reply(started); + }); + + socket.on('detective-minigame-finished', (_data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) { + return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + } + if (!space.detectiveMinigameActive) { + if (serverMapIsPostCaseLobbyB(space)) { + const grant = grantDetectiveEvidenceForCurrentRun(sid, space, [socket.id]); + return reply({ + ok: true, + alreadyOnLobbyB: true, + suspectIndex: grant.suspectIdx, + awardedCard: grant.awarded && grant.awarded[socket.id] != null ? grant.awarded[socket.id] : null, + awardedCards: (grant.awardedList && grant.awardedList[socket.id]) || [], + grade: grant.grade || null, + freeEvidenceCount: grant.freeEvidenceCount || 0, + myPlayerEvidence: clonePlayerEvidence(space, socket.id), + suspectProgress: playerSuspectProgressFromEvidence(space, socket.id), + }); + } + return reply({ ok: false, error: 'ไม่ได้อยู่ในมินิเกมสืบสวน' }); + } + // เล่นมินิเกม 1 ครั้ง = ได้การ์ด 1 ใบ (ไม่ขึ้นเกรด/คะแนน) + returnDetectiveSpaceToLobbyB(sid, space, 'จบมินิเกม — กลับ LobbyB', { allPlayers: true }); + const g = space.lastEvidenceGrant || {}; + reply({ + ok: true, + suspectIndex: typeof g.suspectIdx === 'number' ? g.suspectIdx : -1, + awardedCard: (g.awarded && g.awarded[socket.id] != null) ? g.awarded[socket.id] : null, + awardedCards: (g.awardedList && g.awardedList[socket.id]) || [], + grade: g.grade || null, + freeEvidenceCount: g.freeEvidenceCount || 0, + myPlayerEvidence: clonePlayerEvidence(space, socket.id), + suspectProgress: playerSuspectProgressFromEvidence(space, socket.id), + }); + }); + + /** + * DEBUG/TEST hotkey — เก็บหลักฐาน 1 ใบให้ผู้ต้องสงสัย (suspectIndex 0–2) ทันที + * (ใช้สำหรับเทสต์ Ctrl+1/2/3 — เทียบเท่าเล่นมินิเกมจบ 1 ครั้ง) + */ + socket.on('detective-debug-award-evidence', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + if (!serverMapIsPostCaseLobbyB(space)) return reply({ ok: false, error: 'ต้องอยู่โถง LobbyB' }); + const idx = Math.floor(Number(data && data.suspectIndex)); + if (!(idx >= 0 && idx <= 2)) return reply({ ok: false, error: 'suspectIndex ต้องเป็น 0–2' }); + const card = awardRandomEvidenceCardToPlayer(space, socket.id, idx, 'C'); + if (card == null) return reply({ ok: false, error: 'ไม่มีรูปการ์ดในพูลของ suspect นี้' }); + emitLobbyEvidenceSync(sid, space); + reply({ + ok: true, + suspectIndex: idx, + awardedCard: card, + myPlayerEvidence: clonePlayerEvidence(space, socket.id), + suspectProgress: playerSuspectProgressFromEvidence(space, socket.id), + }); + }); + + // Host เปิดขั้นพิจารณาคดี (ต้องสืบครบ 3 ผู้ต้องสงสัย) + socket.on('suspect-accuse-open', (_data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !serverMapIsPostCaseLobbyB(space)) return reply({ ok: false, error: 'ต้องอยู่โถง LobbyB' }); + if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะ host เปิดพิจารณาคดีได้' }); + const prog = playerSuspectProgressFromEvidence(space, socket.id); + if (![0, 1, 2].every((i) => (prog[i] || 0) >= 1)) return reply({ ok: false, error: 'ต้องสืบครบทั้ง 3 ผู้ต้องสงสัยก่อน (เก็บหลักฐานอย่างน้อย 1 ใบต่อคน)' }); + if (typeof space.culpritIndex !== 'number') space.culpritIndex = Math.floor(Math.random() * 3); + space.suspectPhaseActive = false; + // เริ่ม "ห้องสรุปหลักฐาน" (ไต่สวน 3 ปากคำ) ก่อนเข้าโหวต + space.testimonyActive = true; + space.testimonyRound = 0; + emitTestimonyOpen(sid, space); + reply({ ok: true }); + }); + + // ผู้เล่นส่งหลักฐาน 1–2 ใบในปากคำปัจจุบัน — ใบที่ส่งต้องเป็นใบที่ตัวเองเก็บได้จริง + socket.on('testimony-submit', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + if (!space.testimonyActive) return reply({ ok: false, error: 'ไม่อยู่ในขั้นไต่สวน' }); + if (space.testimonyRevealed) return reply({ ok: false, error: 'ปากคำนี้เปิดเผยแล้ว' }); + const raw = (data && Array.isArray(data.picks)) ? data.picks : []; + const owned = normalizeEvidenceSlotList(clonePlayerEvidence(space, socket.id)[space.testimonyRound] || []); + const required = Math.max(1, Math.min(2, owned.length)); + /* raw = ตำแหน่ง index ใน owned (0..n-1) → เก็บเป็น card object */ + const usedIdx = []; + const picks = []; + raw.forEach((n) => { + const v = Math.floor(Number(n)); + if (v >= 0 && v < owned.length && usedIdx.indexOf(v) === -1) { usedIdx.push(v); picks.push(owned[v]); } + }); + if (owned.length === 0) { + /* ไม่มีหลักฐานของ suspect รอบนี้ — ข้ามได้ (auto-submit ว่าง) */ + if (!space.testimonySubmitted) space.testimonySubmitted = {}; + space.testimonySubmitted[socket.id] = []; + emitTestimonyStatus(sid, space); + /* ไม่ auto-reveal — รอ host กดปุ่ม "เปิดหลักฐานทั้งหมด" เองตาม design */ + return reply({ ok: true, skipped: true }); + } + if (picks.length !== required) { + return reply({ ok: false, error: 'ต้องเลือกหลักฐาน ' + required + ' ใบ (ตามจำนวนที่เก็บได้)' }); + } + if (!space.testimonySubmitted) space.testimonySubmitted = {}; + space.testimonySubmitted[socket.id] = picks.slice(0, required); + emitTestimonyStatus(sid, space); + /* ไม่ auto-reveal — รอ host กดปุ่ม "เปิดหลักฐานทั้งหมด" เองตาม design + (UX: host ต้องเห็นปุ่มเปลี่ยนรูปจาก btn-send → btn-open-card แล้วค่อยกดเอง) */ + reply({ ok: true }); + }); + + // Host บังคับเปิดเผยหลักฐานทั้งหมด (เผื่อมีคนยังไม่ส่ง) + socket.on('testimony-reveal', (_data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะ host' }); + if (!space.testimonyActive || space.testimonyRevealed) return reply({ ok: false, error: 'เปิดเผยไม่ได้ตอนนี้' }); + doTestimonyReveal(sid, space); + reply({ ok: true }); + }); + + // ผู้เล่นกด READY ไปปากคำถัดไป (หลังเปิดเผยผล) — ไม่ auto-advance รอ host กด "เริ่มพิจารณาคดี" + socket.on('testimony-ready-next', (_data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + if (!space.testimonyActive || !space.testimonyRevealed) return reply({ ok: false, error: 'ยังเปิดเผยไม่ได้' }); + if (!space.testimonyReadyNext) space.testimonyReadyNext = {}; + space.testimonyReadyNext[socket.id] = true; + const totalRN = testimonyTotalParticipants(space); + io.to(sid).emit('testimony-ready-update', { + ready: Object.keys(space.testimonyReadyNext), + totalPlayers: totalRN, + hostId: space.hostId, + }); + /* ไม่ auto-advance — รอ host กดปุ่ม "เริ่มพิจารณาคดี" (btn-start-vote.png) เอง */ + reply({ ok: true }); + }); + + // Host กดปุ่ม "เริ่มพิจารณาคดี" (btn-start-vote) — ไปปากคำถัดไป/เข้าโหวต + socket.on('testimony-start-vote', (_data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะ host' }); + if (!space.testimonyActive || !space.testimonyRevealed) return reply({ ok: false, error: 'ยังไม่ถึงเวลา' }); + /* host อาจกดก่อนทุกคน READY — บังคับให้ทุกคน (รวมบอท) ready ก่อน advance เพื่อให้ flow consistent */ + if (!space.testimonyReadyNext) space.testimonyReadyNext = {}; + testimonyBotIds(space).forEach((bid) => { space.testimonyReadyNext[bid] = true; }); + advanceTestimony(sid, space); + reply({ ok: true }); + }); + + // ผู้เล่นโหวตชี้ตัวคนร้าย + socket.on('trial-vote', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + if (space.trialPhase !== 'voting') return reply({ ok: false, error: 'ยังไม่ถึงเวลาโหวต' }); + if (socket.id === space.silencedPlayerId) return reply({ ok: false, error: 'คุณถูกปิดปาก (Silence) — โหวตรอบนี้ไม่ได้', silenced: true }); + let idx = Math.floor(Number(data && data.index)); + if (Number.isNaN(idx) || idx < 0 || idx > 2) return reply({ ok: false, error: 'เลือกผู้ต้องสงสัยไม่ถูกต้อง' }); + space.trialVotes = space.trialVotes || {}; + space.trialVotes[socket.id] = idx; + emitTrialVoteUpdate(sid, space); + const totalTV = trialEligibleVoterTotal(space); + if (Object.keys(space.trialVotes).length >= totalTV && totalTV > 0) { + computeAndEmitTrialResult(sid, space); + } + reply({ ok: true, index: idx }); + }); + + // Host บังคับเปิดเผยผล (เช่นมีคนยังไม่โหวต) + socket.on('trial-reveal', (_data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะ host' }); + if (space.trialPhase !== 'voting') return reply({ ok: false, error: 'ไม่อยู่ในขั้นโหวต' }); + computeAndEmitTrialResult(sid, space); + reply({ ok: true }); + }); + + /* โหวตเลือกผู้เล่น 1 คน (Silence/Ban) */ + socket.on('player-pick-vote', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + const pv = space.pickVote; + if (!pv || pv.resolved) return reply({ ok: false, error: 'ไม่มีการโหวตที่เปิดอยู่' }); + const tid = String((data && data.targetId) || ''); + if (!pv.candidates.some((c) => c.id === tid)) return reply({ ok: false, error: 'เป้าหมายไม่ถูกต้อง' }); + pv.votes[socket.id] = tid; + reply({ ok: true, targetId: tid }); + emitPickVoteUpdate(sid, space); + maybeResolvePickVote(sid, space); + }); + + /** Host Console — ตั้งจำนวนรวม (คน+บอท) ไม่เกิน LOBBY_SLOT_TOTAL */ + socket.on('host-console-apply', (data, cb) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || space.hostId !== socket.id) { + return cb && cb({ ok: false, error: 'เฉพาะ Host เท่านั้น' }); + } + const humans = space.peers.size; + let total = parseInt(data && data.totalSlots, 10); + if (!Number.isFinite(total)) total = humans + effectiveBotSlotCount(space); + total = Math.min(LOBBY_SLOT_TOTAL, Math.max(humans, total)); + let bots = parseInt(data && data.botSlotCount, 10); + if (!Number.isFinite(bots)) bots = total - humans; + bots = Math.max(0, Math.min(LOBBY_SLOT_TOTAL - humans, bots)); + total = humans + bots; + space.botSlotCount = bots; + space.maxPlayers = Math.max(humans, total - bots); + const lobbyBotThemes = syncLobbyBotThemeSlots(space); + io.to(sid).emit('host-console-settings', { + maxPlayers: space.maxPlayers, + botSlotCount: space.botSlotCount, + totalSlots: total, + lobbyBotThemes, + }); + emitLobbyTintSync(sid, space); + cb && cb({ ok: true, maxPlayers: space.maxPlayers, botSlotCount: space.botSlotCount, totalSlots: total, lobbyBotThemes }); + }); + + socket.on('host-console-kick-peer', ({ targetId }, cb) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || space.hostId !== socket.id) { + return cb && cb({ ok: false, error: 'เฉพาะ Host เท่านั้น' }); + } + const tid = targetId != null ? String(targetId) : ''; + if (!tid || tid === socket.id || tid === space.hostId) { + return cb && cb({ ok: false, error: 'ไม่สามารถลบผู้เล่นนี้ได้' }); + } + if (!space.peers.has(tid)) { + return cb && cb({ ok: false, error: 'ไม่พบผู้เล่นในห้อง' }); + } + const targetSock = io.sockets.sockets.get(tid); + if (targetSock) { + targetSock.emit('host-console-kicked', { message: 'คุณถูก Host นำออกจากห้อง' }); + targetSock.leave(sid); + delete targetSock.data.spaceId; + } + space.peers.delete(tid); + io.to(sid).emit('user-left', { id: tid }); + cb && cb({ ok: true }); + }); + + /* พร้อมแล้ว — ไม่บังคับจำนวนคนในห้อง */ + socket.on('set-ready', ({ ready }) => { + for (const [sid, space] of spaces) { + if (space.peers.has(socket.id)) { + const p = space.peers.get(socket.id); + if (p) p.ready = !!ready; + io.to(sid).emit('peer-ready', { id: socket.id, ready: !!ready }); + break; + } + } + }); + + socket.on('start-game', (data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + for (const [sid, space] of spaces) { + if (space.peers.has(socket.id) && space.hostId === socket.id) { + let lobbyLevel = (data && data.lobbyLevel != null) ? String(data.lobbyLevel).trim() : ''; + let caseId = (data && data.caseId != null) ? String(data.caseId).trim() : ''; + if (lobbyLevel && !/^[1-5]$/.test(lobbyLevel)) lobbyLevel = ''; + /* 15 คดี (5 ระดับ × 3 คดี) — caseId 1-15 */ + if (caseId && !/^(?:[1-9]|1[0-5])$/.test(caseId)) caseId = ''; + ensurePostCaseLobbyMapLoaded(); + const mdBefore = getLobbyLayoutMapForSpace(space); + if (mdBefore && mdBefore.gameType === 'quiz') { + const qErr = validateQuizMap(mdBefore); + if (qErr) return reply({ ok: false, error: qErr }); + clearSpaceQuizTimers(space); + startQuizGame(sid, space, mdBefore); + io.to(sid).emit('game-start', { quizMode: true, stayInRoomLobby: true }); + return reply({ ok: true }); + } + const detectiveB = !!(data && data.detectiveLobbyBStart); + if (detectiveB && (!lobbyLevel || !caseId)) { + return reply({ ok: false, error: 'กรุณาเลือกระดับและคดีให้ครบก่อนเริ่ม' }); + } + const mapNameNorm = mdBefore ? String(mdBefore.name || '').trim().toLowerCase() : ''; + const isNamedLobbyA = mapNameNorm === 'lobbya'; + const notZepOrFrogger = !!(mdBefore && mdBefore.gameType !== 'zep' && mdBefore.gameType !== 'frogger' && mdBefore.gameType !== 'gauntlet'); + /* LobbyA: gameType lobby, หรือชื่อฉาก LobbyA (กันเซฟผิดเป็น zep), หรือ wizard + ไม่ใช่มินิเกม zep/frogger/gauntlet (รวม quiz/stack ฯลฯ) */ + const isLobbyADetectiveStart = !!(mdBefore && !lobbySpaceAlreadyLobbyB(space, mdBefore) + && lobbyLevel && caseId && maps.has(POST_CASE_LOBBY_SPACE_ID) + && ( + mdBefore.gameType === 'lobby' + || isNamedLobbyA + || (detectiveB && notZepOrFrogger) + )); + if (detectiveB && lobbyLevel && caseId && !isLobbyADetectiveStart) { + return reply({ ok: false, error: 'ย้ายไป LobbyB ไม่ได้ — ต้องอยู่ฉาก LobbyA (หรือล็อบบี้ชื่อ LobbyA) และเซิร์ฟต้องมีไฟล์แผนที่ LobbyB' }); + } + if (!lobbyHostStandingInStartArea(space, socket.id)) { + return reply({ ok: false, error: 'ยืนในพื้นที่เริ่มเกม (สีส้มในเอดิเตอร์) ก่อนกดเริ่ม' }); + } + if (isLobbyADetectiveStart) { + assignDetectiveSuspectCardMinigames(space); + space.mapId = POST_CASE_LOBBY_SPACE_ID; + space.mapData = maps.get(POST_CASE_LOBBY_SPACE_ID); + const caseNickWl = new Set(); + space.peers.forEach((p) => { + caseNickWl.add(normalizeLobbyNickname(p.nickname)); + }); + space.caseParticipantNicknames = caseNickWl; + space.joinLocked = true; + space.troublesomeOfferSent = false; + space.troublesomeTargetId = null; + space.troublesomeResponseReceived = false; + space.suspectPhaseActive = false; + space.suspectPickIndex = 0; + space.detectiveLobbyLevel = Number(lobbyLevel) || 1; + space.detectiveLobbyCaseId = Number(caseId) || 1; + space.caseId = Number(caseId) || 1; + if (space.troublesomeEligible) space.troublesomeEligible.clear(); + else space.troublesomeEligible = new Set(); + if (space.troublesomeDebTimer) clearTimeout(space.troublesomeDebTimer); + if (space.troublesomeMaxTimer) clearTimeout(space.troublesomeMaxTimer); + space.troublesomeDebTimer = null; + space.troublesomeMaxTimer = null; + initTroublesomeState(space); + let li = 0; + space.peers.forEach((p) => { + const sp = pickSpawnForJoin(space.mapData, li++); + p.x = sp.x; + p.y = sp.y; + p.direction = 'down'; + }); + const peersSnap = [...space.peers.values()].map((p) => ({ + id: p.id, + x: p.x, + y: p.y, + direction: p.direction || 'down', + nickname: p.nickname, + ready: !!p.ready, + characterId: p.characterId ?? null + })); + space.peers.forEach((_p, pid) => { + const sock = io.sockets.sockets.get(pid); + if (!sock) return; + sock.emit('game-start', { + stayInRoomLobby: true, + mapId: POST_CASE_LOBBY_SPACE_ID, + lobbyLevel, + caseId, + peersSnap, + cardMinigames: space.suspectCardMinigames || [], + myPlayerEvidence: clonePlayerEvidence(space, pid), + suspectProgress: playerSuspectProgressFromEvidence(space, pid), + }); + }); + return reply({ ok: true }); + } + const mapId = (data && data.mapId) ? String(data.mapId).trim() : null; + let peersSnap = null; + let gauntletEndsAtEmit = undefined; + if (mapId && maps.has(mapId)) { + const prevMapIdForReturn = space.mapId; + space.mapId = mapId; + space.mapData = maps.get(mapId); + const md = space.mapData; + if (md && md.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(md)) { + space.peers.forEach((p) => { + const sn = snapPositionOntoQuizBattlePathServer(md, Number(p.x), Number(p.y)); + p.x = sn.x; + p.y = sn.y; + }); + } + if (!md || md.gameType !== 'gauntlet') { + stopGauntletTicker(space); + space.gauntletReturnMapId = null; + space.gauntletRun = null; + } + if (md && md.gameType === 'gauntlet') { + stopGauntletTicker(space); + space.gauntletReturnMapId = prevMapIdForReturn; + initGauntletPeersAll(space, md); + ensureGauntletEndsAtIfNeeded(space); + startGauntletTicker(sid, space); + gauntletEndsAtEmit = space.gauntletRun && space.gauntletRun.endsAt != null + ? space.gauntletRun.endsAt + : null; + peersSnap = [...space.peers.values()].map((p) => ({ + id: p.id, + x: p.x, + y: p.y, + direction: p.direction || 'down', + nickname: p.nickname, + ready: !!p.ready, + characterId: p.characterId ?? null, + gauntletJumpTicks: p.gauntletJumpTicks || 0, + gauntletScore: p.gauntletScore || 0, + gauntletEliminated: !!p.gauntletEliminated, + })); + } + if (md && (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space) || isJumpSurviveMissionShellSpace(space) || isSpaceShooterMissionShellSpace(space))) { + if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {}; + space.peers.forEach((_p, id) => { + space.gauntletCrownLobbyReady[id] = false; + }); + space.gauntletRun = newBalloonBossShellGauntletRunState(); + } + } + const gameStartPayload = { + mapId: mapId || undefined, + lobbyLevel: lobbyLevel || undefined, + caseId: caseId || undefined, + peersSnap: peersSnap || undefined, + }; + if (peersSnap) gameStartPayload.gauntletEndsAt = gauntletEndsAtEmit != null ? gauntletEndsAtEmit : null; + if (mapId && maps.has(mapId) && (isBalloonBossMissionShellSpace(space) || isQuizQuestionMissionShellSpace(space) || isStackTowerMissionShellSpace(space) || isJumpSurviveMissionShellSpace(space) || isSpaceShooterMissionShellSpace(space))) { + const grs = space.gauntletRun; + gameStartPayload.gauntletCrownRunHeld = !!(grs && grs.crownRunHeld); + } + io.to(sid).emit('game-start', gameStartPayload); + return reply({ ok: true }); + } + } + reply({ ok: false, error: 'ไม่ใช่ host' }); + }); + + socket.on('disconnect', () => { + qbLeaveRoom(socket); // Quiz Battle cleanup (รันก่อนเสมอ แม้ไม่ได้อยู่ใน space) + let sid = socket.data.spaceId; + let space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) { + for (const [s, sp] of spaces) { + if (sp.peers.has(socket.id)) { sid = s; space = sp; break; } + } + } + if (!space || !space.peers.has(socket.id)) return; + const wasHost = space.hostId === socket.id; + if (space.voiceInits) delete space.voiceInits[socket.id]; + if (space.troublesomeEligible) space.troublesomeEligible.delete(socket.id); + if (space.quizSession && space.quizSession.players) delete space.quizSession.players[socket.id]; + if (space.gauntletPreviewRowsBySocket) space.gauntletPreviewRowsBySocket.delete(socket.id); + if (space.quizCarryLobbyReady && typeof space.quizCarryLobbyReady === 'object') { + delete space.quizCarryLobbyReady[socket.id]; + if (spaceAllowsQuizCarryLobbyRelaxed(space)) { + io.to(sid).emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } }); + } + } + if (space.gauntletCrownLobbyReady && typeof space.gauntletCrownLobbyReady === 'object') { + delete space.gauntletCrownLobbyReady[socket.id]; + if (isCrownLobbyShellSpace(space)) { + io.to(sid).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } }); + } + } + if (space.qbwSolved) space.qbwSolved.delete(socket.id); // Quiz Battle (ฉากเดิน) — เอาคะแนนคนออกแล้ว sync ใหม่ด้านล่าง + space.peers.delete(socket.id); + if (space.peers.size === 0) { + stopGauntletTicker(space); + clearSpaceQuizTimers(space); + space.quizSession = null; + /* ระหว่างมินิเกมสืบสวน / คดีล็อก — อย่าลบห้องเมื่อเปลี่ยนหน้า room-lobby → play.html (รอ join กลับ) */ + const keepCaseSpace = !!(space.detectiveMinigameActive + || (space.joinLocked && space.caseParticipantNicknames && space.caseParticipantNicknames.size > 0)); + if (keepCaseSpace) { + space.hostId = null; + return; + } + spaces.delete(sid); + } else { + if (wasHost) { + const firstPeer = space.peers.values().next().value; + if (firstPeer) { + space.hostId = firstPeer.id; + io.to(sid).emit('host-changed', { hostId: firstPeer.id }); + } + } + io.to(sid).emit('user-left', { id: socket.id }); + if (space.qbwSolved) qbwBroadcastScores(sid, space); // Quiz Battle ฉากเดิน — อัปเดตอันดับหลังมีคนออก + } + }); + + socket.on('gauntlet-jump', () => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return; + const p = space.peers.get(socket.id); + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'gauntlet') return; + if (space.gauntletRun && space.gauntletRun.crownRunHeld) return; + if (isGauntletCrownHeistMapBySpace(space) && p.gauntletEliminated) return; + if ((p.gauntletJumpTicks || 0) > 0) return; + p.gauntletJumpPending = true; + }); + + socket.on('gauntlet-crown-begin-run', (cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false }); + const gr = space.gauntletRun; + if (!gr) return reply({ ok: false }); + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + const okGauntletCrown = md && md.gameType === 'gauntlet' && isGauntletCrownHeistMapBySpace(space); + const okBalloonShell = md && md.gameType === 'balloon_boss' && isBalloonBossMissionShellSpace(space); + if (!okGauntletCrown && !okBalloonShell) return reply({ ok: false }); + const previewRoom = !!(space.previewEditorTest + || (space.isPrivate && String(space.spaceName || '').toLowerCase() === 'preview')); + if (!previewRoom && space.hostId !== socket.id) return reply({ ok: false, error: 'host' }); + if (!gr.crownRunHeld) return reply({ ok: true, already: true }); + gr.crownRunHeld = false; + if (okGauntletCrown) { + ensureGauntletEndsAtIfNeeded(space); + emitGauntletSync(sid, space); + } else { + emitBalloonBossMissionShellGauntletSync(sid, space); + } + return reply({ ok: true }); + }); + + /** Client ส่งแถว y ของบอททดสอบ (ไม่อยู่ใน peers) เพื่อให้ spawn lane obstacles บนแถวนั้น */ + socket.on('gauntlet-preview-rows', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'gauntlet') return; + if (space.gauntletRun && space.gauntletRun.crownRunHeld) return; + const hh = md.height || 15; + if (!space.gauntletPreviewRowsBySocket) space.gauntletPreviewRowsBySocket = new Map(); + const raw = data && Array.isArray(data.ys) ? data.ys : []; + const set = new Set(); + const cap = Math.min(raw.length, 48); + for (let i = 0; i < cap; i++) { + const fy = Math.floor(Number(raw[i])); + if (Number.isFinite(fy) && fy >= 0 && fy < hh) set.add(fy); + } + space.gauntletPreviewRowsBySocket.set(socket.id, set); + }); + + socket.on('quiz-carry-lobby-sync-request', () => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return; + if (!spaceAllowsQuizCarryLobbyRelaxed(space)) return; + if (!space.quizCarryLobbyReady || typeof space.quizCarryLobbyReady !== 'object') { + space.quizCarryLobbyReady = {}; + } + for (const id of space.peers.keys()) { + if (space.quizCarryLobbyReady[id] === undefined) space.quizCarryLobbyReady[id] = false; + } + socket.emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } }); + }); + + socket.on('quiz-carry-lobby-ready', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return; + if (!spaceAllowsQuizCarryLobbyRelaxed(space)) return; + if (!space.quizCarryLobbyReady || typeof space.quizCarryLobbyReady !== 'object') space.quizCarryLobbyReady = {}; + for (const id of space.peers.keys()) { + if (space.quizCarryLobbyReady[id] === undefined) space.quizCarryLobbyReady[id] = false; + } + space.quizCarryLobbyReady[socket.id] = !!(data && data.ready); + io.to(sid).emit('quiz-carry-lobby-sync', { readyMap: { ...space.quizCarryLobbyReady } }); + }); + + socket.on('quiz-carry-lobby-start', (_data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + if (!spaceAllowsQuizCarryLobbyRelaxed(space)) return reply({ ok: false, error: 'ห้องนี้ไม่ใช่โหมด lobby หยิบมาวาง / พรีวิว' }); + if (space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะโฮสต์กด START' }); + const rm = space.quizCarryLobbyReady || {}; + const humanIds = [...space.peers.keys()]; + const allReady = humanIds.length > 0 && humanIds.every((id) => rm[id]); + if (!allReady) return reply({ ok: false, error: 'ยังมีผู้เล่นที่ยังไม่ Ready' }); + io.to(sid).emit('quiz-carry-lobby-started', {}); + reply({ ok: true }); + }); + + socket.on('gauntlet-crown-lobby-sync-request', () => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return; + if (!isCrownLobbyShellSpace(space)) return; + if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') { + space.gauntletCrownLobbyReady = {}; + } + for (const id of space.peers.keys()) { + if (space.gauntletCrownLobbyReady[id] === undefined) space.gauntletCrownLobbyReady[id] = false; + } + socket.emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } }); + }); + + socket.on('gauntlet-crown-lobby-ready', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return; + if (!isCrownLobbyShellSpace(space)) return; + if (!space.gauntletCrownLobbyReady || typeof space.gauntletCrownLobbyReady !== 'object') space.gauntletCrownLobbyReady = {}; + for (const id of space.peers.keys()) { + if (space.gauntletCrownLobbyReady[id] === undefined) space.gauntletCrownLobbyReady[id] = false; + } + space.gauntletCrownLobbyReady[socket.id] = !!(data && data.ready); + io.to(sid).emit('gauntlet-crown-lobby-sync', { readyMap: { ...space.gauntletCrownLobbyReady } }); + }); + + socket.on('gauntlet-crown-lobby-start', (_data, cb) => { + const reply = typeof cb === 'function' ? cb : () => {}; + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return reply({ ok: false, error: 'ไม่อยู่ในห้อง' }); + if (!isCrownLobbyShellSpace(space)) return reply({ ok: false, error: 'ไม่ใช่แมป crown / Mega Virus ภารกิจ' }); + const soloRoom = space.peers.size === 1; + if (!soloRoom && space.hostId !== socket.id) return reply({ ok: false, error: 'เฉพาะโฮสต์กด START' }); + const gr = space.gauntletRun; + if (!gr || !gr.crownRunHeld) return reply({ ok: false, error: 'เริ่มแล้ว' }); + const rm = space.gauntletCrownLobbyReady || {}; + const humanIds = [...space.peers.keys()]; + const allReady = humanIds.length > 0 && humanIds.every((id) => rm[id]); + if (!allReady) return reply({ ok: false, error: 'ยังมีผู้เล่นที่ยังไม่ Ready' }); + if (isQuizQuestionMissionShellSpace(space)) { + const mdQuiz = maps.get(space.mapId) || space.mapData; + if (mdQuiz && mdQuiz.gameType === 'quiz') { + clearSpaceQuizTimers(space); + startQuizGame(sid, space, mdQuiz); + } + } + io.to(sid).emit('gauntlet-crown-lobby-started', {}); + reply({ ok: true }); + }); + + socket.on('move', (data) => { + for (const [sid, space] of spaces) { + if (space.peers.has(socket.id)) { + const p = space.peers.get(socket.id); + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (md && md.gameType === 'gauntlet') { + const out = { id: socket.id, x: p.x, y: p.y, direction: p.direction || 'down', characterId: p.characterId }; + socket.emit('user-move', out); + break; + } + let nx = data.x; + let ny = data.y; + const sess = space.quizSession; + const mdQuiz = (sess && sess.quizMapMd) || md; + if (p && sess && sess.active && mdQuiz && mdQuiz.gameType === 'quiz') { + const st = sess.players && sess.players[socket.id]; + if (st && !st.eliminated) { + const tx = Math.floor(Number(nx)); + const ty = Math.floor(Number(ny)); + const blockTrue = st.cannotTrue && quizCellOn(mdQuiz.quizTrueArea, tx, ty); + const blockFalse = st.cannotFalse && quizCellOn(mdQuiz.quizFalseArea, tx, ty); + if (blockTrue || blockFalse) { + nx = p.x; + ny = p.y; + } + } + } + if (p && md && md.gameType === 'quiz_carry' && md.quizCarryHubArea) { + const txN = Number(nx); + const tyN = Number(ny); + if (Number.isFinite(txN) && Number.isFinite(tyN) && quizCarryFootprintOverlapsHubServer(md, txN, tyN)) { + nx = p.x; + ny = p.y; + } + } + if (p && md && md.gameType === 'quiz_battle') { + const txN = Number(nx); + const tyN = Number(ny); + if (!Number.isFinite(txN) || !Number.isFinite(tyN) || !serverFootprintClearOfWalls(md, txN, tyN)) { + nx = p.x; + ny = p.y; + } else if (quizBattlePathModeActiveServer(md) && !quizBattlePositionAllowedServer(md, txN, tyN)) { + nx = p.x; + ny = p.y; + } + if (quizBattlePathModeActiveServer(md)) { + const sn = snapPositionOntoQuizBattlePathServer(md, Number(nx), Number(ny)); + nx = sn.x; + ny = sn.y; + } + } + if (p && md && md.gameType === 'space_shooter' && data && data.spaceShooterScore != null) { + const ns = Math.floor(Number(data.spaceShooterScore)); + if (Number.isFinite(ns) && ns >= 0) { + const prev = Math.max(0, p.spaceShooterScore | 0); + if (ns <= prev + 35) p.spaceShooterScore = Math.max(prev, ns); + } + } + if (p && md && md.gameType === 'balloon_boss' && data) { + const bbDefaultBalloons = Math.max(1, Math.min(12, Math.floor(Number(md.balloonBossBalloonsPerPlayer)) || 3)); + if (data.balloonBossScore != null) { + const ns = Math.floor(Number(data.balloonBossScore)); + if (Number.isFinite(ns) && ns >= 0) { + const prev = Math.max(0, p.balloonBossScore | 0); + if (ns <= prev + 35) p.balloonBossScore = Math.max(prev, ns); + } + } + if (data.balloonBossBossDmg != null) { + const nd = Math.floor(Number(data.balloonBossBossDmg)); + if (Number.isFinite(nd) && nd >= 0) { + const prevD = Math.max(0, p.balloonBossBossDmg | 0); + if (nd <= prevD + 8) p.balloonBossBossDmg = Math.max(prevD, nd); + } + } + if (data.balloonBossBalloons != null) { + const nb = Math.floor(Number(data.balloonBossBalloons)); + if (Number.isFinite(nb) && nb >= 0) { + const prevB = typeof p.balloonBossBalloons === 'number' && Number.isFinite(p.balloonBossBalloons) + ? Math.floor(p.balloonBossBalloons) + : bbDefaultBalloons; + if (nb <= prevB && nb >= Math.max(0, prevB - 2)) p.balloonBossBalloons = nb; + } + } + if (data.balloonBossEliminated) p.balloonBossEliminated = true; + } + if (p) { + const prevX = Number(p.x); + const prevY = Number(p.y); + const prevDir = p.direction || 'down'; + p.x = nx; + p.y = ny; + p.direction = data.direction || p.direction; + const posUnchanged = Math.abs(nx - prevX) < 1e-5 && Math.abs(ny - prevY) < 1e-5 && p.direction === prevDir; + if (!posUnchanged) { + const out = { id: socket.id, x: nx, y: ny, direction: p.direction, characterId: p.characterId }; + if (md && md.gameType === 'space_shooter') out.spaceShooterScore = Math.max(0, p.spaceShooterScore | 0); + if (md && md.gameType === 'balloon_boss') { + const bbDef = Math.max(1, Math.min(12, Math.floor(Number(md.balloonBossBalloonsPerPlayer)) || 3)); + out.balloonBossScore = Math.max(0, p.balloonBossScore | 0); + out.balloonBossBossDmg = Math.max(0, p.balloonBossBossDmg | 0); + out.balloonBossBalloons = typeof p.balloonBossBalloons === 'number' && Number.isFinite(p.balloonBossBalloons) + ? Math.max(0, Math.floor(p.balloonBossBalloons)) + : bbDef; + out.balloonBossEliminated = !!p.balloonBossEliminated; + } + if (nx !== data.x || ny !== data.y) socket.emit('user-move', out); + socket.to(sid).emit('user-move', out); + } + } + break; + } + } + }); + + socket.on('lobby-interact', (data, ack) => { + const reply = typeof ack === 'function' ? ack : () => {}; + const tx = Math.floor(Number(data && data.x)); + const ty = Math.floor(Number(data && data.y)); + for (const [sid, space] of spaces) { + if (!space.peers.has(socket.id)) continue; + const p = space.peers.get(socket.id); + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md) return reply({ ok: false, error: 'ไม่มีแผนที่' }); + const w = md.width || 20; + const h = md.height || 15; + if (Number.isNaN(tx) || Number.isNaN(ty) || tx < 0 || tx >= w || ty < 0 || ty >= h) { + return reply({ ok: false, error: 'จุดไม่ถูกต้อง' }); + } + if (!md.interactive || !md.interactive[ty] || md.interactive[ty][tx] !== 1) { + return reply({ ok: false, error: 'ไม่มีจุดโต้ตอบที่นี่' }); + } + const px = Math.floor(Number(p.x)); + const py = Math.floor(Number(p.y)); + const neighbors = [[0, 0], [0, -1], [0, 1], [-1, 0], [1, 0]]; + let near = false; + for (const [dx, dy] of neighbors) { + if (px + dx === tx && py + dy === ty) { near = true; break; } + } + if (!near) return reply({ ok: false, error: 'เข้าใกล้จุดสีเขียว (ช่องโต้ตอบ) ก่อน' }); + if (!p.ready) return reply({ ok: false, error: 'กดพร้อมก่อน แล้วค่อยกด F ที่ช่องสีเขียว' }); + const nick = (p.nickname || '').trim() || 'ผู้เล่น'; + io.to(sid).emit('lobby-interact', { id: socket.id, nickname: nick, x: tx, y: ty }); + return reply({ ok: true }); + } + reply({ ok: false, error: 'ไม่ได้อยู่ในห้อง' }); + }); + + socket.on('chat', (text) => { + for (const [sid, space] of spaces) { + if (socket.rooms.has(sid)) { + const nick = space.peers.get(socket.id)?.nickname || ''; + io.to(sid).emit('chat', { id: socket.id, text, time: new Date(), nickname: nick }); + break; + } + } + }); + + socket.on('voice-state', ({ micOn }) => { + for (const [sid, space] of spaces) { + if (space.peers.has(socket.id)) { + const p = space.peers.get(socket.id); + if (p) p.voiceMicOn = micOn !== false; + if (micOn === false && space.voiceInits) delete space.voiceInits[socket.id]; + io.to(sid).emit('peer-voice-state', { id: socket.id, micOn: micOn !== false }); + break; + } + } + }); + + socket.on('voice-activity', ({ level }) => { + const sid = socket.data.spaceId; + if (!sid || !spaces.get(sid)) return; + const until = Date.now() + 400; + io.to(sid).emit('peer-speaking', { id: socket.id, level: typeof level === 'number' ? level : 0.5, until }); + }); + + socket.on('voice-init', (data) => { + for (const [sid, space] of spaces) { + if (socket.rooms.has(sid)) { + if (!space.voiceInits) space.voiceInits = {}; + space.voiceInits[socket.id] = data; + io.to(sid).emit('voice-init', { from: socket.id, data }); + break; + } + } + }); + + socket.on('voice-chunk', (data) => { + for (const [sid, space] of spaces) { + if (socket.rooms.has(sid)) { + socket.to(sid).emit('voice-chunk', { from: socket.id, data }); + break; + } + } + }); + + socket.on('webrtc-signal', (data) => { + const { to, type, sdp, candidate } = data; + if (!to) return; + for (const [sid, space] of spaces) { + if (socket.rooms.has(sid) && space.peers.has(to)) { + io.to(to).emit('webrtc-signal', { from: socket.id, type, sdp, candidate }); + break; + } + } + }); +}); + +loadMaps(); +server.on('error', (err) => { + if (err && (err.code === 'EACCES' || err.code === 'EADDRINUSE')) { + console.error(''); + console.error('[Game] Cannot listen on', HOST + ':' + PORT, '(' + err.code + ').'); + console.error(' Windows AppServ:'); + console.error(' set HOST=127.0.0.1'); + console.error(' set PORT=13010'); + console.error(' node server.js'); + console.error(' Or double-click start-dev.cmd'); + console.error(''); + process.exit(1); + } + throw err; +}); +server.listen(PORT, HOST, () => console.log('Game server listening on http://' + HOST + ':' + PORT + BASE_PATH)); \ No newline at end of file