diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js index e972a4d..64df753 100644 --- a/www/html/Admin/admin.js +++ b/www/html/Admin/admin.js @@ -1,5827 +1,6075 @@ -(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 SOUND_SETTINGS_API = '/Game/api/sound-settings'; - 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: '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) { - 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 : 'topic1', - 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('topic1'); - 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 || 'topic1'); - - 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 || 'topic1') === 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 <= 8; si++) { - var nin = el('stack-block-normal-url-' + si); - var hin = el('stack-block-heavy-url-' + si); - var nv = normArr[si - 1] != null && String(normArr[si - 1]) !== '' ? String(normArr[si - 1]) : STACK_BLOCK_DEFAULT_NORMAL[si - 1]; - var hv = heavyArr[si - 1] != null && String(heavyArr[si - 1]) !== '' ? String(heavyArr[si - 1]) : STACK_BLOCK_DEFAULT_HEAVY[si - 1]; - if (nin) nin.value = nv || ''; - if (hin) hin.value = hv || ''; - updateStackBlockSeatPreview(si); - } - } - /* #4 MG3 8 สีตาม lobby theme — ค่าเริ่มต้น Block-Code-1..8 (ตรง PLAY_LOBBY_THEME_HEX) */ - var STACK_BLOCK_THEME_NAMES = ['1-red', '2-orange', '3-Yellow', '4-Green', '5-Blue', '6-Navy', '7-Violet', '8-Pink']; - var STACK_BLOCK_DEFAULT_NORMAL = STACK_BLOCK_THEME_NAMES.map(function (s) { return '/Game/img/TowerBlock/Block-Code-' + s + '.png'; }); - var STACK_BLOCK_DEFAULT_HEAVY = STACK_BLOCK_THEME_NAMES.map(function (s) { return '/Game/img/TowerBlock/Block-Code-' + s + '-2.png'; }); - - function readStackBlockNormalUrlsFromForm() { - var out = []; - for (var i = 1; i <= 8; 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 <= 8; 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 <= 8; 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); - if (el('game-timing-jump-height')) el('game-timing-jump-height').value = String(data.gauntletJumpHeightMult != null ? data.gauntletJumpHeightMult : 0.52); - el('game-timing-limit-sec').value = String(data.gauntletTimeLimitSec != null ? data.gauntletTimeLimitSec : 0); - /* troublesomeOfferSec/RollMaxSec ย้ายไปแท็บ "บทตัวป่วน" แล้ว */ - 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 inpBp = el('mega-virus-balloons-per-player'); - if (inpBp && Object.prototype.hasOwnProperty.call(data, 'balloonBossBalloonsPerPlayer')) { - var bp = Number(data.balloonBossBalloonsPerPlayer); - inpBp.value = String(Number.isFinite(bp) && bp > 0 ? Math.min(12, Math.floor(bp)) : 0); - } - var inpDh = el('mega-virus-damage-per-hit'); - if (inpDh && Object.prototype.hasOwnProperty.call(data, 'balloonBossDamagePerHit')) { - var dh = Number(data.balloonBossDamagePerHit); - inpDh.value = String(Number.isFinite(dh) && dh > 0 ? Math.max(1, Math.min(100, Math.floor(dh))) : 10); - } - 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() - : ''); - var balloonsPer = el('mega-virus-balloons-per-player') ? parseInt(String(el('mega-virus-balloons-per-player').value), 10) : 0; - if (Number.isNaN(balloonsPer) || balloonsPer < 0) balloonsPer = 0; - balloonsPer = Math.max(0, Math.min(12, balloonsPer)); - var dmgPerHit = el('mega-virus-damage-per-hit') ? parseInt(String(el('mega-virus-damage-per-hit').value), 10) : 10; - if (Number.isNaN(dmgPerHit) || dmgPerHit < 1) dmgPerHit = 10; - dmgPerHit = Math.max(1, Math.min(100, dmgPerHit)); - gameTimingFetch('GET') - .then(function (data) { - data.balloonBossMissionTimeSec = limSs; - data.balloonBossBossImageUrl = bossUrl; - data.balloonBossPlayerBalloonImageUrls = balloonUrls; - data.balloonBossPlayerBalloonFallbackUrl = fbUrl; - data.balloonBossBalloonsPerPlayer = balloonsPer; - data.balloonBossDamagePerHit = dmgPerHit; - 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 applyForcedMinigameKeysToForm(keys) { - var arr = Array.isArray(keys) ? keys : []; - for (var i = 1; i <= 3; i++) { - var sel = el('forced-mg-' + i); - if (sel) sel.value = arr[i - 1] || ''; - } - } - - function readForcedMinigameKeysFromForm() { - var out = []; - for (var i = 1; i <= 3; i++) { - var sel = el('forced-mg-' + i); - out.push(sel ? String(sel.value || '') : ''); - } - return out; - } - - function loadForcedMinigamePanel() { - gameTimingFetch('GET') - .then(function (data) { - applyForcedMinigameKeysToForm(data && data.forcedMinigameKeys); - setMsg('forced-minigame-msg', '', ''); - }) - .catch(function (e) { - setMsg('forced-minigame-msg', e.message || 'โหลดไม่ได้', 'error'); - }); - } - - function saveForcedMinigamePanel() { - var keys = readForcedMinigameKeysFromForm(); - var anyForced = keys.some(function (k) { return !!k; }); - var btn = el('btn-forced-minigame-save'); - if (btn) btn.disabled = true; - gameTimingFetch('GET') - .then(function (data) { - data.forcedMinigameKeys = keys; - return gameTimingFetch('PUT', data); - }) - .then(function (res) { - if (res && typeof res === 'object') applyForcedMinigameKeysToForm(res.forcedMinigameKeys); - setMsg('forced-minigame-msg', anyForced ? 'บันทึกแล้ว — เริ่มคดีใหม่เพื่อเทสต์การ์ดที่ตั้งไว้' : 'กลับเป็นปกติ (สุ่มทั้ง 3 ใบ) แล้ว', 'ok'); - }) - .catch(function (e) { - setMsg('forced-minigame-msg', e.message || 'บันทึกไม่ได้', 'error'); - }) - .then(function () { - if (btn) btn.disabled = false; - }); - } - - var SPECIAL_CARD_GAMES = [ - { id: 'mng8a80o', label: 'Minigame-1 จริงหรือไม่' }, - { id: 'mno9kb07', label: 'Minigame-2 วิ่งหลบสิ่งกีดขวาง' }, - { id: 'mnn93hpi', label: 'Minigame-3 Tower block' }, - { id: 'mnorwqx1', label: 'Minigame-4 คำศัพท์ในกระบวนการยุติธรรม' }, - { id: 'mnptfts2', label: 'Minigame-5 กระโดดขึ้นแท่น' }, - { id: 'mnpz6rkp', label: 'Minigame-6 ยิงอุกาบาต Violent Crime' }, - { id: 'mnq1eml7', label: 'Minigame-7 ยิง Mega Virus' } - ]; - var SPECIAL_CARD_OPTS = [ - { v: '', t: '— ไม่บังคับ —' }, - { v: '1', t: '1 · เพิ่มเวลามินิเกม +10s' }, - { v: '2', t: '2 · เพิ่มเวลาโหวต +10s' }, - { v: '3', t: '3 · แบนผู้เล่น 1 คน' }, - { v: '4', t: '4 · ปิดปากผู้เล่น 1 คน' }, - { v: '5', t: '5 · จับผิดตัว โหวตใหม่' }, - { v: '6', t: '6 · ทุกคน +10 COINS' }, - { v: '7', t: '7 · ทุกคนหลักฐานฟรี +1' } - ]; - var specialCardRowsBuilt = false; - function buildSpecialCardByMapRows() { - if (specialCardRowsBuilt) return; - var wrap = el('special-card-by-map-rows'); - if (!wrap) return; - wrap.innerHTML = ''; - SPECIAL_CARD_GAMES.forEach(function (g) { - var row = document.createElement('div'); - row.style.cssText = 'display:flex;align-items:center;gap:10px;flex-wrap:wrap;'; - var name = document.createElement('span'); - name.style.cssText = 'min-width:240px;color:#cfd6ea;font-size:14px;'; - name.textContent = g.label; - var sel = document.createElement('select'); - sel.id = 'tsc-' + g.id; - sel.style.cssText = 'padding:6px 8px;min-width:300px;font-size:14px;'; - SPECIAL_CARD_OPTS.forEach(function (o) { - var op = document.createElement('option'); - op.value = o.v; - op.textContent = o.t; - sel.appendChild(op); - }); - row.appendChild(name); - row.appendChild(sel); - wrap.appendChild(row); - }); - specialCardRowsBuilt = true; - } - function loadSpecialCardByMapPanel() { - buildSpecialCardByMapRows(); - gameTimingFetch('GET') - .then(function (data) { - var m = (data && data.testSpecialCardByMap) || {}; - SPECIAL_CARD_GAMES.forEach(function (g) { - var sel = el('tsc-' + g.id); - if (sel) sel.value = (m[g.id] != null && m[g.id] !== '') ? String(m[g.id]) : ''; - }); - setMsg('special-card-by-map-msg', '', ''); - }) - .catch(function (e) { setMsg('special-card-by-map-msg', e.message || 'โหลดไม่ได้', 'error'); }); - } - function saveSpecialCardByMapPanel() { - var map = {}; - SPECIAL_CARD_GAMES.forEach(function (g) { - var sel = el('tsc-' + g.id); - var v = sel ? parseInt(String(sel.value || ''), 10) : 0; - if (v >= 1 && v <= 7) map[g.id] = v; - }); - var btn = el('btn-special-card-by-map-save'); - if (btn) btn.disabled = true; - gameTimingFetch('GET') - .then(function (data) { - data.testSpecialCardByMap = map; - return gameTimingFetch('PUT', data); - }) - .then(function (res) { - if (res && res.testSpecialCardByMap) { - var m = res.testSpecialCardByMap; - SPECIAL_CARD_GAMES.forEach(function (g) { - var sel = el('tsc-' + g.id); - if (sel) sel.value = (m[g.id] != null) ? String(m[g.id]) : ''; - }); - } - var any = Object.keys(map).length > 0; - setMsg('special-card-by-map-msg', any ? 'บันทึกแล้ว — เกมที่ตั้งจะขึ้นคำถามพิเศษ + การ์ดนั้นแน่นอน' : 'ล้างการบังคับแล้ว (กลับเป็นสุ่มปกติ)', 'ok'); - }) - .catch(function (e) { setMsg('special-card-by-map-msg', e.message || 'บันทึกไม่ได้', 'error'); }) - .then(function () { if (btn) btn.disabled = false; }); - } - - function loadTroublesomeForcePanel() { - var chk = el('troublesome-force-offer'); - if (!chk) return; - gameTimingFetch('GET') - .then(function (data) { - chk.checked = !!(data && data.troublesomeForceOffer); - setMsg('troublesome-force-msg', '', ''); - }) - .catch(function (e) { setMsg('troublesome-force-msg', e.message || 'โหลดไม่ได้', 'error'); }); - } - function saveTroublesomeForcePanel() { - var chk = el('troublesome-force-offer'); - if (!chk) return; - var on = !!chk.checked; - var btn = el('btn-troublesome-force-save'); - if (btn) btn.disabled = true; - gameTimingFetch('GET') - .then(function (data) { - data.troublesomeForceOffer = on; - return gameTimingFetch('PUT', data); - }) - .then(function (res) { - if (res && typeof res === 'object') chk.checked = !!res.troublesomeForceOffer; - setMsg('troublesome-force-msg', on ? 'บันทึกแล้ว — บทตัวป่วนจะขึ้นเสมอ (เริ่มคดีใหม่เพื่อเทสต์)' : 'ปิดแล้ว — กลับเป็นปกติ (ขึ้นเฉพาะคนจริง ≥ 4)', 'ok'); - }) - .catch(function (e) { setMsg('troublesome-force-msg', e.message || 'บันทึกไม่ได้', 'error'); }) - .then(function () { if (btn) btn.disabled = false; }); - } - - function loadSpecialQuizIconExpirePanel() { - var inp = el('special-quiz-icon-expire-sec'); - if (!inp) return; - gameTimingFetch('GET') - .then(function (data) { - var v = (data && data.specialQuizIconExpireSec != null) ? Math.floor(Number(data.specialQuizIconExpireSec)) : 10; - if (!Number.isFinite(v) || v < 0) v = 10; - inp.value = String(Math.min(120, v)); - setMsg('special-quiz-icon-expire-msg', '', ''); - }) - .catch(function (e) { setMsg('special-quiz-icon-expire-msg', e.message || 'โหลดไม่ได้', 'error'); }); - } - function saveSpecialQuizIconExpirePanel() { - var inp = el('special-quiz-icon-expire-sec'); - if (!inp) return; - var v = Math.floor(Number(inp.value)); - if (!Number.isFinite(v) || v < 0) v = 10; - v = Math.min(120, v); - inp.value = String(v); - var btn = el('btn-special-quiz-icon-expire-save'); - if (btn) btn.disabled = true; - gameTimingFetch('GET') - .then(function (data) { - data.specialQuizIconExpireSec = v; - return gameTimingFetch('PUT', data); - }) - .then(function (res) { - if (res && res.specialQuizIconExpireSec != null) inp.value = String(res.specialQuizIconExpireSec); - setMsg('special-quiz-icon-expire-msg', v > 0 ? ('บันทึกแล้ว — ไอคอนจะหายใน ' + v + ' วินาที (เริ่มเกมใหม่เพื่อเทสต์)') : 'บันทึกแล้ว — ไอคอนจะไม่หาย (อยู่จนจบเกม)', 'ok'); - }) - .catch(function (e) { setMsg('special-quiz-icon-expire-msg', e.message || 'บันทึกไม่ได้', 'error'); }) - .then(function () { 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(1, 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.gauntletJumpHeightMult = (function () { var v = parseFloat(el('game-timing-jump-height') && el('game-timing-jump-height').value); if (Number.isNaN(v)) v = 0.52; return Math.max(0.2, Math.min(1.5, v)); })(); - data.gauntletTimeLimitSec = limS; - /* troublesomeOfferSec/RollMaxSec ย้ายไปแท็บ "บทตัวป่วน" — ไม่แตะที่นี่ (ค่าจาก GET ส่งต่อคงเดิม) */ - 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; - }); - var inApp = name === 'panel-app'; - el('top-actions').hidden = !inApp; - var stats = el('admin-sys-stats'); - if (stats) stats.hidden = !inApp; - var burger = el('btn-sidebar-toggle'); - if (burger) burger.hidden = !inApp; - if (inApp) startSystemStats(); - else stopSystemStats(); - } - - /* ===== ระบบ: ดึง CPU/RAM ของเซิร์ฟเวอร์มาแสดง (อัปเดตทุก 5 วิ) ===== */ - var __sysStatsTimer = null; - function fmtBytes(n) { - if (!(n > 0)) return '0'; - var u = ['B', 'KB', 'MB', 'GB', 'TB']; - var i = Math.floor(Math.log(n) / Math.log(1024)); - i = Math.max(0, Math.min(u.length - 1, i)); - return (n / Math.pow(1024, i)).toFixed(i >= 2 ? 1 : 0) + ' ' + u[i]; - } - function fmtUptime(sec) { - sec = Math.max(0, Math.floor(sec || 0)); - var d = Math.floor(sec / 86400); sec -= d * 86400; - var h = Math.floor(sec / 3600); sec -= h * 3600; - var m = Math.floor(sec / 60); - if (d > 0) return d + 'd ' + h + 'h'; - if (h > 0) return h + 'h ' + m + 'm'; - return m + 'm'; - } - function setBarTone(bar, pct) { - if (!bar) return; - bar.classList.remove('is-warn', 'is-crit'); - if (pct >= 85) bar.classList.add('is-crit'); - else if (pct >= 65) bar.classList.add('is-warn'); - } - function renderSystemStats(s) { - if (!s || !s.ok) return; - var cpuPct = (s.cpu && typeof s.cpu.percent === 'number') ? s.cpu.percent - : (s.cpu ? s.cpu.loadPercent : 0); - var cpuVal = el('sysstat-cpu-val'), cpuBar = el('sysstat-cpu-bar'); - if (cpuVal) cpuVal.textContent = cpuPct + '%'; - if (cpuBar) { cpuBar.style.width = cpuPct + '%'; setBarTone(cpuBar, cpuPct); } - var memPct = s.mem ? s.mem.usedPercent : 0; - var ramVal = el('sysstat-ram-val'), ramBar = el('sysstat-ram-bar'); - if (ramVal && s.mem) ramVal.textContent = memPct + '% ' + fmtBytes(s.mem.used) + '/' + fmtBytes(s.mem.total); - if (ramBar) { ramBar.style.width = memPct + '%'; setBarTone(ramBar, memPct); } - // HDD / ดิสก์ - var diskWrap = el('sysstat-disk-val') ? el('sysstat-disk-val').closest('.sysstat') : null; - if (s.disk) { - var dPct = s.disk.usedPercent; - var dVal = el('sysstat-disk-val'), dBar = el('sysstat-disk-bar'); - if (dVal) dVal.textContent = dPct + '% ' + fmtBytes(s.disk.used) + '/' + fmtBytes(s.disk.total); - if (dBar) { dBar.style.width = dPct + '%'; setBarTone(dBar, dPct); } - if (diskWrap) diskWrap.hidden = false; - } else if (diskWrap) { - diskWrap.hidden = true; // อ่านดิสก์ไม่ได้ (เช่น Node เก่า) → ซ่อน - } - // Node ที่ใช้อยู่ (RAM ของ process เกม) - if (s.node) { - var nPct = typeof s.node.rssPercent === 'number' ? s.node.rssPercent : 0; - var nVal = el('sysstat-node-val'), nBar = el('sysstat-node-bar'); - if (nVal) nVal.textContent = fmtBytes(s.node.rss) + (s.node.version ? ' ' + s.node.version : ''); - if (nBar) { nBar.style.width = Math.min(100, nPct) + '%'; setBarTone(nBar, nPct); } - } - var upVal = el('sysstat-up-val'); - if (upVal && s.uptime) upVal.textContent = fmtUptime(s.uptime.os); - var box = el('admin-sys-stats'); - if (box && s.cpu) box.title = 'CPU ' + cpuPct + '% (load ' + s.cpu.load1 + ', ' + s.cpu.cores + ' cores) · RAM ' - + memPct + '%' - + (s.disk ? ' · HDD ' + s.disk.usedPercent + '% (' + fmtBytes(s.disk.free) + ' ว่าง)' : '') - + (s.node ? ' · Node ' + fmtBytes(s.node.rss) + ' ' + (s.node.version || '') : '') - + ' · uptime ' + fmtUptime(s.uptime && s.uptime.os) + ' · ' + (s.hostname || ''); - } - function pollSystemStats() { - fetch('/Game/api/system-stats', { cache: 'no-store' }) - .then(function (r) { return r.ok ? r.json() : null; }) - .then(function (s) { if (s) renderSystemStats(s); }) - .catch(function () { /* เงียบไว้ ไม่รบกวน admin */ }); - } - function startSystemStats() { - if (__sysStatsTimer) return; - pollSystemStats(); - setTimeout(pollSystemStats, 1200); // ครั้งที่สองให้ได้ค่า CPU% (ครั้งแรก server คืน null) - __sysStatsTimer = setInterval(pollSystemStats, 5000); - } - function stopSystemStats() { - if (__sysStatsTimer) { clearInterval(__sysStatsTimer); __sysStatsTimer = null; } - } - - // ===== 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; })); - /* [2026-07-21] ตั้ง "รูปคดี" = สลับหน้ารายละเอียดไปโหมดประกอบเอง (กรอบ + รูป + ข้อความด้านล่าง) */ - grid.appendChild(caseMediaImagePicker('รูปคดี (โหมดใหม่ — ตั้งแล้วหน้าจะประกอบเอง)', 'case', c.pic || '', function (v) { c.pic = 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); - - /* [2026-07-21] ไอคอนผู้ต้องสงสัย (สี่เหลี่ยมเล็ก) — โชว์เฉพาะหัวหน้าไต่สวน (es3) เท่านั้น - ไม่ตั้ง = ใช้รูปเต็มตัวด้านบน */ - var icoHead = document.createElement('h3'); - icoHead.className = 'cm-section-title'; - icoHead.textContent = 'ไอคอนผู้ต้องสงสัย (3 คน) — หัวหน้าไต่สวน (ไม่ตั้ง = ใช้รูปเต็มตัวด้านบน)'; - root.appendChild(icoHead); - var icoGrid = document.createElement('div'); - icoGrid.className = 'cm-grid'; - if (!Array.isArray(c.icons)) c.icons = (c.suspects || []).slice(); - [0, 1, 2].forEach(function (si) { - icoGrid.appendChild(caseMediaImagePicker('ไอคอน ' + (si + 1), 'suspect-pick', c.icons[si], function (v) { c.icons[si] = v; })); - }); - root.appendChild(icoGrid); - - /* [2026-07-21] ข้อความหน้ารายละเอียดคดี (โหมดใหม่) — แสดงเมื่อตั้ง "รูปคดี" ไว้เท่านั้น - เว้นว่าง = ไม่แสดงบรรทัดนั้นบนหน้าเกม (ค่อยๆ กรอกทีละคดีได้) */ - var txtHead = document.createElement('h3'); - txtHead.className = 'cm-section-title'; - txtHead.textContent = 'ข้อความรายละเอียดคดี (โหมดใหม่ — ใช้เมื่อตั้ง "รูปคดี" แล้ว · เว้นว่าง = ไม่แสดงบรรทัดนั้น)'; - root.appendChild(txtHead); - if (!Array.isArray(c.txtSuspects)) c.txtSuspects = ['', '', '']; - var caseTextField = function (label, get, set, opts) { - opts = opts || {}; - var lb = document.createElement('label'); - lb.className = 'admin-field'; - lb.textContent = label; - var el = document.createElement(opts.multiline ? 'textarea' : 'input'); - if (!opts.multiline) el.type = 'text'; - else el.rows = 4; - el.maxLength = opts.max || 300; - el.value = get() || ''; - el.addEventListener('input', function () { set(el.value); }); - lb.appendChild(el); - return lb; - }; - root.appendChild(caseTextField('ข้อหา', function () { return c.txtCharge; }, function (v) { c.txtCharge = v; })); - root.appendChild(caseTextField('สถานที่เกิดเหตุ', function () { return c.txtPlace; }, function (v) { c.txtPlace = v; })); - root.appendChild(caseTextField('ผู้เสียหาย', function () { return c.txtVictim; }, function (v) { c.txtVictim = v; })); - [0, 1, 2].forEach(function (si) { - root.appendChild(caseTextField('ผู้ต้องสงสัย ' + (si + 1) + ' (ข้อความ)', - function () { return c.txtSuspects[si]; }, function (v) { c.txtSuspects[si] = v; }, { max: 240 })); - }); - root.appendChild(caseTextField('บันทึกเหตุการณ์', function () { return c.txtNote; }, function (v) { c.txtNote = v; }, { multiline: true, max: 1500 })); - - 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' }, - { tab: 'qb-map-editor', panel: 'tab-panel-qb-map-editor', fsBtn: 'btn-qb-map-editor-fullscreen' }, - { tab: 'ai-admin', panel: 'tab-panel-ai-admin', fsBtn: 'btn-ai-admin-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(); loadSpecialQuizIconExpirePanel(); } - 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(); - if (name === 'highscore') loadHighscore(); - if (name === 'achievements') loadAchievementsPanel(); - if (name === 'vote-timing') loadVoteTimingPanel(); - if (name === 'postcase') loadPostCasePanel(); - if (name === 'troublesome') loadTroublesomePanel(); - if (name === 'test-mode') { loadForcedMinigamePanel(); loadSpecialCardByMapPanel(); loadTroublesomeForcePanel(); } - if (name === 'sound') loadSoundPanel(); - if (name === 'qb-map-editor') ensureEmbedFrameSrc('admin-qb-map-editor-frame'); - if (name === 'ai-admin') ensureEmbedFrameSrc('admin-ai-admin-frame'); - } - - /* ฝัง iframe editor แบบ lazy — ตั้ง src จาก data-src ครั้งแรกที่เปิด tab (ไม่โหลดตอนเปิดหน้า Admin) */ - function ensureEmbedFrameSrc(id) { - var f = el(id); - if (f && !f.getAttribute('src') && f.getAttribute('data-src')) f.setAttribute('src', f.getAttribute('data-src')); - } - - 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); - }); - }); - - /* ===== Sidebar: ค้นหาเมนู / ยุบกลุ่ม / เปิด-ปิดบนมือถือ ===== */ - (function setupAdminSidebar() { - var shell = document.querySelector('.admin-shell'); - var nav = document.querySelector('.admin-tabs'); - if (!nav) return; - var seps = Array.prototype.slice.call(nav.querySelectorAll('.admin-tab-sep')); - var tabs = Array.prototype.slice.call(nav.querySelectorAll('.tab')); - - function isMobile() { return window.matchMedia('(max-width: 980px)').matches; } - function closeSidebar() { - if (shell) shell.classList.remove('sidebar-open'); - var b = el('btn-sidebar-toggle'); if (b) b.setAttribute('aria-expanded', 'false'); - var bd = el('admin-sidebar-backdrop'); if (bd) bd.hidden = true; - } - function openSidebar() { - if (shell) shell.classList.add('sidebar-open'); - var b = el('btn-sidebar-toggle'); if (b) b.setAttribute('aria-expanded', 'true'); - var bd = el('admin-sidebar-backdrop'); if (bd) bd.hidden = false; - } - var burger = el('btn-sidebar-toggle'); - if (burger) burger.addEventListener('click', function () { - if (shell && shell.classList.contains('sidebar-open')) closeSidebar(); else openSidebar(); - }); - var backdrop = el('admin-sidebar-backdrop'); - if (backdrop) backdrop.addEventListener('click', closeSidebar); - nav.addEventListener('click', function (e) { - var t = e.target && e.target.closest ? e.target.closest('.tab') : null; - if (t && isMobile()) closeSidebar(); - }); - - /* ยุบ/ขยายกลุ่ม: ซ่อนแท็บที่อยู่หลัง separator จนถึง separator ถัดไป */ - function applyGroupVisibility() { - seps.forEach(function (sep) { - var collapsed = sep.classList.contains('is-collapsed'); - var n = sep.nextElementSibling; - while (n && !n.classList.contains('admin-tab-sep')) { - if (n.classList && n.classList.contains('tab')) n.classList.toggle('is-group-hidden', collapsed); - n = n.nextElementSibling; - } - }); - } - function toggleGroup(sep) { - sep.classList.toggle('is-collapsed'); - applyGroupVisibility(); - } - seps.forEach(function (sep) { - sep.setAttribute('role', 'button'); - sep.setAttribute('tabindex', '0'); - sep.addEventListener('click', function () { toggleGroup(sep); }); - sep.addEventListener('keydown', function (e) { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleGroup(sep); } - }); - }); - - /* ค้นหาเมนู */ - var search = el('admin-nav-search-input'); - var emptyMsg = el('admin-nav-empty'); - if (search) search.addEventListener('input', function () { - var q = search.value.trim().toLowerCase(); - if (q) { - var anyVisible = false; - tabs.forEach(function (t) { - t.classList.remove('is-group-hidden'); - var hit = (t.textContent || '').toLowerCase().indexOf(q) >= 0; - t.classList.toggle('is-search-hidden', !hit); - if (hit) anyVisible = true; - }); - seps.forEach(function (s) { s.style.display = 'none'; }); - if (emptyMsg) emptyMsg.hidden = anyVisible; - } else { - tabs.forEach(function (t) { t.classList.remove('is-search-hidden'); }); - seps.forEach(function (s) { s.style.display = ''; }); - applyGroupVisibility(); - if (emptyMsg) emptyMsg.hidden = true; - } - }); - })(); - - 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'); - bindEmbedPanelFullscreen('btn-qb-map-editor-fullscreen', 'tab-panel-qb-map-editor'); - bindEmbedPanelFullscreen('btn-ai-admin-fullscreen', 'tab-panel-ai-admin'); - - /* 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(); - }); - } - var forcedMgSaveBtn = el('btn-forced-minigame-save'); - if (forcedMgSaveBtn) forcedMgSaveBtn.addEventListener('click', saveForcedMinigamePanel); - var specialCardSaveBtn = el('btn-special-card-by-map-save'); - if (specialCardSaveBtn) specialCardSaveBtn.addEventListener('click', saveSpecialCardByMapPanel); - var troublesomeForceSaveBtn = el('btn-troublesome-force-save'); - if (troublesomeForceSaveBtn) troublesomeForceSaveBtn.addEventListener('click', saveTroublesomeForcePanel); - var sqIconExpireSaveBtn = el('btn-special-quiz-icon-expire-save'); - if (sqIconExpireSaveBtn) sqIconExpireSaveBtn.addEventListener('click', saveSpecialQuizIconExpirePanel); - 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', '', ''); - }); - } - - /* ===== กระดานผู้นำ (High Score) ===== */ - function renderHighscore(rows) { - var tbEl = el('table-highscore'); - if (!tbEl) return; - var tb = tbEl.querySelector('tbody'); - tb.innerHTML = ''; - (rows || []).forEach(function (r) { - var rank = parseInt(r.rank, 10) || 0; - var score = Math.max(0, parseInt(r.score, 10) || 0); - var coins = Math.max(0, parseInt(r.coins, 10) || 0); - var medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : ('#' + rank); - var tr = document.createElement('tr'); - tr.innerHTML = - '' + medal + '' + - '' + escapeHtml(r.name || 'ผู้เล่น') + (r.blocked ? ' บล็อก' : '') + '' + - '' + - '' + - '' + - '' + - '' + coins + '' + - ''; - tb.appendChild(tr); - }); - tb.querySelectorAll('.btn-score-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-score') : null; - var score = Math.max(0, parseInt(inp && inp.value, 10) || 0); - api('accounts.php', { method: 'PATCH', body: { id: id, score: score } }) - .then(function () { setMsg('highscore-msg', 'บันทึกคะแนนแล้ว', 'ok'); return loadHighscore(); }) - .catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); - }); - }); - tb.querySelectorAll('.btn-score-reset').forEach(function (b) { - b.addEventListener('click', function () { - var id = b.getAttribute('data-id'); - if (!confirm('รีเซ็ตคะแนนคนนี้เป็น 0?')) return; - api('leaderboard-admin.php', { method: 'POST', body: { action: 'reset', id: id } }) - .then(function () { return loadHighscore(); }) - .catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); - }); - }); - } - - function loadHighscore() { - return api('leaderboard-admin.php').then(function (r) { - renderHighscore(r.rows); - var n = (r.rows && r.rows.length) || 0; - setMsg('highscore-msg', n ? ('ผู้เล่นมีคะแนน ' + r.total + ' คน') : 'ยังไม่มีผู้เล่นทำคะแนน', ''); - }).catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); - } - - (function bindHighscoreControls() { - var refresh = el('btn-highscore-refresh'); - if (refresh) refresh.addEventListener('click', function () { loadHighscore(); }); - var resetAll = el('btn-highscore-reset-all'); - if (resetAll) resetAll.addEventListener('click', function () { - if (!confirm('รีเซ็ตคะแนนสะสมของผู้เล่นทุกคนเป็น 0? (ย้อนกลับไม่ได้)')) return; - api('leaderboard-admin.php', { method: 'POST', body: { action: 'resetAll' } }) - .then(function (r) { setMsg('highscore-msg', 'รีเซ็ตแล้ว ' + (r.reset || 0) + ' คน', 'ok'); return loadHighscore(); }) - .catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); - }); - })(); - - /* ===== เวลาโหวต (vote timing) ===== */ - function loadVoteTimingPanel() { - gameTimingFetch('GET') - .then(function (data) { - var pk = (data && data.pickVoteSec != null) ? Number(data.pickVoteSec) : 25; - var tr = (data && data.trialVoteSec != null) ? Number(data.trialVoteSec) : 30; - var rv = (data && data.trialResultRevealMs != null) ? Number(data.trialResultRevealMs) : 7000; - if (el('vote-timing-pick')) el('vote-timing-pick').value = String(Number.isFinite(pk) ? pk : 25); - if (el('vote-timing-trial')) el('vote-timing-trial').value = String(Number.isFinite(tr) ? tr : 30); - if (el('vote-timing-reveal')) el('vote-timing-reveal').value = String(Number.isFinite(rv) ? rv : 7000); - setMsg('vote-timing-msg', '', ''); - }) - .catch(function (e) { setMsg('vote-timing-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); }); - } - - (function bindVoteTiming() { - var btn = el('btn-vote-timing-save'); - if (!btn) return; - btn.addEventListener('click', function () { - var pk = Math.max(5, Math.min(120, parseInt((el('vote-timing-pick') || {}).value, 10) || 25)); - var tr = Math.max(5, Math.min(180, parseInt((el('vote-timing-trial') || {}).value, 10) || 30)); - var rv = Math.max(1500, Math.min(20000, parseInt((el('vote-timing-reveal') || {}).value, 10) || 7000)); - btn.disabled = true; - gameTimingFetch('GET') - .then(function (data) { - data.pickVoteSec = pk; - data.trialVoteSec = tr; - data.trialResultRevealMs = rv; - return gameTimingFetch('PUT', data); - }) - .then(function () { return gameTimingFetch('GET'); }) - .then(function (fresh) { - var p = (fresh && fresh.pickVoteSec != null) ? fresh.pickVoteSec : pk; - var t = (fresh && fresh.trialVoteSec != null) ? fresh.trialVoteSec : tr; - var r = (fresh && fresh.trialResultRevealMs != null) ? fresh.trialResultRevealMs : rv; - if (el('vote-timing-pick')) el('vote-timing-pick').value = String(p); - if (el('vote-timing-trial')) el('vote-timing-trial').value = String(t); - if (el('vote-timing-reveal')) el('vote-timing-reveal').value = String(r); - setMsg('vote-timing-msg', 'บันทึกแล้ว — โหวตเลือกผู้เล่น ' + p + 's · ชี้ตัวคนร้าย ' + t + 's · โชว์ผล ' + Math.round(r/1000) + 's · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok'); - }) - .catch(function (e) { setMsg('vote-timing-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); }) - .then(function () { btn.disabled = false; }); - }); - })(); - - /* ===== จบคดี (post-case destination) — Main-Lobby หรือ LobbyA ===== */ - function loadPostCasePanel() { - gameTimingFetch('GET') - .then(function (data) { - var d = (data && data.postCaseDestination === 'lobbya') ? 'lobbya' : 'mainlobby'; - if (el('postcase-dest')) el('postcase-dest').value = d; - setMsg('postcase-msg', '', ''); - }) - .catch(function (e) { setMsg('postcase-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); }); - } - - (function bindPostCase() { - var btn = el('btn-postcase-save'); - if (!btn) return; - btn.addEventListener('click', function () { - var d = ((el('postcase-dest') || {}).value === 'lobbya') ? 'lobbya' : 'mainlobby'; - btn.disabled = true; - gameTimingFetch('GET') - .then(function (data) { data.postCaseDestination = d; return gameTimingFetch('PUT', data); }) - .then(function () { return gameTimingFetch('GET'); }) - .then(function (fresh) { - var v = (fresh && fresh.postCaseDestination === 'lobbya') ? 'lobbya' : 'mainlobby'; - if (el('postcase-dest')) el('postcase-dest').value = v; - setMsg('postcase-msg', 'บันทึกแล้ว — เมื่อจบคดีไป ' + (v === 'lobbya' ? 'LobbyA (ห้องเดิม เริ่มคดีใหม่)' : 'Main-Lobby (เดิม)') + ' · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok'); - }) - .catch(function (e) { setMsg('postcase-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); }) - .then(function () { btn.disabled = false; }); - }); - })(); - - /* ===== เสียง (sound settings) — master/music/sfx volume 0..1 ===== */ - function soundSettingsFetch(method, body) { - var m = method || 'GET'; - var url = SOUND_SETTINGS_API; - if (m === 'GET' && body == null) url += (url.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) { - var plain = (text || r.statusText || 'Request failed').trim(); - if (r.status === 404) plain += ' — รีสตาร์ท Node (Game/server.js) หลัง deploy'; - throw new Error(plain); - } - if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status + ' ' + r.statusText)); - return j; - }); - }); - } - function pctFromVol(v, d) { var n = Number(v); return Math.round((Number.isFinite(n) ? n : d) * 100); } - function loadSoundPanel() { - soundSettingsFetch('GET') - .then(function (d) { - var setR = function (id, pct) { var e = el(id); if (e) { e.value = String(pct); var lbl = el(id + '-val'); if (lbl) lbl.textContent = pct + '%'; } }; - setR('sound-master', pctFromVol(d && d.masterVol, 1.0)); - setR('sound-music', pctFromVol(d && d.musicVol, 0.35)); - setR('sound-sfx', pctFromVol(d && d.sfxVol, 0.75)); - setMsg('sound-msg', '', ''); - }) - .catch(function (e) { setMsg('sound-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/sound-settings', 'error'); }); - } - (function bindSound() { - var btn = el('btn-sound-save'); - if (!btn) return; - btn.addEventListener('click', function () { - var rd = function (id, d) { var v = parseInt((el(id) || {}).value, 10); return Math.max(0, Math.min(100, Number.isFinite(v) ? v : d)) / 100; }; - var body = { masterVol: rd('sound-master', 100), musicVol: rd('sound-music', 35), sfxVol: rd('sound-sfx', 75) }; - btn.disabled = true; - soundSettingsFetch('PUT', body) - .then(function () { return soundSettingsFetch('GET'); }) - .then(function (fresh) { - loadSoundPanel(); - setMsg('sound-msg', 'บันทึกแล้ว — Master ' + pctFromVol(fresh.masterVol, 1) + '% · Music ' + pctFromVol(fresh.musicVol, .35) + '% · SFX ' + pctFromVol(fresh.sfxVol, .75) + '% · ผู้เล่นต้องรีเฟรช', 'ok'); - }) - .catch(function (e) { setMsg('sound-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/sound-settings', 'error'); }) - .then(function () { btn.disabled = false; }); - }); - })(); - - /* ===== บทตัวป่วน (แยกจากหมวด Minigame-2) — troublesomeRollMaxSec + troublesomeOfferSec ===== */ - function loadTroublesomePanel() { - gameTimingFetch('GET') - .then(function (data) { - var rs = (data && data.troublesomeRollMaxSec != null) ? Number(data.troublesomeRollMaxSec) : 8; - var os = (data && data.troublesomeOfferSec != null) ? Number(data.troublesomeOfferSec) : 15; - if (el('troublesome-roll-sec')) el('troublesome-roll-sec').value = String(Number.isFinite(rs) ? rs : 8); - if (el('troublesome-offer-sec')) el('troublesome-offer-sec').value = String(Number.isFinite(os) ? os : 15); - setMsg('troublesome-msg', '', ''); - }) - .catch(function (e) { setMsg('troublesome-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); }); - } - - (function bindTroublesome() { - var btn = el('btn-troublesome-save'); - if (!btn) return; - btn.addEventListener('click', function () { - var rs = Math.max(1, Math.min(30, parseInt((el('troublesome-roll-sec') || {}).value, 10) || 8)); - var os = Math.max(5, Math.min(120, parseInt((el('troublesome-offer-sec') || {}).value, 10) || 15)); - btn.disabled = true; - gameTimingFetch('GET') - .then(function (data) { - data.troublesomeRollMaxSec = rs; - data.troublesomeOfferSec = os; - return gameTimingFetch('PUT', data); - }) - .then(function () { return gameTimingFetch('GET'); }) - .then(function (fresh) { - var r = (fresh && fresh.troublesomeRollMaxSec != null) ? fresh.troublesomeRollMaxSec : rs; - var o = (fresh && fresh.troublesomeOfferSec != null) ? fresh.troublesomeOfferSec : os; - if (el('troublesome-roll-sec')) el('troublesome-roll-sec').value = String(r); - if (el('troublesome-offer-sec')) el('troublesome-offer-sec').value = String(o); - setMsg('troublesome-msg', 'บันทึกแล้ว — เวลาเสนอ ' + r + 's · เวลารับ ' + o + 's · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok'); - }) - .catch(function (e) { setMsg('troublesome-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); }) - .then(function () { btn.disabled = false; }); - }); - })(); - - /* ===== Achievements admin ===== */ - var achvCatalog = []; - var achvCatalogById = {}; - var achvCurrentPlayer = null; - - function achvGroupLabel(g) { - var names = { 1: '1 · Analyst', 2: '2 · Explorer', 3: '3 · Risk', 4: '4 · Support', 5: '5 · Elite' }; - return names[g] || String(g); - } - - function renderAchvCatalog() { - var tb = el('table-achv-catalog'); - if (!tb) return; - tb = tb.querySelector('tbody'); - tb.innerHTML = ''; - achvCatalog.slice().sort(function (a, b) { return (a.g - b.g) || 0; }).forEach(function (a) { - var tr = document.createElement('tr'); - tr.setAttribute('data-achv-id', a.id); - - var tdG = document.createElement('td'); - var sel = document.createElement('select'); - sel.className = 'achv-f-g'; - for (var g = 1; g <= 5; g++) { - var op = document.createElement('option'); - op.value = String(g); op.textContent = achvGroupLabel(g); - if (g === (parseInt(a.g, 10) || 1)) op.selected = true; - sel.appendChild(op); - } - tdG.appendChild(sel); - - var tdT = document.createElement('td'); - var inT = document.createElement('input'); - inT.type = 'text'; inT.className = 'achv-f-title'; inT.value = a.title || ''; inT.style.width = '100%'; - tdT.appendChild(inT); - - var tdD = document.createElement('td'); - var inD = document.createElement('input'); - inD.type = 'text'; inD.className = 'achv-f-desc'; inD.value = a.desc || ''; inD.style.width = '100%'; - tdD.appendChild(inD); - - var tdTar = document.createElement('td'); - var inTar = document.createElement('input'); - inTar.type = 'number'; inTar.min = '1'; inTar.step = '1'; inTar.className = 'achv-f-target'; - inTar.value = String(Math.max(1, parseInt(a.target, 10) || 1)); inTar.style.width = '72px'; - tdTar.appendChild(inTar); - - var tdE = document.createElement('td'); - var inE = document.createElement('input'); - inE.type = 'checkbox'; inE.className = 'achv-f-enabled'; inE.checked = a.enabled !== false; - tdE.appendChild(inE); - - tr.appendChild(tdG); tr.appendChild(tdT); tr.appendChild(tdD); tr.appendChild(tdTar); tr.appendChild(tdE); - tb.appendChild(tr); - }); - } - - function collectAchvCatalogFromTable() { - var rows = document.querySelectorAll('#table-achv-catalog tbody tr'); - var out = []; - rows.forEach(function (tr) { - out.push({ - id: tr.getAttribute('data-achv-id'), - g: parseInt(tr.querySelector('.achv-f-g').value, 10) || 1, - title: tr.querySelector('.achv-f-title').value, - desc: tr.querySelector('.achv-f-desc').value, - target: Math.max(1, parseInt(tr.querySelector('.achv-f-target').value, 10) || 1), - enabled: tr.querySelector('.achv-f-enabled').checked, - }); - }); - return out; - } - - function loadAchvCatalog() { - return api('achievements.php?action=catalog').then(function (r) { - achvCatalog = (r && r.catalog) || []; - achvCatalogById = {}; - achvCatalog.forEach(function (a) { achvCatalogById[a.id] = a; }); - renderAchvCatalog(); - setMsg('achv-catalog-msg', 'โหลดแคตตาล็อก ' + achvCatalog.length + ' รายการ', ''); - }).catch(function (err) { setMsg('achv-catalog-msg', err.message, 'error'); }); - } - - function renderAchvPlayers(rows) { - var tb = el('table-achv-players'); - if (!tb) return; - tb = tb.querySelector('tbody'); - tb.innerHTML = ''; - (rows || []).forEach(function (p) { - var tr = document.createElement('tr'); - var c1 = document.createElement('td'); c1.textContent = p.displayName || 'Guest'; - var c2 = document.createElement('td'); c2.textContent = p.playerKey; c2.style.fontSize = '12px'; c2.style.opacity = '0.8'; - var c3 = document.createElement('td'); c3.textContent = (p.unlocked || 0) + ' / ' + (p.total || 0); - var c4 = document.createElement('td'); c4.textContent = String(p.coins || 0); - var c5 = document.createElement('td'); - var btn = document.createElement('button'); - btn.type = 'button'; btn.className = 'btn btn-ghost'; btn.textContent = 'จัดการ'; - btn.addEventListener('click', function () { openAchvPlayerDetail(p.playerKey, p.displayName); }); - c5.appendChild(btn); - tr.appendChild(c1); tr.appendChild(c2); tr.appendChild(c3); tr.appendChild(c4); tr.appendChild(c5); - tb.appendChild(tr); - }); - } - - function loadAchvPlayers() { - var q = (el('achv-player-search') && el('achv-player-search').value || '').trim().toLowerCase(); - return api('achievements.php?action=players').then(function (r) { - var rows = (r && r.players) || []; - if (q) { - rows = rows.filter(function (p) { - return (p.displayName || '').toLowerCase().indexOf(q) >= 0 || (p.playerKey || '').toLowerCase().indexOf(q) >= 0; - }); - } - renderAchvPlayers(rows); - setMsg('achv-players-msg', 'ผู้เล่น ' + rows.length + ' คน', ''); - }).catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); - } - - function openAchvPlayerDetail(key, name) { - achvCurrentPlayer = key; - var box = el('achv-player-detail'); - if (box) box.hidden = false; - if (el('achv-player-detail-name')) el('achv-player-detail-name').textContent = (name || 'Guest') + ' (' + key + ')'; - api('achievements.php?action=player&playerKey=' + encodeURIComponent(key)).then(function (r) { - var prog = (r && r.progress) || {}; - var tb = el('table-achv-player-detail').querySelector('tbody'); - tb.innerHTML = ''; - achvCatalog.slice().sort(function (a, b) { return (a.g - b.g) || 0; }).forEach(function (a) { - var target = Math.max(1, parseInt(a.target, 10) || 1); - var cur = Math.max(0, parseInt(prog[a.id], 10) || 0); - var tr = document.createElement('tr'); - - var c1 = document.createElement('td'); - c1.innerHTML = '' + achvGroupLabel(a.g).charAt(0) + ' · ' + (a.title || a.id); - var c2 = document.createElement('td'); - var inp = document.createElement('input'); - inp.type = 'number'; inp.min = '0'; inp.max = String(target); inp.step = '1'; inp.value = String(Math.min(cur, target)); - inp.style.width = '70px'; - c2.appendChild(inp); - c2.appendChild(document.createTextNode(' / ' + target + (cur >= target ? ' ✔' : ''))); - - var c3 = document.createElement('td'); - var setBtn = document.createElement('button'); - setBtn.type = 'button'; setBtn.className = 'btn btn-ghost'; setBtn.textContent = 'ตั้งค่า'; - setBtn.style.marginRight = '6px'; - setBtn.addEventListener('click', function () { - api('achievements.php', { method: 'POST', body: { action: 'setProgress', playerKey: key, id: a.id, value: parseInt(inp.value, 10) || 0 } }) - .then(function () { setMsg('achv-players-msg', 'บันทึก ' + a.id, 'ok'); openAchvPlayerDetail(key, name); }) - .catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); - }); - var unBtn = document.createElement('button'); - unBtn.type = 'button'; unBtn.className = 'btn btn-primary'; unBtn.textContent = 'ปลดล็อก'; - unBtn.addEventListener('click', function () { - api('achievements.php', { method: 'POST', body: { action: 'unlock', playerKey: key, id: a.id } }) - .then(function () { setMsg('achv-players-msg', 'ปลดล็อก ' + a.id, 'ok'); openAchvPlayerDetail(key, name); }) - .catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); - }); - c3.appendChild(setBtn); c3.appendChild(unBtn); - - tr.appendChild(c1); tr.appendChild(c2); tr.appendChild(c3); - tb.appendChild(tr); - }); - }).catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); - } - - function showAchvSubtab(which) { - var isCat = which !== 'players'; - if (el('achv-panel-catalog')) el('achv-panel-catalog').hidden = !isCat; - if (el('achv-panel-players')) el('achv-panel-players').hidden = isCat; - if (el('achv-subtab-catalog')) el('achv-subtab-catalog').classList.toggle('is-active', isCat); - if (el('achv-subtab-players')) el('achv-subtab-players').classList.toggle('is-active', !isCat); - if (!isCat) loadAchvPlayers(); - } - - var achvPanelInited = false; - function loadAchievementsPanel() { - if (!achvPanelInited) { - achvPanelInited = true; - var sc = el('achv-subtab-catalog'); if (sc) sc.addEventListener('click', function () { showAchvSubtab('catalog'); }); - var sp = el('achv-subtab-players'); if (sp) sp.addEventListener('click', function () { showAchvSubtab('players'); }); - var save = el('btn-achv-save'); - if (save) save.addEventListener('click', function () { - api('achievements.php', { method: 'POST', body: { action: 'saveCatalog', catalog: collectAchvCatalogFromTable() } }) - .then(function (r) { - achvCatalog = (r && r.catalog) || achvCatalog; - achvCatalogById = {}; achvCatalog.forEach(function (a) { achvCatalogById[a.id] = a; }); - renderAchvCatalog(); - setMsg('achv-catalog-msg', 'บันทึกแคตตาล็อกแล้ว', 'ok'); - }) - .catch(function (err) { setMsg('achv-catalog-msg', err.message, 'error'); }); - }); - var reload = el('btn-achv-reload'); - if (reload) reload.addEventListener('click', function () { loadAchvCatalog(); }); - var resetDef = el('btn-achv-reset-default'); - if (resetDef) resetDef.addEventListener('click', function () { - if (!confirm('คืนค่าแคตตาล็อกกลับเป็นค่าเริ่มต้น 25 รายการ? (ค่าที่แก้ไว้จะหาย)')) return; - api('achievements.php', { method: 'POST', body: { action: 'resetCatalog' } }) - .then(function () { setMsg('achv-catalog-msg', 'คืนค่าเริ่มต้นแล้ว', 'ok'); return loadAchvCatalog(); }) - .catch(function (err) { setMsg('achv-catalog-msg', err.message, 'error'); }); - }); - var pr = el('btn-achv-players-refresh'); if (pr) pr.addEventListener('click', function () { loadAchvPlayers(); }); - var ps = el('achv-player-search'); if (ps) ps.addEventListener('input', function () { loadAchvPlayers(); }); - var rpb = el('btn-achv-player-reset'); - if (rpb) rpb.addEventListener('click', function () { - if (!achvCurrentPlayer) return; - if (!confirm('รีเซ็ต achievement ทั้งหมดของผู้เล่นนี้?')) return; - api('achievements.php', { method: 'POST', body: { action: 'resetPlayer', playerKey: achvCurrentPlayer } }) - .then(function () { setMsg('achv-players-msg', 'รีเซ็ตแล้ว', 'ok'); openAchvPlayerDetail(achvCurrentPlayer, el('achv-player-detail-name') ? el('achv-player-detail-name').textContent : ''); loadAchvPlayers(); }) - .catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); - }); - } - return loadAchvCatalog(); - } - - 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, 'vote-timing': true, troublesome: 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); - }); -})(); +(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 SOUND_SETTINGS_API = '/Game/api/sound-settings'; + 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: '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) { + 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'; + } + + /* [2026-07-27] ชื่อหมวด (topicN) = "ห้อง N — <หัวข้อห้องจาก Admin>" — เดิม hardcode ไม่เปลี่ยนตามที่แก้หัวข้อ + ดึง title จาก quiz-battle-modes.json (แหล่งเดียวกับตารางเปิด/ปิดโหมด) แล้วอัปเดต label ก่อน render */ + function refreshQbBattleCategoryLabels(done) { + api('quiz-battle-modes.php').then(function (j) { + var modes = (j && j.modes) || {}; + for (var i = 0; i < QUIZ_BATTLE_CATEGORIES.length; i++) { + var n = i + 1; + var m = modes[String(n)]; + var t = m && typeof m.title === 'string' ? m.title.trim() : ''; + QUIZ_BATTLE_CATEGORIES[i].labelTh = 'ห้อง ' + n + (t ? ' — ' + t : ''); + } + }).catch(function () { /* อ่าน config ไม่ได้ → คง label เดิม */ }) + .then(function () { if (typeof done === 'function') done(); }); + } + + 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 : 'topic1', + 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('topic1'); + 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 || 'topic1'); + + 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 || 'topic1') === 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) { + /* อัปเดตชื่อหมวดจากหัวข้อห้องล่าสุดก่อน แล้วค่อย render (dropdown กรอง + select ต่อแถว + preview ตรงกัน) */ + refreshQbBattleCategoryLabels(function () { + 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 <= 8; si++) { + var nin = el('stack-block-normal-url-' + si); + var hin = el('stack-block-heavy-url-' + si); + var nv = normArr[si - 1] != null && String(normArr[si - 1]) !== '' ? String(normArr[si - 1]) : STACK_BLOCK_DEFAULT_NORMAL[si - 1]; + var hv = heavyArr[si - 1] != null && String(heavyArr[si - 1]) !== '' ? String(heavyArr[si - 1]) : STACK_BLOCK_DEFAULT_HEAVY[si - 1]; + if (nin) nin.value = nv || ''; + if (hin) hin.value = hv || ''; + updateStackBlockSeatPreview(si); + } + } + /* #4 MG3 8 สีตาม lobby theme — ค่าเริ่มต้น Block-Code-1..8 (ตรง PLAY_LOBBY_THEME_HEX) */ + var STACK_BLOCK_THEME_NAMES = ['1-red', '2-orange', '3-Yellow', '4-Green', '5-Blue', '6-Navy', '7-Violet', '8-Pink']; + var STACK_BLOCK_DEFAULT_NORMAL = STACK_BLOCK_THEME_NAMES.map(function (s) { return '/Game/img/TowerBlock/Block-Code-' + s + '.png'; }); + var STACK_BLOCK_DEFAULT_HEAVY = STACK_BLOCK_THEME_NAMES.map(function (s) { return '/Game/img/TowerBlock/Block-Code-' + s + '-2.png'; }); + + function readStackBlockNormalUrlsFromForm() { + var out = []; + for (var i = 1; i <= 8; 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 <= 8; 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 <= 8; 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); + if (el('game-timing-jump-height')) el('game-timing-jump-height').value = String(data.gauntletJumpHeightMult != null ? data.gauntletJumpHeightMult : 0.52); + el('game-timing-limit-sec').value = String(data.gauntletTimeLimitSec != null ? data.gauntletTimeLimitSec : 0); + /* troublesomeOfferSec/RollMaxSec ย้ายไปแท็บ "บทตัวป่วน" แล้ว */ + 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 inpBp = el('mega-virus-balloons-per-player'); + if (inpBp && Object.prototype.hasOwnProperty.call(data, 'balloonBossBalloonsPerPlayer')) { + var bp = Number(data.balloonBossBalloonsPerPlayer); + inpBp.value = String(Number.isFinite(bp) && bp > 0 ? Math.min(12, Math.floor(bp)) : 0); + } + var inpDh = el('mega-virus-damage-per-hit'); + if (inpDh && Object.prototype.hasOwnProperty.call(data, 'balloonBossDamagePerHit')) { + var dh = Number(data.balloonBossDamagePerHit); + inpDh.value = String(Number.isFinite(dh) && dh > 0 ? Math.max(1, Math.min(100, Math.floor(dh))) : 10); + } + 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() + : ''); + var balloonsPer = el('mega-virus-balloons-per-player') ? parseInt(String(el('mega-virus-balloons-per-player').value), 10) : 0; + if (Number.isNaN(balloonsPer) || balloonsPer < 0) balloonsPer = 0; + balloonsPer = Math.max(0, Math.min(12, balloonsPer)); + var dmgPerHit = el('mega-virus-damage-per-hit') ? parseInt(String(el('mega-virus-damage-per-hit').value), 10) : 10; + if (Number.isNaN(dmgPerHit) || dmgPerHit < 1) dmgPerHit = 10; + dmgPerHit = Math.max(1, Math.min(100, dmgPerHit)); + gameTimingFetch('GET') + .then(function (data) { + data.balloonBossMissionTimeSec = limSs; + data.balloonBossBossImageUrl = bossUrl; + data.balloonBossPlayerBalloonImageUrls = balloonUrls; + data.balloonBossPlayerBalloonFallbackUrl = fbUrl; + data.balloonBossBalloonsPerPlayer = balloonsPer; + data.balloonBossDamagePerHit = dmgPerHit; + 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 applyForcedMinigameKeysToForm(keys) { + var arr = Array.isArray(keys) ? keys : []; + for (var i = 1; i <= 3; i++) { + var sel = el('forced-mg-' + i); + if (sel) sel.value = arr[i - 1] || ''; + } + } + + function readForcedMinigameKeysFromForm() { + var out = []; + for (var i = 1; i <= 3; i++) { + var sel = el('forced-mg-' + i); + out.push(sel ? String(sel.value || '') : ''); + } + return out; + } + + function loadForcedMinigamePanel() { + gameTimingFetch('GET') + .then(function (data) { + applyForcedMinigameKeysToForm(data && data.forcedMinigameKeys); + setMsg('forced-minigame-msg', '', ''); + }) + .catch(function (e) { + setMsg('forced-minigame-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + } + + function saveForcedMinigamePanel() { + var keys = readForcedMinigameKeysFromForm(); + var anyForced = keys.some(function (k) { return !!k; }); + var btn = el('btn-forced-minigame-save'); + if (btn) btn.disabled = true; + gameTimingFetch('GET') + .then(function (data) { + data.forcedMinigameKeys = keys; + return gameTimingFetch('PUT', data); + }) + .then(function (res) { + if (res && typeof res === 'object') applyForcedMinigameKeysToForm(res.forcedMinigameKeys); + setMsg('forced-minigame-msg', anyForced ? 'บันทึกแล้ว — เริ่มคดีใหม่เพื่อเทสต์การ์ดที่ตั้งไว้' : 'กลับเป็นปกติ (สุ่มทั้ง 3 ใบ) แล้ว', 'ok'); + }) + .catch(function (e) { + setMsg('forced-minigame-msg', e.message || 'บันทึกไม่ได้', 'error'); + }) + .then(function () { + if (btn) btn.disabled = false; + }); + } + + var SPECIAL_CARD_GAMES = [ + { id: 'mng8a80o', label: 'Minigame-1 จริงหรือไม่' }, + { id: 'mno9kb07', label: 'Minigame-2 วิ่งหลบสิ่งกีดขวาง' }, + { id: 'mnn93hpi', label: 'Minigame-3 Tower block' }, + { id: 'mnorwqx1', label: 'Minigame-4 คำศัพท์ในกระบวนการยุติธรรม' }, + { id: 'mnptfts2', label: 'Minigame-5 กระโดดขึ้นแท่น' }, + { id: 'mnpz6rkp', label: 'Minigame-6 ยิงอุกาบาต Violent Crime' }, + { id: 'mnq1eml7', label: 'Minigame-7 ยิง Mega Virus' } + ]; + var SPECIAL_CARD_OPTS = [ + { v: '', t: '— ไม่บังคับ —' }, + { v: '1', t: '1 · เพิ่มเวลามินิเกม +10s' }, + { v: '2', t: '2 · เพิ่มเวลาโหวต +10s' }, + { v: '3', t: '3 · แบนผู้เล่น 1 คน' }, + { v: '4', t: '4 · ปิดปากผู้เล่น 1 คน' }, + { v: '5', t: '5 · จับผิดตัว โหวตใหม่' }, + { v: '6', t: '6 · ทุกคน +10 COINS' }, + { v: '7', t: '7 · ทุกคนหลักฐานฟรี +1' } + ]; + var specialCardRowsBuilt = false; + function buildSpecialCardByMapRows() { + if (specialCardRowsBuilt) return; + var wrap = el('special-card-by-map-rows'); + if (!wrap) return; + wrap.innerHTML = ''; + SPECIAL_CARD_GAMES.forEach(function (g) { + var row = document.createElement('div'); + row.style.cssText = 'display:flex;align-items:center;gap:10px;flex-wrap:wrap;'; + var name = document.createElement('span'); + name.style.cssText = 'min-width:240px;color:#cfd6ea;font-size:14px;'; + name.textContent = g.label; + var sel = document.createElement('select'); + sel.id = 'tsc-' + g.id; + sel.style.cssText = 'padding:6px 8px;min-width:300px;font-size:14px;'; + SPECIAL_CARD_OPTS.forEach(function (o) { + var op = document.createElement('option'); + op.value = o.v; + op.textContent = o.t; + sel.appendChild(op); + }); + row.appendChild(name); + row.appendChild(sel); + wrap.appendChild(row); + }); + specialCardRowsBuilt = true; + } + function loadSpecialCardByMapPanel() { + buildSpecialCardByMapRows(); + gameTimingFetch('GET') + .then(function (data) { + var m = (data && data.testSpecialCardByMap) || {}; + SPECIAL_CARD_GAMES.forEach(function (g) { + var sel = el('tsc-' + g.id); + if (sel) sel.value = (m[g.id] != null && m[g.id] !== '') ? String(m[g.id]) : ''; + }); + setMsg('special-card-by-map-msg', '', ''); + }) + .catch(function (e) { setMsg('special-card-by-map-msg', e.message || 'โหลดไม่ได้', 'error'); }); + } + function saveSpecialCardByMapPanel() { + var map = {}; + SPECIAL_CARD_GAMES.forEach(function (g) { + var sel = el('tsc-' + g.id); + var v = sel ? parseInt(String(sel.value || ''), 10) : 0; + if (v >= 1 && v <= 7) map[g.id] = v; + }); + var btn = el('btn-special-card-by-map-save'); + if (btn) btn.disabled = true; + gameTimingFetch('GET') + .then(function (data) { + data.testSpecialCardByMap = map; + return gameTimingFetch('PUT', data); + }) + .then(function (res) { + if (res && res.testSpecialCardByMap) { + var m = res.testSpecialCardByMap; + SPECIAL_CARD_GAMES.forEach(function (g) { + var sel = el('tsc-' + g.id); + if (sel) sel.value = (m[g.id] != null) ? String(m[g.id]) : ''; + }); + } + var any = Object.keys(map).length > 0; + setMsg('special-card-by-map-msg', any ? 'บันทึกแล้ว — เกมที่ตั้งจะขึ้นคำถามพิเศษ + การ์ดนั้นแน่นอน' : 'ล้างการบังคับแล้ว (กลับเป็นสุ่มปกติ)', 'ok'); + }) + .catch(function (e) { setMsg('special-card-by-map-msg', e.message || 'บันทึกไม่ได้', 'error'); }) + .then(function () { if (btn) btn.disabled = false; }); + } + + function loadTroublesomeForcePanel() { + var chk = el('troublesome-force-offer'); + if (!chk) return; + gameTimingFetch('GET') + .then(function (data) { + chk.checked = !!(data && data.troublesomeForceOffer); + setMsg('troublesome-force-msg', '', ''); + }) + .catch(function (e) { setMsg('troublesome-force-msg', e.message || 'โหลดไม่ได้', 'error'); }); + } + function saveTroublesomeForcePanel() { + var chk = el('troublesome-force-offer'); + if (!chk) return; + var on = !!chk.checked; + var btn = el('btn-troublesome-force-save'); + if (btn) btn.disabled = true; + gameTimingFetch('GET') + .then(function (data) { + data.troublesomeForceOffer = on; + return gameTimingFetch('PUT', data); + }) + .then(function (res) { + if (res && typeof res === 'object') chk.checked = !!res.troublesomeForceOffer; + setMsg('troublesome-force-msg', on ? 'บันทึกแล้ว — บทตัวป่วนจะขึ้นเสมอ (เริ่มคดีใหม่เพื่อเทสต์)' : 'ปิดแล้ว — กลับเป็นปกติ (ขึ้นเฉพาะคนจริง ≥ 4)', 'ok'); + }) + .catch(function (e) { setMsg('troublesome-force-msg', e.message || 'บันทึกไม่ได้', 'error'); }) + .then(function () { if (btn) btn.disabled = false; }); + } + + function loadSpecialQuizIconExpirePanel() { + var inp = el('special-quiz-icon-expire-sec'); + if (!inp) return; + gameTimingFetch('GET') + .then(function (data) { + var v = (data && data.specialQuizIconExpireSec != null) ? Math.floor(Number(data.specialQuizIconExpireSec)) : 10; + if (!Number.isFinite(v) || v < 0) v = 10; + inp.value = String(Math.min(120, v)); + setMsg('special-quiz-icon-expire-msg', '', ''); + }) + .catch(function (e) { setMsg('special-quiz-icon-expire-msg', e.message || 'โหลดไม่ได้', 'error'); }); + } + function saveSpecialQuizIconExpirePanel() { + var inp = el('special-quiz-icon-expire-sec'); + if (!inp) return; + var v = Math.floor(Number(inp.value)); + if (!Number.isFinite(v) || v < 0) v = 10; + v = Math.min(120, v); + inp.value = String(v); + var btn = el('btn-special-quiz-icon-expire-save'); + if (btn) btn.disabled = true; + gameTimingFetch('GET') + .then(function (data) { + data.specialQuizIconExpireSec = v; + return gameTimingFetch('PUT', data); + }) + .then(function (res) { + if (res && res.specialQuizIconExpireSec != null) inp.value = String(res.specialQuizIconExpireSec); + setMsg('special-quiz-icon-expire-msg', v > 0 ? ('บันทึกแล้ว — ไอคอนจะหายใน ' + v + ' วินาที (เริ่มเกมใหม่เพื่อเทสต์)') : 'บันทึกแล้ว — ไอคอนจะไม่หาย (อยู่จนจบเกม)', 'ok'); + }) + .catch(function (e) { setMsg('special-quiz-icon-expire-msg', e.message || 'บันทึกไม่ได้', 'error'); }) + .then(function () { 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(1, 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.gauntletJumpHeightMult = (function () { var v = parseFloat(el('game-timing-jump-height') && el('game-timing-jump-height').value); if (Number.isNaN(v)) v = 0.52; return Math.max(0.2, Math.min(1.5, v)); })(); + data.gauntletTimeLimitSec = limS; + /* troublesomeOfferSec/RollMaxSec ย้ายไปแท็บ "บทตัวป่วน" — ไม่แตะที่นี่ (ค่าจาก GET ส่งต่อคงเดิม) */ + 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; + }); + var inApp = name === 'panel-app'; + el('top-actions').hidden = !inApp; + var stats = el('admin-sys-stats'); + if (stats) stats.hidden = !inApp; + var burger = el('btn-sidebar-toggle'); + if (burger) burger.hidden = !inApp; + if (inApp) startSystemStats(); + else stopSystemStats(); + } + + /* ===== ระบบ: ดึง CPU/RAM ของเซิร์ฟเวอร์มาแสดง (อัปเดตทุก 5 วิ) ===== */ + var __sysStatsTimer = null; + function fmtBytes(n) { + if (!(n > 0)) return '0'; + var u = ['B', 'KB', 'MB', 'GB', 'TB']; + var i = Math.floor(Math.log(n) / Math.log(1024)); + i = Math.max(0, Math.min(u.length - 1, i)); + return (n / Math.pow(1024, i)).toFixed(i >= 2 ? 1 : 0) + ' ' + u[i]; + } + function fmtUptime(sec) { + sec = Math.max(0, Math.floor(sec || 0)); + var d = Math.floor(sec / 86400); sec -= d * 86400; + var h = Math.floor(sec / 3600); sec -= h * 3600; + var m = Math.floor(sec / 60); + if (d > 0) return d + 'd ' + h + 'h'; + if (h > 0) return h + 'h ' + m + 'm'; + return m + 'm'; + } + function setBarTone(bar, pct) { + if (!bar) return; + bar.classList.remove('is-warn', 'is-crit'); + if (pct >= 85) bar.classList.add('is-crit'); + else if (pct >= 65) bar.classList.add('is-warn'); + } + function renderSystemStats(s) { + if (!s || !s.ok) return; + var cpuPct = (s.cpu && typeof s.cpu.percent === 'number') ? s.cpu.percent + : (s.cpu ? s.cpu.loadPercent : 0); + var cpuVal = el('sysstat-cpu-val'), cpuBar = el('sysstat-cpu-bar'); + if (cpuVal) cpuVal.textContent = cpuPct + '%'; + if (cpuBar) { cpuBar.style.width = cpuPct + '%'; setBarTone(cpuBar, cpuPct); } + var memPct = s.mem ? s.mem.usedPercent : 0; + var ramVal = el('sysstat-ram-val'), ramBar = el('sysstat-ram-bar'); + if (ramVal && s.mem) ramVal.textContent = memPct + '% ' + fmtBytes(s.mem.used) + '/' + fmtBytes(s.mem.total); + if (ramBar) { ramBar.style.width = memPct + '%'; setBarTone(ramBar, memPct); } + // HDD / ดิสก์ + var diskWrap = el('sysstat-disk-val') ? el('sysstat-disk-val').closest('.sysstat') : null; + if (s.disk) { + var dPct = s.disk.usedPercent; + var dVal = el('sysstat-disk-val'), dBar = el('sysstat-disk-bar'); + if (dVal) dVal.textContent = dPct + '% ' + fmtBytes(s.disk.used) + '/' + fmtBytes(s.disk.total); + if (dBar) { dBar.style.width = dPct + '%'; setBarTone(dBar, dPct); } + if (diskWrap) diskWrap.hidden = false; + } else if (diskWrap) { + diskWrap.hidden = true; // อ่านดิสก์ไม่ได้ (เช่น Node เก่า) → ซ่อน + } + // Node ที่ใช้อยู่ (RAM ของ process เกม) + if (s.node) { + var nPct = typeof s.node.rssPercent === 'number' ? s.node.rssPercent : 0; + var nVal = el('sysstat-node-val'), nBar = el('sysstat-node-bar'); + if (nVal) nVal.textContent = fmtBytes(s.node.rss) + (s.node.version ? ' ' + s.node.version : ''); + if (nBar) { nBar.style.width = Math.min(100, nPct) + '%'; setBarTone(nBar, nPct); } + } + var upVal = el('sysstat-up-val'); + if (upVal && s.uptime) upVal.textContent = fmtUptime(s.uptime.os); + var box = el('admin-sys-stats'); + if (box && s.cpu) box.title = 'CPU ' + cpuPct + '% (load ' + s.cpu.load1 + ', ' + s.cpu.cores + ' cores) · RAM ' + + memPct + '%' + + (s.disk ? ' · HDD ' + s.disk.usedPercent + '% (' + fmtBytes(s.disk.free) + ' ว่าง)' : '') + + (s.node ? ' · Node ' + fmtBytes(s.node.rss) + ' ' + (s.node.version || '') : '') + + ' · uptime ' + fmtUptime(s.uptime && s.uptime.os) + ' · ' + (s.hostname || ''); + } + function pollSystemStats() { + fetch('/Game/api/system-stats', { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (s) { if (s) renderSystemStats(s); }) + .catch(function () { /* เงียบไว้ ไม่รบกวน admin */ }); + } + function startSystemStats() { + if (__sysStatsTimer) return; + pollSystemStats(); + setTimeout(pollSystemStats, 1200); // ครั้งที่สองให้ได้ค่า CPU% (ครั้งแรก server คืน null) + __sysStatsTimer = setInterval(pollSystemStats, 5000); + } + function stopSystemStats() { + if (__sysStatsTimer) { clearInterval(__sysStatsTimer); __sysStatsTimer = null; } + } + + // ===== 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; })); + /* [2026-07-21] ตั้ง "รูปคดี" = สลับหน้ารายละเอียดไปโหมดประกอบเอง (กรอบ + รูป + ข้อความด้านล่าง) */ + grid.appendChild(caseMediaImagePicker('รูปคดี (โหมดใหม่ — ตั้งแล้วหน้าจะประกอบเอง)', 'case', c.pic || '', function (v) { c.pic = 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); + + /* [2026-07-21] ไอคอนผู้ต้องสงสัย (สี่เหลี่ยมเล็ก) — โชว์เฉพาะหัวหน้าไต่สวน (es3) เท่านั้น + ไม่ตั้ง = ใช้รูปเต็มตัวด้านบน */ + var icoHead = document.createElement('h3'); + icoHead.className = 'cm-section-title'; + icoHead.textContent = 'ไอคอนผู้ต้องสงสัย (3 คน) — หัวหน้าไต่สวน (ไม่ตั้ง = ใช้รูปเต็มตัวด้านบน)'; + root.appendChild(icoHead); + var icoGrid = document.createElement('div'); + icoGrid.className = 'cm-grid'; + if (!Array.isArray(c.icons)) c.icons = (c.suspects || []).slice(); + [0, 1, 2].forEach(function (si) { + icoGrid.appendChild(caseMediaImagePicker('ไอคอน ' + (si + 1), 'suspect-pick', c.icons[si], function (v) { c.icons[si] = v; })); + }); + root.appendChild(icoGrid); + + /* [2026-07-21] ข้อความหน้ารายละเอียดคดี (โหมดใหม่) — แสดงเมื่อตั้ง "รูปคดี" ไว้เท่านั้น + เว้นว่าง = ไม่แสดงบรรทัดนั้นบนหน้าเกม (ค่อยๆ กรอกทีละคดีได้) */ + var txtHead = document.createElement('h3'); + txtHead.className = 'cm-section-title'; + txtHead.textContent = 'ข้อความรายละเอียดคดี (โหมดใหม่ — ใช้เมื่อตั้ง "รูปคดี" แล้ว · เว้นว่าง = ไม่แสดงบรรทัดนั้น)'; + root.appendChild(txtHead); + if (!Array.isArray(c.txtSuspects)) c.txtSuspects = ['', '', '']; + var caseTextField = function (label, get, set, opts) { + opts = opts || {}; + var lb = document.createElement('label'); + lb.className = 'admin-field'; + lb.textContent = label; + var el = document.createElement(opts.multiline ? 'textarea' : 'input'); + if (!opts.multiline) el.type = 'text'; + else el.rows = 4; + el.maxLength = opts.max || 300; + el.value = get() || ''; + el.addEventListener('input', function () { set(el.value); }); + lb.appendChild(el); + return lb; + }; + root.appendChild(caseTextField('ข้อหา', function () { return c.txtCharge; }, function (v) { c.txtCharge = v; })); + root.appendChild(caseTextField('สถานที่เกิดเหตุ', function () { return c.txtPlace; }, function (v) { c.txtPlace = v; })); + root.appendChild(caseTextField('ผู้เสียหาย', function () { return c.txtVictim; }, function (v) { c.txtVictim = v; })); + [0, 1, 2].forEach(function (si) { + root.appendChild(caseTextField('ผู้ต้องสงสัย ' + (si + 1) + ' (ข้อความ)', + function () { return c.txtSuspects[si]; }, function (v) { c.txtSuspects[si] = v; }, { max: 240 })); + }); + root.appendChild(caseTextField('บันทึกเหตุการณ์', function () { return c.txtNote; }, function (v) { c.txtNote = v; }, { multiline: true, max: 1500 })); + + 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' }, + { tab: 'qb-map-editor', panel: 'tab-panel-qb-map-editor', fsBtn: 'btn-qb-map-editor-fullscreen' }, + { tab: 'ai-admin', panel: 'tab-panel-ai-admin', fsBtn: 'btn-ai-admin-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(); loadSpecialQuizIconExpirePanel(); } + 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(); + if (name === 'highscore') loadHighscore(); + if (name === 'achievements') loadAchievementsPanel(); + if (name === 'vote-timing') loadVoteTimingPanel(); + if (name === 'postcase') loadPostCasePanel(); + if (name === 'troublesome') loadTroublesomePanel(); + if (name === 'test-mode') { loadForcedMinigamePanel(); loadSpecialCardByMapPanel(); loadTroublesomeForcePanel(); } + if (name === 'sound') loadSoundPanel(); + if (name === 'map-editor') ensureEmbedFrameSrc('admin-map-editor-frame'); + if (name === 'qb-map-editor') ensureEmbedFrameSrc('admin-qb-map-editor-frame'); + if (name === 'ai-admin') ensureEmbedFrameSrc('admin-ai-admin-frame'); + } + + /* ฝัง iframe editor แบบ lazy — ตั้ง src จาก data-src ครั้งแรกที่เปิด tab (ไม่โหลดตอนเปิดหน้า Admin) */ + function ensureEmbedFrameSrc(id) { + var f = el(id); + if (f && !f.getAttribute('src') && f.getAttribute('data-src')) f.setAttribute('src', f.getAttribute('data-src')); + } + + 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); + }); + }); + + /* ===== สิทธิ์ระดับหมวด — ซ่อน tab ที่บัญชีนี้เข้าไม่ได้ [2026-07-22] ===== + ⚠️ นี่เป็นแค่ UX · ตัวกันจริงอยู่ที่ PHP (require_tab) เพราะซ่อนปุ่มไม่ใช่ security */ + var adminAllowedTabs = null; /* null = ยังไม่รู้ (แสดงทุก tab ไปก่อน) */ + + function applyTabPermissions(tabs) { + if (!Array.isArray(tabs) || !tabs.length) return; /* ว่าง = ทุกหมวด */ + adminAllowedTabs = tabs; + var firstAllowed = ''; + document.querySelectorAll('.tab').forEach(function (t) { + var name = t.getAttribute('data-tab'); + /* เปลี่ยนรหัสผ่านตัวเอง ต้องเข้าได้เสมอ ไม่งั้นล็อกตัวเองออกจากการเปลี่ยนรหัส */ + var ok = name === 'change-password' || tabs.indexOf(name) >= 0; + t.hidden = !ok; + if (ok && !firstAllowed && name !== 'change-password') firstAllowed = name; + }); + /* ถ้ากำลังอยู่หมวดที่ไม่มีสิทธิ์ (เช่นค่า default ตอนโหลด) → เด้งไปหมวดแรกที่เข้าได้ */ + var active = document.querySelector('.tab.is-active'); + var activeName = active && active.getAttribute('data-tab'); + if (activeName && activeName !== 'change-password' && tabs.indexOf(activeName) < 0 && firstAllowed) { + setTab(firstAllowed); + } + /* ซ่อนหัวข้อคั่นกลุ่มที่ไม่เหลือ tab แล้ว (กันหัวข้อลอย) */ + document.querySelectorAll('.admin-tab-sep').forEach(function (sep) { + var n = sep.nextElementSibling; + var any = false; + while (n && !n.classList.contains('admin-tab-sep')) { + if (n.classList.contains('tab') && !n.hidden) { any = true; break; } + n = n.nextElementSibling; + } + sep.hidden = !any; + }); + } + + /* เรียกจาก loadAll() หลังล็อกอินแล้วเท่านั้น — ยิงตอนหน้ายังไม่ล็อกอินจะได้ 401 เปล่าๆ */ + + /* ===== Quiz Battle: เปิด/ปิด 10 โหมด + วันหมดเขต (เวลาไทย) [2026-07-22] ===== */ + (function bindQuizBattleModes() { + var tbody = el('qb-modes-tbody'); + if (!tbody) return; + var msg = el('qb-modes-msg'); + var nowEl = el('qb-modes-now'); + + /* escape สำหรับใส่ใน value="" ของ input (หัวข้อเป็นข้อความไทยที่แอดมินพิมพ์เอง) */ + function attr(s) { + return String(s == null ? '' : s) + .replace(/&/g, '&').replace(/"/g, '"') + .replace(//g, '>'); + } + + function say(t, bad) { + if (!msg) return; + msg.textContent = t || ''; + /* [2026-07-27] สำเร็จ = เขียวเด่น · error = แดง · ว่าง = default — ให้ user เห็น feedback ชัด (เดิม default จางมองไม่เห็น) */ + msg.style.color = bad ? '#ff8fa3' : (t ? '#9ece6a' : ''); + msg.style.fontWeight = t ? '600' : ''; + } + + function stateLabel(m) { + if (!m.enabled) return 'ปิดอยู่'; + if (m.expired) return 'หมดเวลาแล้ว'; + if (m.openUntil) return 'เปิด · ถึง ' + m.openUntil + ''; + return 'เปิด (ไม่จำกัด)'; + } + + function render(data) { + var modes = (data && data.modes) || {}; + if (nowEl) nowEl.textContent = 'เวลาไทยตอนนี้: ' + (data.nowThai || '-') + (data.updatedAt ? (' · แก้ล่าสุด ' + data.updatedAt + (data.updatedBy ? (' โดย ' + data.updatedBy) : '')) : ''); + var html = ''; + for (var i = 1; i <= 10; i++) { + var m = modes[String(i)] || { title: '', enabled: true, openUntil: '', maxPlayers: 50 }; + var until = (m.openUntil || '').replace(' ', 'T'); + var mp = (m.maxPlayers != null && m.maxPlayers >= 1) ? Math.min(50, m.maxPlayers | 0) : 50; + html += '' + + '' + i + '' + + '' + + '' + + '' + + '' + + '' + stateLabel(m) + '' + + ''; + } + tbody.innerHTML = html; + } + + function load() { + say('กำลังโหลด…'); + api('quiz-battle-modes.php') + .then(function (j) { render(j); say(''); }) + .catch(function (e) { say('โหลดไม่สำเร็จ: ' + e.message, true); }); + } + + function collect() { + var out = {}; + tbody.querySelectorAll('tr[data-room]').forEach(function (tr) { + var n = tr.getAttribute('data-room'); + var on = tr.querySelector('.qb-mode-on'); + var until = tr.querySelector('.qb-mode-until'); + var maxEl = tr.querySelector('.qb-mode-max'); + var titleEl = tr.querySelector('.qb-mode-title'); + var mp = maxEl ? parseInt(maxEl.value, 10) : 50; + if (!(mp >= 1)) mp = 50; + mp = Math.min(50, mp); + out[n] = { + title: titleEl ? String(titleEl.value || '').trim() : '', + enabled: !!(on && on.checked), + /* datetime-local ให้ 'YYYY-MM-DDTHH:MM' → เก็บเป็นช่องว่างคั่น (เวลาไทยตรงๆ) */ + openUntil: (until && until.value ? String(until.value).slice(0, 16).replace('T', ' ') : ''), + maxPlayers: mp, + }; + }); + return out; + } + + function save() { + say('กำลังบันทึก…'); + api('quiz-battle-modes.php', { method: 'PUT', body: { modes: collect() } }) + .then(function (j) { render(j); say('บันทึกแล้ว'); }) + .catch(function (e) { say('บันทึกไม่สำเร็จ: ' + e.message, true); }); + } + + function setAll(on) { + tbody.querySelectorAll('.qb-mode-on').forEach(function (c) { c.checked = on; }); + } + + if (el('btn-qb-modes-reload')) el('btn-qb-modes-reload').addEventListener('click', load); + if (el('btn-qb-modes-save')) el('btn-qb-modes-save').addEventListener('click', save); + if (el('btn-qb-modes-all-on')) el('btn-qb-modes-all-on').addEventListener('click', function () { setAll(true); }); + if (el('btn-qb-modes-all-off')) el('btn-qb-modes-all-off').addEventListener('click', function () { setAll(false); }); + /* ไม่ load() ตรงนี้ — หน้ายังไม่ล็อกอินจะได้ 401 · ให้ loadAll() เรียกหลังล็อกอินแทน */ + window.__qbModesLoad = load; + })(); + + /* ===== Sidebar: ค้นหาเมนู / ยุบกลุ่ม / เปิด-ปิดบนมือถือ ===== */ + (function setupAdminSidebar() { + var shell = document.querySelector('.admin-shell'); + var nav = document.querySelector('.admin-tabs'); + if (!nav) return; + var seps = Array.prototype.slice.call(nav.querySelectorAll('.admin-tab-sep')); + var tabs = Array.prototype.slice.call(nav.querySelectorAll('.tab')); + + function isMobile() { return window.matchMedia('(max-width: 980px)').matches; } + function closeSidebar() { + if (shell) shell.classList.remove('sidebar-open'); + var b = el('btn-sidebar-toggle'); if (b) b.setAttribute('aria-expanded', 'false'); + var bd = el('admin-sidebar-backdrop'); if (bd) bd.hidden = true; + } + function openSidebar() { + if (shell) shell.classList.add('sidebar-open'); + var b = el('btn-sidebar-toggle'); if (b) b.setAttribute('aria-expanded', 'true'); + var bd = el('admin-sidebar-backdrop'); if (bd) bd.hidden = false; + } + var burger = el('btn-sidebar-toggle'); + if (burger) burger.addEventListener('click', function () { + if (shell && shell.classList.contains('sidebar-open')) closeSidebar(); else openSidebar(); + }); + var backdrop = el('admin-sidebar-backdrop'); + if (backdrop) backdrop.addEventListener('click', closeSidebar); + nav.addEventListener('click', function (e) { + var t = e.target && e.target.closest ? e.target.closest('.tab') : null; + if (t && isMobile()) closeSidebar(); + }); + + /* ยุบ/ขยายกลุ่ม: ซ่อนแท็บที่อยู่หลัง separator จนถึง separator ถัดไป */ + function applyGroupVisibility() { + seps.forEach(function (sep) { + var collapsed = sep.classList.contains('is-collapsed'); + var n = sep.nextElementSibling; + while (n && !n.classList.contains('admin-tab-sep')) { + if (n.classList && n.classList.contains('tab')) n.classList.toggle('is-group-hidden', collapsed); + n = n.nextElementSibling; + } + }); + } + function toggleGroup(sep) { + sep.classList.toggle('is-collapsed'); + applyGroupVisibility(); + } + seps.forEach(function (sep) { + sep.setAttribute('role', 'button'); + sep.setAttribute('tabindex', '0'); + sep.addEventListener('click', function () { toggleGroup(sep); }); + sep.addEventListener('keydown', function (e) { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleGroup(sep); } + }); + }); + + /* ค้นหาเมนู */ + var search = el('admin-nav-search-input'); + var emptyMsg = el('admin-nav-empty'); + if (search) search.addEventListener('input', function () { + var q = search.value.trim().toLowerCase(); + if (q) { + var anyVisible = false; + tabs.forEach(function (t) { + t.classList.remove('is-group-hidden'); + var hit = (t.textContent || '').toLowerCase().indexOf(q) >= 0; + t.classList.toggle('is-search-hidden', !hit); + if (hit) anyVisible = true; + }); + seps.forEach(function (s) { s.style.display = 'none'; }); + if (emptyMsg) emptyMsg.hidden = anyVisible; + } else { + tabs.forEach(function (t) { t.classList.remove('is-search-hidden'); }); + seps.forEach(function (s) { s.style.display = ''; }); + applyGroupVisibility(); + if (emptyMsg) emptyMsg.hidden = true; + } + }); + })(); + + 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'); + bindEmbedPanelFullscreen('btn-qb-map-editor-fullscreen', 'tab-panel-qb-map-editor'); + bindEmbedPanelFullscreen('btn-ai-admin-fullscreen', 'tab-panel-ai-admin'); + + /* 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(); + }); + } + var forcedMgSaveBtn = el('btn-forced-minigame-save'); + if (forcedMgSaveBtn) forcedMgSaveBtn.addEventListener('click', saveForcedMinigamePanel); + var specialCardSaveBtn = el('btn-special-card-by-map-save'); + if (specialCardSaveBtn) specialCardSaveBtn.addEventListener('click', saveSpecialCardByMapPanel); + var troublesomeForceSaveBtn = el('btn-troublesome-force-save'); + if (troublesomeForceSaveBtn) troublesomeForceSaveBtn.addEventListener('click', saveTroublesomeForcePanel); + var sqIconExpireSaveBtn = el('btn-special-quiz-icon-expire-save'); + if (sqIconExpireSaveBtn) sqIconExpireSaveBtn.addEventListener('click', saveSpecialQuizIconExpirePanel); + 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', '', ''); + }); + } + + /* ===== กระดานผู้นำ (High Score) ===== */ + function renderHighscore(rows) { + var tbEl = el('table-highscore'); + if (!tbEl) return; + var tb = tbEl.querySelector('tbody'); + tb.innerHTML = ''; + (rows || []).forEach(function (r) { + var rank = parseInt(r.rank, 10) || 0; + var score = Math.max(0, parseInt(r.score, 10) || 0); + var coins = Math.max(0, parseInt(r.coins, 10) || 0); + var medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : ('#' + rank); + var tr = document.createElement('tr'); + tr.innerHTML = + '' + medal + '' + + '' + escapeHtml(r.name || 'ผู้เล่น') + (r.blocked ? ' บล็อก' : '') + '' + + '' + + '' + + '' + + '' + + '' + coins + '' + + ''; + tb.appendChild(tr); + }); + tb.querySelectorAll('.btn-score-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-score') : null; + var score = Math.max(0, parseInt(inp && inp.value, 10) || 0); + api('accounts.php', { method: 'PATCH', body: { id: id, score: score } }) + .then(function () { setMsg('highscore-msg', 'บันทึกคะแนนแล้ว', 'ok'); return loadHighscore(); }) + .catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); + }); + }); + tb.querySelectorAll('.btn-score-reset').forEach(function (b) { + b.addEventListener('click', function () { + var id = b.getAttribute('data-id'); + if (!confirm('รีเซ็ตคะแนนคนนี้เป็น 0?')) return; + api('leaderboard-admin.php', { method: 'POST', body: { action: 'reset', id: id } }) + .then(function () { return loadHighscore(); }) + .catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); + }); + }); + } + + function loadHighscore() { + return api('leaderboard-admin.php').then(function (r) { + renderHighscore(r.rows); + var n = (r.rows && r.rows.length) || 0; + setMsg('highscore-msg', n ? ('ผู้เล่นมีคะแนน ' + r.total + ' คน') : 'ยังไม่มีผู้เล่นทำคะแนน', ''); + }).catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); + } + + (function bindHighscoreControls() { + var refresh = el('btn-highscore-refresh'); + if (refresh) refresh.addEventListener('click', function () { loadHighscore(); }); + var resetAll = el('btn-highscore-reset-all'); + if (resetAll) resetAll.addEventListener('click', function () { + if (!confirm('รีเซ็ตคะแนนสะสมของผู้เล่นทุกคนเป็น 0? (ย้อนกลับไม่ได้)')) return; + api('leaderboard-admin.php', { method: 'POST', body: { action: 'resetAll' } }) + .then(function (r) { setMsg('highscore-msg', 'รีเซ็ตแล้ว ' + (r.reset || 0) + ' คน', 'ok'); return loadHighscore(); }) + .catch(function (err) { setMsg('highscore-msg', err.message, 'error'); }); + }); + })(); + + /* ===== เวลาโหวต (vote timing) ===== */ + function loadVoteTimingPanel() { + gameTimingFetch('GET') + .then(function (data) { + var pk = (data && data.pickVoteSec != null) ? Number(data.pickVoteSec) : 25; + var tr = (data && data.trialVoteSec != null) ? Number(data.trialVoteSec) : 30; + var rv = (data && data.trialResultRevealMs != null) ? Number(data.trialResultRevealMs) : 7000; + if (el('vote-timing-pick')) el('vote-timing-pick').value = String(Number.isFinite(pk) ? pk : 25); + if (el('vote-timing-trial')) el('vote-timing-trial').value = String(Number.isFinite(tr) ? tr : 30); + if (el('vote-timing-reveal')) el('vote-timing-reveal').value = String(Number.isFinite(rv) ? rv : 7000); + setMsg('vote-timing-msg', '', ''); + }) + .catch(function (e) { setMsg('vote-timing-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); }); + } + + (function bindVoteTiming() { + var btn = el('btn-vote-timing-save'); + if (!btn) return; + btn.addEventListener('click', function () { + var pk = Math.max(5, Math.min(120, parseInt((el('vote-timing-pick') || {}).value, 10) || 25)); + var tr = Math.max(5, Math.min(180, parseInt((el('vote-timing-trial') || {}).value, 10) || 30)); + var rv = Math.max(1500, Math.min(20000, parseInt((el('vote-timing-reveal') || {}).value, 10) || 7000)); + btn.disabled = true; + gameTimingFetch('GET') + .then(function (data) { + data.pickVoteSec = pk; + data.trialVoteSec = tr; + data.trialResultRevealMs = rv; + return gameTimingFetch('PUT', data); + }) + .then(function () { return gameTimingFetch('GET'); }) + .then(function (fresh) { + var p = (fresh && fresh.pickVoteSec != null) ? fresh.pickVoteSec : pk; + var t = (fresh && fresh.trialVoteSec != null) ? fresh.trialVoteSec : tr; + var r = (fresh && fresh.trialResultRevealMs != null) ? fresh.trialResultRevealMs : rv; + if (el('vote-timing-pick')) el('vote-timing-pick').value = String(p); + if (el('vote-timing-trial')) el('vote-timing-trial').value = String(t); + if (el('vote-timing-reveal')) el('vote-timing-reveal').value = String(r); + setMsg('vote-timing-msg', 'บันทึกแล้ว — โหวตเลือกผู้เล่น ' + p + 's · ชี้ตัวคนร้าย ' + t + 's · โชว์ผล ' + Math.round(r/1000) + 's · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok'); + }) + .catch(function (e) { setMsg('vote-timing-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); }) + .then(function () { btn.disabled = false; }); + }); + })(); + + /* ===== จบคดี (post-case destination) — Main-Lobby หรือ LobbyA ===== */ + function loadPostCasePanel() { + gameTimingFetch('GET') + .then(function (data) { + var d = (data && data.postCaseDestination === 'lobbya') ? 'lobbya' : 'mainlobby'; + if (el('postcase-dest')) el('postcase-dest').value = d; + setMsg('postcase-msg', '', ''); + }) + .catch(function (e) { setMsg('postcase-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); }); + } + + (function bindPostCase() { + var btn = el('btn-postcase-save'); + if (!btn) return; + btn.addEventListener('click', function () { + var d = ((el('postcase-dest') || {}).value === 'lobbya') ? 'lobbya' : 'mainlobby'; + btn.disabled = true; + gameTimingFetch('GET') + .then(function (data) { data.postCaseDestination = d; return gameTimingFetch('PUT', data); }) + .then(function () { return gameTimingFetch('GET'); }) + .then(function (fresh) { + var v = (fresh && fresh.postCaseDestination === 'lobbya') ? 'lobbya' : 'mainlobby'; + if (el('postcase-dest')) el('postcase-dest').value = v; + setMsg('postcase-msg', 'บันทึกแล้ว — เมื่อจบคดีไป ' + (v === 'lobbya' ? 'LobbyA (ห้องเดิม เริ่มคดีใหม่)' : 'Main-Lobby (เดิม)') + ' · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok'); + }) + .catch(function (e) { setMsg('postcase-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); }) + .then(function () { btn.disabled = false; }); + }); + })(); + + /* ===== เสียง (sound settings) — master/music/sfx volume 0..1 ===== */ + function soundSettingsFetch(method, body) { + var m = method || 'GET'; + var url = SOUND_SETTINGS_API; + if (m === 'GET' && body == null) url += (url.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) { + var plain = (text || r.statusText || 'Request failed').trim(); + if (r.status === 404) plain += ' — รีสตาร์ท Node (Game/server.js) หลัง deploy'; + throw new Error(plain); + } + if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status + ' ' + r.statusText)); + return j; + }); + }); + } + function pctFromVol(v, d) { var n = Number(v); return Math.round((Number.isFinite(n) ? n : d) * 100); } + function loadSoundPanel() { + soundSettingsFetch('GET') + .then(function (d) { + var setR = function (id, pct) { var e = el(id); if (e) { e.value = String(pct); var lbl = el(id + '-val'); if (lbl) lbl.textContent = pct + '%'; } }; + setR('sound-master', pctFromVol(d && d.masterVol, 1.0)); + setR('sound-music', pctFromVol(d && d.musicVol, 0.35)); + setR('sound-sfx', pctFromVol(d && d.sfxVol, 0.75)); + setMsg('sound-msg', '', ''); + }) + .catch(function (e) { setMsg('sound-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/sound-settings', 'error'); }); + } + (function bindSound() { + var btn = el('btn-sound-save'); + if (!btn) return; + btn.addEventListener('click', function () { + var rd = function (id, d) { var v = parseInt((el(id) || {}).value, 10); return Math.max(0, Math.min(100, Number.isFinite(v) ? v : d)) / 100; }; + var body = { masterVol: rd('sound-master', 100), musicVol: rd('sound-music', 35), sfxVol: rd('sound-sfx', 75) }; + btn.disabled = true; + soundSettingsFetch('PUT', body) + .then(function () { return soundSettingsFetch('GET'); }) + .then(function (fresh) { + loadSoundPanel(); + setMsg('sound-msg', 'บันทึกแล้ว — Master ' + pctFromVol(fresh.masterVol, 1) + '% · Music ' + pctFromVol(fresh.musicVol, .35) + '% · SFX ' + pctFromVol(fresh.sfxVol, .75) + '% · ผู้เล่นต้องรีเฟรช', 'ok'); + }) + .catch(function (e) { setMsg('sound-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/sound-settings', 'error'); }) + .then(function () { btn.disabled = false; }); + }); + })(); + + /* ===== บทตัวป่วน (แยกจากหมวด Minigame-2) — troublesomeRollMaxSec + troublesomeOfferSec ===== */ + function loadTroublesomePanel() { + gameTimingFetch('GET') + .then(function (data) { + var rs = (data && data.troublesomeRollMaxSec != null) ? Number(data.troublesomeRollMaxSec) : 8; + var os = (data && data.troublesomeOfferSec != null) ? Number(data.troublesomeOfferSec) : 15; + if (el('troublesome-roll-sec')) el('troublesome-roll-sec').value = String(Number.isFinite(rs) ? rs : 8); + if (el('troublesome-offer-sec')) el('troublesome-offer-sec').value = String(Number.isFinite(os) ? os : 15); + setMsg('troublesome-msg', '', ''); + }) + .catch(function (e) { setMsg('troublesome-msg', (e.message || 'โหลดไม่ได้') + ' — เช็ค /Game/api/game-timing', 'error'); }); + } + + (function bindTroublesome() { + var btn = el('btn-troublesome-save'); + if (!btn) return; + btn.addEventListener('click', function () { + var rs = Math.max(1, Math.min(30, parseInt((el('troublesome-roll-sec') || {}).value, 10) || 8)); + var os = Math.max(5, Math.min(120, parseInt((el('troublesome-offer-sec') || {}).value, 10) || 15)); + btn.disabled = true; + gameTimingFetch('GET') + .then(function (data) { + data.troublesomeRollMaxSec = rs; + data.troublesomeOfferSec = os; + return gameTimingFetch('PUT', data); + }) + .then(function () { return gameTimingFetch('GET'); }) + .then(function (fresh) { + var r = (fresh && fresh.troublesomeRollMaxSec != null) ? fresh.troublesomeRollMaxSec : rs; + var o = (fresh && fresh.troublesomeOfferSec != null) ? fresh.troublesomeOfferSec : os; + if (el('troublesome-roll-sec')) el('troublesome-roll-sec').value = String(r); + if (el('troublesome-offer-sec')) el('troublesome-offer-sec').value = String(o); + setMsg('troublesome-msg', 'บันทึกแล้ว — เวลาเสนอ ' + r + 's · เวลารับ ' + o + 's · ผู้เล่นต้องรีเฟรช/เข้าห้องใหม่', 'ok'); + }) + .catch(function (e) { setMsg('troublesome-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็ค Node + /Game/api/game-timing', 'error'); }) + .then(function () { btn.disabled = false; }); + }); + })(); + + /* ===== Achievements admin ===== */ + var achvCatalog = []; + var achvCatalogById = {}; + var achvCurrentPlayer = null; + + function achvGroupLabel(g) { + var names = { 1: '1 · Analyst', 2: '2 · Explorer', 3: '3 · Risk', 4: '4 · Support', 5: '5 · Elite' }; + return names[g] || String(g); + } + + function renderAchvCatalog() { + var tb = el('table-achv-catalog'); + if (!tb) return; + tb = tb.querySelector('tbody'); + tb.innerHTML = ''; + achvCatalog.slice().sort(function (a, b) { return (a.g - b.g) || 0; }).forEach(function (a) { + var tr = document.createElement('tr'); + tr.setAttribute('data-achv-id', a.id); + + var tdG = document.createElement('td'); + var sel = document.createElement('select'); + sel.className = 'achv-f-g'; + for (var g = 1; g <= 5; g++) { + var op = document.createElement('option'); + op.value = String(g); op.textContent = achvGroupLabel(g); + if (g === (parseInt(a.g, 10) || 1)) op.selected = true; + sel.appendChild(op); + } + tdG.appendChild(sel); + + var tdT = document.createElement('td'); + var inT = document.createElement('input'); + inT.type = 'text'; inT.className = 'achv-f-title'; inT.value = a.title || ''; inT.style.width = '100%'; + tdT.appendChild(inT); + + var tdD = document.createElement('td'); + var inD = document.createElement('input'); + inD.type = 'text'; inD.className = 'achv-f-desc'; inD.value = a.desc || ''; inD.style.width = '100%'; + tdD.appendChild(inD); + + var tdTar = document.createElement('td'); + var inTar = document.createElement('input'); + inTar.type = 'number'; inTar.min = '1'; inTar.step = '1'; inTar.className = 'achv-f-target'; + inTar.value = String(Math.max(1, parseInt(a.target, 10) || 1)); inTar.style.width = '72px'; + tdTar.appendChild(inTar); + + var tdE = document.createElement('td'); + var inE = document.createElement('input'); + inE.type = 'checkbox'; inE.className = 'achv-f-enabled'; inE.checked = a.enabled !== false; + tdE.appendChild(inE); + + tr.appendChild(tdG); tr.appendChild(tdT); tr.appendChild(tdD); tr.appendChild(tdTar); tr.appendChild(tdE); + tb.appendChild(tr); + }); + } + + function collectAchvCatalogFromTable() { + var rows = document.querySelectorAll('#table-achv-catalog tbody tr'); + var out = []; + rows.forEach(function (tr) { + out.push({ + id: tr.getAttribute('data-achv-id'), + g: parseInt(tr.querySelector('.achv-f-g').value, 10) || 1, + title: tr.querySelector('.achv-f-title').value, + desc: tr.querySelector('.achv-f-desc').value, + target: Math.max(1, parseInt(tr.querySelector('.achv-f-target').value, 10) || 1), + enabled: tr.querySelector('.achv-f-enabled').checked, + }); + }); + return out; + } + + function loadAchvCatalog() { + return api('achievements.php?action=catalog').then(function (r) { + achvCatalog = (r && r.catalog) || []; + achvCatalogById = {}; + achvCatalog.forEach(function (a) { achvCatalogById[a.id] = a; }); + renderAchvCatalog(); + setMsg('achv-catalog-msg', 'โหลดแคตตาล็อก ' + achvCatalog.length + ' รายการ', ''); + }).catch(function (err) { setMsg('achv-catalog-msg', err.message, 'error'); }); + } + + function renderAchvPlayers(rows) { + var tb = el('table-achv-players'); + if (!tb) return; + tb = tb.querySelector('tbody'); + tb.innerHTML = ''; + (rows || []).forEach(function (p) { + var tr = document.createElement('tr'); + var c1 = document.createElement('td'); c1.textContent = p.displayName || 'Guest'; + var c2 = document.createElement('td'); c2.textContent = p.playerKey; c2.style.fontSize = '12px'; c2.style.opacity = '0.8'; + var c3 = document.createElement('td'); c3.textContent = (p.unlocked || 0) + ' / ' + (p.total || 0); + var c4 = document.createElement('td'); c4.textContent = String(p.coins || 0); + var c5 = document.createElement('td'); + var btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'btn btn-ghost'; btn.textContent = 'จัดการ'; + btn.addEventListener('click', function () { openAchvPlayerDetail(p.playerKey, p.displayName); }); + c5.appendChild(btn); + tr.appendChild(c1); tr.appendChild(c2); tr.appendChild(c3); tr.appendChild(c4); tr.appendChild(c5); + tb.appendChild(tr); + }); + } + + function loadAchvPlayers() { + var q = (el('achv-player-search') && el('achv-player-search').value || '').trim().toLowerCase(); + return api('achievements.php?action=players').then(function (r) { + var rows = (r && r.players) || []; + if (q) { + rows = rows.filter(function (p) { + return (p.displayName || '').toLowerCase().indexOf(q) >= 0 || (p.playerKey || '').toLowerCase().indexOf(q) >= 0; + }); + } + renderAchvPlayers(rows); + setMsg('achv-players-msg', 'ผู้เล่น ' + rows.length + ' คน', ''); + }).catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); + } + + function openAchvPlayerDetail(key, name) { + achvCurrentPlayer = key; + var box = el('achv-player-detail'); + if (box) box.hidden = false; + if (el('achv-player-detail-name')) el('achv-player-detail-name').textContent = (name || 'Guest') + ' (' + key + ')'; + api('achievements.php?action=player&playerKey=' + encodeURIComponent(key)).then(function (r) { + var prog = (r && r.progress) || {}; + var tb = el('table-achv-player-detail').querySelector('tbody'); + tb.innerHTML = ''; + achvCatalog.slice().sort(function (a, b) { return (a.g - b.g) || 0; }).forEach(function (a) { + var target = Math.max(1, parseInt(a.target, 10) || 1); + var cur = Math.max(0, parseInt(prog[a.id], 10) || 0); + var tr = document.createElement('tr'); + + var c1 = document.createElement('td'); + c1.innerHTML = '' + achvGroupLabel(a.g).charAt(0) + ' · ' + (a.title || a.id); + var c2 = document.createElement('td'); + var inp = document.createElement('input'); + inp.type = 'number'; inp.min = '0'; inp.max = String(target); inp.step = '1'; inp.value = String(Math.min(cur, target)); + inp.style.width = '70px'; + c2.appendChild(inp); + c2.appendChild(document.createTextNode(' / ' + target + (cur >= target ? ' ✔' : ''))); + + var c3 = document.createElement('td'); + var setBtn = document.createElement('button'); + setBtn.type = 'button'; setBtn.className = 'btn btn-ghost'; setBtn.textContent = 'ตั้งค่า'; + setBtn.style.marginRight = '6px'; + setBtn.addEventListener('click', function () { + api('achievements.php', { method: 'POST', body: { action: 'setProgress', playerKey: key, id: a.id, value: parseInt(inp.value, 10) || 0 } }) + .then(function () { setMsg('achv-players-msg', 'บันทึก ' + a.id, 'ok'); openAchvPlayerDetail(key, name); }) + .catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); + }); + var unBtn = document.createElement('button'); + unBtn.type = 'button'; unBtn.className = 'btn btn-primary'; unBtn.textContent = 'ปลดล็อก'; + unBtn.addEventListener('click', function () { + api('achievements.php', { method: 'POST', body: { action: 'unlock', playerKey: key, id: a.id } }) + .then(function () { setMsg('achv-players-msg', 'ปลดล็อก ' + a.id, 'ok'); openAchvPlayerDetail(key, name); }) + .catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); + }); + c3.appendChild(setBtn); c3.appendChild(unBtn); + + tr.appendChild(c1); tr.appendChild(c2); tr.appendChild(c3); + tb.appendChild(tr); + }); + }).catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); + } + + function showAchvSubtab(which) { + var isCat = which !== 'players'; + if (el('achv-panel-catalog')) el('achv-panel-catalog').hidden = !isCat; + if (el('achv-panel-players')) el('achv-panel-players').hidden = isCat; + if (el('achv-subtab-catalog')) el('achv-subtab-catalog').classList.toggle('is-active', isCat); + if (el('achv-subtab-players')) el('achv-subtab-players').classList.toggle('is-active', !isCat); + if (!isCat) loadAchvPlayers(); + } + + var achvPanelInited = false; + function loadAchievementsPanel() { + if (!achvPanelInited) { + achvPanelInited = true; + var sc = el('achv-subtab-catalog'); if (sc) sc.addEventListener('click', function () { showAchvSubtab('catalog'); }); + var sp = el('achv-subtab-players'); if (sp) sp.addEventListener('click', function () { showAchvSubtab('players'); }); + var save = el('btn-achv-save'); + if (save) save.addEventListener('click', function () { + api('achievements.php', { method: 'POST', body: { action: 'saveCatalog', catalog: collectAchvCatalogFromTable() } }) + .then(function (r) { + achvCatalog = (r && r.catalog) || achvCatalog; + achvCatalogById = {}; achvCatalog.forEach(function (a) { achvCatalogById[a.id] = a; }); + renderAchvCatalog(); + setMsg('achv-catalog-msg', 'บันทึกแคตตาล็อกแล้ว', 'ok'); + }) + .catch(function (err) { setMsg('achv-catalog-msg', err.message, 'error'); }); + }); + var reload = el('btn-achv-reload'); + if (reload) reload.addEventListener('click', function () { loadAchvCatalog(); }); + var resetDef = el('btn-achv-reset-default'); + if (resetDef) resetDef.addEventListener('click', function () { + if (!confirm('คืนค่าแคตตาล็อกกลับเป็นค่าเริ่มต้น 25 รายการ? (ค่าที่แก้ไว้จะหาย)')) return; + api('achievements.php', { method: 'POST', body: { action: 'resetCatalog' } }) + .then(function () { setMsg('achv-catalog-msg', 'คืนค่าเริ่มต้นแล้ว', 'ok'); return loadAchvCatalog(); }) + .catch(function (err) { setMsg('achv-catalog-msg', err.message, 'error'); }); + }); + var pr = el('btn-achv-players-refresh'); if (pr) pr.addEventListener('click', function () { loadAchvPlayers(); }); + var ps = el('achv-player-search'); if (ps) ps.addEventListener('input', function () { loadAchvPlayers(); }); + var rpb = el('btn-achv-player-reset'); + if (rpb) rpb.addEventListener('click', function () { + if (!achvCurrentPlayer) return; + if (!confirm('รีเซ็ต achievement ทั้งหมดของผู้เล่นนี้?')) return; + api('achievements.php', { method: 'POST', body: { action: 'resetPlayer', playerKey: achvCurrentPlayer } }) + .then(function () { setMsg('achv-players-msg', 'รีเซ็ตแล้ว', 'ok'); openAchvPlayerDetail(achvCurrentPlayer, el('achv-player-detail-name') ? el('achv-player-detail-name').textContent : ''); loadAchvPlayers(); }) + .catch(function (err) { setMsg('achv-players-msg', err.message, 'error'); }); + }); + } + return loadAchvCatalog(); + } + + 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 + ? '—' + : ''; + /* สิทธิ์หมวด: super = ทุกหมวดเสมอ · tabs ว่าง = ทุกหมวด (บัญชีเก่า) */ + var permTxt; + if ((a.role || 'admin') === 'super') permTxt = 'ทุกหมวด (super)'; + else if (!Array.isArray(a.tabs) || !a.tabs.length) permTxt = 'ทุกหมวด'; + else permTxt = '' + a.tabs.length + ' หมวด'; + tr.innerHTML = + '' + escapeHtml(a.username) + '' + + '' + escapeHtml(a.role || 'admin') + '' + + '' + permTxt + '' + + '' + 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); + }); + } + + /* ===== สิทธิ์รายหมวด: ช่องติ๊ก + บันทึก [2026-07-22] ===== */ + var ADMIN_TAB_LABELS = null; /* data-tab -> ชื่อที่โชว์ (ดึงจากเมนูซ้ายจริง จะได้ไม่ต้องมี list ซ้ำ) */ + var adminPermCache = []; /* รายชื่อแอดมินล่าสุด (ไว้เติมช่องติ๊กตอนเลือกคน) */ + + function adminTabList() { + if (ADMIN_TAB_LABELS) return ADMIN_TAB_LABELS; + ADMIN_TAB_LABELS = []; + document.querySelectorAll('.tab').forEach(function (t) { + var name = t.getAttribute('data-tab'); + if (!name || name === 'change-password') return; /* เข้าได้เสมอ ไม่ต้องให้ติ๊ก */ + var lb = t.querySelector('.tab-label'); + ADMIN_TAB_LABELS.push({ name: name, label: (lb ? lb.textContent : name).trim() }); + }); + return ADMIN_TAB_LABELS; + } + + function buildAdminPermBoxes() { + var box = el('admin-perm-tabs'); + if (!box || box.dataset.built === '1') return; + box.dataset.built = '1'; + box.innerHTML = adminTabList().map(function (t) { + return ''; + }).join(''); + } + + function setAdminPermChecks(tabs) { + var set = {}; + (tabs || []).forEach(function (t) { set[t] = 1; }); + document.querySelectorAll('.adm-perm-cb').forEach(function (c) { c.checked = !!set[c.value]; }); + } + + function fillAdminPermSelect(admins, myId) { + var sel = el('select-admin-perm-target'); + if (!sel) return; + var keep = sel.value; + sel.innerHTML = ''; + (admins || []).forEach(function (a) { + if (!a.id) return; + var opt = document.createElement('option'); + opt.value = a.id; + opt.textContent = a.username + ' (' + (a.role || 'admin') + ')' + (a.id === myId ? ' — คุณ' : ''); + sel.appendChild(opt); + }); + if (keep) sel.value = keep; + } + + (function bindAdminPerms() { + var sel = el('select-admin-perm-target'); + if (!sel) return; + var msg = function (t, bad) { + var m = el('admin-perm-msg'); + if (m) { m.textContent = t || ''; m.style.color = bad ? '#ff8fa3' : ''; } + }; + sel.addEventListener('change', function () { + buildAdminPermBoxes(); + var a = adminPermCache.filter(function (x) { return x.id === sel.value; })[0]; + if (!a) { setAdminPermChecks([]); msg(''); return; } + setAdminPermChecks(a.tabs || []); + msg((a.role === 'super') ? 'บัญชี super เข้าได้ทุกหมวดเสมอ — แก้ตรงนี้ไม่มีผล' : ''); + }); + if (el('btn-admin-perm-all')) el('btn-admin-perm-all').addEventListener('click', function () { + buildAdminPermBoxes(); + document.querySelectorAll('.adm-perm-cb').forEach(function (c) { c.checked = true; }); + }); + if (el('btn-admin-perm-none')) el('btn-admin-perm-none').addEventListener('click', function () { + document.querySelectorAll('.adm-perm-cb').forEach(function (c) { c.checked = false; }); + }); + if (el('btn-admin-perm-save')) el('btn-admin-perm-save').addEventListener('click', function () { + if (!sel.value) { msg('เลือกแอดมินก่อน', true); return; } + var tabs = []; + document.querySelectorAll('.adm-perm-cb').forEach(function (c) { if (c.checked) tabs.push(c.value); }); + msg('กำลังบันทึก…'); + api('admins.php', { method: 'PATCH', body: { id: sel.value, tabs: tabs } }) + .then(function (r) { + msg(r.message || 'บันทึกแล้ว'); + return loadAdmins(); + }) + .catch(function (e) { msg('บันทึกไม่สำเร็จ: ' + e.message, true); }); + }); + })(); + + function loadAdmins() { + return api('admins.php').then(function (r) { + return api('session.php').then(function (s) { + var myId = s.admin && s.admin.id; + adminPermCache = r.admins || []; + renderAdmins(r.admins, myId); + fillAdminResetSelect(r.admins, myId); + buildAdminPermBoxes(); + fillAdminPermSelect(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, 'vote-timing': true, troublesome: 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) { + /* สิทธิ์รายหมวด — ใช้ admin ที่ boot() คืนมา (มี tabsEffective) ไม่ต้องยิง session.php ซ้ำ */ + if (admin && Array.isArray(admin.tabsEffective)) applyTabPermissions(admin.tabsEffective); + if (typeof window.__qbModesLoad === 'function') { + try { window.__qbModesLoad(); } catch (e) { /* ไม่ให้ล้มทั้งหน้า */ } + } + 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/Admin/api/_common.php b/www/html/Admin/api/_common.php index 9fc99e8..a5da1eb 100644 --- a/www/html/Admin/api/_common.php +++ b/www/html/Admin/api/_common.php @@ -111,9 +111,95 @@ function is_super(array $admin): bool return ($admin['role'] ?? '') === 'super'; } +/** + * รายชื่อหมวด (tab) ทั้งหมดในหน้า admin — ต้องตรงกับ data-tab ใน Admin/index.html + * ใช้เป็น whitelist เวลาบันทึกสิทธิ์ กันค่ามั่วหลุดเข้า store + */ +function admin_all_tabs(): array +{ + return [ + 'accounts', 'admins', 'oauth', 'change-password', + 'achievements', 'ai-admin', 'case-media', 'characters', 'evidence-cards', + 'game-timing', 'highscore', 'map-editor', 'postcase', 'qb-map-editor', + 'quiz', 'quiz-battle', 'quiz-carry', 'sound', 'special-quiz', + 'jump-survive', 'mega-virus', 'space-shooter', 'stack-game', + 'test-mode', 'troublesome', 'vote-timing', + ]; +} + +/** + * หมวดที่แอดมินคนนี้เข้าได้จริง + * - super = ทุกหมวดเสมอ + * - ไม่มีคีย์ tabs หรือ tabs ว่าง = ทุกหมวด (ความเข้ากันได้กับบัญชีเดิมที่สร้างก่อนมีระบบสิทธิ์) + */ +function admin_tabs(array $a): array +{ + if (is_super($a)) { + return admin_all_tabs(); + } + $t = $a['tabs'] ?? null; + if (!is_array($t) || count($t) === 0) { + return admin_all_tabs(); + } + return array_values(array_intersect(admin_all_tabs(), $t)); +} + +function admin_can(array $a, string $tab): bool +{ + return in_array($tab, admin_tabs($a), true); +} + +/** กันที่ API: แอดมินที่ไม่มีสิทธิ์หมวดนี้ ยิงตรงมาก็ต้องโดนปฏิเสธ (ซ่อน tab ฝั่งหน้าเว็บอย่างเดียวไม่ใช่ security) */ +function require_tab(string $tab): array +{ + $a = current_admin(); + if (!$a) { + json_response(['ok' => false, 'error' => 'Unauthorized'], 401); + } + if (!admin_can($a, $tab)) { + json_response(['ok' => false, 'error' => 'บัญชีนี้ไม่มีสิทธิ์เข้าหมวด "' . $tab . '"'], 403); + } + return $a; +} + +/** + * บาง endpoint เป็น "config ก้อนเดียวที่หลายหมวดใช้ร่วมกัน" (เช่น quiz-settings.json) + * ถ้า gate ไว้หมวดเดียวจะพังหมวดอื่น → ผ่านถ้ามีสิทธิ์ "อย่างน้อย 1 หมวด" ในลิสต์ + */ +function require_any_tab(array $tabs): array +{ + $a = current_admin(); + if (!$a) { + json_response(['ok' => false, 'error' => 'Unauthorized'], 401); + } + foreach ($tabs as $t) { + if (admin_can($a, $t)) { + return $a; + } + } + json_response(['ok' => false, 'error' => 'บัญชีนี้ไม่มีสิทธิ์เข้าหมวดที่เกี่ยวข้อง'], 403); +} + +/** normalize ค่า tabs ที่รับมาจาก client ให้เหลือเฉพาะที่อยู่ใน whitelist */ +function sanitize_tabs($raw): array +{ + if (!is_array($raw)) { + return []; + } + $all = admin_all_tabs(); + $out = []; + foreach ($raw as $t) { + if (is_string($t) && in_array($t, $all, true) && !in_array($t, $out, true)) { + $out[] = $t; + } + } + return $out; +} + function strip_admin(array $a): array { unset($a['passwordHash']); + $a['tabsEffective'] = admin_tabs($a); return $a; } diff --git a/www/html/Admin/api/_player_account.php b/www/html/Admin/api/_player_account.php new file mode 100644 index 0000000..4bff9d1 --- /dev/null +++ b/www/html/Admin/api/_player_account.php @@ -0,0 +1,116 @@ + $a) { + if (($a['providerUserId'] ?? '') === $key) { + return (int) $i; + } + } + return -1; +} + +function find_account_by_player_key(array $accounts, string $key): ?array +{ + $idx = find_account_index_by_player_key($accounts, $key); + return $idx >= 0 ? $accounts[$idx] : null; +} + +function default_guest_account(string $key, string $notes = 'auto', array $extra = []): array +{ + $now = gmdate('c'); + return array_merge([ + 'id' => new_id(), + 'email' => '', + 'displayName' => 'Guest', + 'loginType' => 'guest', + 'providerUserId' => $key, + 'notes' => $notes, + 'blocked' => false, + 'coins' => 0, + 'createdAt' => $now, + 'updatedAt' => $now, + ], $extra); +} + +function google_player_key(string $googleSub): string +{ + $sub = trim($googleSub); + if ($sub === '') { + return ''; + } + if (str_starts_with($sub, 'g_')) { + return $sub; + } + return 'g_' . $sub; +} + +function merge_guest_account_into(array &$store, int $targetIdx, string $guestKey): void +{ + if ($guestKey === '' || !valid_player_key($guestKey)) { + return; + } + $guestIdx = find_account_index_by_player_key($store['accounts'] ?? [], $guestKey); + if ($guestIdx < 0 || $guestIdx === $targetIdx) { + return; + } + $guest = $store['accounts'][$guestIdx]; + if (($guest['loginType'] ?? '') !== 'guest') { + return; + } + + $target = &$store['accounts'][$targetIdx]; + $target['coins'] = max(0, (int) ($target['coins'] ?? 0)) + max(0, (int) ($guest['coins'] ?? 0)); + $target['score'] = max(0, (int) ($target['score'] ?? 0)) + max(0, (int) ($guest['score'] ?? 0)); + + if (isset($guest['scoreByCase']) && is_array($guest['scoreByCase'])) { + if (!isset($target['scoreByCase']) || !is_array($target['scoreByCase'])) { + $target['scoreByCase'] = []; + } + foreach ($guest['scoreByCase'] as $cid => $sc) { + $target['scoreByCase'][$cid] = max(0, (int) ($target['scoreByCase'][$cid] ?? 0)) + max(0, (int) $sc); + } + } + + if (isset($guest['achievements']) && is_array($guest['achievements'])) { + if (!isset($target['achievements']) || !is_array($target['achievements'])) { + $target['achievements'] = []; + } + foreach ($guest['achievements'] as $aid => $cnt) { + $target['achievements'][$aid] = max((int) ($target['achievements'][$aid] ?? 0), (int) $cnt); + } + } + + if (isset($guest['daily']) && is_array($guest['daily']) && empty($target['daily'])) { + $target['daily'] = $guest['daily']; + } + + if (!isset($target['lobbyColorThemeIndex']) && isset($guest['lobbyColorThemeIndex'])) { + $target['lobbyColorThemeIndex'] = $guest['lobbyColorThemeIndex']; + } + if (!isset($target['lobbySkinToneIndex']) && isset($guest['lobbySkinToneIndex'])) { + $target['lobbySkinToneIndex'] = $guest['lobbySkinToneIndex']; + } + + if (($target['displayName'] ?? '') === '' || ($target['displayName'] ?? '') === 'Guest') { + $gn = trim((string) ($guest['displayName'] ?? '')); + if ($gn !== '' && $gn !== 'Guest') { + $target['displayName'] = $gn; + } + } + + array_splice($store['accounts'], $guestIdx, 1); + if ($guestIdx < $targetIdx) { + $targetIdx--; + } +} diff --git a/www/html/Admin/api/_player_log.php b/www/html/Admin/api/_player_log.php new file mode 100644 index 0000000..883d069 --- /dev/null +++ b/www/html/Admin/api/_player_log.php @@ -0,0 +1,317 @@ + 1, + 'stats' => [ + 'totalEvents' => 0, + 'totalVisits' => 0, + 'uniquePlayers' => 0, + 'visitsToday' => 0, + 'todayKey' => '', + ], + 'players' => [], + 'events' => [], + ]; +} + +function player_log_read(): array +{ + if (!is_file(PLAYER_LOG_FILE)) { + return player_log_default(); + } + $raw = @file_get_contents(PLAYER_LOG_FILE); + $j = json_decode($raw ?: '{}', true); + if (!is_array($j)) { + return player_log_default(); + } + return array_replace_recursive(player_log_default(), $j); +} + +function player_log_write(array $data): bool +{ + if (!is_dir(ADMIN_PRIVATE_DIR)) { + if (!@mkdir(ADMIN_PRIVATE_DIR, 0750, true)) { + return false; + } + } + $tmp = PLAYER_LOG_FILE . '.tmp.' . bin2hex(random_bytes(4)); + $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + if ($json === false) { + return false; + } + if (file_put_contents($tmp, $json, LOCK_EX) === false) { + return false; + } + if (!rename($tmp, PLAYER_LOG_FILE)) { + @unlink($tmp); + return false; + } + return true; +} + +function player_log_today_key(): string +{ + $tz = new DateTimeZone('Asia/Bangkok'); + return (new DateTimeImmutable('now', $tz))->format('Y-m-d'); +} + +function player_log_reset_today_if_needed(array &$log): void +{ + $today = player_log_today_key(); + if (($log['stats']['todayKey'] ?? '') !== $today) { + $log['stats']['todayKey'] = $today; + $log['stats']['visitsToday'] = 0; + } +} + +function player_log_allowed_events(): array +{ + return [ + 'login_guest' => true, + 'login_google' => true, + 'login_facebook' => true, + 'lobby_enter' => true, + 'room_enter' => true, + 'game_enter' => true, + 'quiz_battle_enter' => true, + 'logout' => true, + ]; +} + +function player_log_sanitize_name(string $raw): string +{ + $s = trim($raw); + $s = preg_replace('/[\x00-\x1f\x7f]/u', '', $s) ?? ''; + if (function_exists('mb_substr')) { + return mb_substr($s, 0, 32); + } + return substr($s, 0, 32); +} + +function player_log_sanitize_page(string $raw): string +{ + $s = trim($raw); + if (function_exists('mb_substr')) { + return mb_substr($s, 0, 180); + } + return substr($s, 0, 180); +} + +/** event ที่นับเป็น "การเข้าใช้งาน" */ +function player_log_counts_as_visit(string $event): bool +{ + return isset([ + 'login_guest' => true, + 'login_google' => true, + 'login_facebook' => true, + 'lobby_enter' => true, + 'room_enter' => true, + 'game_enter' => true, + ][$event]); +} + +function player_log_track( + string $playerKey, + string $event, + string $displayName = '', + string $loginType = 'guest', + string $page = '', + array $meta = [] +): array { + $allowed = player_log_allowed_events(); + if (!isset($allowed[$event])) { + return ['ok' => false, 'error' => 'unknown event']; + } + if (!valid_player_key($playerKey)) { + return ['ok' => false, 'error' => 'invalid playerKey']; + } + + $lt = in_array($loginType, ['guest', 'google', 'facebook', 'email'], true) ? $loginType : 'guest'; + $name = player_log_sanitize_name($displayName); + $page = player_log_sanitize_page($page); + $now = gmdate('c'); + $nowTs = time(); + + $log = player_log_read(); + player_log_reset_today_if_needed($log); + + if (!isset($log['players']) || !is_array($log['players'])) { + $log['players'] = []; + } + if (!isset($log['events']) || !is_array($log['events'])) { + $log['events'] = []; + } + + $isNewPlayer = !isset($log['players'][$playerKey]); + if ($isNewPlayer) { + $log['players'][$playerKey] = [ + 'playerKey' => $playerKey, + 'displayName' => $name !== '' ? $name : 'ผู้เล่น', + 'loginType' => $lt, + 'visitCount' => 0, + 'eventCount' => 0, + 'firstSeenAt' => $now, + 'lastSeenAt' => $now, + 'lastVisitAt' => null, + 'lastEvent' => $event, + 'lastPage' => $page, + ]; + $log['stats']['uniquePlayers'] = count($log['players']); + } + + $p = &$log['players'][$playerKey]; + if ($name !== '') { + $p['displayName'] = $name; + } + $p['loginType'] = $lt; + $p['lastSeenAt'] = $now; + $p['lastEvent'] = $event; + if ($page !== '') { + $p['lastPage'] = $page; + } + $p['eventCount'] = max(0, (int) ($p['eventCount'] ?? 0)) + 1; + + $countVisit = false; + if (player_log_counts_as_visit($event)) { + $lastVisitAt = isset($p['lastVisitAt']) ? strtotime((string) $p['lastVisitAt']) : false; + if ($lastVisitAt === false || ($nowTs - $lastVisitAt) >= PLAYER_LOG_SESSION_GAP_SEC) { + $countVisit = true; + $p['visitCount'] = max(0, (int) ($p['visitCount'] ?? 0)) + 1; + $p['lastVisitAt'] = $now; + $log['stats']['totalVisits'] = max(0, (int) ($log['stats']['totalVisits'] ?? 0)) + 1; + $log['stats']['visitsToday'] = max(0, (int) ($log['stats']['visitsToday'] ?? 0)) + 1; + } + } + + $log['stats']['totalEvents'] = max(0, (int) ($log['stats']['totalEvents'] ?? 0)) + 1; + + $ev = [ + 'id' => bin2hex(random_bytes(8)), + 'at' => $now, + 'playerKey' => $playerKey, + 'displayName' => (string) ($p['displayName'] ?? ''), + 'loginType' => $lt, + 'event' => $event, + 'page' => $page, + 'meta' => is_array($meta) ? $meta : [], + 'countedVisit' => $countVisit, + ]; + array_unshift($log['events'], $ev); + if (count($log['events']) > PLAYER_LOG_MAX_EVENTS) { + $log['events'] = array_slice($log['events'], 0, PLAYER_LOG_MAX_EVENTS); + } + + if (!player_log_write($log)) { + return ['ok' => false, 'error' => 'บันทึก log ไม่สำเร็จ']; + } + + return [ + 'ok' => true, + 'countedVisit' => $countVisit, + 'visitCount' => (int) ($p['visitCount'] ?? 0), + ]; +} + +function player_log_public_summary(array $log): array +{ + player_log_reset_today_if_needed($log); + return [ + 'totalEvents' => max(0, (int) ($log['stats']['totalEvents'] ?? 0)), + 'totalVisits' => max(0, (int) ($log['stats']['totalVisits'] ?? 0)), + 'uniquePlayers' => max(0, (int) ($log['stats']['uniquePlayers'] ?? count($log['players'] ?? []))), + 'visitsToday' => max(0, (int) ($log['stats']['visitsToday'] ?? 0)), + 'todayKey' => (string) ($log['stats']['todayKey'] ?? player_log_today_key()), + 'eventCap' => PLAYER_LOG_MAX_EVENTS, + 'storedEvents' => count($log['events'] ?? []), + ]; +} + +function player_log_players_list(array $log, string $search = '', int $limit = 200): array +{ + $rows = []; + foreach ($log['players'] ?? [] as $p) { + if (!is_array($p)) { + continue; + } + $key = (string) ($p['playerKey'] ?? ''); + if ($key === '') { + continue; + } + if ($search !== '') { + $hay = strtolower($key . ' ' . ($p['displayName'] ?? '') . ' ' . ($p['loginType'] ?? '')); + if (strpos($hay, strtolower($search)) === false) { + continue; + } + } + $rows[] = [ + 'playerKey' => $key, + 'displayName' => (string) ($p['displayName'] ?? 'ผู้เล่น'), + 'loginType' => (string) ($p['loginType'] ?? 'guest'), + 'visitCount' => max(0, (int) ($p['visitCount'] ?? 0)), + 'eventCount' => max(0, (int) ($p['eventCount'] ?? 0)), + 'firstSeenAt' => (string) ($p['firstSeenAt'] ?? ''), + 'lastSeenAt' => (string) ($p['lastSeenAt'] ?? ''), + 'lastVisitAt' => (string) ($p['lastVisitAt'] ?? ''), + 'lastEvent' => (string) ($p['lastEvent'] ?? ''), + 'lastPage' => (string) ($p['lastPage'] ?? ''), + ]; + } + usort($rows, static function ($a, $b) { + return strcmp((string) ($b['lastSeenAt'] ?? ''), (string) ($a['lastSeenAt'] ?? '')); + }); + if ($limit > 0 && count($rows) > $limit) { + $rows = array_slice($rows, 0, $limit); + } + return $rows; +} + +function player_log_events_list(array $log, array $opts = []): array +{ + $search = trim((string) ($opts['search'] ?? '')); + $event = trim((string) ($opts['event'] ?? '')); + $playerKey = trim((string) ($opts['playerKey'] ?? '')); + $limit = max(1, min(500, (int) ($opts['limit'] ?? 100))); + $offset = max(0, (int) ($opts['offset'] ?? 0)); + + $rows = []; + foreach ($log['events'] ?? [] as $ev) { + if (!is_array($ev)) { + continue; + } + if ($event !== '' && ($ev['event'] ?? '') !== $event) { + continue; + } + if ($playerKey !== '' && ($ev['playerKey'] ?? '') !== $playerKey) { + continue; + } + if ($search !== '') { + $hay = strtolower( + ($ev['playerKey'] ?? '') . ' ' . + ($ev['displayName'] ?? '') . ' ' . + ($ev['event'] ?? '') . ' ' . + ($ev['page'] ?? '') + ); + if (strpos($hay, strtolower($search)) === false) { + continue; + } + } + $rows[] = $ev; + } + $total = count($rows); + if ($offset > 0) { + $rows = array_slice($rows, $offset); + } + if (count($rows) > $limit) { + $rows = array_slice($rows, 0, $limit); + } + return ['rows' => $rows, 'total' => $total]; +} diff --git a/www/html/Admin/api/accounts.php b/www/html/Admin/api/accounts.php index aa78d24..fde1721 100644 --- a/www/html/Admin/api/accounts.php +++ b/www/html/Admin/api/accounts.php @@ -1,119 +1,120 @@ - true, 'accounts' => $list]); -} - -if ($method === 'POST') { - $body = require_json_body(); - $store = read_store(); - $coins = isset($body['coins']) ? (int)$body['coins'] : 0; - $coins = max(0, $coins); - $acc = [ - 'id' => new_id(), - 'email' => trim((string)($body['email'] ?? '')), - 'displayName' => trim((string)($body['displayName'] ?? '')), - 'loginType' => in_array($body['loginType'] ?? '', ['guest', 'facebook', 'google', 'email'], true) - ? $body['loginType'] : 'guest', - 'providerUserId' => trim((string)($body['providerUserId'] ?? '')), - 'notes' => trim((string)($body['notes'] ?? '')), - 'blocked' => !empty($body['blocked']), - 'coins' => $coins, - 'createdAt' => gmdate('c'), - 'updatedAt' => gmdate('c'), - ]; - $store['accounts'][] = $acc; - if (!write_store($store)) { - json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); - } - json_response(['ok' => true, 'account' => $acc]); -} - -if ($method === 'PATCH') { - $body = require_json_body(); - $id = trim((string)($body['id'] ?? '')); - if ($id === '') { - json_response(['ok' => false, 'error' => 'ระบุ id'], 400); - } - $store = read_store(); - $found = false; - foreach ($store['accounts'] ?? [] as $i => $a) { - if (($a['id'] ?? '') !== $id) { - continue; - } - $found = true; - if (array_key_exists('email', $body)) { - $store['accounts'][$i]['email'] = trim((string)$body['email']); - } - if (array_key_exists('displayName', $body)) { - $store['accounts'][$i]['displayName'] = trim((string)$body['displayName']); - } - if (array_key_exists('loginType', $body)) { - $lt = $body['loginType']; - if (in_array($lt, ['guest', 'facebook', 'google', 'email'], true)) { - $store['accounts'][$i]['loginType'] = $lt; - } - } - if (array_key_exists('providerUserId', $body)) { - $store['accounts'][$i]['providerUserId'] = trim((string)$body['providerUserId']); - } - if (array_key_exists('notes', $body)) { - $store['accounts'][$i]['notes'] = trim((string)$body['notes']); - } - if (array_key_exists('blocked', $body)) { - $store['accounts'][$i]['blocked'] = !empty($body['blocked']); - } - if (array_key_exists('coins', $body)) { - $store['accounts'][$i]['coins'] = max(0, (int)$body['coins']); - } - if (array_key_exists('score', $body)) { - $store['accounts'][$i]['score'] = max(0, (int)$body['score']); - } - if (array_key_exists('coinsDelta', $body)) { - $cur = max(0, (int)($store['accounts'][$i]['coins'] ?? 0)); - $store['accounts'][$i]['coins'] = max(0, $cur + (int)$body['coinsDelta']); - } - $store['accounts'][$i]['updatedAt'] = gmdate('c'); - break; - } - if (!$found) { - json_response(['ok' => false, 'error' => 'ไม่พบบัญชี'], 404); - } - if (!write_store($store)) { - json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); - } - json_response(['ok' => true]); -} - -if ($method === 'DELETE') { - $id = trim((string)($_GET['id'] ?? '')); - if ($id === '') { - json_response(['ok' => false, 'error' => 'ระบุ id'], 400); - } - $store = read_store(); - $store['accounts'] = array_values(array_filter( - $store['accounts'] ?? [], - static fn($a) => ($a['id'] ?? '') !== $id - )); - if (!write_store($store)) { - json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); - } - json_response(['ok' => true]); -} - -json_response(['ok' => false, 'error' => 'Method not allowed'], 405); + true, 'accounts' => $list]); +} + +if ($method === 'POST') { + $body = require_json_body(); + $store = read_store(); + $coins = isset($body['coins']) ? (int)$body['coins'] : 0; + $coins = max(0, $coins); + $acc = [ + 'id' => new_id(), + 'email' => trim((string)($body['email'] ?? '')), + 'displayName' => trim((string)($body['displayName'] ?? '')), + 'loginType' => in_array($body['loginType'] ?? '', ['guest', 'facebook', 'google', 'email'], true) + ? $body['loginType'] : 'guest', + 'providerUserId' => trim((string)($body['providerUserId'] ?? '')), + 'notes' => trim((string)($body['notes'] ?? '')), + 'blocked' => !empty($body['blocked']), + 'coins' => $coins, + 'createdAt' => gmdate('c'), + 'updatedAt' => gmdate('c'), + ]; + $store['accounts'][] = $acc; + if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); + } + json_response(['ok' => true, 'account' => $acc]); +} + +if ($method === 'PATCH') { + $body = require_json_body(); + $id = trim((string)($body['id'] ?? '')); + if ($id === '') { + json_response(['ok' => false, 'error' => 'ระบุ id'], 400); + } + $store = read_store(); + $found = false; + foreach ($store['accounts'] ?? [] as $i => $a) { + if (($a['id'] ?? '') !== $id) { + continue; + } + $found = true; + if (array_key_exists('email', $body)) { + $store['accounts'][$i]['email'] = trim((string)$body['email']); + } + if (array_key_exists('displayName', $body)) { + $store['accounts'][$i]['displayName'] = trim((string)$body['displayName']); + } + if (array_key_exists('loginType', $body)) { + $lt = $body['loginType']; + if (in_array($lt, ['guest', 'facebook', 'google', 'email'], true)) { + $store['accounts'][$i]['loginType'] = $lt; + } + } + if (array_key_exists('providerUserId', $body)) { + $store['accounts'][$i]['providerUserId'] = trim((string)$body['providerUserId']); + } + if (array_key_exists('notes', $body)) { + $store['accounts'][$i]['notes'] = trim((string)$body['notes']); + } + if (array_key_exists('blocked', $body)) { + $store['accounts'][$i]['blocked'] = !empty($body['blocked']); + } + if (array_key_exists('coins', $body)) { + $store['accounts'][$i]['coins'] = max(0, (int)$body['coins']); + } + if (array_key_exists('score', $body)) { + $store['accounts'][$i]['score'] = max(0, (int)$body['score']); + } + if (array_key_exists('coinsDelta', $body)) { + $cur = max(0, (int)($store['accounts'][$i]['coins'] ?? 0)); + $store['accounts'][$i]['coins'] = max(0, $cur + (int)$body['coinsDelta']); + } + $store['accounts'][$i]['updatedAt'] = gmdate('c'); + break; + } + if (!$found) { + json_response(['ok' => false, 'error' => 'ไม่พบบัญชี'], 404); + } + if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); + } + json_response(['ok' => true]); +} + +if ($method === 'DELETE') { + $id = trim((string)($_GET['id'] ?? '')); + if ($id === '') { + json_response(['ok' => false, 'error' => 'ระบุ id'], 400); + } + $store = read_store(); + $store['accounts'] = array_values(array_filter( + $store['accounts'] ?? [], + static fn($a) => ($a['id'] ?? '') !== $id + )); + if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); + } + json_response(['ok' => true]); +} + +json_response(['ok' => false, 'error' => 'Method not allowed'], 405); diff --git a/www/html/Admin/api/achievements.php b/www/html/Admin/api/achievements.php index f95df4f..c274970 100644 --- a/www/html/Admin/api/achievements.php +++ b/www/html/Admin/api/achievements.php @@ -1,382 +1,397 @@ - accounts[].achievements { id: count } - * - * Public (ไม่ต้องล็อกอิน): - * GET ?action=state&playerKey=KEY -> { ok, catalog:[...], progress:{id:count} } - * GET ?action=catalog -> { ok, catalog:[...] } - * - * Server-only (มี secret game-award-secret.txt) — สำหรับ auto-track ภายหลัง: - * POST {action:'progress', secret, playerKey, id, inc?, set?} - * - * Admin (ต้องล็อกอิน session): - * POST {action:'saveCatalog', catalog:[...]} - * GET ?action=players -> รายชื่อผู้เล่น + จำนวนที่ปลดล็อก - * GET ?action=player&playerKey=KEY -> progress ของผู้เล่นคนเดียว - * POST {action:'setProgress', playerKey, id, value} - * POST {action:'unlock', playerKey, id} -> set = target - * POST {action:'resetPlayer', playerKey} -> ล้าง progress ทั้งหมด - */ -require __DIR__ . '/_common.php'; - -date_default_timezone_set('Asia/Bangkok'); - -define('ACHV_CATALOG_FILE', ADMIN_PRIVATE_DIR . '/achievements.json'); - -function achv_default_catalog(): array -{ - return [ - ['id' => 'a1_first_deduction', 'g' => 1, 'title' => 'First Deduction', 'desc' => 'โหวตถูกตัวคนร้ายเป็นครั้งแรก', 'target' => 1, 'enabled' => true], - ['id' => 'a2_sharp_eye', 'g' => 1, 'title' => 'Sharp Eye', 'desc' => 'สะสมหลักฐานระดับมีน้ำหนัก (Silver) ครบ 10 ใบ', 'target' => 10, 'enabled' => true], - ['id' => 'a3_mind_architect', 'g' => 1, 'title' => 'Mind Architect', 'desc' => 'สะสมหลักฐานครบทุกระดับ (ทั่วไป, มีน้ำหนัก, ชี้ชัด) ใน 1 คดี', 'target' => 1, 'enabled' => true], - ['id' => 'a4_logic_over_luck', 'g' => 1, 'title' => 'Logic Over Luck', 'desc' => 'โหวตถูกโดยไม่พึ่งหลักฐานชี้ชัด (Legendary) เลย 3 ครั้ง', 'target' => 3, 'enabled' => true], - ['id' => 'a5_truth_hunter', 'g' => 1, 'title' => 'Truth Hunter', 'desc' => 'จับคนร้ายถูกตัวสะสมครบ 20 คดี', 'target' => 20, 'enabled' => true], - ['id' => 'a6_unbreakable_logic', 'g' => 1, 'title' => 'Unbreakable Logic', 'desc' => 'โหวตถูกตัวติดกัน 5 คดีรวด', 'target' => 5, 'enabled' => true], - - ['id' => 'b1_evidence_collector', 'g' => 2, 'title' => 'Evidence Collector', 'desc' => 'สะสมหลักฐานระดับทั่วไป (Common) ครบ 20 ใบ', 'target' => 20, 'enabled' => true], - ['id' => 'b2_relentless_investigator', 'g' => 2, 'title' => 'Relentless Investigator', 'desc' => 'เล่น Mini Game ครบทุกรอบ ใน 1 คดี', 'target' => 1, 'enabled' => true], - ['id' => 'b3_hidden_fragment', 'g' => 2, 'title' => 'Hidden Fragment', 'desc' => 'ค้นพบหลักฐานระดับชี้ชัด (Legendary/Gold) เป็นครั้งแรก', 'target' => 1, 'enabled' => true], - ['id' => 'b4_data_miner', 'g' => 2, 'title' => 'Data Miner', 'desc' => 'สะสมการ์ดหลักฐานรวมครบ 100 ใบ', 'target' => 100, 'enabled' => true], - ['id' => 'b5_deep_scanner', 'g' => 2, 'title' => 'Deep Scanner', 'desc' => 'เก็บไอเทมช่วยเหลือ (ตำรวจ/ทนาย) สะสมครบ 10 ครั้ง', 'target' => 10, 'enabled' => true], - - ['id' => 'c1_early_accusation', 'g' => 3, 'title' => 'Early Accusation', 'desc' => 'ชี้ตัวคนร้ายก่อนที่จะเปิดหลักฐานครบ', 'target' => 1, 'enabled' => true], - ['id' => 'c2_high_stakes', 'g' => 3, 'title' => 'High Stakes', 'desc' => 'ชี้ตัวคนร้ายถูกโดยมีหลักฐานในมือไม่เกิน 3 ใบ', 'target' => 1, 'enabled' => true], - ['id' => 'c3_quick_draw', 'g' => 3, 'title' => 'Quick Draw', 'desc' => 'ชี้ตัวคนร้ายเร็วที่สุดในทีมสะสมครบ 10 ครั้ง', 'target' => 10, 'enabled' => true], - ['id' => 'c4_clutch_mind', 'g' => 3, 'title' => 'Clutch Mind', 'desc' => 'โหวตถูกในช่วง 10 วินาทีสุดท้ายก่อนหมดเวลา', 'target' => 1, 'enabled' => true], - ['id' => 'c5_lone_wolf', 'g' => 3, 'title' => 'Lone Wolf', 'desc' => 'โหวตสวนทางกับเสียงส่วนใหญ่ของทีม (คุณถูกคนเดียว)', 'target' => 1, 'enabled' => true], - - ['id' => 'd1_minigame_solver', 'g' => 4, 'title' => 'Minigame Solver', 'desc' => 'เอาชีวิตรอด / เล่นมินิเกมสำเร็จ 20 ครั้ง', 'target' => 20, 'enabled' => true], - ['id' => 'd2_silent_guardian', 'g' => 4, 'title' => 'Silent Guardian', 'desc' => 'ไม่โดนโหวตใช้การ์ด Event พิเศษ เลยตลอด 5 คดี', 'target' => 5, 'enabled' => true], - ['id' => 'd3_the_backbone', 'g' => 4, 'title' => 'The Backbone', 'desc' => 'ส่งมอบหลักฐานให้เพื่อนวิเคราะห์ครบ 30 ใบ', 'target' => 30, 'enabled' => true], - ['id' => 'd4_flawless_diver', 'g' => 4, 'title' => 'Flawless Diver', 'desc' => 'ไม่โหวตจับผิดตัวเลยตลอด 15 คดี', 'target' => 15, 'enabled' => true], - - ['id' => 'e1_the_observer', 'g' => 5, 'title' => 'The Observer', 'desc' => 'เล่นจบ 15 คดีโดยไม่เคยสัมผัสหลักฐานชี้ชัด (Legendary) เลยสักครั้ง', 'target' => 15, 'enabled' => true], - ['id' => 'e2_the_impostor', 'g' => 5, 'title' => 'The Impostor', 'desc' => 'รับบทเป็น "ตัวป่วน" ครั้งแรก', 'target' => 1, 'enabled' => true], - ['id' => 'e3_master_of_doubt', 'g' => 5, 'title' => 'Master of Doubt', 'desc' => 'เอาชนะคดีในฐานะตัวป่วนได้สำเร็จ', 'target' => 1, 'enabled' => true], - ['id' => 'e4_agent_of_chaos', 'g' => 5, 'title' => 'Agent of Chaos', 'desc' => 'รับบทเป็นตัวป่วนสะสมครบ 10 ครั้ง', 'target' => 10, 'enabled' => true], - ['id' => 'e5_slippery_eel', 'g' => 5, 'title' => 'Slippery Eel', 'desc' => 'เป็นตัวป่วนแต่รอดพ้นจากการถูกจับได้ (ไม่ถูกโหวตออก) จนจบเกม 5 ครั้ง', 'target' => 5, 'enabled' => true], - ]; -} - -function achv_sanitize_catalog(array $arr): array -{ - $out = []; - $seen = []; - foreach ($arr as $row) { - if (!is_array($row)) continue; - $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($row['id'] ?? '')); - if ($id === '' || isset($seen[$id])) continue; - $seen[$id] = true; - $g = (int) ($row['g'] ?? 1); - if ($g < 1 || $g > 5) $g = 1; - $title = trim((string) ($row['title'] ?? '')); - $desc = trim((string) ($row['desc'] ?? '')); - if (function_exists('mb_substr')) { - $title = mb_substr($title, 0, 60); - $desc = mb_substr($desc, 0, 200); - } - $target = (int) ($row['target'] ?? 1); - if ($target < 1) $target = 1; - if ($target > 100000) $target = 100000; - $out[] = [ - 'id' => $id, - 'g' => $g, - 'title' => $title !== '' ? $title : $id, - 'desc' => $desc, - 'target' => $target, - 'enabled' => !isset($row['enabled']) || !empty($row['enabled']), - ]; - } - return $out; -} - -function achv_load_catalog(): array -{ - if (is_file(ACHV_CATALOG_FILE)) { - $raw = @file_get_contents(ACHV_CATALOG_FILE); - $j = json_decode($raw ?: '[]', true); - if (is_array($j) && $j) { - $c = achv_sanitize_catalog($j); - if ($c) return $c; - } - } - return achv_default_catalog(); -} - -function achv_save_catalog(array $catalog): bool -{ - if (!is_dir(ADMIN_PRIVATE_DIR)) { - if (!@mkdir(ADMIN_PRIVATE_DIR, 0750, true)) return false; - } - $tmp = ACHV_CATALOG_FILE . '.tmp.' . bin2hex(random_bytes(4)); - $json = json_encode($catalog, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - if ($json === false) return false; - if (file_put_contents($tmp, $json, LOCK_EX) === false) return false; - if (!rename($tmp, ACHV_CATALOG_FILE)) { @unlink($tmp); return false; } - return true; -} - -function achv_valid_key(string $key): bool -{ - return (bool) preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key); -} - -/* หา index บัญชี guest ตาม playerKey (สร้างใหม่ถ้าไม่มี) */ -function achv_find_or_create(array &$store, string $key): int -{ - foreach ($store['accounts'] as $i => $a) { - if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) { - return $i; - } - } - $store['accounts'][] = [ - 'id' => new_id(), - 'email' => '', - 'displayName' => 'Guest', - 'loginType' => 'guest', - 'providerUserId' => $key, - 'notes' => 'auto: achievements', - 'blocked' => false, - 'coins' => 0, - 'achievements' => new \stdClass(), - 'createdAt' => gmdate('c'), - 'updatedAt' => gmdate('c'), - ]; - return count($store['accounts']) - 1; -} - -function achv_progress_of(array $account): array -{ - $p = $account['achievements'] ?? []; - if (!is_array($p)) return []; - $out = []; - foreach ($p as $k => $v) { - $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $k); - if ($id === '') continue; - $out[$id] = max(0, (int) $v); - } - return $out; -} - -$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; -$action = (string) ($_GET['action'] ?? ''); -if ($method === 'POST') { - $body = require_json_body(); - if (!$action) $action = (string) ($body['action'] ?? ''); -} else { - $body = []; -} - -/* ---------- Public ---------- */ -if ($action === 'catalog' && $method === 'GET') { - json_response(['ok' => true, 'catalog' => achv_load_catalog()]); -} - -if ($action === 'state' && $method === 'GET') { - $key = trim((string) ($_GET['playerKey'] ?? '')); - if (!achv_valid_key($key)) { - json_response(['ok' => false, 'error' => 'playerKey ไม่ถูกต้อง'], 400); - } - $catalog = achv_load_catalog(); - $store = read_store(); - $progress = []; - foreach ($store['accounts'] as $a) { - if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) { - $progress = achv_progress_of($a); - break; - } - } - json_response(['ok' => true, 'catalog' => $catalog, 'progress' => (object) $progress]); -} - -/* ---------- Server-only (secret) : auto-track ภายหลัง ---------- */ -if ($action === 'progress' && $method === 'POST') { - $secretFile = ADMIN_PRIVATE_DIR . '/game-award-secret.txt'; - $expected = is_file($secretFile) ? trim((string) @file_get_contents($secretFile)) : ''; - $secret = (string) ($body['secret'] ?? ''); - if ($expected === '' || strlen($secret) < 16 || !hash_equals($expected, $secret)) { - json_response(['ok' => false, 'error' => 'unauthorized'], 403); - } - $key = trim((string) ($body['playerKey'] ?? '')); - $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($body['id'] ?? '')); - if (!achv_valid_key($key) || $id === '') { - json_response(['ok' => false, 'error' => 'bad params'], 400); - } - $catalog = achv_load_catalog(); - $target = 0; - foreach ($catalog as $c) { if ($c['id'] === $id) { $target = (int) $c['target']; break; } } - if ($target <= 0) json_response(['ok' => false, 'error' => 'unknown id'], 404); - - $store = read_store(); - $i = achv_find_or_create($store, $key); - $cur = achv_progress_of($store['accounts'][$i]); - if (isset($body['set'])) { - $val = max(0, min($target, (int) $body['set'])); - } else { - $inc = (int) ($body['inc'] ?? 1); - $val = max(0, min($target, ($cur[$id] ?? 0) + $inc)); - } - $cur[$id] = $val; - $store['accounts'][$i]['achievements'] = (object) $cur; - $store['accounts'][$i]['updatedAt'] = gmdate('c'); - if (!write_store($store)) json_response(['ok' => false, 'error' => 'save failed'], 500); - json_response(['ok' => true, 'id' => $id, 'value' => $val, 'unlocked' => $val >= $target]); -} - -/* streak — สำหรับ achievement แบบ "ติดต่อกัน" (a6/d4/e1): event 'hit' นับต่อ, 'miss' รีเซ็ต 0. - * เก็บ current streak แยกใน accounts[].achvStreak {id:count} · achievement = best streak ที่เคยถึง (ไม่ลดลงเวลารีเซ็ต) */ -if ($action === 'streak' && $method === 'POST') { - $secretFile = ADMIN_PRIVATE_DIR . '/game-award-secret.txt'; - $expected = is_file($secretFile) ? trim((string) @file_get_contents($secretFile)) : ''; - $secret = (string) ($body['secret'] ?? ''); - if ($expected === '' || strlen($secret) < 16 || !hash_equals($expected, $secret)) { - json_response(['ok' => false, 'error' => 'unauthorized'], 403); - } - $key = trim((string) ($body['playerKey'] ?? '')); - $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($body['id'] ?? '')); - $event = (string) ($body['event'] ?? ''); - if (!achv_valid_key($key) || $id === '' || ($event !== 'hit' && $event !== 'miss')) { - json_response(['ok' => false, 'error' => 'bad params'], 400); - } - $catalog = achv_load_catalog(); - $target = 0; - foreach ($catalog as $c) { if ($c['id'] === $id) { $target = (int) $c['target']; break; } } - if ($target <= 0) json_response(['ok' => false, 'error' => 'unknown id'], 404); - - $store = read_store(); - $i = achv_find_or_create($store, $key); - $acct = $store['accounts'][$i]; - $streaks = (isset($acct['achvStreak']) && is_array($acct['achvStreak'])) ? $acct['achvStreak'] : []; - $sc = max(0, (int) ($streaks[$id] ?? 0)); - $sc = ($event === 'hit') ? ($sc + 1) : 0; - $streaks[$id] = $sc; - $cur = achv_progress_of($acct); - $best = max(0, min($target, max((int) ($cur[$id] ?? 0), $sc))); /* achievement = best ไม่ลดลง */ - $cur[$id] = $best; - $store['accounts'][$i]['achievements'] = (object) $cur; - $store['accounts'][$i]['achvStreak'] = (object) $streaks; - $store['accounts'][$i]['updatedAt'] = gmdate('c'); - if (!write_store($store)) json_response(['ok' => false, 'error' => 'save failed'], 500); - json_response(['ok' => true, 'id' => $id, 'streak' => $sc, 'value' => $best, 'unlocked' => $best >= $target]); -} - -/* ---------- Admin ---------- */ -require_login(); - -if ($action === 'saveCatalog' && $method === 'POST') { - $catIn = (isset($body['catalog']) && is_array($body['catalog'])) ? $body['catalog'] : null; - if ($catIn === null) json_response(['ok' => false, 'error' => 'ไม่มี catalog'], 400); - $clean = achv_sanitize_catalog($catIn); - if (!$clean) json_response(['ok' => false, 'error' => 'catalog ว่าง'], 400); - if (!achv_save_catalog($clean)) { - json_response(['ok' => false, 'error' => 'บันทึก achievements.json ไม่สำเร็จ (chown ให้ user เว็บ)'], 500); - } - json_response(['ok' => true, 'catalog' => $clean]); -} - -if ($action === 'resetCatalog' && $method === 'POST') { - $def = achv_default_catalog(); - if (!achv_save_catalog($def)) { - json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); - } - json_response(['ok' => true, 'catalog' => $def]); -} - -if ($action === 'players' && $method === 'GET') { - $catalog = achv_load_catalog(); - $byId = []; - foreach ($catalog as $c) $byId[$c['id']] = (int) $c['target']; - $store = read_store(); - $rows = []; - foreach ($store['accounts'] as $a) { - if (($a['loginType'] ?? '') !== 'guest') continue; - $key = (string) ($a['providerUserId'] ?? ''); - if ($key === '') continue; - $prog = achv_progress_of($a); - $unlocked = 0; - foreach ($prog as $id => $v) { - if (isset($byId[$id]) && $v >= $byId[$id]) $unlocked++; - } - $rows[] = [ - 'playerKey' => $key, - 'displayName' => (string) ($a['displayName'] ?? ($a['lbName'] ?? 'Guest')), - 'coins' => max(0, (int) ($a['coins'] ?? 0)), - 'blocked' => !empty($a['blocked']), - 'unlocked' => $unlocked, - 'total' => count($byId), - 'updatedAt' => (string) ($a['updatedAt'] ?? ''), - ]; - } - json_response(['ok' => true, 'players' => $rows, 'total' => count($byId)]); -} - -if ($action === 'player' && $method === 'GET') { - $key = trim((string) ($_GET['playerKey'] ?? '')); - if (!achv_valid_key($key)) json_response(['ok' => false, 'error' => 'playerKey ไม่ถูกต้อง'], 400); - $store = read_store(); - foreach ($store['accounts'] as $a) { - if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) { - json_response(['ok' => true, 'playerKey' => $key, 'displayName' => (string) ($a['displayName'] ?? 'Guest'), 'progress' => (object) achv_progress_of($a)]); - } - } - json_response(['ok' => true, 'playerKey' => $key, 'displayName' => 'Guest', 'progress' => (object) []]); -} - -if ($action === 'setProgress' && $method === 'POST') { - $key = trim((string) ($body['playerKey'] ?? '')); - $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($body['id'] ?? '')); - if (!achv_valid_key($key) || $id === '') json_response(['ok' => false, 'error' => 'bad params'], 400); - $catalog = achv_load_catalog(); - $target = 0; - foreach ($catalog as $c) { if ($c['id'] === $id) { $target = (int) $c['target']; break; } } - if ($target <= 0) json_response(['ok' => false, 'error' => 'unknown id'], 404); - $value = max(0, min($target, (int) ($body['value'] ?? 0))); - $store = read_store(); - $i = achv_find_or_create($store, $key); - $cur = achv_progress_of($store['accounts'][$i]); - $cur[$id] = $value; - $store['accounts'][$i]['achievements'] = (object) $cur; - $store['accounts'][$i]['updatedAt'] = gmdate('c'); - if (!write_store($store)) json_response(['ok' => false, 'error' => 'save failed'], 500); - json_response(['ok' => true, 'id' => $id, 'value' => $value, 'unlocked' => $value >= $target]); -} - -if ($action === 'unlock' && $method === 'POST') { - $key = trim((string) ($body['playerKey'] ?? '')); - $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($body['id'] ?? '')); - if (!achv_valid_key($key) || $id === '') json_response(['ok' => false, 'error' => 'bad params'], 400); - $catalog = achv_load_catalog(); - $target = 0; - foreach ($catalog as $c) { if ($c['id'] === $id) { $target = (int) $c['target']; break; } } - if ($target <= 0) json_response(['ok' => false, 'error' => 'unknown id'], 404); - $store = read_store(); - $i = achv_find_or_create($store, $key); - $cur = achv_progress_of($store['accounts'][$i]); - $cur[$id] = $target; - $store['accounts'][$i]['achievements'] = (object) $cur; - $store['accounts'][$i]['updatedAt'] = gmdate('c'); - if (!write_store($store)) json_response(['ok' => false, 'error' => 'save failed'], 500); - json_response(['ok' => true, 'id' => $id, 'value' => $target, 'unlocked' => true]); -} - -if ($action === 'resetPlayer' && $method === 'POST') { - $key = trim((string) ($body['playerKey'] ?? '')); - if (!achv_valid_key($key)) json_response(['ok' => false, 'error' => 'playerKey ไม่ถูกต้อง'], 400); - $store = read_store(); - foreach ($store['accounts'] as $idx => $a) { - if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) { - $store['accounts'][$idx]['achievements'] = new \stdClass(); - $store['accounts'][$idx]['updatedAt'] = gmdate('c'); - if (!write_store($store)) json_response(['ok' => false, 'error' => 'save failed'], 500); - json_response(['ok' => true]); - } - } - json_response(['ok' => true]); -} - -json_response(['ok' => false, 'error' => 'unknown action'], 400); + accounts[].achievements { id: count } + * + * Public (ไม่ต้องล็อกอิน): + * GET ?action=state&playerKey=KEY -> { ok, catalog:[...], progress:{id:count} } + * GET ?action=catalog -> { ok, catalog:[...] } + * + * Server-only (มี secret game-award-secret.txt) — สำหรับ auto-track ภายหลัง: + * POST {action:'progress', secret, playerKey, id, inc?, set?} + * + * Admin (ต้องล็อกอิน session): + * POST {action:'saveCatalog', catalog:[...]} + * GET ?action=players -> รายชื่อผู้เล่น + จำนวนที่ปลดล็อก + * GET ?action=player&playerKey=KEY -> progress ของผู้เล่นคนเดียว + * POST {action:'setProgress', playerKey, id, value} + * POST {action:'unlock', playerKey, id} -> set = target + * POST {action:'resetPlayer', playerKey} -> ล้าง progress ทั้งหมด + */ +require __DIR__ . '/_common.php'; + +date_default_timezone_set('Asia/Bangkok'); + +define('ACHV_CATALOG_FILE', ADMIN_PRIVATE_DIR . '/achievements.json'); + +function achv_default_catalog(): array +{ + return [ + ['id' => 'a1_first_deduction', 'g' => 1, 'title' => 'First Deduction', 'desc' => 'โหวตถูกตัวคนร้ายเป็นครั้งแรก', 'target' => 1, 'enabled' => true], + ['id' => 'a2_sharp_eye', 'g' => 1, 'title' => 'Sharp Eye', 'desc' => 'สะสมหลักฐานระดับมีน้ำหนัก (Silver) ครบ 10 ใบ', 'target' => 10, 'enabled' => true], + ['id' => 'a3_mind_architect', 'g' => 1, 'title' => 'Mind Architect', 'desc' => 'สะสมหลักฐานครบทุกระดับ (ทั่วไป, มีน้ำหนัก, ชี้ชัด) ใน 1 คดี', 'target' => 1, 'enabled' => true], + ['id' => 'a4_logic_over_luck', 'g' => 1, 'title' => 'Logic Over Luck', 'desc' => 'โหวตถูกโดยไม่พึ่งหลักฐานชี้ชัด (Legendary) เลย 3 ครั้ง', 'target' => 3, 'enabled' => true], + ['id' => 'a5_truth_hunter', 'g' => 1, 'title' => 'Truth Hunter', 'desc' => 'จับคนร้ายถูกตัวสะสมครบ 20 คดี', 'target' => 20, 'enabled' => true], + ['id' => 'a6_unbreakable_logic', 'g' => 1, 'title' => 'Unbreakable Logic', 'desc' => 'โหวตถูกตัวติดกัน 5 คดีรวด', 'target' => 5, 'enabled' => true], + + ['id' => 'b1_evidence_collector', 'g' => 2, 'title' => 'Evidence Collector', 'desc' => 'สะสมหลักฐานระดับทั่วไป (Common) ครบ 20 ใบ', 'target' => 20, 'enabled' => true], + ['id' => 'b2_relentless_investigator', 'g' => 2, 'title' => 'Relentless Investigator', 'desc' => 'เล่น Mini Game ครบทุกรอบ ใน 1 คดี', 'target' => 1, 'enabled' => true], + ['id' => 'b3_hidden_fragment', 'g' => 2, 'title' => 'Hidden Fragment', 'desc' => 'ค้นพบหลักฐานระดับชี้ชัด (Legendary/Gold) เป็นครั้งแรก', 'target' => 1, 'enabled' => true], + ['id' => 'b4_data_miner', 'g' => 2, 'title' => 'Data Miner', 'desc' => 'สะสมการ์ดหลักฐานรวมครบ 100 ใบ', 'target' => 100, 'enabled' => true], + ['id' => 'b5_deep_scanner', 'g' => 2, 'title' => 'Deep Scanner', 'desc' => 'เก็บไอเทมช่วยเหลือ (ตำรวจ/ทนาย) สะสมครบ 10 ครั้ง', 'target' => 10, 'enabled' => true], + + ['id' => 'c1_early_accusation', 'g' => 3, 'title' => 'Early Accusation', 'desc' => 'ชี้ตัวคนร้ายก่อนที่จะเปิดหลักฐานครบ', 'target' => 1, 'enabled' => true], + ['id' => 'c2_high_stakes', 'g' => 3, 'title' => 'High Stakes', 'desc' => 'ชี้ตัวคนร้ายถูกโดยมีหลักฐานในมือไม่เกิน 3 ใบ', 'target' => 1, 'enabled' => true], + ['id' => 'c3_quick_draw', 'g' => 3, 'title' => 'Quick Draw', 'desc' => 'ชี้ตัวคนร้ายเร็วที่สุดในทีมสะสมครบ 10 ครั้ง', 'target' => 10, 'enabled' => true], + ['id' => 'c4_clutch_mind', 'g' => 3, 'title' => 'Clutch Mind', 'desc' => 'โหวตถูกในช่วง 10 วินาทีสุดท้ายก่อนหมดเวลา', 'target' => 1, 'enabled' => true], + ['id' => 'c5_lone_wolf', 'g' => 3, 'title' => 'Lone Wolf', 'desc' => 'โหวตสวนทางกับเสียงส่วนใหญ่ของทีม (คุณถูกคนเดียว)', 'target' => 1, 'enabled' => true], + + ['id' => 'd1_minigame_solver', 'g' => 4, 'title' => 'Minigame Solver', 'desc' => 'เอาชีวิตรอด / เล่นมินิเกมสำเร็จ 20 ครั้ง', 'target' => 20, 'enabled' => true], + ['id' => 'd2_silent_guardian', 'g' => 4, 'title' => 'Silent Guardian', 'desc' => 'ไม่โดนโหวตใช้การ์ด Event พิเศษ เลยตลอด 5 คดี', 'target' => 5, 'enabled' => true], + ['id' => 'd3_the_backbone', 'g' => 4, 'title' => 'The Backbone', 'desc' => 'ส่งมอบหลักฐานให้เพื่อนวิเคราะห์ครบ 30 ใบ', 'target' => 30, 'enabled' => true], + ['id' => 'd4_flawless_diver', 'g' => 4, 'title' => 'Flawless Diver', 'desc' => 'ไม่โหวตจับผิดตัวเลยตลอด 15 คดี', 'target' => 15, 'enabled' => true], + + ['id' => 'e1_the_observer', 'g' => 5, 'title' => 'The Observer', 'desc' => 'เล่นจบ 15 คดีโดยไม่เคยสัมผัสหลักฐานชี้ชัด (Legendary) เลยสักครั้ง', 'target' => 15, 'enabled' => true], + ['id' => 'e2_the_impostor', 'g' => 5, 'title' => 'The Impostor', 'desc' => 'รับบทเป็น "ตัวป่วน" ครั้งแรก', 'target' => 1, 'enabled' => true], + ['id' => 'e3_master_of_doubt', 'g' => 5, 'title' => 'Master of Doubt', 'desc' => 'เอาชนะคดีในฐานะตัวป่วนได้สำเร็จ', 'target' => 1, 'enabled' => true], + ['id' => 'e4_agent_of_chaos', 'g' => 5, 'title' => 'Agent of Chaos', 'desc' => 'รับบทเป็นตัวป่วนสะสมครบ 10 ครั้ง', 'target' => 10, 'enabled' => true], + ['id' => 'e5_slippery_eel', 'g' => 5, 'title' => 'Slippery Eel', 'desc' => 'เป็นตัวป่วนแต่รอดพ้นจากการถูกจับได้ (ไม่ถูกโหวตออก) จนจบเกม 5 ครั้ง', 'target' => 5, 'enabled' => true], + ]; +} + +function achv_sanitize_catalog(array $arr): array +{ + $out = []; + $seen = []; + foreach ($arr as $row) { + if (!is_array($row)) continue; + $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($row['id'] ?? '')); + if ($id === '' || isset($seen[$id])) continue; + $seen[$id] = true; + $g = (int) ($row['g'] ?? 1); + if ($g < 1 || $g > 5) $g = 1; + $title = trim((string) ($row['title'] ?? '')); + $desc = trim((string) ($row['desc'] ?? '')); + if (function_exists('mb_substr')) { + $title = mb_substr($title, 0, 60); + $desc = mb_substr($desc, 0, 200); + } + $target = (int) ($row['target'] ?? 1); + if ($target < 1) $target = 1; + if ($target > 100000) $target = 100000; + $out[] = [ + 'id' => $id, + 'g' => $g, + 'title' => $title !== '' ? $title : $id, + 'desc' => $desc, + 'target' => $target, + 'enabled' => !isset($row['enabled']) || !empty($row['enabled']), + ]; + } + return $out; +} + +function achv_load_catalog(): array +{ + if (is_file(ACHV_CATALOG_FILE)) { + $raw = @file_get_contents(ACHV_CATALOG_FILE); + $j = json_decode($raw ?: '[]', true); + if (is_array($j) && $j) { + $c = achv_sanitize_catalog($j); + if ($c) return $c; + } + } + return achv_default_catalog(); +} + +function achv_save_catalog(array $catalog): bool +{ + if (!is_dir(ADMIN_PRIVATE_DIR)) { + if (!@mkdir(ADMIN_PRIVATE_DIR, 0750, true)) return false; + } + $tmp = ACHV_CATALOG_FILE . '.tmp.' . bin2hex(random_bytes(4)); + $json = json_encode($catalog, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + if ($json === false) return false; + if (file_put_contents($tmp, $json, LOCK_EX) === false) return false; + if (!rename($tmp, ACHV_CATALOG_FILE)) { @unlink($tmp); return false; } + return true; +} + +function achv_valid_key(string $key): bool +{ + return (bool) preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key); +} + +/* หา index บัญชี guest ตาม playerKey (สร้างใหม่ถ้าไม่มี) */ +function achv_find_or_create(array &$store, string $key): int +{ + foreach ($store['accounts'] as $i => $a) { + if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) { + return $i; + } + } + $store['accounts'][] = [ + 'id' => new_id(), + 'email' => '', + 'displayName' => 'Guest', + 'loginType' => 'guest', + 'providerUserId' => $key, + 'notes' => 'auto: achievements', + 'blocked' => false, + 'coins' => 0, + 'achievements' => new \stdClass(), + 'createdAt' => gmdate('c'), + 'updatedAt' => gmdate('c'), + ]; + return count($store['accounts']) - 1; +} + +function achv_progress_of(array $account): array +{ + $p = $account['achievements'] ?? []; + if (!is_array($p)) return []; + $out = []; + foreach ($p as $k => $v) { + $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $k); + if ($id === '') continue; + $out[$id] = max(0, (int) $v); + } + return $out; +} + +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; +$action = (string) ($_GET['action'] ?? ''); +if ($method === 'POST') { + $body = require_json_body(); + if (!$action) $action = (string) ($body['action'] ?? ''); +} else { + $body = []; +} + +/* ---------- Public ---------- */ +if ($action === 'catalog' && $method === 'GET') { + json_response(['ok' => true, 'catalog' => achv_load_catalog()]); +} + +if ($action === 'state' && $method === 'GET') { + $key = trim((string) ($_GET['playerKey'] ?? '')); + if (!achv_valid_key($key)) { + json_response(['ok' => false, 'error' => 'playerKey ไม่ถูกต้อง'], 400); + } + $catalog = achv_load_catalog(); + $store = read_store(); + $progress = []; + foreach ($store['accounts'] as $a) { + if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) { + $progress = achv_progress_of($a); + break; + } + } + json_response(['ok' => true, 'catalog' => $catalog, 'progress' => (object) $progress]); +} + +/* ---------- Server-only (secret) : auto-track ภายหลัง ---------- */ +if ($action === 'progress' && $method === 'POST') { + $secretFile = ADMIN_PRIVATE_DIR . '/game-award-secret.txt'; + $expected = is_file($secretFile) ? trim((string) @file_get_contents($secretFile)) : ''; + $secret = (string) ($body['secret'] ?? ''); + if ($expected === '' || strlen($secret) < 16 || !hash_equals($expected, $secret)) { + json_response(['ok' => false, 'error' => 'unauthorized'], 403); + } + $key = trim((string) ($body['playerKey'] ?? '')); + $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($body['id'] ?? '')); + if (!achv_valid_key($key) || $id === '') { + json_response(['ok' => false, 'error' => 'bad params'], 400); + } + $catalog = achv_load_catalog(); + $target = 0; + foreach ($catalog as $c) { if ($c['id'] === $id) { $target = (int) $c['target']; break; } } + if ($target <= 0) json_response(['ok' => false, 'error' => 'unknown id'], 404); + + $store = read_store(); + $i = achv_find_or_create($store, $key); + $cur = achv_progress_of($store['accounts'][$i]); + if (isset($body['set'])) { + $val = max(0, min($target, (int) $body['set'])); + } else { + $inc = (int) ($body['inc'] ?? 1); + $val = max(0, min($target, ($cur[$id] ?? 0) + $inc)); + } + $cur[$id] = $val; + $store['accounts'][$i]['achievements'] = (object) $cur; + $store['accounts'][$i]['updatedAt'] = gmdate('c'); + if (!write_store($store)) json_response(['ok' => false, 'error' => 'save failed'], 500); + json_response(['ok' => true, 'id' => $id, 'value' => $val, 'unlocked' => $val >= $target]); +} + +/* ---------- Server-only (secret) : streak [2026-07-27] ---------- + * ก่อนหน้านี้ "ไม่มี branch นี้เลย" → POST action=streak จาก Game/server.js:2264 ตกไปถึง + * require_login() ด้านล่าง แล้วได้ 401 Unauthorized ทุกครั้ง = achievement แบบ streak + * (a6_unbreakable_logic / d4_flawless_diver / e1_the_observer) ไม่เคยถูกบันทึกเลย + * + * สัญญาที่ server.js คาดไว้: {playerKey, id, event:'hit'|'miss'} + * hit → streak ปัจจุบัน +1 + * miss → streak ปัจจุบัน = 0 + * ค่า achievement ที่เก็บ = **สถิติสูงสุด (best) ไม่ลดลง** ตามคอมเมนต์ที่ server.js เขียนไว้ + * streak ที่กำลังนับอยู่เก็บแยกใน achievementStreaks เพื่อไม่ให้ทับ progress ปกติ + */ +if ($action === 'streak' && $method === 'POST') { + $secretFile = ADMIN_PRIVATE_DIR . '/game-award-secret.txt'; + $expected = is_file($secretFile) ? trim((string) @file_get_contents($secretFile)) : ''; + $secret = (string) ($body['secret'] ?? ''); + if ($expected === '' || strlen($secret) < 16 || !hash_equals($expected, $secret)) { + json_response(['ok' => false, 'error' => 'unauthorized'], 403); + } + $key = trim((string) ($body['playerKey'] ?? '')); + $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($body['id'] ?? '')); + $event = (string) ($body['event'] ?? ''); + if (!achv_valid_key($key) || $id === '' || ($event !== 'hit' && $event !== 'miss')) { + json_response(['ok' => false, 'error' => 'bad params'], 400); + } + $catalog = achv_load_catalog(); + $target = 0; + foreach ($catalog as $c) { if ($c['id'] === $id) { $target = (int) $c['target']; break; } } + if ($target <= 0) json_response(['ok' => false, 'error' => 'unknown id'], 404); + + $store = read_store(); + $i = achv_find_or_create($store, $key); + $runs = []; + if (isset($store['accounts'][$i]['achievementStreaks'])) { + $runs = (array) $store['accounts'][$i]['achievementStreaks']; + } + $run = max(0, (int) ($runs[$id] ?? 0)); + $run = ($event === 'miss') ? 0 : $run + 1; + $runs[$id] = $run; + + $cur = achv_progress_of($store['accounts'][$i]); + /* best ไม่ลดลง — miss ไม่ทำให้ achievement ที่เคยได้หายไป */ + $best = max((int) ($cur[$id] ?? 0), min($target, $run)); + $cur[$id] = $best; + + $store['accounts'][$i]['achievementStreaks'] = (object) $runs; + $store['accounts'][$i]['achievements'] = (object) $cur; + $store['accounts'][$i]['updatedAt'] = gmdate('c'); + if (!write_store($store)) json_response(['ok' => false, 'error' => 'save failed'], 500); + json_response(['ok' => true, 'id' => $id, 'event' => $event, 'streak' => $run, 'value' => $best, 'unlocked' => $best >= $target]); +} + +/* ---------- Admin ---------- */ +require_login(); +require_tab('achievements'); + +if ($action === 'saveCatalog' && $method === 'POST') { + $catIn = (isset($body['catalog']) && is_array($body['catalog'])) ? $body['catalog'] : null; + if ($catIn === null) json_response(['ok' => false, 'error' => 'ไม่มี catalog'], 400); + $clean = achv_sanitize_catalog($catIn); + if (!$clean) json_response(['ok' => false, 'error' => 'catalog ว่าง'], 400); + if (!achv_save_catalog($clean)) { + json_response(['ok' => false, 'error' => 'บันทึก achievements.json ไม่สำเร็จ (chown ให้ user เว็บ)'], 500); + } + json_response(['ok' => true, 'catalog' => $clean]); +} + +if ($action === 'resetCatalog' && $method === 'POST') { + $def = achv_default_catalog(); + if (!achv_save_catalog($def)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); + } + json_response(['ok' => true, 'catalog' => $def]); +} + +if ($action === 'players' && $method === 'GET') { + $catalog = achv_load_catalog(); + $byId = []; + foreach ($catalog as $c) $byId[$c['id']] = (int) $c['target']; + $store = read_store(); + $rows = []; + foreach ($store['accounts'] as $a) { + if (($a['loginType'] ?? '') !== 'guest') continue; + $key = (string) ($a['providerUserId'] ?? ''); + if ($key === '') continue; + $prog = achv_progress_of($a); + $unlocked = 0; + foreach ($prog as $id => $v) { + if (isset($byId[$id]) && $v >= $byId[$id]) $unlocked++; + } + $rows[] = [ + 'playerKey' => $key, + 'displayName' => (string) ($a['displayName'] ?? ($a['lbName'] ?? 'Guest')), + 'coins' => max(0, (int) ($a['coins'] ?? 0)), + 'blocked' => !empty($a['blocked']), + 'unlocked' => $unlocked, + 'total' => count($byId), + 'updatedAt' => (string) ($a['updatedAt'] ?? ''), + ]; + } + json_response(['ok' => true, 'players' => $rows, 'total' => count($byId)]); +} + +if ($action === 'player' && $method === 'GET') { + $key = trim((string) ($_GET['playerKey'] ?? '')); + if (!achv_valid_key($key)) json_response(['ok' => false, 'error' => 'playerKey ไม่ถูกต้อง'], 400); + $store = read_store(); + foreach ($store['accounts'] as $a) { + if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) { + json_response(['ok' => true, 'playerKey' => $key, 'displayName' => (string) ($a['displayName'] ?? 'Guest'), 'progress' => (object) achv_progress_of($a)]); + } + } + json_response(['ok' => true, 'playerKey' => $key, 'displayName' => 'Guest', 'progress' => (object) []]); +} + +if ($action === 'setProgress' && $method === 'POST') { + $key = trim((string) ($body['playerKey'] ?? '')); + $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($body['id'] ?? '')); + if (!achv_valid_key($key) || $id === '') json_response(['ok' => false, 'error' => 'bad params'], 400); + $catalog = achv_load_catalog(); + $target = 0; + foreach ($catalog as $c) { if ($c['id'] === $id) { $target = (int) $c['target']; break; } } + if ($target <= 0) json_response(['ok' => false, 'error' => 'unknown id'], 404); + $value = max(0, min($target, (int) ($body['value'] ?? 0))); + $store = read_store(); + $i = achv_find_or_create($store, $key); + $cur = achv_progress_of($store['accounts'][$i]); + $cur[$id] = $value; + $store['accounts'][$i]['achievements'] = (object) $cur; + $store['accounts'][$i]['updatedAt'] = gmdate('c'); + if (!write_store($store)) json_response(['ok' => false, 'error' => 'save failed'], 500); + json_response(['ok' => true, 'id' => $id, 'value' => $value, 'unlocked' => $value >= $target]); +} + +if ($action === 'unlock' && $method === 'POST') { + $key = trim((string) ($body['playerKey'] ?? '')); + $id = preg_replace('/[^a-zA-Z0-9_]/', '', (string) ($body['id'] ?? '')); + if (!achv_valid_key($key) || $id === '') json_response(['ok' => false, 'error' => 'bad params'], 400); + $catalog = achv_load_catalog(); + $target = 0; + foreach ($catalog as $c) { if ($c['id'] === $id) { $target = (int) $c['target']; break; } } + if ($target <= 0) json_response(['ok' => false, 'error' => 'unknown id'], 404); + $store = read_store(); + $i = achv_find_or_create($store, $key); + $cur = achv_progress_of($store['accounts'][$i]); + $cur[$id] = $target; + $store['accounts'][$i]['achievements'] = (object) $cur; + $store['accounts'][$i]['updatedAt'] = gmdate('c'); + if (!write_store($store)) json_response(['ok' => false, 'error' => 'save failed'], 500); + json_response(['ok' => true, 'id' => $id, 'value' => $target, 'unlocked' => true]); +} + +if ($action === 'resetPlayer' && $method === 'POST') { + $key = trim((string) ($body['playerKey'] ?? '')); + if (!achv_valid_key($key)) json_response(['ok' => false, 'error' => 'playerKey ไม่ถูกต้อง'], 400); + $store = read_store(); + foreach ($store['accounts'] as $idx => $a) { + if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) { + $store['accounts'][$idx]['achievements'] = new \stdClass(); + $store['accounts'][$idx]['updatedAt'] = gmdate('c'); + if (!write_store($store)) json_response(['ok' => false, 'error' => 'save failed'], 500); + json_response(['ok' => true]); + } + } + json_response(['ok' => true]); +} + +json_response(['ok' => false, 'error' => 'unknown action'], 400); diff --git a/www/html/Admin/api/admins.php b/www/html/Admin/api/admins.php index 440e859..4b7b3d8 100644 --- a/www/html/Admin/api/admins.php +++ b/www/html/Admin/api/admins.php @@ -1,127 +1,141 @@ - false, 'error' => 'Unauthorized'], 401); -} - -$method = $_SERVER['REQUEST_METHOD']; - -if ($method === 'GET') { - $list = array_map('strip_admin', read_store()['admins'] ?? []); - json_response(['ok' => true, 'admins' => $list]); -} - -if (!is_super($me)) { - json_response(['ok' => false, 'error' => 'เฉพาะ super admin จัดการบัญชีแอดมินได้'], 403); -} - -if ($method === 'POST') { - $body = require_json_body(); - $username = trim((string)($body['username'] ?? '')); - $password = (string)($body['password'] ?? ''); - $role = ($body['role'] ?? 'admin') === 'super' ? 'super' : 'admin'; - if ($role === 'super' && !is_super($me)) { - json_response(['ok' => false, 'error' => 'สร้าง super admin ได้เฉพาะ super เท่านั้น'], 403); - } - if ($username === '' || strlen($username) < 2) { - json_response(['ok' => false, 'error' => 'ชื่อผู้ใช้สั้นเกินไป'], 400); - } - if (strlen($password) < 8) { - json_response(['ok' => false, 'error' => 'รหัสผ่านอย่างน้อย 8 ตัวอักษร'], 400); - } - $store = read_store(); - foreach ($store['admins'] ?? [] as $x) { - if (strcasecmp($x['username'] ?? '', $username) === 0) { - json_response(['ok' => false, 'error' => 'ชื่อผู้ใช้ซ้ำ'], 400); - } - } - $store['admins'][] = [ - 'id' => new_id(), - 'username' => $username, - 'passwordHash' => password_hash($password, PASSWORD_DEFAULT), - 'role' => $role, - 'createdAt' => gmdate('c'), - ]; - if (!write_store($store)) { - json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); - } - json_response(['ok' => true]); -} - -if ($method === 'PATCH') { - $body = require_json_body(); - $targetId = trim((string)($body['id'] ?? '')); - $newPassword = (string)($body['newPassword'] ?? ''); - if ($targetId === '') { - json_response(['ok' => false, 'error' => 'ระบุ id แอดมิน'], 400); - } - if (strlen($newPassword) < 8) { - json_response(['ok' => false, 'error' => 'รหัสใหม่อย่างน้อย 8 ตัวอักษร'], 400); - } - if ($targetId === $me['id']) { - json_response(['ok' => false, 'error' => 'เปลี่ยนรหัสตัวเองให้ใช้เมนู "เปลี่ยนรหัสผ่านของคุณ"'], 400); - } - $store = read_store(); - $updated = false; - foreach ($store['admins'] ?? [] as $i => $a) { - if (($a['id'] ?? '') !== $targetId) { - continue; - } - $store['admins'][$i]['passwordHash'] = password_hash($newPassword, PASSWORD_DEFAULT); - $updated = true; - break; - } - if (!$updated) { - json_response(['ok' => false, 'error' => 'ไม่พบแอดมิน'], 404); - } - if (!write_store($store)) { - json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); - } - json_response(['ok' => true, 'message' => 'ตั้งรหัสใหม่แล้ว']); -} - -if ($method === 'DELETE') { - $id = trim((string)($_GET['id'] ?? '')); - if ($id === '') { - json_response(['ok' => false, 'error' => 'ระบุ id'], 400); - } - if ($id === $me['id']) { - json_response(['ok' => false, 'error' => 'ลบบัญชีตัวเองไม่ได้'], 400); - } - $store = read_store(); - $admins = $store['admins'] ?? []; - $next = []; - $removed = false; - foreach ($admins as $a) { - if (($a['id'] ?? '') === $id) { - $removed = true; - continue; - } - $next[] = $a; - } - if (!$removed) { - json_response(['ok' => false, 'error' => 'ไม่พบผู้ใช้'], 404); - } - $supers = 0; - foreach ($next as $a) { - if (($a['role'] ?? '') === 'super') { - $supers++; - } - } - if ($supers < 1) { - json_response(['ok' => false, 'error' => 'ต้องมี super admin อย่างน้อย 1 คน'], 400); - } - $store['admins'] = $next; - if (!write_store($store)) { - json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); - } - json_response(['ok' => true]); -} - -json_response(['ok' => false, 'error' => 'Method not allowed'], 405); + false, 'error' => 'Unauthorized'], 401); +} + +$method = $_SERVER['REQUEST_METHOD']; + +if ($method === 'GET') { + $list = array_map('strip_admin', read_store()['admins'] ?? []); + json_response(['ok' => true, 'admins' => $list]); +} + +if (!is_super($me)) { + json_response(['ok' => false, 'error' => 'เฉพาะ super admin จัดการบัญชีแอดมินได้'], 403); +} + +if ($method === 'POST') { + $body = require_json_body(); + $username = trim((string)($body['username'] ?? '')); + $password = (string)($body['password'] ?? ''); + $role = ($body['role'] ?? 'admin') === 'super' ? 'super' : 'admin'; + if ($role === 'super' && !is_super($me)) { + json_response(['ok' => false, 'error' => 'สร้าง super admin ได้เฉพาะ super เท่านั้น'], 403); + } + if ($username === '' || strlen($username) < 2) { + json_response(['ok' => false, 'error' => 'ชื่อผู้ใช้สั้นเกินไป'], 400); + } + if (strlen($password) < 8) { + json_response(['ok' => false, 'error' => 'รหัสผ่านอย่างน้อย 8 ตัวอักษร'], 400); + } + $store = read_store(); + foreach ($store['admins'] ?? [] as $x) { + if (strcasecmp($x['username'] ?? '', $username) === 0) { + json_response(['ok' => false, 'error' => 'ชื่อผู้ใช้ซ้ำ'], 400); + } + } + /* สิทธิ์ระดับหมวด — super ไม่ต้องเก็บ (ได้ทุกหมวดเสมอ) · admin ที่ส่ง tabs ว่างมา = ทุกหมวด (พฤติกรรมเดิม) */ + $store['admins'][] = [ + 'id' => new_id(), + 'username' => $username, + 'passwordHash' => password_hash($password, PASSWORD_DEFAULT), + 'role' => $role, + 'tabs' => $role === 'super' ? [] : sanitize_tabs($body['tabs'] ?? []), + 'createdAt' => gmdate('c'), + ]; + if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); + } + json_response(['ok' => true]); +} + +if ($method === 'PATCH') { + $body = require_json_body(); + $targetId = trim((string)($body['id'] ?? '')); + $newPassword = (string)($body['newPassword'] ?? ''); + /* PATCH ทำได้ 2 อย่าง: ตั้งรหัสใหม่ และ/หรือ แก้สิทธิ์หมวด — ต้องส่งมาอย่างน้อย 1 อย่าง */ + $wantTabs = array_key_exists('tabs', $body); + if ($targetId === '') { + json_response(['ok' => false, 'error' => 'ระบุ id แอดมิน'], 400); + } + if ($newPassword === '' && !$wantTabs) { + json_response(['ok' => false, 'error' => 'ไม่มีอะไรให้แก้ (ส่ง newPassword หรือ tabs)'], 400); + } + if ($newPassword !== '' && strlen($newPassword) < 8) { + json_response(['ok' => false, 'error' => 'รหัสใหม่อย่างน้อย 8 ตัวอักษร'], 400); + } + if ($newPassword !== '' && $targetId === $me['id']) { + json_response(['ok' => false, 'error' => 'เปลี่ยนรหัสตัวเองให้ใช้เมนู "เปลี่ยนรหัสผ่านของคุณ"'], 400); + } + $store = read_store(); + $updated = false; + foreach ($store['admins'] ?? [] as $i => $a) { + if (($a['id'] ?? '') !== $targetId) { + continue; + } + if ($newPassword !== '') { + $store['admins'][$i]['passwordHash'] = password_hash($newPassword, PASSWORD_DEFAULT); + } + if ($wantTabs) { + /* super ได้ทุกหมวดอยู่แล้ว — เก็บ tabs ว่างไว้ กันเข้าใจผิดว่าถูกจำกัด */ + $store['admins'][$i]['tabs'] = (($a['role'] ?? '') === 'super') ? [] : sanitize_tabs($body['tabs']); + } + $updated = true; + break; + } + if (!$updated) { + json_response(['ok' => false, 'error' => 'ไม่พบแอดมิน'], 404); + } + if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); + } + json_response(['ok' => true, 'message' => $newPassword !== '' ? 'ตั้งรหัสใหม่แล้ว' : 'บันทึกสิทธิ์หมวดแล้ว']); +} + +if ($method === 'DELETE') { + $id = trim((string)($_GET['id'] ?? '')); + if ($id === '') { + json_response(['ok' => false, 'error' => 'ระบุ id'], 400); + } + if ($id === $me['id']) { + json_response(['ok' => false, 'error' => 'ลบบัญชีตัวเองไม่ได้'], 400); + } + $store = read_store(); + $admins = $store['admins'] ?? []; + $next = []; + $removed = false; + foreach ($admins as $a) { + if (($a['id'] ?? '') === $id) { + $removed = true; + continue; + } + $next[] = $a; + } + if (!$removed) { + json_response(['ok' => false, 'error' => 'ไม่พบผู้ใช้'], 404); + } + $supers = 0; + foreach ($next as $a) { + if (($a['role'] ?? '') === 'super') { + $supers++; + } + } + if ($supers < 1) { + json_response(['ok' => false, 'error' => 'ต้องมี super admin อย่างน้อย 1 คน'], 400); + } + $store['admins'] = $next; + if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); + } + json_response(['ok' => true]); +} + +json_response(['ok' => false, 'error' => 'Method not allowed'], 405); diff --git a/www/html/Admin/api/ai-settings.php b/www/html/Admin/api/ai-settings.php new file mode 100644 index 0000000..42fd4b4 --- /dev/null +++ b/www/html/Admin/api/ai-settings.php @@ -0,0 +1,121 @@ + { model, models[], hasKey, intent, rag_enabled, rag_context } (ไม่ส่ง api key ดิบ) + * PUT -> { openai_api_key?, model?, intent?, rag_enabled?, rag_context? } + */ +require __DIR__ . '/_common.php'; + +require_login(); +require_tab('ai-admin'); + +define('AI_SETTINGS_FILE', dirname(__DIR__, 2) . '/Game/data/ai-settings.json'); + +function ai_models(): array +{ + return [ + '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 ai_defaults(): array +{ + return [ + 'openai_api_key' => '', + 'model' => 'gpt-4o-mini', + 'intent' => '', + 'rag_enabled' => false, + 'rag_context' => '', + ]; +} + +function ai_read(): array +{ + $base = ai_defaults(); + if (!is_file(AI_SETTINGS_FILE)) { + return $base; + } + $raw = @file_get_contents(AI_SETTINGS_FILE); + $j = json_decode($raw ?: '{}', true); + if (!is_array($j)) { + return $base; + } + return array_replace($base, array_intersect_key($j, $base)); +} + +function ai_write(array $s): bool +{ + $dir = dirname(AI_SETTINGS_FILE); + if (!is_dir($dir) && !@mkdir($dir, 0755, true)) { + return false; + } + $json = json_encode($s, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + if ($json === false) { + return false; + } + $tmp = AI_SETTINGS_FILE . '.tmp.' . bin2hex(random_bytes(4)); + if (file_put_contents($tmp, $json, LOCK_EX) === false) { + return false; + } + if (!rename($tmp, AI_SETTINGS_FILE)) { + @unlink($tmp); + return false; + } + return true; +} + +function ai_public_payload(array $s): array +{ + $key = trim((string) ($s['openai_api_key'] ?? '')); + return [ + 'ok' => true, + 'model' => (string) ($s['model'] ?? 'gpt-4o-mini') ?: 'gpt-4o-mini', + 'models' => ai_models(), + 'hasKey' => $key !== '', + 'intent' => (string) ($s['intent'] ?? ''), + 'rag_enabled' => !empty($s['rag_enabled']), + 'rag_context' => (string) ($s['rag_context'] ?? ''), + ]; +} + +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + +if ($method === 'GET') { + json_response(ai_public_payload(ai_read())); +} + +if ($method === 'PUT' || $method === 'POST') { + $body = require_json_body(); + $s = ai_read(); + if (array_key_exists('openai_api_key', $body)) { + $s['openai_api_key'] = trim((string) $body['openai_api_key']); + } + if (array_key_exists('model', $body)) { + $m = trim((string) $body['model']); + $s['model'] = $m !== '' ? $m : 'gpt-4o-mini'; + } + if (array_key_exists('intent', $body)) { + $s['intent'] = (string) $body['intent']; + } + if (array_key_exists('rag_enabled', $body)) { + $s['rag_enabled'] = !empty($body['rag_enabled']); + } + if (array_key_exists('rag_context', $body)) { + $s['rag_context'] = (string) $body['rag_context']; + } + if (!ai_write($s)) { + json_response(['ok' => false, 'error' => 'บันทึก ai-settings.json ไม่สำเร็จ'], 500); + } + json_response(['ok' => true]); +} + +json_response(['ok' => false, 'error' => 'Method not allowed'], 405); diff --git a/www/html/Admin/api/game-quiz-carry-plaque-upload.php b/www/html/Admin/api/game-quiz-carry-plaque-upload.php index 1d862d4..f200c03 100644 --- a/www/html/Admin/api/game-quiz-carry-plaque-upload.php +++ b/www/html/Admin/api/game-quiz-carry-plaque-upload.php @@ -1,69 +1,70 @@ - false, 'error' => 'ต้องเปิด PHP extension curl · Enable php-curl'], 500); -} - -$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; -if ($method !== 'POST') { - json_response(['ok' => false, 'error' => 'Method not allowed'], 405); -} - -$raw = file_get_contents('php://input'); -if ($raw === false) { - $raw = ''; -} -$contentLen = isset($_SERVER['CONTENT_LENGTH']) ? (int) $_SERVER['CONTENT_LENGTH'] : 0; -if ($raw === '' && $contentLen > 0) { - json_response([ - 'ok' => false, - 'error' => 'รับ body ไม่ได้ (มักเกิดจาก nginx client_max_body_size หรือ PHP post_max_size เล็กเกินไป) — ต้องตั้งอย่างน้อย ~20M สำหรับ /Admin/api/*.php · Request body was discarded (raise nginx body limit + PHP post_max_size)', - ], 413); -} - -$port = preg_replace('/[^0-9]/', '', (string)(getenv('GAME_NODE_PORT') ?: '3001')) ?: '3001'; -$host = getenv('GAME_NODE_INTERNAL_HOST') ?: '127.0.0.1'; -$target = 'http://' . $host . ':' . $port . '/Game/api/quiz-carry-plaque-upload'; - -$ch = curl_init($target); -if ($ch === false) { - json_response(['ok' => false, 'error' => 'curl init failed'], 500); -} - -curl_setopt_array($ch, [ - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $raw, - CURLOPT_HTTPHEADER => [ - 'Accept: application/json', - 'Content-Type: application/json', - ], - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 120, -]); - -$out = curl_exec($ch); -$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); -$err = curl_error($ch); -curl_close($ch); - -if ($out === false || $out === '') { - json_response([ - 'ok' => false, - 'error' => 'ไม่ต่อถึงเซิร์ฟเวอร์เกม (Node) — ตรวจ systemd/pm2 และพอร์ต ' . $port . ' · ' . $err, - ], 502); -} - -http_response_code($code > 0 ? $code : 500); -header('Content-Type: application/json; charset=utf-8'); -header('X-Content-Type-Options: nosniff'); -header('Cache-Control: no-store'); -echo $out; + false, 'error' => 'ต้องเปิด PHP extension curl · Enable php-curl'], 500); +} + +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; +if ($method !== 'POST') { + json_response(['ok' => false, 'error' => 'Method not allowed'], 405); +} + +$raw = file_get_contents('php://input'); +if ($raw === false) { + $raw = ''; +} +$contentLen = isset($_SERVER['CONTENT_LENGTH']) ? (int) $_SERVER['CONTENT_LENGTH'] : 0; +if ($raw === '' && $contentLen > 0) { + json_response([ + 'ok' => false, + 'error' => 'รับ body ไม่ได้ (มักเกิดจาก nginx client_max_body_size หรือ PHP post_max_size เล็กเกินไป) — ต้องตั้งอย่างน้อย ~20M สำหรับ /Admin/api/*.php · Request body was discarded (raise nginx body limit + PHP post_max_size)', + ], 413); +} + +$port = preg_replace('/[^0-9]/', '', (string)(getenv('GAME_NODE_PORT') ?: '3001')) ?: '3001'; +$host = getenv('GAME_NODE_INTERNAL_HOST') ?: '127.0.0.1'; +$target = 'http://' . $host . ':' . $port . '/Game/api/quiz-carry-plaque-upload'; + +$ch = curl_init($target); +if ($ch === false) { + json_response(['ok' => false, 'error' => 'curl init failed'], 500); +} + +curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $raw, + CURLOPT_HTTPHEADER => [ + 'Accept: application/json', + 'Content-Type: application/json', + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 120, +]); + +$out = curl_exec($ch); +$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); +$err = curl_error($ch); +curl_close($ch); + +if ($out === false || $out === '') { + json_response([ + 'ok' => false, + 'error' => 'ไม่ต่อถึงเซิร์ฟเวอร์เกม (Node) — ตรวจ systemd/pm2 และพอร์ต ' . $port . ' · ' . $err, + ], 502); +} + +http_response_code($code > 0 ? $code : 500); +header('Content-Type: application/json; charset=utf-8'); +header('X-Content-Type-Options: nosniff'); +header('Cache-Control: no-store'); +echo $out; diff --git a/www/html/Admin/api/game-quiz-settings.php b/www/html/Admin/api/game-quiz-settings.php index ffa98fb..84a5818 100644 --- a/www/html/Admin/api/game-quiz-settings.php +++ b/www/html/Admin/api/game-quiz-settings.php @@ -12,6 +12,7 @@ declare(strict_types=1); require __DIR__ . '/_common.php'; require_login(); +require_any_tab(['quiz','quiz-carry','quiz-battle','special-quiz','game-timing','jump-survive','mega-virus','space-shooter','stack-game','evidence-cards','postcase','troublesome','vote-timing']); if (!function_exists('curl_init')) { json_response(['ok' => false, 'error' => 'ต้องเปิด PHP extension curl (ติดตั้งแพ็กเกจ php-curl แล้วรีสตาร์ท PHP-FPM)'], 500); diff --git a/www/html/Admin/api/leaderboard-admin.php b/www/html/Admin/api/leaderboard-admin.php index ef911a0..1cbbf3b 100644 --- a/www/html/Admin/api/leaderboard-admin.php +++ b/www/html/Admin/api/leaderboard-admin.php @@ -1,94 +1,95 @@ - (string) ($a['id'] ?? ''), - 'name' => $name, - 'score' => $score, - 'coins' => max(0, (int) ($a['coins'] ?? 0)), - 'blocked' => !empty($a['blocked']), - 'loginType' => (string) ($a['loginType'] ?? ''), - 'key' => (string) ($a['providerUserId'] ?? ''), - ]; - } - usort($rows, function ($x, $y) { - if ($y['score'] !== $x['score']) { - return $y['score'] - $x['score']; - } - return strcmp((string) $x['name'], (string) $y['name']); - }); - foreach ($rows as $i => &$r) { - $r['rank'] = $i + 1; - } - unset($r); - json_response(['ok' => true, 'rows' => $rows, 'total' => count($rows)]); -} - -if ($method === 'POST') { - $body = require_json_body(); - $action = (string) ($body['action'] ?? ''); - - if ($action === 'resetAll') { - $store = read_store(); - $n = 0; - foreach (($store['accounts'] ?? []) as $i => $a) { - if ((int) ($a['score'] ?? 0) !== 0) { - $store['accounts'][$i]['score'] = 0; - $store['accounts'][$i]['updatedAt'] = gmdate('c'); - $n++; - } - } - if (!write_store($store)) { - json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); - } - json_response(['ok' => true, 'reset' => $n]); - } - - if ($action === 'reset') { - $id = trim((string) ($body['id'] ?? '')); - if ($id === '') { - json_response(['ok' => false, 'error' => 'ระบุ id'], 400); - } - $store = read_store(); - $found = false; - foreach (($store['accounts'] ?? []) as $i => $a) { - if (($a['id'] ?? '') === $id) { - $store['accounts'][$i]['score'] = 0; - $store['accounts'][$i]['updatedAt'] = gmdate('c'); - $found = true; - break; - } - } - if (!$found) { - json_response(['ok' => false, 'error' => 'ไม่พบบัญชี'], 404); - } - if (!write_store($store)) { - json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); - } - json_response(['ok' => true]); - } - - json_response(['ok' => false, 'error' => 'action ไม่ถูกต้อง'], 400); -} - -json_response(['ok' => false, 'error' => 'Use GET or POST'], 405); + (string) ($a['id'] ?? ''), + 'name' => $name, + 'score' => $score, + 'coins' => max(0, (int) ($a['coins'] ?? 0)), + 'blocked' => !empty($a['blocked']), + 'loginType' => (string) ($a['loginType'] ?? ''), + 'key' => (string) ($a['providerUserId'] ?? ''), + ]; + } + usort($rows, function ($x, $y) { + if ($y['score'] !== $x['score']) { + return $y['score'] - $x['score']; + } + return strcmp((string) $x['name'], (string) $y['name']); + }); + foreach ($rows as $i => &$r) { + $r['rank'] = $i + 1; + } + unset($r); + json_response(['ok' => true, 'rows' => $rows, 'total' => count($rows)]); +} + +if ($method === 'POST') { + $body = require_json_body(); + $action = (string) ($body['action'] ?? ''); + + if ($action === 'resetAll') { + $store = read_store(); + $n = 0; + foreach (($store['accounts'] ?? []) as $i => $a) { + if ((int) ($a['score'] ?? 0) !== 0) { + $store['accounts'][$i]['score'] = 0; + $store['accounts'][$i]['updatedAt'] = gmdate('c'); + $n++; + } + } + if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); + } + json_response(['ok' => true, 'reset' => $n]); + } + + if ($action === 'reset') { + $id = trim((string) ($body['id'] ?? '')); + if ($id === '') { + json_response(['ok' => false, 'error' => 'ระบุ id'], 400); + } + $store = read_store(); + $found = false; + foreach (($store['accounts'] ?? []) as $i => $a) { + if (($a['id'] ?? '') === $id) { + $store['accounts'][$i]['score'] = 0; + $store['accounts'][$i]['updatedAt'] = gmdate('c'); + $found = true; + break; + } + } + if (!$found) { + json_response(['ok' => false, 'error' => 'ไม่พบบัญชี'], 404); + } + if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); + } + json_response(['ok' => true]); + } + + json_response(['ok' => false, 'error' => 'action ไม่ถูกต้อง'], 400); +} + +json_response(['ok' => false, 'error' => 'Use GET or POST'], 405); diff --git a/www/html/Admin/api/oauth-google-callback.php b/www/html/Admin/api/oauth-google-callback.php new file mode 100644 index 0000000..2123a7e --- /dev/null +++ b/www/html/Admin/api/oauth-google-callback.php @@ -0,0 +1,181 @@ + false, 'error' => 'Use POST'], 405); +} + +$body = require_json_body(); +$code = trim((string) ($body['code'] ?? '')); +$redirectUri = trim((string) ($body['redirectUri'] ?? '')); +$mergeGuestKey = trim((string) ($body['mergeGuestKey'] ?? '')); + +if ($code === '') { + json_response(['ok' => false, 'error' => 'missing code'], 400); +} + +$oauth = read_store()['oauth'] ?? []; +$clientId = trim((string) ($oauth['googleClientId'] ?? '')); +$clientSecret = trim((string) ($oauth['googleClientSecret'] ?? '')); +$configuredRedirect = trim((string) ($oauth['googleRedirectUri'] ?? '')); + +if ($clientId === '' || $clientSecret === '') { + json_response(['ok' => false, 'error' => 'ยังไม่ได้ตั้งค่า Google OAuth ใน Admin'], 503); +} + +if ($redirectUri === '') { + $redirectUri = $configuredRedirect; +} +if ($redirectUri === '') { + json_response(['ok' => false, 'error' => 'missing redirectUri'], 400); +} + +if ($configuredRedirect !== '' && $redirectUri !== $configuredRedirect) { + json_response(['ok' => false, 'error' => 'redirectUri ไม่ตรงกับที่ตั้งใน Admin'], 400); +} + +if (!function_exists('curl_init')) { + json_response(['ok' => false, 'error' => 'PHP cURL ไม่พร้อมใช้งาน'], 500); +} + +$tokenPayload = http_build_query([ + 'code' => $code, + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'redirect_uri' => $redirectUri, + 'grant_type' => 'authorization_code', +]); + +$ch = curl_init('https://oauth2.googleapis.com/token'); +curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $tokenPayload, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'], + CURLOPT_TIMEOUT => 20, +]); +$tokenRaw = curl_exec($ch); +$tokenHttp = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +$tokenJson = json_decode((string) $tokenRaw, true); +if ($tokenHttp < 200 || $tokenHttp >= 300 || !is_array($tokenJson)) { + json_response(['ok' => false, 'error' => 'แลก token จาก Google ไม่สำเร็จ', 'detail' => (string) $tokenRaw], 502); +} + +$accessToken = trim((string) ($tokenJson['access_token'] ?? '')); +if ($accessToken === '') { + json_response(['ok' => false, 'error' => 'Google ไม่คืน access_token', 'detail' => $tokenJson], 502); +} + +$ch2 = curl_init('https://www.googleapis.com/oauth2/v3/userinfo'); +curl_setopt_array($ch2, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $accessToken], + CURLOPT_TIMEOUT => 20, +]); +$userRaw = curl_exec($ch2); +$userHttp = (int) curl_getinfo($ch2, CURLINFO_HTTP_CODE); +curl_close($ch2); + +$user = json_decode((string) $userRaw, true); +if ($userHttp < 200 || $userHttp >= 300 || !is_array($user)) { + json_response(['ok' => false, 'error' => 'อ่านข้อมูลผู้ใช้จาก Google ไม่สำเร็จ'], 502); +} + +$googleSub = trim((string) ($user['sub'] ?? '')); +if ($googleSub === '') { + json_response(['ok' => false, 'error' => 'Google ไม่คืน sub (user id)'], 502); +} + +$playerKey = google_player_key($googleSub); +if (!valid_player_key($playerKey)) { + json_response(['ok' => false, 'error' => 'playerKey จาก Google ไม่ถูกต้อง'], 500); +} + +$email = trim((string) ($user['email'] ?? '')); +$name = trim((string) ($user['name'] ?? '')); +if ($name === '') { + $name = trim((string) ($user['given_name'] ?? '')); +} +if ($name === '') { + $name = $email !== '' ? preg_replace('/@.*$/', '', $email) : 'Google Player'; +} +if (function_exists('mb_substr')) { + $name = mb_substr($name, 0, 32); +} else { + $name = substr($name, 0, 32); +} + +$picture = trim((string) ($user['picture'] ?? '')); +$now = gmdate('c'); + +$store = read_store(); +if (!isset($store['accounts']) || !is_array($store['accounts'])) { + $store['accounts'] = []; +} + +$idx = find_account_index_by_player_key($store['accounts'], $playerKey); +$isNew = $idx < 0; + +if ($idx < 0) { + $store['accounts'][] = [ + 'id' => new_id(), + 'email' => $email, + 'displayName' => $name, + 'loginType' => 'google', + 'providerUserId' => $playerKey, + 'googleSub' => $googleSub, + 'avatarUrl' => $picture, + 'notes' => 'oauth: google', + 'blocked' => false, + 'coins' => 0, + 'createdAt' => $now, + 'updatedAt' => $now, + ]; + $idx = count($store['accounts']) - 1; +} else { + $store['accounts'][$idx]['loginType'] = 'google'; + $store['accounts'][$idx]['email'] = $email !== '' ? $email : ($store['accounts'][$idx]['email'] ?? ''); + if ($name !== '') { + $store['accounts'][$idx]['displayName'] = $name; + } + $store['accounts'][$idx]['googleSub'] = $googleSub; + if ($picture !== '') { + $store['accounts'][$idx]['avatarUrl'] = $picture; + } + $store['accounts'][$idx]['updatedAt'] = $now; +} + +if (!empty($store['accounts'][$idx]['blocked'])) { + json_response(['ok' => false, 'error' => 'บัญชีนี้ถูกระงับ'], 403); +} + +if ($isNew && $mergeGuestKey !== '') { + merge_guest_account_into($store, $idx, $mergeGuestKey); +} + +if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกบัญชีไม่สำเร็จ'], 500); +} + +$acc = $store['accounts'][$idx] ?? []; +json_response([ + 'ok' => true, + 'playerKey' => $playerKey, + 'loginType' => 'google', + 'displayName' => (string) ($acc['displayName'] ?? $name), + 'email' => (string) ($acc['email'] ?? $email), + 'avatarUrl' => (string) ($acc['avatarUrl'] ?? $picture), + 'coins' => max(0, (int) ($acc['coins'] ?? 0)), + 'accountId' => (string) ($acc['id'] ?? ''), + 'isNew' => $isNew, +]); diff --git a/www/html/Admin/api/oauth.php b/www/html/Admin/api/oauth.php index ebc7d16..5badd4c 100644 --- a/www/html/Admin/api/oauth.php +++ b/www/html/Admin/api/oauth.php @@ -1,33 +1,34 @@ - true, 'oauth' => $o]); -} - -if ($_SERVER['REQUEST_METHOD'] !== 'PUT' && $_SERVER['REQUEST_METHOD'] !== 'POST') { - json_response(['ok' => false, 'error' => 'Use GET or PUT'], 405); -} - -$body = require_json_body(); -$store = read_store(); -$cur = $store['oauth'] ?? []; -$keys = [ - 'facebookAppId', 'facebookAppSecret', 'facebookRedirectUri', - 'googleClientId', 'googleClientSecret', 'googleRedirectUri', -]; -foreach ($keys as $k) { - if (array_key_exists($k, $body)) { - $cur[$k] = trim((string)$body[$k]); - } -} -$store['oauth'] = $cur; -if (!write_store($store)) { - json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); -} -json_response(['ok' => true, 'oauth' => $cur]); + true, 'oauth' => $o]); +} + +if ($_SERVER['REQUEST_METHOD'] !== 'PUT' && $_SERVER['REQUEST_METHOD'] !== 'POST') { + json_response(['ok' => false, 'error' => 'Use GET or PUT'], 405); +} + +$body = require_json_body(); +$store = read_store(); +$cur = $store['oauth'] ?? []; +$keys = [ + 'facebookAppId', 'facebookAppSecret', 'facebookRedirectUri', + 'googleClientId', 'googleClientSecret', 'googleRedirectUri', +]; +foreach ($keys as $k) { + if (array_key_exists($k, $body)) { + $cur[$k] = trim((string)$body[$k]); + } +} +$store['oauth'] = $cur; +if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500); +} +json_response(['ok' => true, 'oauth' => $cur]); diff --git a/www/html/Admin/api/player-log.php b/www/html/Admin/api/player-log.php new file mode 100644 index 0000000..0d6316d --- /dev/null +++ b/www/html/Admin/api/player-log.php @@ -0,0 +1,85 @@ + false, 'error' => 'unknown action'], 400); + } + + $playerKey = trim((string) ($body['playerKey'] ?? '')); + $event = trim((string) ($body['event'] ?? '')); + $displayName = trim((string) ($body['displayName'] ?? '')); + $loginType = trim((string) ($body['loginType'] ?? 'guest')); + $page = trim((string) ($body['page'] ?? '')); + $meta = isset($body['meta']) && is_array($body['meta']) ? $body['meta'] : []; + + $result = player_log_track($playerKey, $event, $displayName, $loginType, $page, $meta); + if (empty($result['ok'])) { + json_response($result, 400); + } + json_response($result); +} + +if ($method === 'GET') { + require_login(); + $action = (string) ($_GET['action'] ?? 'summary'); + $log = player_log_read(); + + if ($action === 'summary') { + json_response([ + 'ok' => true, + 'summary' => player_log_public_summary($log), + 'eventTypes' => array_keys(player_log_allowed_events()), + ]); + } + + if ($action === 'players') { + $search = trim((string) ($_GET['search'] ?? '')); + $limit = (int) ($_GET['limit'] ?? 200); + json_response([ + 'ok' => true, + 'players' => player_log_players_list($log, $search, $limit), + ]); + } + + if ($action === 'events') { + $result = player_log_events_list($log, [ + 'search' => (string) ($_GET['search'] ?? ''), + 'event' => (string) ($_GET['event'] ?? ''), + 'playerKey' => (string) ($_GET['playerKey'] ?? ''), + 'limit' => (int) ($_GET['limit'] ?? 100), + 'offset' => (int) ($_GET['offset'] ?? 0), + ]); + json_response([ + 'ok' => true, + 'events' => $result['rows'], + 'total' => $result['total'], + ]); + } + + json_response(['ok' => false, 'error' => 'unknown action'], 400); +} + +if ($method === 'DELETE') { + require_login(); + if (!player_log_write(player_log_default())) { + json_response(['ok' => false, 'error' => 'ล้าง log ไม่สำเร็จ'], 500); + } + json_response(['ok' => true]); +} + +json_response(['ok' => false, 'error' => 'Use GET, POST or DELETE'], 405); diff --git a/www/html/Admin/api/quiz-battle-modes.php b/www/html/Admin/api/quiz-battle-modes.php new file mode 100644 index 0000000..04214fe --- /dev/null +++ b/www/html/Admin/api/quiz-battle-modes.php @@ -0,0 +1,227 @@ + 'กฎหมายใกล้ตัว', + 2 => 'กฎหมายสิทธิพื้นฐาน', + 3 => 'กฎหมายจราจร', + 4 => 'คดีเกี่ยวกับทรัพย์', + 5 => 'คดีหมิ่นประมาท', + 6 => 'คดีทางเพศ', + 7 => 'คดีอาชญากรรม', + 8 => 'อาชญากรรมออนไลน์', + 9 => 'กระบวนการยุติธรรม', + 10 => 'งานบริการกระทรวงยุติธรรม', + ]; +} + +function qb_default_modes(): array +{ + $titles = qb_default_titles(); + $modes = []; + for ($i = 1; $i <= QB_ROOM_COUNT; $i++) { + $modes[(string)$i] = [ + 'title' => $titles[$i] ?? ('ห้อง ' . $i), + 'enabled' => true, + 'openUntil' => '', + 'maxPlayers' => QB_MAX_PLAYERS_CAP, + ]; + } + return $modes; +} + +/** ชื่อหัวข้อห้อง — แก้ได้ [2026-07-23] · ตัดช่องว่าง/อักขระควบคุม จำกัด 60 ตัว · ว่าง → คืน default */ +function qb_clean_title($raw, string $default): string +{ + if (!is_string($raw)) { + return $default; + } + $s = preg_replace('/[\x00-\x1F\x7F]+/u', ' ', $raw); // ตัดอักขระควบคุม/ขึ้นบรรทัด + $s = trim(preg_replace('/\s+/u', ' ', $s ?? '')); + if ($s === '') { + return $default; + } + if (function_exists('mb_substr')) { + $s = mb_substr($s, 0, 60, 'UTF-8'); + } else { + $s = substr($s, 0, 180); + } + return $s; +} + +/** 'YYYY-MM-DD HH:MM' เท่านั้น (เวลาไทย) — ค่าอื่นถือว่าไม่ได้ตั้ง */ +function qb_clean_until($raw): string +{ + if (!is_string($raw)) { + return ''; + } + $s = trim($raw); + if ($s === '') { + return ''; + } + $s = str_replace('T', ' ', $s); + if (strlen($s) === 16 && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $s) === 1) { + [$d, $t] = explode(' ', $s, 2); + [$y, $m, $dd] = array_map('intval', explode('-', $d)); + if (checkdate($m, $dd, $y)) { + [$hh, $mm] = array_map('intval', explode(':', $t)); + if ($hh >= 0 && $hh <= 23 && $mm >= 0 && $mm <= 59) { + return $s; + } + } + } + return ''; +} + +function qb_read_modes(): array +{ + $out = ['version' => 1, 'updatedAt' => '', 'modes' => qb_default_modes()]; + $raw = @file_get_contents(QB_MODES_FILE); + if ($raw === false || $raw === '') { + return $out; + } + $j = json_decode($raw, true); + if (!is_array($j) || !isset($j['modes']) || !is_array($j['modes'])) { + return $out; + } + $titles = qb_default_titles(); + foreach ($out['modes'] as $k => $def) { + $src = $j['modes'][$k] ?? null; + if (!is_array($src)) { + continue; + } + $out['modes'][$k] = [ + /* [2026-07-23] หัวข้อแก้ได้แล้ว — ใช้ค่าที่เก็บไว้ ถ้าว่าง fallback เป็น default ของห้องนั้น */ + 'title' => qb_clean_title($src['title'] ?? '', $titles[(int)$k] ?? $def['title']), + 'enabled' => !isset($src['enabled']) || (bool)$src['enabled'], + 'openUntil' => qb_clean_until($src['openUntil'] ?? ''), + 'maxPlayers' => qb_clamp_max($src['maxPlayers'] ?? QB_MAX_PLAYERS_CAP), + ]; + } + $out['updatedAt'] = is_string($j['updatedAt'] ?? null) ? $j['updatedAt'] : ''; + return $out; +} + +function qb_write_modes(array $data): bool +{ + $dir = dirname(QB_MODES_FILE); + if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) { + return false; + } + $tmp = QB_MODES_FILE . '.tmp'; + $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($json === false || @file_put_contents($tmp, $json, LOCK_EX) === false) { + return false; + } + /* เขียน tmp แล้ว rename — กันไฟล์พังครึ่งทางถ้า Node อ่านพอดีจังหวะ */ + if (!@rename($tmp, QB_MODES_FILE)) { + @unlink($tmp); + return false; + } + @chmod(QB_MODES_FILE, 0664); + return true; +} + +/** สถานะที่คำนวณแล้ว ณ เวลาไทยตอนนี้ — ให้หน้า admin โชว์ว่าห้องไหน "ปิดไปแล้ว" */ +function qb_state(array $mode): array +{ + $tz = new DateTimeZone(QB_TZ); + $now = new DateTimeImmutable('now', $tz); + $until = $mode['openUntil'] ?? ''; + $expired = false; + if ($until !== '') { + $end = DateTimeImmutable::createFromFormat('Y-m-d H:i', $until, $tz); + $expired = ($end !== false) && ($now > $end); + } + return [ + 'open' => (bool)($mode['enabled'] ?? true) && !$expired, + 'expired' => $expired, + ]; +} + +$method = $_SERVER['REQUEST_METHOD']; + +if ($method === 'GET') { + $data = qb_read_modes(); + $tz = new DateTimeZone(QB_TZ); + $now = new DateTimeImmutable('now', $tz); + foreach ($data['modes'] as $k => $m) { + $data['modes'][$k] = array_merge($m, qb_state($m)); + } + $data['nowThai'] = $now->format('Y-m-d H:i'); + $data['tz'] = QB_TZ; + json_response(['ok' => true] + $data); +} + +if ($method === 'PUT' || $method === 'POST') { + $body = require_json_body(); + $src = $body['modes'] ?? null; + if (!is_array($src)) { + json_response(['ok' => false, 'error' => 'ต้องส่ง modes มาด้วย'], 400); + } + $cur = qb_read_modes(); + $next = $cur['modes']; + foreach ($next as $k => $def) { + $m = $src[$k] ?? null; + if (!is_array($m)) { + continue; + } + $next[$k] = [ + /* [2026-07-23] หัวข้อแก้ได้ — sanitize + ถ้าไม่ส่ง/ว่าง คงค่าเดิม (ไม่ reset เป็น default) */ + 'title' => qb_clean_title($m['title'] ?? ($def['title'] ?? ''), $def['title'] ?? ('ห้อง ' . $k)), + 'enabled' => !isset($m['enabled']) || (bool)$m['enabled'], + 'openUntil' => qb_clean_until($m['openUntil'] ?? ''), + /* [2026-07-23] จำนวนคนต่อห้อง — ถ้าไม่ส่งมา คงค่าเดิมไว้ (ไม่ reset เป็น 50) */ + 'maxPlayers' => qb_clamp_max($m['maxPlayers'] ?? ($def['maxPlayers'] ?? QB_MAX_PLAYERS_CAP)), + ]; + } + $me = current_admin(); + $payload = [ + 'version' => 1, + 'updatedAt' => (new DateTimeImmutable('now', new DateTimeZone(QB_TZ)))->format('Y-m-d H:i'), + 'updatedBy' => $me['username'] ?? '', + 'tz' => QB_TZ, + 'modes' => $next, + ]; + if (!qb_write_modes($payload)) { + json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ (สิทธิ์เขียนไฟล์?)'], 500); + } + foreach ($payload['modes'] as $k => $m) { + $payload['modes'][$k] = array_merge($m, qb_state($m)); + } + json_response(['ok' => true, 'message' => 'บันทึกแล้ว'] + $payload); +} + +json_response(['ok' => false, 'error' => 'Use GET / PUT'], 405); diff --git a/www/html/Admin/api/room-approvals.php b/www/html/Admin/api/room-approvals.php new file mode 100644 index 0000000..08a98e4 --- /dev/null +++ b/www/html/Admin/api/room-approvals.php @@ -0,0 +1,212 @@ + false, 'error' => 'ต้องเปิด PHP extension curl'], 500); +} + +function room_moderation_disk_path(): string +{ + return dirname(__DIR__, 2) . '/Game/data/room-moderation.json'; +} + +function room_approval_history_path(): string +{ + return ADMIN_PRIVATE_DIR . '/room-approval-history.json'; +} + +function sync_room_moderation_disk(bool $enabled): void +{ + $path = room_moderation_disk_path(); + $dir = dirname($path); + if (!is_dir($dir)) { + @mkdir($dir, 0755, true); + } + $json = json_encode(['enabled' => $enabled], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + if ($json !== false) { + @file_put_contents($path, $json, LOCK_EX); + } +} + +function read_room_approval_history(): array +{ + $path = room_approval_history_path(); + if (!is_file($path)) { + return []; + } + $raw = @file_get_contents($path); + $j = json_decode($raw ?: '[]', true); + return is_array($j) ? $j : []; +} + +function append_room_approval_history(array $row): void +{ + $rows = read_room_approval_history(); + array_unshift($rows, $row); + if (count($rows) > 500) { + $rows = array_slice($rows, 0, 500); + } + if (!is_dir(ADMIN_PRIVATE_DIR)) { + @mkdir(ADMIN_PRIVATE_DIR, 0750, true); + } + $json = json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + if ($json !== false) { + @file_put_contents(room_approval_history_path(), $json, LOCK_EX); + } +} + +function node_room_approvals_url(string $suffix = ''): string +{ + $port = preg_replace('/[^0-9]/', '', (string)(getenv('GAME_NODE_PORT') ?: '13010')) ?: '13010'; + $host = getenv('GAME_NODE_INTERNAL_HOST') ?: '127.0.0.1'; + return 'http://' . $host . ':' . $port . '/Game/api/admin/room-approvals' . $suffix; +} + +function proxy_node_room_approvals(string $method, ?string $body = null, string $suffix = ''): array +{ + $ch = curl_init(node_room_approvals_url($suffix)); + if ($ch === false) { + return ['ok' => false, 'error' => 'curl init failed', 'httpCode' => 0]; + } + $headers = ['Accept: application/json']; + if ($body !== null && $method !== 'GET') { + $headers[] = 'Content-Type: application/json'; + } + $opts = [ + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 12, + ]; + if ($body !== null && $method !== 'GET') { + $opts[CURLOPT_POSTFIELDS] = $body; + } + curl_setopt_array($ch, $opts); + $out = curl_exec($ch); + $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + $err = curl_error($ch); + curl_close($ch); + if ($out === false || $out === '') { + return ['ok' => false, 'error' => 'ไม่ต่อถึงเซิร์ฟเวอร์เกม (Node): ' . $err, 'httpCode' => $code]; + } + $j = json_decode($out, true); + if (!is_array($j)) { + return ['ok' => false, 'error' => 'ตอบกลับจาก Node ไม่ถูกต้อง', 'httpCode' => $code]; + } + $j['httpCode'] = $code; + return $j; +} + +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; +$admin = current_admin(); + +if ($method === 'GET') { + $action = isset($_GET['action']) ? (string)$_GET['action'] : 'pending'; + $store = read_store(); + $enabled = !empty($store['roomModeration']['enabled']); + sync_room_moderation_disk($enabled); + + if ($action === 'settings') { + json_response([ + 'ok' => true, + 'enabled' => $enabled, + ]); + } + + if ($action === 'history') { + json_response([ + 'ok' => true, + 'history' => read_room_approval_history(), + 'enabled' => $enabled, + ]); + } + + $node = proxy_node_room_approvals('GET'); + if (empty($node['ok'])) { + json_response([ + 'ok' => true, + 'enabled' => $enabled, + 'pending' => [], + 'nodeError' => $node['error'] ?? 'Node unavailable', + ]); + } + json_response([ + 'ok' => true, + 'enabled' => $enabled, + 'nodeModerationEnabled' => !empty($node['moderationEnabled']), + 'pending' => $node['pending'] ?? [], + ]); +} + +if ($method === 'PATCH') { + $raw = file_get_contents('php://input') ?: '{}'; + $body = json_decode($raw, true); + if (!is_array($body)) { + json_response(['ok' => false, 'error' => 'ข้อมูลไม่ถูกต้อง'], 400); + } + $store = read_store(); + if (!isset($store['roomModeration']) || !is_array($store['roomModeration'])) { + $store['roomModeration'] = ['enabled' => true]; + } + if (array_key_exists('enabled', $body)) { + $store['roomModeration']['enabled'] = !!$body['enabled']; + } + if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกการตั้งค่าไม่สำเร็จ'], 500); + } + sync_room_moderation_disk(!empty($store['roomModeration']['enabled'])); + json_response(['ok' => true, 'enabled' => !empty($store['roomModeration']['enabled'])]); +} + +if ($method === 'POST') { + $raw = file_get_contents('php://input') ?: '{}'; + $body = json_decode($raw, true); + if (!is_array($body)) { + json_response(['ok' => false, 'error' => 'ข้อมูลไม่ถูกต้อง'], 400); + } + $spaceId = isset($body['spaceId']) ? trim((string)$body['spaceId']) : ''; + $act = isset($body['action']) ? trim((string)$body['action']) : ''; + $note = isset($body['note']) ? trim((string)$body['note']) : ''; + if ($spaceId === '' || !in_array($act, ['approve', 'reject', 'dismiss'], true)) { + json_response(['ok' => false, 'error' => 'ต้องระบุ spaceId และ action (reject|dismiss)'], 400); + } + + $meta = isset($body['meta']) && is_array($body['meta']) ? $body['meta'] : []; + $payload = json_encode([ + 'spaceId' => $spaceId, + 'action' => $act, + 'note' => $note, + ], JSON_UNESCAPED_UNICODE); + $result = proxy_node_room_approvals('POST', $payload, ''); + if (empty($result['ok'])) { + json_response(['ok' => false, 'error' => $result['error'] ?? 'ดำเนินการไม่สำเร็จ'], !empty($result['httpCode']) && $result['httpCode'] >= 400 ? (int)$result['httpCode'] : 400); + } + + append_room_approval_history([ + 'at' => gmdate('c'), + 'action' => $act, + 'spaceId' => $spaceId, + 'spaceName' => $meta['spaceName'] ?? $spaceId, + 'creatorDisplayName' => $meta['creatorDisplayName'] ?? '', + 'creatorPlayerKey' => $meta['creatorPlayerKey'] ?? '', + 'adminUsername' => is_array($admin) ? ($admin['username'] ?? '') : '', + 'note' => $note, + 'kicked' => isset($result['kicked']) ? (int)$result['kicked'] : null, + ]); + + json_response([ + 'ok' => true, + 'action' => $act, + 'spaceId' => $spaceId, + 'kicked' => $result['kicked'] ?? null, + ]); +} + +json_response(['ok' => false, 'error' => 'Method not allowed'], 405); diff --git a/www/html/Admin/api/sound-settings.php b/www/html/Admin/api/sound-settings.php new file mode 100644 index 0000000..295f550 --- /dev/null +++ b/www/html/Admin/api/sound-settings.php @@ -0,0 +1,91 @@ + { masterVol, musicVol, sfxVol } ค่า 0..1 + * PUT -> body JSON เดียวกัน → บันทึก + */ +require __DIR__ . '/_common.php'; + +require_login(); +require_tab('sound'); + +define('SOUND_SETTINGS_FILE', dirname(__DIR__, 2) . '/Game/data/sound-settings.json'); + +function sound_defaults(): array +{ + return ['masterVol' => 1.0, 'musicVol' => 0.35, 'sfxVol' => 0.75]; +} + +function sound_clamp($v, float $default): float +{ + $n = is_numeric($v) ? (float) $v : $default; + if (!is_finite($n)) { + return $default; + } + return max(0.0, min(1.0, $n)); +} + +function sound_read(): array +{ + $base = sound_defaults(); + if (!is_file(SOUND_SETTINGS_FILE)) { + return $base; + } + $raw = @file_get_contents(SOUND_SETTINGS_FILE); + $j = json_decode($raw ?: '{}', true); + if (!is_array($j)) { + return $base; + } + return [ + 'masterVol' => sound_clamp($j['masterVol'] ?? null, $base['masterVol']), + 'musicVol' => sound_clamp($j['musicVol'] ?? null, $base['musicVol']), + 'sfxVol' => sound_clamp($j['sfxVol'] ?? null, $base['sfxVol']), + ]; +} + +function sound_write(array $s): bool +{ + $dir = dirname(SOUND_SETTINGS_FILE); + if (!is_dir($dir) && !@mkdir($dir, 0755, true)) { + return false; + } + $json = json_encode($s, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + if ($json === false) { + return false; + } + $tmp = SOUND_SETTINGS_FILE . '.tmp.' . bin2hex(random_bytes(4)); + if (file_put_contents($tmp, $json, LOCK_EX) === false) { + return false; + } + if (!rename($tmp, SOUND_SETTINGS_FILE)) { + @unlink($tmp); + return false; + } + return true; +} + +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + +if ($method === 'GET') { + json_response(sound_read()); +} + +if ($method === 'PUT' || $method === 'POST') { + $body = require_json_body(); + $cur = sound_read(); + $next = [ + 'masterVol' => array_key_exists('masterVol', $body) ? sound_clamp($body['masterVol'], $cur['masterVol']) : $cur['masterVol'], + 'musicVol' => array_key_exists('musicVol', $body) ? sound_clamp($body['musicVol'], $cur['musicVol']) : $cur['musicVol'], + 'sfxVol' => array_key_exists('sfxVol', $body) ? sound_clamp($body['sfxVol'], $cur['sfxVol']) : $cur['sfxVol'], + ]; + if (!sound_write($next)) { + json_response(['ok' => false, 'error' => 'บันทึก sound-settings.json ไม่สำเร็จ'], 500); + } + json_response(['ok' => true, 'settings' => $next]); +} + +json_response(['ok' => false, 'error' => 'Method not allowed'], 405); diff --git a/www/html/Admin/api/system-settings.php b/www/html/Admin/api/system-settings.php new file mode 100644 index 0000000..4f9ecb3 --- /dev/null +++ b/www/html/Admin/api/system-settings.php @@ -0,0 +1,75 @@ + ROOM_RETENTION_MAX_DAYS) { + return ROOM_RETENTION_MAX_DAYS; + } + return $n; +} + +function system_settings_payload(array $store): array +{ + $settings = $store['systemSettings'] ?? []; + return [ + 'roomRetentionDays' => clamp_room_retention_days($settings['roomRetentionDays'] ?? ROOM_RETENTION_MIN_DAYS), + 'minRoomRetentionDays' => ROOM_RETENTION_MIN_DAYS, + 'maxRoomRetentionDays' => ROOM_RETENTION_MAX_DAYS, + ]; +} + +function sync_game_system_settings(array $payload): bool +{ + $dir = dirname(GAME_SYSTEM_SETTINGS); + if (!is_dir($dir) && !@mkdir($dir, 0755, true)) { + return false; + } + $data = [ + 'roomRetentionDays' => $payload['roomRetentionDays'], + 'updatedAt' => gmdate('c'), + ]; + $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + return $json !== false && file_put_contents(GAME_SYSTEM_SETTINGS, $json, LOCK_EX) !== false; +} + +$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + +if ($method === 'GET') { + $store = read_store(); + $payload = system_settings_payload($store); + sync_game_system_settings($payload); + json_response(['ok' => true] + $payload); +} + +if ($method === 'PATCH' || $method === 'PUT' || $method === 'POST') { + $body = require_json_body(); + if (!array_key_exists('roomRetentionDays', $body)) { + json_response(['ok' => false, 'error' => 'กรุณาระบุจำนวนวันจัดเก็บห้อง'], 400); + } + $store = read_store(); + $store['systemSettings'] = $store['systemSettings'] ?? []; + $store['systemSettings']['roomRetentionDays'] = clamp_room_retention_days($body['roomRetentionDays']); + if (!write_store($store)) { + json_response(['ok' => false, 'error' => 'บันทึกการตั้งค่าไม่สำเร็จ'], 500); + } + $payload = system_settings_payload($store); + if (!sync_game_system_settings($payload)) { + json_response(['ok' => false, 'error' => 'บันทึกค่าไปยัง Game/data ไม่สำเร็จ'], 500); + } + json_response(['ok' => true] + $payload); +} + +json_response(['ok' => false, 'error' => 'Method not allowed'], 405); diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html index 1979ab0..8fb0ce7 100644 --- a/www/html/Admin/index.html +++ b/www/html/Admin/index.html @@ -1,1432 +1,1492 @@ - - - - - - - - - Admin — JD JUSTICE DIVERS - - - - - - - - -
-
- -
- -
-

ศูนย์ควบคุม Admin

-

JD JUSTICE DIVERS

-
-
- - -
- -
- - - - - -
-
- - - - + + + + + + + + + Admin — JD JUSTICE DIVERS + + + + + + + + +
+
+ +
+ +
+

ศูนย์ควบคุม Admin

+

JD JUSTICE DIVERS

+
+
+ + +
+ +
+ + + + + +
+
+ + + + diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json index 792946d..3cdc758 100644 --- a/www/html/Admin/private/store.json +++ b/www/html/Admin/private/store.json @@ -1511,7 +1511,7 @@ "blocked": false, "coins": 95, "createdAt": "2026-07-08T05:26:41+00:00", - "updatedAt": "2026-07-12T07:34:23+00:00", + "updatedAt": "2026-07-24T06:43:56+00:00", "daily": { "anchorMs": 1783530000000, "claimedDays": [ @@ -1523,7 +1523,7 @@ false, false ], - "lockUntilMs": 1783875600000 + "lockUntilMs": 0 }, "lobbyColorThemeIndex": 5, "lobbySkinToneIndex": 1, @@ -1766,7 +1766,9 @@ "blocked": false, "coins": 0, "createdAt": "2026-07-08T12:09:31+00:00", - "updatedAt": "2026-07-08T12:09:31+00:00" + "updatedAt": "2026-07-24T06:58:25+00:00", + "lobbyColorThemeIndex": 1, + "lobbySkinToneIndex": 1 }, { "id": "5a94f9199f1412978b1aee8a", @@ -2550,6 +2552,316 @@ "coins": 0, "createdAt": "2026-07-22T10:30:33+00:00", "updatedAt": "2026-07-22T10:30:33+00:00" + }, + { + "id": "8657a2946b21496af9ccdea3", + "email": "", + "displayName": "Guest", + "loginType": "guest", + "providerUserId": "p_1784801056518_7957fwa372e", + "notes": "auto: player-coins", + "blocked": false, + "coins": 0, + "createdAt": "2026-07-23T10:04:16+00:00", + "updatedAt": "2026-07-23T10:04:16+00:00" + }, + { + "id": "f024cf64e179b05a77477135", + "email": "", + "displayName": "ผู้เล่น", + "loginType": "guest", + "providerUserId": "p_1785154354326_2awlhldhd43", + "notes": "auto: player-coins", + "blocked": false, + "coins": 70, + "createdAt": "2026-07-27T12:12:35+00:00", + "updatedAt": "2026-07-27T12:46:20+00:00", + "daily": { + "anchorMs": 1785085200000, + "claimedDays": [ + true, + false, + false, + false, + false, + false, + false + ], + "lockUntilMs": 1785171600000 + }, + "lobbyColorThemeIndex": 1, + "lobbySkinToneIndex": 2, + "achievements": { + "d1_minigame_solver": 6, + "b4_data_miner": 4, + "b1_evidence_collector": 1, + "a2_sharp_eye": 3 + }, + "score": 60, + "scoreByCase": { + "1": 60 + }, + "lbName": "ผู้เล่น" + }, + { + "id": "e0dee6ff0f043ae1185cfb75", + "email": "", + "displayName": "Guest", + "loginType": "guest", + "providerUserId": "p_1785155025679_2e0ebuyh4jf", + "notes": "auto: player-coins", + "blocked": false, + "coins": 10, + "createdAt": "2026-07-27T12:23:46+00:00", + "updatedAt": "2026-07-27T12:23:57+00:00", + "daily": { + "anchorMs": 1785085200000, + "claimedDays": [ + true, + false, + false, + false, + false, + false, + false + ], + "lockUntilMs": 1785171600000 + } + }, + { + "id": "d8112edcb8843332822ffff5", + "email": "", + "displayName": "Guest", + "loginType": "guest", + "providerUserId": "p_1785440715860_m68pkewoqqj", + "notes": "auto: player-coins", + "blocked": false, + "coins": 0, + "createdAt": "2026-07-30T19:45:14+00:00", + "updatedAt": "2026-07-30T19:45:14+00:00" + }, + { + "id": "de6d3738a5488613f71cccd6", + "email": "", + "displayName": "Guest", + "loginType": "guest", + "providerUserId": "p_1785440746710_4wt8vub7giu", + "notes": "auto: player-coins", + "blocked": false, + "coins": 0, + "createdAt": "2026-07-30T19:45:45+00:00", + "updatedAt": "2026-07-30T19:46:23+00:00", + "achievements": { + "d2_silent_guardian": 1, + "d4_flawless_diver": 0 + }, + "achievementStreaks": { + "d4_flawless_diver": 0 + } + }, + { + "id": "1b190c9ddb1b40ac663bb4da", + "email": "", + "displayName": "Guest", + "loginType": "guest", + "providerUserId": "p_1785440759059_1e4otuhtc83", + "notes": "auto: player-coins", + "blocked": false, + "coins": 0, + "createdAt": "2026-07-30T19:45:58+00:00", + "updatedAt": "2026-07-30T19:45:58+00:00" + }, + { + "id": "4b3f3099f6b09472c43d2004", + "email": "", + "displayName": "Guest", + "loginType": "guest", + "providerUserId": "p_1785441060100_8gmc11h9ok", + "notes": "auto: player-coins", + "blocked": false, + "coins": 0, + "createdAt": "2026-07-30T19:50:59+00:00", + "updatedAt": "2026-07-30T19:50:59+00:00" + }, + { + "id": "3919a0a09796c8373f5c65f3", + "email": "", + "displayName": "S1", + "loginType": "guest", + "providerUserId": "sk0-6hxlcur", + "notes": "auto: game-award", + "blocked": false, + "coins": 90, + "score": 90, + "scoreByCase": { + "1": 90 + }, + "lbName": "S1", + "createdAt": "2026-07-31T02:58:20+00:00", + "updatedAt": "2026-07-31T03:06:42+00:00", + "achievements": { + "d1_minigame_solver": 9, + "b4_data_miner": 9, + "b1_evidence_collector": 8, + "a2_sharp_eye": 1, + "a1_first_deduction": 1, + "a5_truth_hunter": 1, + "a4_logic_over_luck": 1, + "c3_quick_draw": 1, + "b2_relentless_investigator": 1, + "d2_silent_guardian": 1, + "e1_the_observer": 1, + "a6_unbreakable_logic": 1, + "d4_flawless_diver": 1 + }, + "achievementStreaks": { + "e1_the_observer": 1, + "a6_unbreakable_logic": 1, + "d4_flawless_diver": 1 + } + }, + { + "id": "cb287a9acc916834310c3445", + "email": "", + "displayName": "S1", + "loginType": "guest", + "providerUserId": "sk0-dyhhlv1", + "notes": "auto: game-award", + "blocked": false, + "coins": 90, + "score": 90, + "scoreByCase": { + "1": 90 + }, + "lbName": "S1", + "createdAt": "2026-07-31T03:08:00+00:00", + "updatedAt": "2026-07-31T03:19:43+00:00", + "achievements": { + "d1_minigame_solver": 9, + "b4_data_miner": 9, + "b1_evidence_collector": 7, + "a2_sharp_eye": 2, + "b2_relentless_investigator": 1, + "d2_silent_guardian": 1, + "e1_the_observer": 1, + "a6_unbreakable_logic": 0, + "d4_flawless_diver": 0 + }, + "achievementStreaks": { + "e1_the_observer": 1, + "a6_unbreakable_logic": 0, + "d4_flawless_diver": 0 + } + }, + { + "id": "594be80630b54845b783608e", + "email": "", + "displayName": "S1", + "loginType": "guest", + "providerUserId": "sk0-yutf5bk", + "notes": "auto: game-award", + "blocked": false, + "coins": 90, + "score": 90, + "scoreByCase": { + "1": 90 + }, + "lbName": "S1", + "createdAt": "2026-07-31T03:21:04+00:00", + "updatedAt": "2026-07-31T03:31:06+00:00", + "achievements": { + "d1_minigame_solver": 9, + "b4_data_miner": 9, + "b1_evidence_collector": 9, + "b2_relentless_investigator": 1, + "d2_silent_guardian": 1, + "e1_the_observer": 1, + "a6_unbreakable_logic": 0, + "d4_flawless_diver": 0 + }, + "achievementStreaks": { + "e1_the_observer": 1, + "a6_unbreakable_logic": 0, + "d4_flawless_diver": 0 + } + }, + { + "id": "8da4977bb7d712cb20b5b465", + "email": "", + "displayName": "S2", + "loginType": "guest", + "providerUserId": "sk1-ioqnz6b", + "notes": "auto: game-award", + "blocked": false, + "coins": 72, + "score": 72, + "scoreByCase": { + "1": 72 + }, + "lbName": "S2", + "createdAt": "2026-07-31T03:21:04+00:00", + "updatedAt": "2026-07-31T03:31:06+00:00", + "achievements": { + "d1_minigame_solver": 9, + "b4_data_miner": 9, + "b1_evidence_collector": 8, + "a2_sharp_eye": 1, + "b2_relentless_investigator": 1, + "d2_silent_guardian": 1, + "e1_the_observer": 1, + "a6_unbreakable_logic": 0, + "d4_flawless_diver": 0 + }, + "achievementStreaks": { + "e1_the_observer": 1, + "a6_unbreakable_logic": 0, + "d4_flawless_diver": 0 + } + }, + { + "id": "f7c654b294b62cc3e32dc1b1", + "email": "", + "displayName": "S1", + "loginType": "guest", + "providerUserId": "sk0-egisog4", + "notes": "auto: achievements", + "blocked": false, + "coins": 40, + "achievements": { + "d1_minigame_solver": 5, + "b4_data_miner": 4, + "b1_evidence_collector": 4 + }, + "createdAt": "2026-07-31T03:31:51+00:00", + "updatedAt": "2026-07-31T03:36:43+00:00", + "score": 40, + "scoreByCase": { + "1": 40 + }, + "lbName": "S1" + }, + { + "id": "5e6ee98b463feaaefc44f396", + "email": "", + "displayName": "S2", + "loginType": "guest", + "providerUserId": "sk1-0tsp4wv", + "notes": "auto: achievements", + "blocked": false, + "coins": 32, + "achievements": { + "a2_sharp_eye": 1, + "d1_minigame_solver": 3, + "b1_evidence_collector": 4, + "b4_data_miner": 3 + }, + "createdAt": "2026-07-31T03:31:51+00:00", + "updatedAt": "2026-07-31T03:36:43+00:00", + "score": 32, + "scoreByCase": { + "1": 32 + }, + "lbName": "S2" } ] } \ No newline at end of file diff --git a/www/html/Game/data/ai-settings.json b/www/html/Game/data/ai-settings.json index e480d65..632b56e 100644 --- a/www/html/Game/data/ai-settings.json +++ b/www/html/Game/data/ai-settings.json @@ -1,4 +1,7 @@ { - "openai_api_key": "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", - "model": "gpt-4-turbo" + "openai_api_key": "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", + "model": "gpt-4-turbo", + "intent": "", + "rag_enabled": false, + "rag_context": "" } \ No newline at end of file diff --git a/www/html/Game/data/maps/qbroom1.json b/www/html/Game/data/maps/qbroom1.json index 8fa7ce1..a9e6b1d 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,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,0,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,0,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,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,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,1,1,1,1,0,0,0,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,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":false,"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,1,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,1,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,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,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,0,0,0,0,1,1,1,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,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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","quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,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,2,0,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,5,0,0,0,6,0,0,0,7,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,9,0,0,0,10,0,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,13,0,0,0,14,0,0,0,15,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,17,0,0,0,18,0,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,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,2,0,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,4,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,0,5,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,6,0,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,0,5,0,0,0,0,0],[0,0,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,9,0,0,0,10,0,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0],[0,0,0,0,0,15,0,0,0,14,0,0,0,13,0,12,12,0,0,0],[0,0,0,0,0,15,0,0,0,14,0,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,14,0,0,0,13,0,0,0,0,0,0],[0,0,16,16,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,20,0,20,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,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,2,0,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0,0,0],[0,0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,4,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,0,5,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,6,0,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,0,5,0,0,0,0,0],[0,0,8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,9,0,0,0,10,0,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0],[0,0,0,0,0,15,0,0,0,14,0,0,0,13,0,12,12,0,0,0],[0,0,0,0,0,15,0,0,0,14,0,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,14,0,0,0,13,0,0,0,0,0,0],[0,0,16,16,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,20,0,20,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,0,13,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,17,0,0,0,18,0,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0],[0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0],[0,1,1,1,1,0,0,1,1,1,1,1,1,0,0,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],[0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,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,0,0,0,0,0,0]]} \ 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,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,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,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,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,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,0,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,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,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,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,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,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,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,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,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0],[0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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":false,"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,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,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]],"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,1,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,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,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,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,0,0,0,0,0,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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,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],[1,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,1,1,1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,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,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","quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDx":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDy":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,193,0,0,0,193,0,0,193,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,265,0,0,0,265,0,0,265,0,125,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,197,0,337,0,0,0,337,0,0,337,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,309,0,0,0,309,0,0,309,0,169,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,141,0,281,0,0,0,281,0,0,281,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,112,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFences":[{"x":5,"y":15.19,"w":10,"h":0.22},{"x":5,"y":18.91,"w":10,"h":0.22},{"x":5,"y":22.63,"w":10,"h":0.22}],"quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"specialQuizSpawnArea":[]} \ 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 fb97dd6..4fdb7ca 100644 --- a/www/html/Game/data/maps/qbroom10.json +++ b/www/html/Game/data/maps/qbroom10.json @@ -1,13284 +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, - 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, - 0, - 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, - 0, - 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, - 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, - 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, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 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, - 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": false, - "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, - 1, - 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, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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", - "quizBattlePathGateArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 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, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 4, - 4, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 12, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 16, - 16, - 0, - 16, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 16, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 19, - 20, - 0, - 20, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ], - "quizBattleFinishArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ] -} \ 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,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,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,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,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,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,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,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,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,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,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,0,0,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,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0],[0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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":false,"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,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,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]],"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,1,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,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,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,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,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,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,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,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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","quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDx":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDy":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,221,0,0,0,221,0,0,221,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,310,0,0,0,310,0,0,310,0,170,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,158,0,298,0,0,0,298,0,0,298,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,287,0,0,0,287,0,0,287,0,147,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,136,0,276,0,0,0,276,0,0,276,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,125,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFences":[{"x":5,"y":11.92,"w":10,"h":0.22},{"x":5,"y":15.8,"w":10,"h":0.22},{"x":5,"y":19.69,"w":10,"h":0.22},{"x":5,"y":23.58,"w":10,"h":0.22}],"quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"specialQuizSpawnArea":[]} \ 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 09ba321..adb4012 100644 --- a/www/html/Game/data/maps/qbroom2.json +++ b/www/html/Game/data/maps/qbroom2.json @@ -1,13284 +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, - 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, - 0, - 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, - 0, - 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, - 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, - 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, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 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, - 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": false, - "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, - 1, - 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, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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", - "quizBattlePathGateArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 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, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 4, - 4, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 12, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 16, - 16, - 0, - 16, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 16, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 19, - 20, - 0, - 20, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ], - "quizBattleFinishArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ] -} \ 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,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,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,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,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,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,0,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,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,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,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,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,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,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,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,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0],[0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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":false,"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,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,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]],"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,1,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,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,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,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,0,0,0,0,0,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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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","quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDx":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDy":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,195,0,0,0,195,0,0,195,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,273,0,0,0,273,0,0,273,0,133,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,210,0,350,0,0,0,350,0,0,350,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,327,0,0,0,327,0,0,327,0,187,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,164,0,304,0,0,0,304,0,0,304,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,142,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFences":[{"x":5,"y":15.32,"w":10,"h":0.22},{"x":5,"y":19.09,"w":10,"h":0.22},{"x":5,"y":22.86,"w":10,"h":0.22},{"x":5,"y":26.64,"w":10,"h":0.22}],"quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"specialQuizSpawnArea":[]} \ 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 69dc63a..d4b59c7 100644 --- a/www/html/Game/data/maps/qbroom3.json +++ b/www/html/Game/data/maps/qbroom3.json @@ -1,13284 +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, - 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, - 0, - 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, - 0, - 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, - 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, - 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, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 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, - 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": false, - "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, - 1, - 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, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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", - "quizBattlePathGateArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 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, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 4, - 4, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 12, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 16, - 16, - 0, - 16, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 16, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 19, - 20, - 0, - 20, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ], - "quizBattleFinishArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ] -} \ 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,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,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,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,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,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,0,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,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,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,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,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,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,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,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,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0],[0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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":false,"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,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,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]],"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,1,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,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,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,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,0,0,0,0,0,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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,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,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,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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","quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDx":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDy":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,190,0,0,0,190,0,0,190,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,264,0,0,0,264,0,0,264,0,124,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,197,0,337,0,0,0,337,0,0,337,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,310,0,0,0,310,0,0,310,0,170,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,144,0,284,0,0,0,284,0,0,284,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,117,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFences":[{"x":5,"y":15.19,"w":10,"h":0.22},{"x":5,"y":18.92,"w":10,"h":0.22},{"x":5,"y":22.66,"w":10,"h":0.22}],"quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"specialQuizSpawnArea":[]} \ 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 22f752f..09a00ec 100644 --- a/www/html/Game/data/maps/qbroom4.json +++ b/www/html/Game/data/maps/qbroom4.json @@ -1,13284 +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, - 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, - 0, - 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, - 0, - 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, - 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, - 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, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 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, - 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": false, - "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, - 1, - 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, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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", - "quizBattlePathGateArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 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, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 4, - 4, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 12, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 16, - 16, - 0, - 16, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 16, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 19, - 20, - 0, - 20, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ], - "quizBattleFinishArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ] -} \ 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,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,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,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,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,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,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,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,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,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,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,0,0,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,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0],[0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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":false,"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,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,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]],"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,1,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,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,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,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,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,1,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,1,1,0,0],[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,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,0,0,0,0,0,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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","quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDx":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDy":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,215,0,0,0,215,0,0,215,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,301,0,0,0,301,0,0,301,0,161,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,147,0,287,0,0,0,287,0,0,287,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,273,0,0,0,273,0,0,273,0,133,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,119,0,259,0,0,0,259,0,0,259,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,206,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFences":[{"x":5,"y":11.83,"w":10,"h":0.22},{"x":5,"y":15.69,"w":10,"h":0.22},{"x":5,"y":19.55,"w":10,"h":0.22},{"x":5,"y":27.28,"w":10,"h":0.22}],"quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"specialQuizSpawnArea":[]} \ 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 f95547f..5e84a69 100644 --- a/www/html/Game/data/maps/qbroom5.json +++ b/www/html/Game/data/maps/qbroom5.json @@ -1,13284 +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, - 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, - 0, - 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, - 0, - 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, - 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, - 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, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 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, - 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": false, - "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, - 1, - 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, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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", - "quizBattlePathGateArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 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, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 4, - 4, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 12, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 16, - 16, - 0, - 16, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 16, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 19, - 20, - 0, - 20, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ], - "quizBattleFinishArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ] -} \ 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,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,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,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,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,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,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,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,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,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,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,0,0,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,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0],[0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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":false,"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,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,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]],"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,1,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,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,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,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,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,1,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,1,1,0,0],[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,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,0,0,0,0,0,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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","quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDx":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDy":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,228,0,0,0,228,0,0,228,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,313,0,0,0,313,0,0,313,0,173,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,158,0,298,0,0,0,298,0,0,298,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,284,0,0,0,284,0,0,284,0,144,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,129,0,269,0,0,0,269,0,0,269,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,214,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFences":[{"x":5,"y":11.95,"w":10,"h":0.22},{"x":5,"y":15.8,"w":10,"h":0.22},{"x":5,"y":19.66,"w":10,"h":0.22},{"x":5,"y":27.36,"w":10,"h":0.22}],"quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"specialQuizSpawnArea":[]} \ 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 6c490cd..2d1abb9 100644 --- a/www/html/Game/data/maps/qbroom6.json +++ b/www/html/Game/data/maps/qbroom6.json @@ -1,13284 +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, - 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, - 0, - 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, - 0, - 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, - 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, - 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, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 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, - 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": false, - "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, - 1, - 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, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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", - "quizBattlePathGateArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 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, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 4, - 4, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 12, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 16, - 16, - 0, - 16, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 16, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 19, - 20, - 0, - 20, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ], - "quizBattleFinishArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ] -} \ 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,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,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,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,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,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,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,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,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,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,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,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,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,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,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0],[0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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":false,"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,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,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]],"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,1,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,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,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,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,0,0,0,0,0,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,1,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,1,1,0,0],[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,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,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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","quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDx":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDy":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,212,0,0,0,212,0,0,212,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,292,0,0,0,292,0,0,292,0,152,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,131,0,271,0,0,0,271,0,0,271,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,351,0,0,0,351,0,0,351,0,211,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,191,0,331,0,0,0,331,0,0,331,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,171,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFences":[{"x":5,"y":11.74,"w":10,"h":0.22},{"x":5,"y":19.33,"w":10,"h":0.22},{"x":5,"y":23.13,"w":10,"h":0.22},{"x":5,"y":26.93,"w":10,"h":0.22}],"quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"specialQuizSpawnArea":[]} \ 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 5014bdf..0fd0a3a 100644 --- a/www/html/Game/data/maps/qbroom7.json +++ b/www/html/Game/data/maps/qbroom7.json @@ -1,13284 +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, - 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, - 0, - 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, - 0, - 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, - 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, - 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, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 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, - 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": false, - "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, - 1, - 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, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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", - "quizBattlePathGateArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 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, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 4, - 4, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 12, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 16, - 16, - 0, - 16, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 16, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 19, - 20, - 0, - 20, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ], - "quizBattleFinishArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ] -} \ 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,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,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,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,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,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,0,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,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,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,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,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,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,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,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,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0],[0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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":false,"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,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,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]],"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,1,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,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,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,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,0,0,0,0,0,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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,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,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,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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","quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDx":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDy":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,198,0,0,0,198,0,0,198,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,272,0,0,0,272,0,0,272,0,132,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,206,0,346,0,0,0,346,0,0,346,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,319,0,0,0,319,0,0,319,0,179,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,153,0,293,0,0,0,293,0,0,293,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFences":[{"x":5,"y":15.28,"w":10,"h":0.22},{"x":5,"y":19.01,"w":10,"h":0.22},{"x":5,"y":22.75,"w":10,"h":0.22}],"quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"specialQuizSpawnArea":[]} \ 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 b2de8ca..b2ef26f 100644 --- a/www/html/Game/data/maps/qbroom8.json +++ b/www/html/Game/data/maps/qbroom8.json @@ -1,13284 +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, - 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, - 0, - 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, - 0, - 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, - 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, - 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, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 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, - 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": false, - "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, - 1, - 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, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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", - "quizBattlePathGateArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 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, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 4, - 4, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 12, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 16, - 16, - 0, - 16, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 16, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 19, - 20, - 0, - 20, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ], - "quizBattleFinishArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ] -} \ 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,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,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,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,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,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,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,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,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,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,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,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,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,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,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0],[0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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":false,"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,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,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]],"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,1,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,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,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,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,0,0,0,0,0,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,1,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,1,1,0,0],[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,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,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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","quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDx":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDy":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,202,0,0,0,202,0,0,202,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,277,0,0,0,277,0,0,277,0,137,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,113,0,253,0,0,0,253,0,0,253,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,328,0,0,0,328,0,0,328,0,188,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,164,0,304,0,0,0,304,0,0,304,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,139,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFences":[{"x":5,"y":11.59,"w":10,"h":0.22},{"x":5,"y":19.1,"w":10,"h":0.22},{"x":5,"y":22.86,"w":10,"h":0.22},{"x":5,"y":26.61,"w":10,"h":0.22}],"quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"specialQuizSpawnArea":[]} \ 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 11aab28..449f462 100644 --- a/www/html/Game/data/maps/qbroom9.json +++ b/www/html/Game/data/maps/qbroom9.json @@ -1,13284 +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, - 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, - 0, - 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, - 0, - 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, - 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, - 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, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 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, - 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": false, - "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, - 1, - 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, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 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, - 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, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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", - "quizBattlePathGateArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 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, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 0, - 0, - 3, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 4, - 4, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 0, - 0, - 0, - 6, - 0, - 0, - 0, - 5, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 10, - 0, - 0, - 0, - 11, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 12, - 12, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 15, - 0, - 0, - 0, - 14, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 16, - 16, - 0, - 16, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 16, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 0, - 19, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 17, - 0, - 0, - 0, - 18, - 0, - 0, - 19, - 20, - 0, - 20, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ], - "quizBattleFinishArea": [ - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0 - ], - [ - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 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, - 0, - 0, - 0, - 0, - 0, - 0 - ] - ] -} \ 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,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,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,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,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,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,0,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,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,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,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,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,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,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,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,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0],[0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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":false,"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,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,1,0,0,1,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,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0],[0,0,0,0,1,0,1,0,0,0,1,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,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]],"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,1,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,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,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,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,0,0,0,0,0,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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,1,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,1,1,0,0],[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,0,0],[0,0,1,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,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,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,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,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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","quizBattlePathGateArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,7,0,0,0,3,0,0,3,0,3,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,15,0,0,0,11,0,0,11,0,11,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFinishArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeQuestionArea":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0],[0,0,0,0,0,0,7,0,0,0,6,0,0,5,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0],[0,0,0,0,0,0,15,0,0,0,14,0,0,13,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDx":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,26,0,0,0,6,0,0,88,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,26,0,26,0,0,0,6,0,0,88,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,70,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleDomeDrawDy":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,196,0,0,0,196,0,0,196,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,268,0,0,0,268,0,0,268,0,128,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,200,0,340,0,0,0,340,0,0,340,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,313,0,0,0,313,0,0,313,0,173,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,145,0,285,0,0,0,285,0,0,285,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,117,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattleFences":[{"x":5,"y":15.22,"w":10,"h":0.22},{"x":5,"y":18.95,"w":10,"h":0.22},{"x":5,"y":22.67,"w":10,"h":0.22}],"quizBattleDomeComp":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,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,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,1,0,0,0,2,0,0,3,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,4,0,0,0,5,0,0,6,0,7,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,8,0,9,0,0,0,10,0,0,11,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,12,0,0,0,13,0,0,14,0,15,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,16,0,17,0,0,0,18,0,0,19,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"quizBattlePathGateRequired":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,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,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,1,0,0,0,2,2,0,3,3,3,3,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,7,7,0,3,3,3,0,3,3,0,3,0,3,3,0,0],[0,0,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,7,7,0,8,0,9,9,9,0,10,10,0,11,11,11,11,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,15,15,0,11,11,11,0,11,11,0,11,0,11,11,0,0],[0,0,15,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,15,15,0,16,0,17,17,17,0,18,18,0,19,19,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,20,20,20,20,20,20,20,20,20,20,20,0,19,19,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],"specialQuizSpawnArea":[]} \ No newline at end of file diff --git a/www/html/Game/data/quiz-battle-leaderboard.json b/www/html/Game/data/quiz-battle-leaderboard.json index af14f89..72f4e9f 100644 --- a/www/html/Game/data/quiz-battle-leaderboard.json +++ b/www/html/Game/data/quiz-battle-leaderboard.json @@ -1 +1 @@ -{"qbroom1":[{"name":"Nam","score":18,"characterId":"char-1777017632279","ts":1782909257585},{"name":"ผู้เล่น9514","score":17,"characterId":"char-1777017632279","ts":1781850667717},{"name":"MONE","score":10,"characterId":"char-1777017632279","ts":1781850749437},{"name":"tomato","score":9,"characterId":"char-1777017632279","ts":1781850748552},{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781171902962},{"name":"ผู้เล่น1991","score":5,"characterId":"char-1777017632279","ts":1781850950078},{"name":"ผู้เล่น8916","score":4,"characterId":"char-1777017632279","ts":1781074083031},{"name":"ผู้เล่น2821","score":4,"characterId":"char-1777017632279","ts":1781079179108},{"name":"Weenwild","score":4,"characterId":"char-1777017632279","ts":1781849863580},{"name":"ผู้เล่น2359","score":0,"characterId":"char-1777017632279","ts":1783664802662},{"name":"ผู้เล่น1576","score":0,"characterId":"char-1777017632279","ts":1784258335514}],"qbroom2":[{"name":"ผู้เล่น5703","score":14,"characterId":"char-1777017632279","ts":1784258779407},{"name":"ผู้เล่น4472","score":13,"characterId":"char-1777017632279","ts":1781851033914},{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781175468983},{"name":"ผู้เล่น2612","score":1,"characterId":"char-1777017632279","ts":1781079252376},{"name":"ผู้เล่น1948","score":0,"characterId":"char-1777017632279","ts":1783664954451}],"qbroom3":[{"name":"Q","score":9,"characterId":"char-1777017632279","ts":1781175654893},{"name":"Weenwild","score":2,"characterId":"char-1777017632279","ts":1781849898452},{"name":"ผู้เล่น8622","score":0,"characterId":"char-1777017632279","ts":1783842317274}],"qbroom5":[{"name":"Q","score":6,"characterId":"char-1777017632279","ts":1781675299850}],"qbroom6":[{"name":"Q","score":3,"characterId":"char-1777017632279","ts":1781675383175},{"name":"ผู้เล่น1752","score":1,"characterId":"char-1777017632279","ts":1784273155119}],"qbroom9":[{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781696345322}],"qbroom4":[{"name":"ผู้เล่น9337","score":20,"characterId":"char-1777017632279","ts":1784270128376}],"qbroom7":[{"name":"ผู้เล่น4119","score":3,"characterId":"char-1777017632279","ts":1784270217530}]} \ No newline at end of file +{"qbroom1":[{"name":"Nam","score":18,"characterId":"char-1777017632279","ts":1782909257585},{"name":"ผู้เล่น9514","score":17,"characterId":"char-1777017632279","ts":1781850667717},{"name":"MONE","score":10,"characterId":"char-1777017632279","ts":1781850749437},{"name":"tomato","score":9,"characterId":"char-1777017632279","ts":1781850748552},{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781171902962},{"name":"ผู้เล่น1991","score":5,"characterId":"char-1777017632279","ts":1781850950078},{"name":"ผู้เล่น8916","score":4,"characterId":"char-1777017632279","ts":1781074083031},{"name":"ผู้เล่น2821","score":4,"characterId":"char-1777017632279","ts":1781079179108},{"name":"Weenwild","score":4,"characterId":"char-1777017632279","ts":1781849863580},{"name":"play1","score":1,"characterId":"char-1777017632279","ts":1785441076910},{"name":"ผู้เล่น2359","score":0,"characterId":"char-1777017632279","ts":1783664802662},{"name":"ผู้เล่น1576","score":0,"characterId":"char-1777017632279","ts":1784258335514}],"qbroom2":[{"name":"ผู้เล่น5703","score":14,"characterId":"char-1777017632279","ts":1784258779407},{"name":"ผู้เล่น4472","score":13,"characterId":"char-1777017632279","ts":1781851033914},{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781175468983},{"name":"ผู้เล่น2612","score":1,"characterId":"char-1777017632279","ts":1781079252376},{"name":"play2","score":1,"characterId":"char-1777017632279","ts":1785440817269},{"name":"ผู้เล่น1948","score":0,"characterId":"char-1777017632279","ts":1783664954451}],"qbroom3":[{"name":"Q","score":9,"characterId":"char-1777017632279","ts":1781175654893},{"name":"Weenwild","score":2,"characterId":"char-1777017632279","ts":1781849898452},{"name":"play3","score":1,"characterId":"char-1777017632279","ts":1785441122522},{"name":"ผู้เล่น8622","score":0,"characterId":"char-1777017632279","ts":1783842317274}],"qbroom5":[{"name":"Q","score":6,"characterId":"char-1777017632279","ts":1781675299850},{"name":"play5","score":1,"characterId":"char-1777017632279","ts":1785440912224}],"qbroom6":[{"name":"Q","score":3,"characterId":"char-1777017632279","ts":1781675383175},{"name":"ผู้เล่น1752","score":1,"characterId":"char-1777017632279","ts":1784273155119},{"name":"play6","score":1,"characterId":"char-1777017632279","ts":1785441196130}],"qbroom9":[{"name":"Q","score":7,"characterId":"char-1777017632279","ts":1781696345322},{"name":"play9","score":1,"characterId":"char-1777017632279","ts":1785441009369}],"qbroom4":[{"name":"ผู้เล่น9337","score":20,"characterId":"char-1777017632279","ts":1784270128376},{"name":"play4","score":1,"characterId":"char-1777017632279","ts":1785440881506}],"qbroom7":[{"name":"Q","score":4,"characterId":"char-1777017632279","ts":1784785097780},{"name":"ผู้เล่น4119","score":3,"characterId":"char-1777017632279","ts":1784270217530},{"name":"play7","score":1,"characterId":"char-1777017632279","ts":1785440972161}],"qbroom8":[{"name":"play8","score":1,"characterId":"char-1777017632279","ts":1785440990472}],"qbroom10":[{"name":"play10","score":1,"characterId":"char-1777017632279","ts":1785441027840}]} \ No newline at end of file diff --git a/www/html/Game/data/quiz-battle-modes.json b/www/html/Game/data/quiz-battle-modes.json new file mode 100644 index 0000000..80419a6 --- /dev/null +++ b/www/html/Game/data/quiz-battle-modes.json @@ -0,0 +1,68 @@ +{ + "version": 1, + "updatedAt": "2026-07-27 00:17", + "updatedBy": "admin", + "tz": "Asia/Bangkok", + "modes": { + "1": { + "title": "กฎหมายใกล้ตัวสิ", + "enabled": true, + "openUntil": "", + "maxPlayers": 50 + }, + "2": { + "title": "กฎหมายสิทธิพื้นฐาน", + "enabled": true, + "openUntil": "", + "maxPlayers": 50 + }, + "3": { + "title": "กฎหมายจราจร", + "enabled": true, + "openUntil": "", + "maxPlayers": 50 + }, + "4": { + "title": "คดีเกี่ยวกับทรัพย์", + "enabled": true, + "openUntil": "", + "maxPlayers": 50 + }, + "5": { + "title": "คดีหมิ่นประมาท", + "enabled": true, + "openUntil": "", + "maxPlayers": 50 + }, + "6": { + "title": "คดีทางเพศ", + "enabled": true, + "openUntil": "", + "maxPlayers": 50 + }, + "7": { + "title": "คดีอาชญากรรม", + "enabled": true, + "openUntil": "", + "maxPlayers": 50 + }, + "8": { + "title": "อาชญากรรมออนไลน์", + "enabled": true, + "openUntil": "", + "maxPlayers": 50 + }, + "9": { + "title": "กระบวนการยุติธรรม", + "enabled": true, + "openUntil": "", + "maxPlayers": 50 + }, + "10": { + "title": "งานบริการกระทรวงยุติธรรม", + "enabled": true, + "openUntil": "", + "maxPlayers": 50 + } + } +} \ No newline at end of file diff --git a/www/html/Game/public/ai-admin.html b/www/html/Game/public/ai-admin.html index 11f6c53..f04e30a 100644 --- a/www/html/Game/public/ai-admin.html +++ b/www/html/Game/public/ai-admin.html @@ -56,9 +56,10 @@

การตั้งค่าระบบ

- - - เว้นว่างถ้าไม่ต้องการเปลี่ยน + + + กำลังตรวจสอบ… + เว้นว่างถ้าไม่ต้องการเปลี่ยน · คีย์ที่ขึ้นต้น sk-or- ระบบจะส่งไป OpenRouter ให้อัตโนมัติ
@@ -156,9 +157,24 @@ document.getElementById('rag-enabled').checked = !!data.rag_enabled; document.getElementById('rag-context').value = data.rag_context != null ? data.rag_context : ''; document.getElementById('rag-context-wrap').style.opacity = data.rag_enabled ? '1' : '0.7'; + showKeyStatus(data); }); } + /* [2026-07-23] เดิมช่องคีย์ถูกล้างทุกครั้งหลังเซฟ และ hasKey ที่ API ส่งมาไม่เคยถูกแสดง + → ผู้ใช้ไม่มีทางรู้ว่าคีย์ถูกเก็บไว้จริงไหม = "กดเซฟแล้วมันไม่เซฟ" */ + function showKeyStatus(data) { + var el = document.getElementById('key-status'); + if (!el || !data) return; + if (data.hasKey) { + el.style.color = '#9ece6a'; + el.textContent = '✓ มีคีย์บันทึกอยู่แล้ว: ' + (data.keyMasked || '') + ' → ใช้ ' + (data.provider || '?'); + } else { + el.style.color = '#f7768e'; + el.textContent = '✗ ยังไม่มีคีย์ในระบบ'; + } + } + document.getElementById('rag-enabled').addEventListener('change', function () { document.getElementById('rag-context-wrap').style.opacity = this.checked ? '1' : '0.7'; }); @@ -204,6 +220,7 @@ msgEl.textContent = 'บันทึกแล้ว'; msgEl.style.display = 'block'; document.getElementById('openai-key').value = ''; + showKeyStatus(data); /* ยืนยันทันทีว่าคีย์ที่เก็บอยู่คือใบไหน/เจ้าไหน */ } else { errEl.textContent = (data && data.error) || 'บันทึกไม่สำเร็จ'; } diff --git a/www/html/Game/public/editor.html b/www/html/Game/public/editor.html index d503fcd..b049cc2 100644 --- a/www/html/Game/public/editor.html +++ b/www/html/Game/public/editor.html @@ -1,589 +1,589 @@ - - - - - - - - Editor ฉาก — Game - - - - - - - -
-

Editor ฉาก

-
-
-
-

ประเภทเกม

-
- -
- - -
- -
-

โหลด / ตั้งชื่อฉาก

-
- - - -
-
- -
-

ขนาดกริด

-
- - - - - -
-
ชนกำแพง · กล่องตรวจ
- - -
-
-
โซนห้ามซ้อน · กล่องตรวจ
- - -
- - -
-
- -
-

เครื่องมือวาด

- -
- - - - - - - - - - - - - - - -
-
- - -
-
-
- - - -
-

พื้นหลัง

-
- - -
- - - -
- - -
-
-
- - 100% - - - -
-
- - -
-

บันทึกแล้วจะได้รหัสฉาก ใช้รหัสนี้ตอนสร้างห้องในล็อบบี้

-
-
- - -
v —
- - - - - - - - + + + + + + + + Editor ฉาก — Game + + + + + + + +
+

Editor ฉาก

+
+
+
+

ประเภทเกม

+
+ +
+ + +
+ +
+

โหลด / ตั้งชื่อฉาก

+
+ + + +
+
+ +
+

ขนาดกริด

+
+ + + + + +
+
ชนกำแพง · กล่องตรวจ
+ + +
+
+
โซนห้ามซ้อน · กล่องตรวจ
+ + +
+ + +
+
+ +
+

เครื่องมือวาด

+ +
+ + + + + + + + + + + + + + + +
+
+ + +
+
+
+ + + +
+

พื้นหลัง

+
+ + +
+ + + +
+ + +
+
+
+ + 100% + + + +
+
+ + +
+

บันทึกแล้วจะได้รหัสฉาก ใช้รหัสนี้ตอนสร้างห้องในล็อบบี้

+
+
+ + +
v —
+ + + + + + + + diff --git a/www/html/Game/public/img/qb-scenes/room-1.jpg b/www/html/Game/public/img/qb-scenes/room-1.jpg index 8961f9d..46adde3 100644 Binary files a/www/html/Game/public/img/qb-scenes/room-1.jpg and b/www/html/Game/public/img/qb-scenes/room-1.jpg differ diff --git a/www/html/Game/public/img/qb-scenes/room-10.jpg b/www/html/Game/public/img/qb-scenes/room-10.jpg index b55b6d7..8d85d49 100644 Binary files a/www/html/Game/public/img/qb-scenes/room-10.jpg and b/www/html/Game/public/img/qb-scenes/room-10.jpg differ diff --git a/www/html/Game/public/img/qb-scenes/room-2.jpg b/www/html/Game/public/img/qb-scenes/room-2.jpg index 0cb806c..82d55b5 100644 Binary files a/www/html/Game/public/img/qb-scenes/room-2.jpg and b/www/html/Game/public/img/qb-scenes/room-2.jpg differ diff --git a/www/html/Game/public/img/qb-scenes/room-3.jpg b/www/html/Game/public/img/qb-scenes/room-3.jpg index 23fce15..c39fbd4 100644 Binary files a/www/html/Game/public/img/qb-scenes/room-3.jpg and b/www/html/Game/public/img/qb-scenes/room-3.jpg differ diff --git a/www/html/Game/public/img/qb-scenes/room-4.jpg b/www/html/Game/public/img/qb-scenes/room-4.jpg index 913e30c..734077c 100644 Binary files a/www/html/Game/public/img/qb-scenes/room-4.jpg and b/www/html/Game/public/img/qb-scenes/room-4.jpg differ diff --git a/www/html/Game/public/img/qb-scenes/room-5.jpg b/www/html/Game/public/img/qb-scenes/room-5.jpg index 7419c37..00ebf2b 100644 Binary files a/www/html/Game/public/img/qb-scenes/room-5.jpg and b/www/html/Game/public/img/qb-scenes/room-5.jpg differ diff --git a/www/html/Game/public/img/qb-scenes/room-6.jpg b/www/html/Game/public/img/qb-scenes/room-6.jpg index 6c72bb1..f15a66b 100644 Binary files a/www/html/Game/public/img/qb-scenes/room-6.jpg and b/www/html/Game/public/img/qb-scenes/room-6.jpg differ diff --git a/www/html/Game/public/img/qb-scenes/room-7.jpg b/www/html/Game/public/img/qb-scenes/room-7.jpg index bd6c308..50b08b4 100644 Binary files a/www/html/Game/public/img/qb-scenes/room-7.jpg and b/www/html/Game/public/img/qb-scenes/room-7.jpg differ diff --git a/www/html/Game/public/img/qb-scenes/room-8.jpg b/www/html/Game/public/img/qb-scenes/room-8.jpg index bf923bb..c7a0633 100644 Binary files a/www/html/Game/public/img/qb-scenes/room-8.jpg and b/www/html/Game/public/img/qb-scenes/room-8.jpg differ diff --git a/www/html/Game/public/img/qb-scenes/room-9.jpg b/www/html/Game/public/img/qb-scenes/room-9.jpg index 3622735..cec3d36 100644 Binary files a/www/html/Game/public/img/qb-scenes/room-9.jpg and b/www/html/Game/public/img/qb-scenes/room-9.jpg differ diff --git a/www/html/Game/public/img/qb-ui/Quiz-Lock.png b/www/html/Game/public/img/qb-ui/Quiz-Lock.png new file mode 100644 index 0000000..fe5d026 Binary files /dev/null and b/www/html/Game/public/img/qb-ui/Quiz-Lock.png differ diff --git a/www/html/Game/public/img/qb-ui/Quiz-Unlock.png b/www/html/Game/public/img/qb-ui/Quiz-Unlock.png new file mode 100644 index 0000000..a67aae8 Binary files /dev/null and b/www/html/Game/public/img/qb-ui/Quiz-Unlock.png differ diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 121d2eb..91df0ea 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -1,27892 +1,28015 @@ -(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'; - /* Card 3 Ban — ผู้ถูกแบนรอบนี้: เข้ามาดูเพื่อนเล่นได้ แต่ควบคุมตัวละคร/ทำแอ็กชันไม่ได้ (รอบหน้าเล่นปกติ) */ - const playBannedSpectator = params.get('banned') === '1'; - /* Card 3 Ban (กรณีโหวตแบน "บอท") — slot ของ __pv_bot_N ที่ถูกแบน → ไม่ spawn ลงเล่นรอบนี้ (-1 = ไม่มี) */ - const playBannedBotSlot = (() => { const v = parseInt(params.get('banBot'), 10); return Number.isFinite(v) && v >= 0 ? v : -1; })(); - /* shield โปร่งใสคลุมทั้งจอ (z 200) กิน pointer/touch ทุกอย่าง → ปุ่มเกม(jump z59/drop z60/joystick/shoot)+canvas กดไม่ได้ - แต่ต่ำกว่า modal (z 10000+) เลย special quiz / สรุปผล ยังกดได้ปกติ + ป้ายบอกสถานะด้านบน */ - function setupBannedSpectatorUi() { - if (!playBannedSpectator || document.getElementById('banned-spectator-shield')) return; - /* หน้า how-to/pregame = "ตัวคุมรอบ" (READY/START) ไม่ใช่การเล่น → ต้องอยู่เหนือ shield เสมอ - บั๊ก: how-to ของ MG2 (คลาส gch-mg2-mock) มี z-index แค่ 56 — ต่ำกว่า shield (200) → โดนคลุมทั้งแผง - → host ที่โดนแบนกด START ไม่ได้ และ START เป็นสิทธิ์ host เท่านั้น = ทั้งห้องค้างถาวร - (เลย์เอาต์อื่นรอดเพราะ z สูงอยู่แล้ว: crown legacy 10140 / quiz-carry 10078) - ปล่อยให้กดได้ปลอดภัย: server กัน non-host กด START และไม่นับ ready ของคนถูกแบนอยู่แล้ว */ - const zfix = document.createElement('style'); - zfix.id = 'banned-spectator-zfix'; - zfix.textContent = '#gauntlet-crown-howto-overlay,#quiz-carry-pregame-overlay{z-index:260!important}'; - document.head.appendChild(zfix); - const shield = document.createElement('div'); - shield.id = 'banned-spectator-shield'; - shield.style.cssText = 'position:fixed;inset:0;z-index:200;background:transparent;pointer-events:auto;cursor:not-allowed;touch-action:none;-webkit-tap-highlight-color:transparent'; - ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'click', 'dblclick', 'touchstart', 'touchend', 'touchmove', 'wheel', 'contextmenu'].forEach(function (ev) { - shield.addEventListener(ev, function (e) { e.stopPropagation(); e.preventDefault(); }, { passive: false }); - }); - document.body.appendChild(shield); - const banner = document.createElement('div'); - banner.id = 'banned-spectator-banner'; - banner.style.cssText = 'position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:201;pointer-events:none;background:linear-gradient(180deg,#2a1430,#160a1d);border:1px solid rgba(247,118,142,.6);border-radius:14px;padding:10px 20px;text-align:center;box-shadow:0 10px 30px rgba(0,0,0,.55)'; - banner.innerHTML = '
BAN CARD
' - + '
คุณถูกแบนรอบนี้ · กำลังดูเพื่อนเล่น
' - + '
รอบหน้ากลับมาเล่นได้ตามปกติ
'; - document.body.appendChild(banner); - } - 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 */ } - } - - /* #5 หน้าโหลดตอนเข้าเกม — กันเห็นจอเปล่า/asset แว่บก่อนฉากพร้อม. โชว์ทันที ซ่อนเมื่อ draw() เฟรมแรกมี mapData - (+ หน่วงเล็กน้อยให้ asset แรกโหลด) มี safety timeout กันค้าง. ไม่โชว์ใน editor embed preview */ - let playLoadingHidden = false; - let playLoadingHideScheduled = false; - const PLAY_LOADING_MIN_MS = 450; /* โชว์อย่างน้อยเท่านี้ กันแว่บหาย (ไม่ทันเห็น) */ - let playLoadingShownAt = (typeof performance !== 'undefined' ? performance.now() : Date.now()); - function nowMsPlay() { return (typeof performance !== 'undefined' ? performance.now() : Date.now()); } - function hidePlayLoadingOverlay() { - if (playLoadingHidden) return; - const elapsed = nowMsPlay() - playLoadingShownAt; - if (elapsed < PLAY_LOADING_MIN_MS) { setTimeout(hidePlayLoadingOverlay, PLAY_LOADING_MIN_MS - elapsed); return; } - playLoadingHidden = true; - try { - const el = document.getElementById('play-loading-overlay'); - if (el) { el.style.opacity = '0'; el.style.pointerEvents = 'none'; setTimeout(function () { if (el && el.parentNode) el.parentNode.removeChild(el); }, 340); } - } catch (_e) { /* ignore */ } - } - (function showPlayLoadingOverlay() { - /* editor embed preview: ไม่ต้องมี loading → เอา overlay static ออก */ - if (previewMode && editorEmbedReturn) { - playLoadingHidden = true; - try { const pe = document.getElementById('play-loading-overlay'); if (pe && pe.parentNode) pe.parentNode.removeChild(pe); } catch (_e) {} - return; - } - /* [2026-07-17] เสียง warp "ตอนวาร์ปเข้ามินิเกม" — เล่นพร้อม "คลิป/จอโหลด" ตั้งแต่เข้า play.html - (user request: เดิมอยู่ตอนฉากโหลดเสร็จ = หลังคลิป). play.html เป็นมินิเกมล้วน (LobbyB อยู่ที่ - room-lobby.html เสมอ — room-lobby.js:7876) → ปลอดภัยที่จะเล่นตอน page load. gate กันเคส lobby - map (mn8nx46h) หลุดมา + ต้องมี map param จริง. warp เป็นเสียง root (ROOT_SFX) resolve ได้โดยไม่พึ่ง - mapData ที่ยังไม่มาตอนนี้. autoplay: engagement สูงพอเล่นได้ (เดิมที่ playClientReadyEmitted ก็เล่นได้ - โดยไม่มี gesture ใหม่บนหน้านี้) */ - try { - var _warpMap = ''; - try { _warpMap = String(new URLSearchParams(location.search).get('map') || '').trim(); } catch (_e) { _warpMap = ''; } - if (window.jdSound && _warpMap && _warpMap !== 'mn8nx46h') { - try { window.jdSound.playSfx('warp'); } catch (_e) { /* ignore */ } - } - } catch (_e) { /* ignore */ } - try { - if (!document.body) return; - /* overlay เป็น static ใน play.html แล้ว — ถ้าไม่มี (html เก่า cache) ค่อย inject เสริม */ - if (!document.getElementById('play-loading-overlay')) { - const css = document.createElement('style'); - css.textContent = '' - + '#play-loading-overlay{position:fixed;inset:0;z-index:99990;display:flex;flex-direction:column;align-items:center;' - + 'justify-content:center;gap:18px;background:radial-gradient(circle at 50% 42%,#10203c,#060b16 75%);transition:opacity .32s;}' - + '#play-loading-overlay .plo-spin{width:62px;height:62px;border-radius:50%;border:6px solid rgba(120,190,255,.18);' - + 'border-top-color:#5fd0ff;animation:ploSpin .85s linear infinite;}' - + '#play-loading-overlay .plo-text{color:#cfe8ff;font:600 19px system-ui,"Kanit",sans-serif;letter-spacing:.5px;text-shadow:0 2px 10px rgba(0,0,0,.5);}' - + '@keyframes ploSpin{to{transform:rotate(360deg);}}'; - document.head.appendChild(css); - const ov = document.createElement('div'); - ov.id = 'play-loading-overlay'; - ov.innerHTML = '
กำลังโหลดเกม…
'; - document.body.insertBefore(ov, document.body.firstChild); - } - /* safety กันค้าง — แต่ "อย่าซ่อนถ้าเกมยังไม่ render" (เน็ตช้า join นาน → ถ้าซ่อน 8 วิ = จอดำเปล่า ไม่รู้สถานะ) - เช็คทุก 1.5 วิ: เกมเริ่มวาดแล้ว (playLoadingHideScheduled) → ปล่อย path ปกติซ่อน · ยังไม่วาด+ช้า → โชว์ "เน็ตช้า (N วิ)" คง spinner · เกิน 30 วิ ค่อยซ่อนกันค้างถาวร */ - const _ploT0 = Date.now(); - const _ploCheck = function () { - if (playLoadingHidden || playLoadingHideScheduled) return; /* ซ่อนแล้ว/เกมเริ่มวาดแล้ว */ - const waited = Date.now() - _ploT0; - if (waited > 30000) { hidePlayLoadingOverlay(); return; } /* hard limit กันค้างถาวร */ - if (waited > 5000) { - const txt = document.querySelector('#play-loading-overlay .plo-text'); - if (txt) txt.textContent = 'เน็ตช้า… กำลังโหลด (' + Math.floor(waited / 1000) + ' วิ)'; - } - setTimeout(_ploCheck, 1500); - }; - setTimeout(_ploCheck, 1500); - } catch (_e) { playLoadingHidden = true; } - })(); - 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'; - /* ใช้ map จริง (currentPlayMapId) ไม่ใช่ ?map= ใน URL — live play ไม่มี ?map= → เดิม false → - mission flow ไม่ทำงาน → MG4 จบหลังตอบ 1 ข้อ (bug #41). เป็นฟังก์ชันเพราะ map รู้ตอน join */ - function quizCarryUseMissionSummaryOverlay() { return currentPlayMapId() === 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.5; /* ต้องตรงกับ server — miss เมื่อ overlap < 50% บล็อก */ - /** จุดกลางแท่งที่วาง 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 = 34; - const SPACE_SHOOTER_ASTEROID_RADIUS_AT_MIN_HP = 8; - - /* PRNG แบบ seed ได้ (mulberry32) — ใช้สุ่มสิ่งแวดล้อมมินิเกมแบบ deterministic - seed เดียวกัน (อิง liveBeginsAt ของ server) → ทุกเครื่องได้ลำดับสุ่มเดียวกัน = อุกาบาต/บอลลูนชุดเดียวกัน */ - function makeSeededRng(seed) { - let t = (seed >>> 0) || 1; - return function () { - t = (t + 0x6D2B79F5) >>> 0; - let r = Math.imul(t ^ (t >>> 15), 1 | t); - r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r; - return ((r ^ (r >>> 14)) >>> 0) / 4294967296; - }; - } - /* seed ต่อ index ของ spawn — รวม anchor (liveBeginsAt) กับ index ให้ก้อนแต่ละลำดับสุ่มต่างกันแต่ตรงกันทุกเครื่อง */ - function envSpawnSeed(anchorMs, index) { - return (((anchorMs >>> 0) ^ Math.imul((index | 0) + 1, 2654435761)) >>> 0); - } - - 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; - if (window.jdSound) { try { window.jdSound.playSfx('crash', 'mg6'); } catch (e) { /* ignore */ } } /* ตัวเอง + บอท (host) ตกรอบ = ได้ยินเสียง · peer-human ตกรอบ = เล่นตอนรับ spaceShooterEliminated ใน user-move */ - spaceShooterPopups.push({ x: cx, y: cy - 50, text: 'OUT', until: Date.now() + 1400 }); - } - break; - } - } - - const wasElim = !!me.spaceShooterEliminated; - if (myId != null) tryHit(me, me.spaceShooterCx, me.spaceShooterCy); - /* คิดการชนเฉพาะยานของเรา (เจ้าของตัดสินเอง) + บอท (host ตัดสิน) เท่านั้น - ผู้เล่นมนุษย์คนอื่นรอสถานะ eliminated/hits จาก move ของเจ้าตัว — กัน "ตายไม่พร้อมกัน" - (เดิมทุกเครื่องคิดชนให้ทุกยานด้วยตำแหน่งดาวเคราะห์ local ที่ต่างกัน → ตัดสินไม่ตรงกัน) */ - others.forEach(function (o, id) { - if (!o) return; - /* host คิดการชนให้ "บอท" รวม takeover bot (id=socket-id, flag isSpaceShooterServerBot) — เดิมเช็คแค่ isPreviewBotId → ข้าม takeover bot = มันไม่มีวันตาย */ - if (!isMePlayHost()) return; - if (!(isPreviewBotId(id) || o.isSpaceShooterServerBot)) return; - if (spaceShooterServerAuthActive()) { - /* server-auth (MG6 เกมจริง): host คิดการชนบอท (asteroid deterministic + ตำแหน่งบอทจาก server) - แล้ว "รายงาน" จำนวนชนให้ server ตัดสินตายจริง — ไม่ตัดสินตายเอง local (กันสถานะตายขัดกัน) */ - const h0 = Math.max(0, Number(o.spaceShooterHits) || 0); - tryHit(o, o.spaceShooterCx, o.spaceShooterCy); - const h1 = Math.max(0, Number(o.spaceShooterHits) || 0); - if (h1 > h0 && socket) { - try { socket.emit('space-shooter-bot-hit', { id, hits: h1 }); } catch (e) { /* ignore */ } - } - } else { - tryHit(o, o.spaceShooterCx, o.spaceShooterCy); - } - }); - /* เพิ่งตาย → broadcast ทันที ไม่รอ throttle 90ms ให้ทุกเครื่องเห็นพร้อมกัน */ - if (!wasElim && me.spaceShooterEliminated && socket && myId != null) { - try { - spaceShooterLastMoveEmit = Date.now(); - socket.emit('move', { - x: me.x, y: me.y, direction: me.direction, - spaceShooterScore: Math.max(0, me.spaceShooterScore | 0), - spaceShooterHits: Math.max(0, Number(me.spaceShooterHits) || 0), - spaceShooterEliminated: true, - }); - } catch (eE) { /* ignore */ } - } - if (spaceShooterMissionEveryParticipantEliminatedPlay()) { - /* host ตัดสิน all_dead + broadcast คะแนน; non-host รอรับ space-shooter-over */ - if (isMePlayHost()) triggerSpaceShooterOverPlay('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(); - }; - /* รายงาน score/survived ของเกมที่ client นับเอง (stack/jump_survive) → server ใช้จัดอันดับเหรียญ - (เดิมส่ง {} เปล่า → reportedMiniScore/Survived ไม่ถูกตั้ง → coin ranking MG3/MG5 เพี้ยน) */ - let finishReport = {}; - try { - if (isStack() && stackMini) finishReport = { score: Math.max(0, stackMini.score | 0), survived: true }; - else if (isJumpSurvive()) finishReport = { score: 0, survived: !jumpSurviveEliminated }; - } catch (eFR) { /* ignore */ } - if (socket && socket.connected) { - socket.emit('detective-minigame-finished', finishReport, 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 */ } - /* MG2: เข้าเส้นชัยแล้วแต่ยังมีคนวิ่งอยู่ → รอในฉาก (server จะพากลับ LobbyB พร้อมกันตอนทุกคนจบ) */ - if (res && res.holdInScene) { - showGauntletFinishWaitOverlay(); - return; - } - /* โชว์การ์ดหลักฐานที่เพิ่งได้ "ในฉากมินิเกม" ก่อนกลับ LobbyB */ - if (res && typeof res.suspectIndex === 'number' && res.suspectIndex >= 0) { - showInSceneEvidenceReveal(res, go); - } else { - go(); - } - }); - } else { - go(); - } - } - - /** MG2: ป้าย "เข้าเส้นชัยแล้ว — รอผู้เล่นอื่น" ระหว่างรอคนที่ยังวิ่งอยู่ (จะกลับ LobbyB พร้อมกันตอนเกมจบ) */ - function showGauntletFinishWaitOverlay() { - var ov = document.getElementById('gauntlet-finish-wait'); - if (!ov) { - ov = document.createElement('div'); - ov.id = 'gauntlet-finish-wait'; - ov.style.cssText = 'position:fixed;inset:0;z-index:100000;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;background:radial-gradient(ellipse at center,rgba(10,16,32,.82),rgba(6,9,20,.95));backdrop-filter:blur(3px)'; - ov.innerHTML = - '
🏁 เข้าเส้นชัยแล้ว!
' + - '
รอผู้เล่นคนอื่นเข้าเส้นชัย...
' + - '
• • •
'; - document.body.appendChild(ov); - } - ov.style.display = 'flex'; - } - - /* แจ้ง host ว่าทำไม START ยังกดเริ่มไม่ได้ (เดิม reply ถูก ignore เงียบ → ปุ่มเหมือนเสีย) - ใช้ร่วม MG2/5/6/7 (crown lobby shell) — โชว์ "รอผู้เล่น x/N" หรือ "ยังมีคนไม่ Ready" แล้วซ่อนเอง */ - function showCrownStartBlockedNoticePlay(r) { - if (!r) return; - let msg = r.error || 'ยังเริ่มไม่ได้'; - if (r.waitingPlayers) msg = 'รอผู้เล่นเข้าเกมให้ครบก่อน (' + (r.present || 0) + '/' + (r.total || 0) + ')'; - let ov = document.getElementById('crown-start-notice'); - if (!ov) { - ov = document.createElement('div'); - ov.id = 'crown-start-notice'; - ov.style.cssText = 'position:fixed;left:50%;top:14%;transform:translateX(-50%);z-index:100001;padding:12px 22px;border-radius:14px;background:rgba(20,12,16,.92);border:1px solid rgba(255,120,120,.5);color:#ffd9d9;font:700 18px/1.3 Kanit,system-ui,sans-serif;text-align:center;box-shadow:0 6px 24px rgba(0,0,0,.5);pointer-events:none'; - document.body.appendChild(ov); - } - ov.textContent = '⏳ ' + msg; - ov.style.display = 'block'; - clearTimeout(ov._hideT); - ov._hideT = setTimeout(function () { ov.style.display = 'none'; }, 2600); - } - - /* ===== หน้าโชว์การ์ดหลักฐานในฉากมินิเกม (ก่อนกลับ 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: 'การ์ดชี้คนร้าย' }; - - /** กล่องดูการ์ดหลักฐานแบบขยาย (zoom) — ปิดด้วยปุ่ม ×, คลิกพื้นหลัง, หรือ Esc */ - function openEvidenceCardLightboxPlay(imageUrl, label, rarity) { - if (!imageUrl) return; - var rar = String(rarity || 'common').toLowerCase(); - var glow = EVIDENCE_RARITY_GLOW[rar] || EVIDENCE_RARITY_GLOW.common; - var ov = document.getElementById('evidence-card-lightbox'); - if (!ov) { - ov = document.createElement('div'); - ov.id = 'evidence-card-lightbox'; - ov.style.cssText = 'position:fixed;inset:0;z-index:100001;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(5,8,18,.88);backdrop-filter:blur(4px)'; - ov.innerHTML = - '' + - '
' + - '' + - '
' + - '
'; - document.body.appendChild(ov); - var closeFn = function () { ov.style.display = 'none'; }; - ov.addEventListener('click', function (e) { if (e.target === ov || (e.target.closest && e.target.closest('.ecl-close'))) closeFn(); }); - document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && ov.style.display !== 'none') closeFn(); }); - } - var img = ov.querySelector('.ecl-img'); - var cap = ov.querySelector('.ecl-cap'); - if (img) { img.style.boxShadow = '0 0 44px ' + glow + ',0 22px 60px rgba(0,0,0,.6)'; img.src = imageUrl; } - if (cap) cap.textContent = label || ''; - ov.style.display = 'flex'; - } - - 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'; - /* scroll container + inner margin:auto → มือถือแนวตั้งที่เนื้อหาสูงเกินจอ เลื่อนถึงปุ่มได้ (เดิม justify center ไม่มี scroll → ปุ่มหลุดล่างจอ) */ - ov.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(5,8,18,.9);display:flex;overflow-y:auto;-webkit-overflow-scrolling:touch;backdrop-filter:blur(3px);padding:16px 12px;box-sizing:border-box'; - var inner = document.createElement('div'); - inner.style.cssText = 'margin:auto;display:flex;flex-direction:column;align-items:center;gap:clamp(12px,2.4vh,22px);width:100%;max-width:640px'; - var title = document.createElement('div'); - title.style.cssText = 'font:900 clamp(20px,5.5vw,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:auto;height:auto;max-width:min(72vw,360px);max-height:52vh;border-radius:14px;box-shadow:0 0 36px ' + glow + ',0 18px 50px rgba(0,0,0,.6);cursor:zoom-in'; - img.title = 'แตะเพื่อดูใหญ่'; - img.addEventListener('click', function () { openEvidenceCardLightboxPlay(c.imageUrl, EVIDENCE_RARITY_LABEL[rar] || '', rar); }); - 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:clamp(11px,1.8vh,14px) clamp(26px,8vw,40px);font:800 clamp(15px,4.4vw,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);flex:0 0 auto'; - btn.addEventListener('click', function () { - ov.style.transition = 'opacity .25s'; ov.style.opacity = '0'; - setTimeout(finish, 220); - }); - inner.appendChild(title); - inner.appendChild(row); - inner.appendChild(btn); - ov.appendChild(inner); - 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; - /* MG3 server-authoritative: เปิดเมื่อได้รับ stack-state ที่ srv:true → client เลิก host-sim - (ลำดับตา/แกว่ง/ดรอปบอท/จบเกม = server เป็นเจ้าของ; ผู้เล่นส่งเจตนา stack-drop-req) */ - let stackServerAuthPlay = false; - /* server-auth swing: extrapolate phase จาก sample ล่าสุดของ server (กัน drift → บล็อกวาปตอนปล่อย) */ - let stackSrvPhaseAnchor = 0; - let stackSrvPhaseAnchorAt = 0; - /* เฟสของบล็อกที่ "วาดออกจอจริง" เฟรมล่าสุด = สิ่งที่ผู้เล่นเห็นและเล็ง — ใช้ส่งตอนกดวาง - (stackMini.phase อาจเพิ่ง SNAP ไปแล้ว: dPh>0.9 → วาปทันที ระหว่างเฟรมที่เห็นกับตอน handler ทำงาน - → ส่งเฟสหลังวาป = server คิดว่าเยื้องสุดแกว่ง → "วางกลางพอดีแต่ลงเยื้อง/ร่วง") */ - let stackRenderedPhase = null; - /* freeze เฟสที่เห็น ณ ตอนกดวาง ไว้จน echo ดรอปกลับมา → บล็อกไม่แกว่งต่อแล้วเด้งกลับช่วงรอ RTT */ - let stackLocalSwingFrozen = false; - let stackLocalSwingFrozenAt = 0; - - /** ลำดับตา MG3 = ผู้เล่นจริงทุกคน + บอท เรียงตาม id แบบคงที่ → ทุกเครื่องได้ลำดับ/ที่นั่งเดียวกัน (shared tower) - (เดิม [human=ตัวเอง seat1 + bots] ต่อเครื่อง → แต่ละเครื่องมองตัวเองเป็นคนเดียว เลยแยกตึกกัน) */ - function rebuildStackPreviewTurnOrder() { - if (!playBotsEnabled() || !mapData || mapData.gameType !== 'stack') { - stackPreviewTurnOrder = null; - return; - } - if (stackServerAuthPlay) return; /* server-auth: ลำดับตามาจาก stack-state เท่านั้น */ - if (!detectiveCaseFillBots) { - /* editor preview (คนเดียว): คงพฤติกรรมเดิม [human ตัวเอง seat1 + บอท] */ - const bots = [...others.keys()].filter(isPreviewBotId).sort(); - stackPreviewTurnOrder = [{ kind: 'human', seat: 1, id: myId != null ? String(myId) : '__me__' }]; - for (let i = 0; i < STACK_PREVIEW_TURN_COUNT - 1; i++) { - stackPreviewTurnOrder.push({ kind: 'bot', seat: i + 2, id: bots[i] || null, botId: bots[i] || null }); - } - stackPreviewTurnIndex = 0; - stackPreviewBotThinkUntil = 0; - markStackNextDropVisualDirty(); - return; - } - /* เล่นจริง: non-host ไม่สร้าง order เอง — ใช้ของ host จาก stack-state (กัน order ไม่ตรง/client ตกหล่นช่วงโหลด) */ - if (!isMePlayHost()) return; - /* host: ผู้เล่นจริงทุกคน (ตัด banned spectator กัน stall) + บอท เรียง id → ที่นั่งคงที่; preserve คนที่กำลังถึงตา */ - const prevActorId = (stackCurrentTurnEntryPlay() && stackCurrentTurnEntryPlay().id) || null; - const parts = []; - /* dedup คนซ้ำ (ghost reconnect) ก่อนสร้างคิวตา — กัน seat ผี (ตาที่ไม่มีใครวาง = เกมค้าง) - ⚠️ เทียบด้วย playerKey เท่านั้น — **ห้าม dedup ด้วยชื่อ**: 2 คนตั้งชื่อเหมือนกัน (เช่น "MONE") = คนละ key = คนละคน - ต้องได้ seat ทั้งคู่ (เดิม fallback ชื่อ → client ที่ชื่อซ้ำ host ถูกตัด = "ไม่ได้เล่นเลย") */ - const seenHumanKey = new Set(); - const tryAddHuman = (id, ent) => { - const pk = (ent && ent.playerKey) ? String(ent.playerKey) : (String(id) === String(myId) ? (typeof getJdPlayerKey === 'function' ? getJdPlayerKey() : '') : ''); - if (pk) { - if (seenHumanKey.has(pk)) return; /* playerKey เดียวกัน = ghost reconnect → ข้าม */ - seenHumanKey.add(pk); - } - parts.push({ kind: 'human', id: String(id) }); - }; - if (myId != null && !playBannedSpectator) tryAddHuman(myId, me); - others.forEach((o, id) => { - if (isPreviewBotId(id)) { parts.push({ kind: 'bot', id: String(id), botId: String(id) }); return; } - if (o && o.bannedSpectator) return; /* คนถูกแบน = ดูเฉยๆ ไม่เข้าคิววาง */ - tryAddHuman(id, o); - }); - parts.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); - const capped = parts.slice(0, STACK_PREVIEW_TURN_COUNT); - stackPreviewTurnOrder = capped.map((p, i) => ({ - kind: p.kind, seat: i + 1, id: p.id, botId: p.kind === 'bot' ? p.botId : null, - })); - let ni = -1; - if (prevActorId != null) ni = stackPreviewTurnOrder.findIndex((e) => String(e.id) === String(prevActorId)); - if (ni >= 0) stackPreviewTurnIndex = ni; - else if (stackPreviewTurnIndex >= stackPreviewTurnOrder.length) stackPreviewTurnIndex = 0; - stackPreviewBotThinkUntil = 0; - markStackNextDropVisualDirty(); - broadcastStackStatePlay(); - } - - /** host broadcast ลำดับตา + index ปัจจุบัน → non-host ใช้ตรงกัน (single source of truth) */ - function broadcastStackStatePlay() { - if (stackServerAuthPlay) return; /* server-auth: server เป็นเจ้าของ stack-state */ - if (!detectiveCaseFillBots || !isMePlayHost() || !socket || myId == null || !stackTurnOrderReadyPlay()) return; - try { - socket.emit('stack-state', { - idx: stackPreviewTurnIndex, - /* theme ต่อที่นั่ง (host เป็นเจ้าของ) → ทุกจอลงสี block ตรงกัน (เดิม non-host คำนวณสีเองจาก playTint → ไม่ตรง 2 จอ) */ - order: stackPreviewTurnOrder.map((e) => ({ kind: e.kind, seat: e.seat, id: e.id, botId: e.botId || null, theme: stackSeatThemeForBroadcastPlay(e.seat) })), - /* สีของ block ที่ "วางแล้ว" ทั้งหอ (host authoritative) → client sync เขียนทับ layer ที่เพี้ยนค้าง (จากจังหวะ dual-host ต้นเกม) */ - lthemes: (stackMini && Array.isArray(stackMini.layers)) ? stackMini.layers.map(function (l) { return (l && l.theme >= 1 && l.theme <= 8) ? l.theme : 0; }) : [], - }); - logStackSeatColors('host-bcast'); - } catch (e) { /* ignore */ } - } - - /** order พร้อมใช้ (มีอย่างน้อย 1 ที่นั่ง) */ - function stackTurnOrderReadyPlay() { - return !!(stackPreviewTurnOrder && stackPreviewTurnOrder.length >= 1); - } - /** (host) คิวตาตรงกับผู้เล่น/บอทปัจจุบันไหม — ใช้ auto-heal late-join (client เข้าทีหลังต้องได้ seat ไม่งั้น "เล่นไม่ได้" + ไม่มีสี) - เทียบรายชื่อคน (me + others ที่ไม่ใช่บอท/ไม่ถูกแบน) + บอท กับ entry ในคิว; ต่างเมื่อไหร่ = ต้อง rebuild */ - let _stackHumansSig = ''; - function stackOrderNeedsRebuildPlay() { - if (!stackTurnOrderReadyPlay() || !Array.isArray(stackPreviewTurnOrder)) return true; - /* เช็คเฉพาะ "คน" (humans ≤ maxPlayers จึงลงคิวครบเสมอ ไม่โดน cap ตัด) — บอทจัดการใน rebuild เอง */ - const cur = []; - if (myId != null && !playBannedSpectator) cur.push(String(myId)); - others.forEach((o, id) => { - if (isPreviewBotId(id)) return; - if (o && o.bannedSpectator) return; - cur.push(String(id)); - }); - cur.sort(); - const sig = cur.join('|'); - if (sig === _stackHumansSig) return false; /* คนไม่เปลี่ยน → ไม่ต้องเช็ค (กัน rebuild ถี่) */ - const ord = []; - stackPreviewTurnOrder.forEach((e) => { if (e && e.kind !== 'bot' && !e.botId && e.id != null) ord.push(String(e.id)); }); - ord.sort(); - const need = (sig !== ord.join('|')); - if (!need) _stackHumansSig = sig; /* ตรงแล้ว → cache กันเช็คซ้ำทุกเฟรม */ - return need; - } - function stackCurrentTurnEntryPlay() { - if (!stackTurnOrderReadyPlay()) return null; - return stackPreviewTurnOrder[stackPreviewTurnIndex % stackPreviewTurnOrder.length] || null; - } - function isMyStackTurn() { - if (!playBotsEnabled()) return true; /* เล่นเดี่ยว → กดได้ */ - if (detectiveCaseFillBots && !stackTurnOrderReadyPlay()) return false; /* เล่นจริง: รอ order จาก host ก่อน */ - if (!stackTurnOrderReadyPlay()) return true; - const cur = stackCurrentTurnEntryPlay(); - if (!cur || cur.kind !== 'human') return false; - if (!detectiveCaseFillBots) return true; /* editor preview: human = previewer คนเดียว */ - return String(cur.id) === String(myId); - } - function isStackCurrentBotTurnPlay() { - const cur = stackCurrentTurnEntryPlay(); - return !!(cur && cur.kind === 'bot'); - } - /** เจ้าของตาที่วาง broadcast ผลการวาง → ทุกเครื่อง apply เหมือนกัน + เลื่อนตาพร้อมกัน */ - function broadcastStackDropPlay(hit, seat, heavy, actorId, isBot) { - if (!detectiveCaseFillBots || !socket || myId == null || !hit) return; /* broadcast เฉพาะเล่นจริง */ - try { - socket.emit('stack-drop', { - placedCx: hit.placedCx, placedW: hit.placedW, miss: !!hit.miss, perfect: !!hit.perfect, - pts: hit.pts || 0, progressDelta: hit.progressDelta || 0, landOffsetTiles: hit.landOffsetTiles || 0, - overlap: hit.overlap, supportRatio: hit.supportRatio, delta: hit.delta || 0, - seat: seat || 1, heavy: !!heavy, actorId: actorId != null ? String(actorId) : '', isBot: !!isBot, - /* สี (theme) ที่ host/เจ้าของตา resolve ตอนวาง → ส่งไปด้วย ให้บล็อกที่วางบนทุกจอใช้สีเดียวกัน (กัน host/client คนละสี) — never-null */ - theme: stackSeatThemeForBroadcastPlay(seat), - }); - } catch (e) { /* ignore */ } - } - - function advanceStackPreviewTurn() { - if (!playBotsEnabled() || !stackTurnOrderReadyPlay()) return; - if (stackServerAuthPlay) return; /* server-auth: idx มาจาก server (stack-state) เท่านั้น — กัน double-advance */ - /* เล่นจริง: index คุมโดย host เท่านั้น (non-host รับจาก stack-state) กันลำดับตาเหลื่อมกัน */ - if (detectiveCaseFillBots && !isMePlayHost()) return; - stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % stackPreviewTurnOrder.length; - stackPreviewBotThinkUntil = 0; - markStackNextDropVisualDirty(); - broadcastStackStatePlay(); - } - - function isStackPreviewHumanTurn() { - if (!playBotsEnabled() || !stackTurnOrderReadyPlay()) return true; - const cur = stackCurrentTurnEntryPlay(); - return !!(cur && cur.kind === 'human'); - } - - function getStackPreviewCurrentSeat() { - if (!stackTurnOrderReadyPlay()) return 1; - const cur = stackCurrentTurnEntryPlay(); - return cur && cur.seat ? cur.seat : stackPreviewTurnIndex + 1; - } - - function getStackDropActorSeat() { - if (playBotsEnabled() && stackTurnOrderReadyPlay()) { - return getStackPreviewCurrentSeat(); - } - return 1; - } - - function markStackNextDropVisualDirty() { - if (stackMini) stackMini.nextDropVisualDirty = true; - } - - /* #4 MG3 block สีตาม user — Block-Code-1..8 ตรง lobby theme (PLAY_LOBBY_THEME_HEX) + heavy '-2' */ - const STACK_BLOCK_THEME_NAMES = ['1-red', '2-orange', '3-Yellow', '4-Green', '5-Blue', '6-Navy', '7-Violet', '8-Pink']; - const STACK_BLOCK_THEME_NORMAL = STACK_BLOCK_THEME_NAMES.map(function (s) { return BASE + '/img/TowerBlock/Block-Code-' + s + '.png'; }); - const STACK_BLOCK_THEME_HEAVY = STACK_BLOCK_THEME_NAMES.map(function (s) { return BASE + '/img/TowerBlock/Block-Code-' + s + '-2.png'; }); - function stackThemeNormalUrlPlay(theme) { return (theme >= 1 && theme <= 8) ? STACK_BLOCK_THEME_NORMAL[theme - 1] : ''; } - function stackThemeHeavyUrlPlay(theme) { return (theme >= 1 && theme <= 8) ? STACK_BLOCK_THEME_HEAVY[theme - 1] : ''; } - /** theme (1-8) ของผู้เล่นที่ครอบครอง seat นั้นในลำดับตา — ใช้ลงสี block ตามผู้วาง (ไม่แตะ seat/turn logic) */ - function stackSeatColorThemePlay(seat) { - if (!isStack() || !stackTurnOrderReadyPlay()) return null; - const s = Math.floor(Number(seat)) || 0; - const entry = stackPreviewTurnOrder.find(function (e) { return e && e.seat === s; }); - if (!entry) return null; - /* (1) สีที่ host broadcast มาทาง stack-state = SINGLE SOURCE OF TRUTH → client ยึดค่า host เสมอ (สีตรง host ทุกจอ) - เกม MG3 host เป็นเจ้าของลำดับตา/สี; host เองไม่มี entry.theme (คำนวณสดตอน broadcast) → ตกไป (2) authoritative - เหตุที่ต้อง broadcast-first: ถ้า client คำนวณเอง (lobbyThemeIndexSrv/playLobbyBotThemes ของ client) แล้วข้อมูลมาช้า/ไม่ตรง - → สีหลุดจาก host เพี้ยนเรื่อย ๆ. ใช้ค่า host = ไม่มีทางหลุด */ - const bcTheme = parsePlayLobbyThemeIndex(entry.theme); - if (bcTheme) return bcTheme; - /* (2) authoritative ต่อ entity (สำหรับ "host เอง" + ตอน client ยังไม่ได้รับ broadcast): - bot → playLobbyBotThemes[slot] (server) ; human → lobbyThemeIndexSrv (= server lobbyColorThemeIndex) */ - const botId = entry.botId || (entry.kind === 'bot' ? entry.id : null); - if (botId && isPreviewBotId(botId)) { - const slot = parseInt(String(botId).slice(PREVIEW_BOT_PREFIX.length), 10); - const bt = (slot >= 0 && Array.isArray(playLobbyBotThemes)) ? playLobbyBotThemes[slot] : null; - const botIdx = bt ? parsePlayLobbyThemeIndex(bt.themeIndex) : null; - if (botIdx) return botIdx; - } else if (entry.id != null) { - const entH = (myId != null && String(entry.id) === String(myId)) ? me : (others.get(entry.id) || others.get(String(entry.id))); - const humanIdx = entH ? parsePlayLobbyThemeIndex(entH.lobbyThemeIndexSrv) : null; - if (humanIdx) return humanIdx; - } - /* (3) ทางเลือกสุดท้าย: เดาจาก playTint (nearest hex — อาจไม่ตรงข้ามจอ) */ - let ent = null; - if (entry.id != null) ent = (myId != null && String(entry.id) === String(myId)) ? me : (others.get(entry.id) || others.get(String(entry.id))); - if (!ent && entry.botId) ent = others.get(entry.botId) || others.get(String(entry.botId)); - return ent ? playLobbyThemeIndexForEntityPlay(ent) : null; - } - - /** สีที่ host จะ broadcast — "ไม่มีทาง null": resolve ปกติ ไม่ได้ค่อย default ตาม seat - → client (broadcast-first) ยึดค่า host ได้เสมอ ไม่ตกไปคำนวณเอง = สีตรง host ทุกจอแน่นอน */ - function stackSeatThemeForBroadcastPlay(seat) { - const t = stackSeatColorThemePlay(seat); - if (t >= 1 && t <= 8) return t; - const s = Math.floor(Number(seat)) || 1; - return ((s - 1) % 8) + 1; - } - - /** ส่ง log ไป /gamelog (ดูรวมที่หน้าเว็บ) */ - function gamelogPost(cat, ev, detail, who) { - try { - fetch('/gamelog/log.php', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, keepalive: true, - body: JSON.stringify({ cat: cat || 'อื่นๆ', room: (typeof spaceId === 'string' ? spaceId : ''), who: who || '', ev: ev || '', detail: detail || '', src: 'client' }), - }).catch(function () {}); - } catch (e) { /* ignore */ } - } - /** หมวด gamelog ตามเกมปัจจุบัน (MG1-MG7) */ - function gameLogCatForPlay() { - try { - var gt = mapData && mapData.gameType; - var m = { quiz: 'MG1', gauntlet: 'MG2', stack: 'MG3', quiz_carry: 'MG4', jump_survive: 'MG5', space_shooter: 'MG6', balloon_boss: 'MG7' }; - return (gt && m[gt]) || 'อื่นๆ'; - } catch (e) { return 'อื่นๆ'; } - } - /** วินิจฉัยสี block MG3 ต่อ seat — เทียบ "สี avatar (playTint)" vs "สี block (resolved)"; ถ้าไม่ตรง → ส่งเข้า /gamelog - เรียกเอง: window.__mg3ColorLog() หรือ auto throttle */ - let _mg3ColorLogAt = 0; - let _mg3SnapAt = 0; - let _mg3LastMismatchKey = ''; - function logStackSeatColors(tag, force) { - if (!force) return; /* diagnostic ปิด auto (2026-07-04 · MG3 สี confirmed) — เรียกมือ window.__mg3ColorLog() ยังได้ */ - try { - if (!isStack() || !stackTurnOrderReadyPlay()) return; - const now = Date.now(); - if (!force && now - _mg3ColorLogAt < 2000) return; - _mg3ColorLogAt = now; - const rows = stackPreviewTurnOrder.map(function (e) { - let ent = null; - if (e.id != null) ent = (myId != null && String(e.id) === String(myId)) ? me : (others.get(e.id) || others.get(String(e.id))); - if (!ent && e.botId) ent = others.get(e.botId) || others.get(String(e.botId)); - const tint = ent && ent.playTint && ent.playTint.body ? ent.playTint.body : ''; - const avatarIdx = tint ? nearestIndexFromHexPlay(tint, PLAY_LOBBY_THEME_HEX) : null; /* สีจริงของ avatar (จาก playTint) */ - const blockIdx = stackSeatColorThemePlay(e.seat); /* สี block ที่จะวาด */ - return { seat: e.seat, kind: e.kind, nick: (ent && ent.nickname) || String(e.id || e.botId || '').slice(0, 6), avatarIdx: avatarIdx, blockIdx: blockIdx, srv: (ent && ent.lobbyThemeIndexSrv) || null, bc: e.theme || null, ok: (avatarIdx && blockIdx) ? (avatarIdx === blockIdx) : null }; - }); - console.log('[mg3-color/' + (tag || '') + '] host=' + isMePlayHost(), JSON.stringify(rows)); - const role = isMePlayHost() ? 'host' : 'client'; - /* (A) snapshot ทุก ~5 วิ จาก "ทั้ง host และ client" → เทียบใน /gamelog ได้: block(=สีที่วาด) av(=สี avatar) srv bc */ - if (now - _mg3SnapAt > 5000) { - _mg3SnapAt = now; - /* รายชื่อ "คน" ใน others (เทียบกับคิว) — หา root "client หายจากคิว" */ - const othH = []; - if (myId != null) othH.push((me && me.nickname ? me.nickname : 'me') + '(self' + (playBannedSpectator ? ',BAN' : '') + ')'); - others.forEach(function (o, id) { - if (isPreviewBotId(id)) return; - othH.push((o && o.nickname ? o.nickname : String(id).slice(0, 5)) + (o && o.bannedSpectator ? '(BAN)' : '')); - }); - const ordSeats = rows.length; - /* block ที่ "วางแล้ว" (frozen layer.theme) — ตัวที่ user เห็นเพี้ยน; เทียบ host vs client ทีละ layer (seat:theme) */ - let lyr = ''; - if (stackMini && Array.isArray(stackMini.layers)) { - const L = stackMini.layers; - lyr = ' || layers(' + L.length + ')=[' + L.slice(-12).map(function (l) { return (l && l.seat != null ? l.seat : '?') + ':' + (l && l.theme != null ? l.theme : 'null'); }).join(',') + ']'; - } - const summary = 'order=' + ordSeats + 'seat othHumans=[' + othH.join(',') + '] :: ' - + rows.map(function (r) { return 's' + r.seat + ':' + (r.nick || '?').slice(0, 5) + ' blk=' + r.blockIdx + ' av=' + r.avatarIdx; }).join(' | ') + lyr; - gamelogPost('MG3', 'color-snap', summary, role); - } - /* (B) mismatch (avatar≠block บนจอเดียวกัน) → ส่งทันที (เปลี่ยนค่อยส่งซ้ำ) */ - const bad = rows.filter(function (r) { return r.ok === false; }); - if (bad.length) { - const key = role + '|' + bad.map(function (r) { return r.seat + ':' + r.avatarIdx + '!=' + r.blockIdx; }).join(','); - if (key !== _mg3LastMismatchKey) { - _mg3LastMismatchKey = key; - const detail = bad.map(function (r) { return r.nick + ' seat' + r.seat + ' avatar#' + r.avatarIdx + ' แต่ block#' + r.blockIdx + ' (srv' + r.srv + '/bc' + r.bc + ')'; }).join(' | '); - gamelogPost('MG3', 'color-mismatch', detail, role); - } - } - } catch (e) { /* ignore */ } - } - window.__mg3ColorLog = function () { logStackSeatColors('manual', true); }; - - function pickStackBlockHeavyVisual(seat) { - const slot = Math.max(0, Math.min(7, (Math.floor(Number(seat)) || 1) - 1)); - let hU = normalizeGauntletAssetUrlForPlay(playStackBlockHeavyUrls[slot] || ''); - /* ถ้าใช้สี theme (Block-Code) — heavy variant '-2' มีครบ ใช้แทน admin slot */ - const _theme = stackSeatColorThemePlay(seat); - if (_theme && stackThemeHeavyUrlPlay(_theme)) hU = stackThemeHeavyUrlPlay(_theme); - /** 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.pendingDropTheme = null; /* ดรอป local/host → ใช้ stackSeatColorThemePlay เอง (remote ตั้งทับทีหลัง) */ - stackMini.widthTiles = stackBlockWidthTilesForPlay(stackMini.initialWidthTiles, !!stackMini.pendingDropHeavy); - stackMini.nextDropVisualDirty = false; - } - - function normalizePlayStackSixUrlsFromTiming(arr) { - const out = ['', '', '', '', '', '', '', '']; /* 8 slot ตาม lobby theme (#4) */ - if (!Array.isArray(arr)) return out; - for (let i = 0; i < 8; i++) { - out[i] = normalizeGauntletAssetUrlForPlay(typeof arr[i] === 'string' ? arr[i] : ''); - if (out[i]) ensureGauntletAssetImage(out[i]); - } - return out; - } - - function resolveStackBlockSpriteRec(seat, heavy, themeOverride) { - const tryHeavy = !!heavy; - /* #4 สี block = theme ของผู้เล่นที่ seat นั้น (Block-Code-1..8) ถ้า resolve ได้ (มี turn order) - — fallback ไป admin slot URLs ถ้าไม่มี theme (เกม stack อื่น/ยังไม่มีลำดับตา) - themeOverride = สีที่ "freeze" ไว้บน layer ตอนวาง → กัน re-resolve เปลี่ยนสีเมื่อ turn order เปลี่ยน (สี block ที่วางแล้วคงที่+ตรงทุกจอ) */ - const theme = (themeOverride >= 1 && themeOverride <= 8) ? themeOverride : stackSeatColorThemePlay(seat); - if (theme) { - const tn = stackThemeNormalUrlPlay(theme); - const th = stackThemeHeavyUrlPlay(theme); - const torder = tryHeavy ? [th, tn] : [tn, th]; - for (let ti = 0; ti < torder.length; ti++) { - if (torder[ti]) return ensureGauntletAssetImage(torder[ti]); - } - } - const slot = Math.max(0, Math.min(7, (Math.floor(Number(seat)) || 1) - 1)); - 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, o.themeOverride); - 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'); - - /** popup แจ้งเตือน/ผิดพลาด ดีไซน์เอง (แทน alert() ของเบราว์เซอร์ที่หน้าตาไม่เข้าธีม) - opts: { type:'error'|'info', okText, onClose } — onClose เรียกเมื่อกดตกลง/ปิด */ - function showPlayAlert(message, opts) { - opts = opts || {}; - const prev = document.getElementById('play-alert-modal'); - if (prev) prev.remove(); - const overlay = document.createElement('div'); - overlay.id = 'play-alert-modal'; - overlay.className = 'play-alert-modal' + (opts.type === 'error' ? ' play-alert-modal--error' : ''); - const card = document.createElement('div'); - card.className = 'play-alert-card'; - card.setAttribute('role', 'alertdialog'); - card.setAttribute('aria-modal', 'true'); - const backdrop = document.createElement('div'); - backdrop.className = 'play-alert-backdrop'; - const icon = document.createElement('div'); - icon.className = 'play-alert-icon'; - icon.textContent = opts.type === 'error' ? '!' : 'i'; - const msg = document.createElement('div'); - msg.className = 'play-alert-msg'; - msg.textContent = (message == null ? '' : String(message)); - const ok = document.createElement('button'); - ok.type = 'button'; - ok.className = 'play-alert-ok'; - ok.textContent = opts.okText || 'ตกลง'; - card.appendChild(icon); - card.appendChild(msg); - card.appendChild(ok); - overlay.appendChild(backdrop); - overlay.appendChild(card); - document.body.appendChild(overlay); - let closed = false; - const done = function () { - if (closed) return; - closed = true; - overlay.remove(); - document.removeEventListener('keydown', onKey, true); - if (typeof opts.onClose === 'function') { try { opts.onClose(); } catch (e) { /* ignore */ } } - }; - const onKey = function (e) { - if (e.key === 'Enter' || e.key === 'Escape') { e.preventDefault(); done(); } - }; - ok.addEventListener('click', done); - if (!opts.requireButton) backdrop.addEventListener('click', done); - document.addEventListener('keydown', onKey, true); - setTimeout(function () { try { ok.focus(); } catch (e) { /* ignore */ } }, 30); - return { close: done }; - } - window.__jdShowPlayAlert = showPlayAlert; - let mapData = null, tileSize = 32, myId = null; - let stackMini = null; - let stackFall = null; - let stackLastSwingEmit = 0; /* throttle broadcast เฟสแกว่ง MG3 (เจ้าของตา → คนอื่น mirror) */ - let stackLastStateEmit = 0; /* throttle broadcast ลำดับตา+index MG3 (host → non-host adopt) */ - let stackRemoteDropQueue = []; /* คิวดรอปจาก host/คนอื่น (MG3) — ทยอย apply ทีละอันกันทิ้ง */ - /** เศษบล็อกที่พลาด/หล่น → ตกแบบ physics หลุดขอบจอ (visual เท่านั้น ไม่กระทบคะแนน/sync) */ - let stackMissDebris = []; - let _stackDebrisLastT = 0; - /** 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; - /** Space Shooter (violent_crime): life HUD โหลดจาก /img/ViolentCrime/ แยกจาก TowerBlock ของ Stack */ - let violentCrimeLifeBarImg = null; - let violentCrimeLifeHitImg = null; - let violentCrimeLifeFullImg = 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(); - /* BG อวกาศเป็นภาพ illustration (loop 1024w สเกลขึ้น ~1.9x) — เปิด smoothing ไม่ให้แตกเป็นบล็อก (stack tower BG กว้าง ~1920 จึงคงปิดไว้) */ - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; - 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; - /** ความเร็วเดินเข้าเส้นชัยตอนจบ (ช่อง/วินาที) — ค่อยๆ เดินแทนกระพริบ snap */ - const GAUNTLET_FINISH_WALK_TILES_PER_SEC = 7; - /** เพดานเวลาเดินเข้าเส้นชัย (กันค้างถ้าใครติด) — ต้อง < server grace (GAUNTLET_FINISH_WALK_MS) */ - const GAUNTLET_FINISH_WALK_MAX_MS = 2600; - 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; - /* G1: scroll ผูกเวลา server (ทุกเครื่องตรงกัน ไม่ขึ้น resolution/frame-rate/host) — scroll = base + speed × (serverNow − anchor) */ - let gauntletCrownRunwayLiveAnchorServerMs = 0; /* เวลาเริ่ม live (server epoch) */ - let gauntletCrownRunwayServerLiveStartMs = 0; /* liveStartMs จาก server (gauntlet-sync) — anchor เดียวกันทุกเครื่อง */ - let gauntletCrownRunwayBaseScrollPx = 0; /* ค่า scroll ตอน snap (resolution-independent เท่ากันทุกเครื่อง) */ - let gauntletCrownRunwayBgFinishLatched = false; - /** ส่งสัญญาณ "ถึงเส้นชัย" ให้ server แล้วหรือยัง (กันส่งซ้ำ) — server จะหยุด obstacle/collision */ - let gauntletFinishPhaseSignaled = false; - /** กำลังเดินค่อยๆ เข้าเส้นชัย (ก่อน freeze) — true ระหว่างทุกคนทยอยเดินมาที่เส้น */ - let gauntletCrownRunwayWalkActive = false; - /** เวลาเริ่มเดินเข้าเส้นชัย (ms) — ใช้เพดานเวลา GAUNTLET_FINISH_WALK_MAX_MS */ - let gauntletCrownRunwayWalkStartMs = 0; - /** >0 = หยุดเลื่อนแล้ว (timestamp ms) รอ POST_STOP ก่อน latch */ - let gauntletCrownRunwayBgStripFreezeSinceMs = 0; - /** คอลัมน์แผนที่ที่ล็อกตอน freeze — แนบตัวนำแทนขอบขวาสูงสุดของแมป */ - let gauntletCrownRunwayFinishAlignLatchedWorldX = null; - /** พรีวิว 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(); - } - - function gauntletCrownRunwayFinishLineInArtFracPlay() { - const raw = mapData && mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; - let lineInArt = Number(raw.finishLineInFinishArtFrac); - if (!Number.isFinite(lineInArt)) { - lineInArt = gauntletCrownRunwayBgMapEligiblePlay() ? 0.34 : 0.12; - } - return Math.max(0, Math.min(0.92, lineInArt)); - } - - function gauntletCrownRunwayFinishLineStripXPlay(g) { - if (!g) return null; - return g.finishStart + g.w4Safe * gauntletCrownRunwayFinishLineInArtFracPlay(); - } - - /** โลกแมป X ของจุดอ้างอิงในงานศิลป์ FINISH = stripX − scrollPx (ไม่ขึ้นกับกล้อง) */ - function gauntletCrownRunwayFinishLineMapWorldXPlay() { - const g = gauntletCrownRunwayScrollGeometryPlay(); - if (!g) return null; - const stripX = gauntletCrownRunwayFinishLineStripXPlay(g); - if (stripX == null) return null; - return stripX - gauntletCrownRunwayBgScrollPx; - } - - function gauntletCrownRunwayStripFrozenPlay() { - return gauntletCrownRunwayWalkActive || gauntletCrownRunwayBgStripFreezeSinceMs > 0 || gauntletCrownRunwayBgFinishLatched; - } - - /** จุดอ้างอิงกลางตัวนำ (map world px) — ใช้หยุด FINISH ตรงตัวละคร ไม่ใช่ขอบขวาแมป */ - function gauntletCrownRunwayLeadingEntityAlignWorldXPlay() { - if (!mapData || !isGauntletCrownHeistMapPlay()) return null; - const ts = mapData.tileSize || 32; - const { cw } = getCharacterFootprintWH(mapData); - const mx = getGauntletCrownHeistMaxEntityXPlay(mapData); - /* แนบเส้นชัยกับ "ตัวนำหน้าสุดที่ยังไม่ตาย" (tx มากสุด) → ตัวนำหยุดที่เส้นพอดี ไม่เลยเส้น - (ตอนหยุด เราจะ snap ตัวที่เหลือมายืนที่เส้นเดียวกันด้วย ดู gauntletSnapLivingToFinishPlay) */ - let leadTx = -Infinity; - const consider = (ent) => { - if (!ent || ent.gauntletEliminated) return; - let tx = Number.isFinite(ent.tx) ? ent.tx : Number(ent.x); - if (!Number.isFinite(tx)) return; - leadTx = Math.max(leadTx, tx); - }; - consider(me); - others.forEach((o, id) => { - if (!playBotsEnabled() && isPreviewBotId(id)) return; - consider(o); - }); - if (!Number.isFinite(leadTx) || leadTx === -Infinity) return null; - let alignWorld = (leadTx + cw * 0.5) * ts; - if (mx != null && Number.isFinite(mx)) { - alignWorld = Math.min(alignWorld, (mx + cw * 0.5) * ts); - } - return alignWorld; - } - - /** ตอนถึงเส้นชัย: ดึงตัวที่ยังไม่ตายทุกตัวมายืนที่ "เส้นชัย" — ไม่มีใครเลยเส้น, คนตายไม่ย้าย - เป้าหมาย = ตำแหน่งเส้นชัยที่ latch ไว้ (คงที่) ถ้ายังไม่ latch ใช้ tx ตัวนำสุด - เรียกทุกเฟรมระหว่าง freeze/latched เพื่อกันบอทที่ sim ยังขยับ "ไหล" หลุดจากเส้น */ - function gauntletSnapLivingToFinishPlay() { - if (!mapData || !isGauntletCrownHeistMapPlay()) return; - const ts = mapData.tileSize || 32; - const { cw } = getCharacterFootprintWH(mapData); - let targetTx = null; - if (gauntletCrownRunwayFinishAlignLatchedWorldX != null) { - targetTx = gauntletCrownRunwayFinishAlignLatchedWorldX / ts - cw * 0.5; - } else { - let leadTx = -Infinity; - const consider = (ent) => { - if (!ent || ent.gauntletEliminated) return; - const tx = Number.isFinite(ent.tx) ? ent.tx : Number(ent.x); - if (Number.isFinite(tx)) leadTx = Math.max(leadTx, tx); - }; - consider(me); - others.forEach((o, id) => { if (!playBotsEnabled() && isPreviewBotId(id)) return; consider(o); }); - if (Number.isFinite(leadTx) && leadTx !== -Infinity) targetTx = leadTx; - } - if (targetTx == null || !Number.isFinite(targetTx)) return; - const snap = (ent) => { - if (!ent || ent.gauntletEliminated) return; - if (Number.isFinite(ent.x)) ent.x = targetTx; - if (ent.tx != null) ent.tx = targetTx; - }; - snap(me); - others.forEach((o, id) => { if (!playBotsEnabled() && isPreviewBotId(id)) return; snap(o); }); - } - - /** tx เป้าหมายที่เส้นชัย (คอลัมน์ที่ทุกคนต้องมายืน) — null ถ้าคำนวณไม่ได้ */ - function gauntletFinishTargetTxPlay() { - if (!mapData || !isGauntletCrownHeistMapPlay()) return null; - const ts = mapData.tileSize || 32; - const { cw } = getCharacterFootprintWH(mapData); - if (gauntletCrownRunwayFinishAlignLatchedWorldX != null) { - return gauntletCrownRunwayFinishAlignLatchedWorldX / ts - cw * 0.5; - } - let leadTx = -Infinity; - const consider = (ent) => { - if (!ent || ent.gauntletEliminated) return; - const tx = Number.isFinite(ent.tx) ? ent.tx : Number(ent.x); - if (Number.isFinite(tx)) leadTx = Math.max(leadTx, tx); - }; - consider(me); - others.forEach((o, id) => { if (!playBotsEnabled() && isPreviewBotId(id)) return; consider(o); }); - return (Number.isFinite(leadTx) && leadTx !== -Infinity) ? leadTx : null; - } - - /** เดินทุกคนที่ยังไม่ตาย "ค่อยๆ" เข้าหาเส้นชัยทีละเฟรม (แทน snap กระพริบ) - คืน true เมื่อทุกคนถึงเส้นแล้ว (เริ่ม freeze→จบเกมต่อ) */ - function gauntletWalkLivingToFinishStepPlay(dtSec) { - const targetTx = gauntletFinishTargetTxPlay(); - if (targetTx == null || !Number.isFinite(targetTx)) return true; - const step = Math.max(0.04, GAUNTLET_FINISH_WALK_TILES_PER_SEC * Math.max(0, Number(dtSec) || 0)); - const eps = 0.06; - let allArrived = true; - const walk = (ent) => { - if (!ent || ent.gauntletEliminated) return; - let x = Number.isFinite(ent.x) ? ent.x : (Number.isFinite(ent.tx) ? ent.tx : targetTx); - if (x < targetTx - eps) { - x = Math.min(targetTx, x + step); - ent.direction = 'right'; - ent.isWalking = true; - allArrived = false; - } else { - x = targetTx; /* ไม่ให้ใครเลยเส้นชัย (เลยมา → ดึงกลับมาที่เส้น) */ - ent.isWalking = false; - } - ent.x = x; - ent.tx = x; - }; - walk(me); - others.forEach((o, id) => { if (!playBotsEnabled() && isPreviewBotId(id)) return; walk(o); }); - return allArrived; - } - - /** เริ่มเฟส "เดินเข้าเส้นชัย": ล็อก align → snap scroll → แจ้ง server หยุด obstacle ทันที (กันตายระหว่างเดิน) - จากนั้น tick จะค่อยๆ เดินทุกคนมาที่เส้น แล้วค่อยเข้า freeze→จบเกม */ - function gauntletCrownRunwayBeginWalkToFinishPlay() { - if (gauntletCrownRunwayWalkActive || gauntletCrownRunwayBgStripFreezeSinceMs > 0 || gauntletCrownRunwayBgFinishLatched) return; - if (gauntletCrownRunwayFinishAlignLatchedWorldX == null) { - const leadAlign = gauntletCrownRunwayLeadingEntityAlignWorldXPlay(); - if (leadAlign != null) gauntletCrownRunwayFinishAlignLatchedWorldX = leadAlign; - } - gauntletCrownRunwaySnapScrollToFinishStopPlay(); - gauntletCrownRunwayWalkActive = true; - gauntletCrownRunwayWalkStartMs = Date.now(); - /* G3: เข้าเฟสเดินเข้าเส้นชัย → เคลียร์ท่ากระโดด (กันค้างลอยกลางอากาศ — ต้องเดินเข้าเส้น ไม่ใช่กระโดดค้าง) */ - meGauntletJumpTicks = 0; meGauntletJumpVis = 0; - others.forEach((o) => { if (o) { o.gauntletJumpTicks = 0; o.gauntletJumpVis = 0; } }); - /* หมายเหตุ: การเคลียร์อุปสรรค "หายจากจอ" ให้ทำตอนได้ event 'gauntlet-finish-clear' จาก server - (= จังหวะเดียวกับ server หยุด collision) กัน "หายแล้วยังชน" — ไม่เคลียร์ที่นี่ (เร็วไป half-RTT) */ - /* แจ้ง server ครั้งเดียว: เข้าโซนเส้นชัย → หยุด obstacle + collision (กันโดนชน/-10 ตอนกำลังเดินเข้าเส้น) - server จะรอ grace (GAUNTLET_FINISH_WALK_MS) ให้เดินครบก่อนค่อยจบ */ - if (!gauntletFinishPhaseSignaled) { - gauntletFinishPhaseSignaled = true; - try { socket.emit('gauntlet-finish-phase'); } catch (_e) { /* ignore */ } - } - try { syncGauntletCrownJumpButton(); } catch (_sj) { /* ignore */ } - } - - - /** - * คอลัมน์บนกริดที่เส้น FINISH ต้องแนบ (map world px) - * gauntletCrownRunwayBg.finishAlignMapTileX | finishAlignMapTileFrac — ถ้าไม่ตั้ง ใช้ตำแหน่งตัวนำ - */ - function gauntletCrownRunwayFinishAlignMapWorldXPlay() { - if (!mapData) return null; - if (gauntletCrownRunwayFinishAlignLatchedWorldX != null) { - return gauntletCrownRunwayFinishAlignLatchedWorldX; - } - const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; - const ts = mapData.tileSize || 32; - const tileX = Number(raw.finishAlignMapTileX); - if (Number.isFinite(tileX)) return (tileX + 0.5) * ts; - const tileFrac = Number(raw.finishAlignMapTileFrac); - if (Number.isFinite(tileFrac)) { - const w = mapData.width || 20; - return (Math.max(0, Math.min(1, tileFrac)) * w + 0.5) * ts; - } - if (gauntletCrownRunwayBgMapEligiblePlay()) { - return gauntletCrownRunwayLeadingEntityAlignWorldXPlay(); - } - return null; - } - - /** ตำแหน่งเส้น FINISH บนจอ (0=ซ้าย 1=ขวา) จากขอบซ้าย viewport — ใช้หยุดเลื่อน + fade สิ่งกีดขวาง */ - function gauntletCrownRunwayFinishLineScreenFracPlay(g) { - if (!g) return null; - const finishLineWorldX = gauntletCrownRunwayFinishLineStripXPlay(g); - if (finishLineWorldX == null) return null; - 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() { - /* เดิม: ซ่อนอุปสรรค (alpha 0) ตาม "ตำแหน่งเส้นชัยบนจอ" (screenFrac<=stopAt / finishLatched) — เกิด - "ก่อน" server หยุด collision (finishPhase) → อาการ "อุปสรรคหายแล้วแต่ยังชน". - ใหม่: โชว์อุปสรรคเต็มเสมอ → ให้มัน "หาย" ตอน server เคลียร์จริงเท่านั้น (finishPhase = คนแรกถึงเส้นชัย - + หยุด collision พร้อมกัน ผ่าน event 'gauntlet-finish-clear' ที่ล้าง render buffer) = หาย+ไม่ชน พร้อมกัน */ - return 1; - } - - 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) { - gauntletCrownRunwayMaintainFinishScrollPlay(); - 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); - /* หลัง latch ยังวาดแถบรันเวย์ที่ scroll ค้าง — อย่า switch เป็น cover เต็มแผนที่ (ทำให้ FINISH กระโดดซ้าย) */ - /* ใช้ความกว้างมองเห็นแบบ 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)); - /* clamp S>=0 ทุกเฟรม → START segment เริ่มที่ขอบซ้ายจอเสมอ ไม่โชว์พื้นดำ (strip-x<0) ทางซ้าย - (แก้ "เริ่มไม่ชิดซ้ายสุด" — display-only ไม่แตะ base scroll จึงไม่เลื่อน alignment กับอุปสรรค) */ - const scrollView = Math.max(0, 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(); - /* แนวนอน: คลิปเต็มความกว้างที่มองเห็น (worldMinX..worldMaxX) แทนตัดที่ความกว้างแมพ (mapWpx) - → จอกว้างทุก resolution รันเวย์โชว์ยาวเต็ม ไม่เหลือขอบดำซ้าย-ขวา · แนวตั้งคงคลิป=ความสูงแมพ (ไม่กระทบเส้นชัย) */ - ctx.rect(worldMinX, 0, cwR, 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 ถึงตำแหน่งหยุด (แนบคอลัมน์แผนที่ หรือสัดส่วนจอแบบเดิม) */ - function gauntletCrownRunwayScrollStripStopZoneReachedPlay() { - const g = gauntletCrownRunwayScrollGeometryPlay(); - if (!g) return false; - const alignWorldX = gauntletCrownRunwayFinishAlignMapWorldXPlay(); - if (alignWorldX != null) { - /* world-based: หยุดเมื่อ "เส้นชัย (world X)" ถึงตัวนำ — ไม่ขึ้นกับความละเอียดจอ - (เดิมมี gate g.scrollView >= minScrollForFinishPass ที่ผูก cwWorld = canvas.width/zoom - → จอกว้างต้อง scroll มากกว่าก่อนหยุด = ตัวละครวิ่งเลยเส้นชัยบนจอที่ resolution ต่างกัน) */ - const finishMapX = gauntletCrownRunwayFinishLineMapWorldXPlay(); - if (finishMapX == null) return false; - return finishMapX <= alignWorldX + 1.5; - } - /* fallback (ไม่มีตัวนำให้ align) — ใช้ gate scroll + สัดส่วนจอแบบเดิม */ - if (g.scrollView < g.minScrollForFinishPass - 0.01) return false; - const screenFrac = gauntletCrownRunwayFinishLineScreenFracPlay(g); - if (screenFrac == null) return false; - return screenFrac <= gauntletCrownRunwayFinishStopViewportFracPlay() + 0.008; - } - - /** ค่าตำแหน่ง scroll ที่ทำให้เส้น FINISH หยุดตรงจุด (คืน null ถ้าคำนวณไม่ได้) — ใช้ทั้ง snap และ clamp กันวิ่งเลยเส้นชัย */ - function gauntletCrownRunwayFinishStopScrollPxPlay() { - if (!mapData || !canvas || !gauntletCrownRunwayBgDrawActivePlay()) return null; - const g = gauntletCrownRunwayScrollGeometryPlay(); - if (!g) return null; - const finishLineStripX = gauntletCrownRunwayFinishLineStripXPlay(g); - if (finishLineStripX == null) return null; - const alignWorldX = gauntletCrownRunwayFinishAlignMapWorldXPlay(); - if (alignWorldX != null) { - return finishLineStripX - alignWorldX; - } - const stopVp = gauntletCrownRunwayFinishStopViewportFracPlay(); - const targetScrollView = finishLineStripX - stopVp * g.cwWorld; - const zGaRaw = computePlayCameraZDrawPlay(); - const gCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zGaRaw); - if (!gCam) return null; - const worldMinX = gCam.px - canvas.width / (2 * zGaRaw); - return targetScrollView - worldMinX; - } - - /** จัด scroll ให้จุดอ้างอิงใน FINISH ตรงคอลัมน์แผนที่ (หรือ finishStopViewportFrac บนจอ) */ - function gauntletCrownRunwaySnapScrollToFinishStopPlay() { - const t = gauntletCrownRunwayFinishStopScrollPxPlay(); - if (t == null) return; - gauntletCrownRunwayBgScrollPx = t; - } - - /** ลำดับการหยุดที่เส้นชัย: ล็อก align (ครั้งเดียว) → เริ่มเฟสเดินค่อยๆ เข้าเส้นชัย (ไม่ snap กระพริบ) - tick จะเดินทุกคนมาที่เส้นจนครบแล้วค่อยเข้า freeze→จบเกม */ - function gauntletCrownRunwayBeginFinishStopPlay() { - gauntletCrownRunwayBeginWalkToFinishPlay(); - try { draw(); } catch (_d) { /* ignore */ } - } - - /** โหมดจอเท่านั้น: กล้องเลื่อนหลัง freeze ทำให้ FINISH หลุด — รีสแนปทุกเฟรม */ - function gauntletCrownRunwayMaintainFinishScrollPlay() { - if (!gauntletCrownRunwayBgStripFreezeSinceMs && !gauntletCrownRunwayBgFinishLatched) return; - if (!gauntletCrownRunwayBgDrawActivePlay()) return; - if (gauntletCrownRunwayFinishAlignMapWorldXPlay() != null) return; - gauntletCrownRunwaySnapScrollToFinishStopPlay(); - } - - 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; - /* G1: เก็บ base (ค่า snap นี้ — เท่ากันทุกเครื่อง) + anchor เวลาเริ่ม (server epoch) → scroll คำนวณจากเวลา server */ - gauntletCrownRunwayBaseScrollPx = gauntletCrownRunwayBgScrollPx; - /* anchor = liveStartMs จาก server (gauntlet-sync) — authoritative เดียวกันทุกเครื่อง + สดเสมอ - (เดิมใช้ playMissionLiveBeginsAt ที่ค้างจากเกมก่อน → scroll กระโดด/host-client ไม่ตรง) - fallback: liveBeginsAt ถ้าสด ไม่งั้น serverNow() */ - if (gauntletCrownRunwayServerLiveStartMs > 0) { - gauntletCrownRunwayLiveAnchorServerMs = gauntletCrownRunwayServerLiveStartMs; - } else { - const _lba = (typeof playMissionLiveBeginsAt !== 'undefined') ? Number(playMissionLiveBeginsAt) : 0; - const _nowS = serverNow(); - const _el = _nowS - _lba; - gauntletCrownRunwayLiveAnchorServerMs = (_lba > 0 && _el >= -2000 && _el < 10000) ? _lba : _nowS; - } - gauntletCrownRunwaySpawnScrollSnapped = true; - } - - function reloadGauntletCrownRunwayBgFromMap() { - gauntletCrownRunwayBgImgs = [null, null, null, null, null]; - gauntletCrownRunwayBgScrollPx = 0; - gauntletCrownRunwayLiveAnchorServerMs = 0; /* G1: รีเซ็ต anchor รอบใหม่ */ - gauntletCrownRunwayServerLiveStartMs = 0; - gauntletCrownRunwayBaseScrollPx = 0; - gauntletCrownRunwayBgFinishLatched = false; - gauntletFinishPhaseSignaled = false; - gauntletCrownRunwayWalkActive = false; - gauntletCrownRunwayWalkStartMs = 0; - gauntletCrownRunwayBgStripFreezeSinceMs = 0; - gauntletCrownRunwayFinishAlignLatchedWorldX = null; - 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]; - } - } - - /** ทุกคนจริง (me + others ที่ไม่ใช่บอทเติม) ตกขอบหมด = จบเกมเป็น gameover ทันที (ไม่ต้องรอเดินเข้าเส้นชัย) - บอทเติม (__lobby_bot_N) ไม่นับ — server ตัดสิน all_dead จาก space.peers (คนจริงเท่านั้น) เหมือนกัน - เดิมนับบอทด้วยเมื่อ playBotsEnabled()=true (ห้องจริงเปิดเสมอ) → บอทไม่ตายเลย = เช็คนี้ไม่ทริกเกอร์เลย */ - function gauntletCrownAllEntitiesEliminatedPlay() { - let total = 0, elim = 0; - if (Number.isFinite(me.x) || Number.isFinite(me.y)) { total++; if (me.gauntletEliminated) elim++; } - others.forEach((o, id) => { - if (!o) return; - if (isPreviewBotId(id)) return; - total++; if (o.gauntletEliminated) elim++; - }); - return total >= 1 && elim === total; - } - - function tickGauntletCrownRunwayBgPlay(dtSec) { - if (!gauntletCrownRunwayBgDrawActivePlay()) return; - /* หยุดเลื่อน BG ระหว่างคำถามพิเศษ (special-quiz freeze) — กัน BG/เส้นชัยเดินตอนค้างตอบ */ - if (specialQuizFreeze) return; - /** พรีรัน (howto / countdown) — ห้ามเลื่อนก่อน GO · Mega Virus ใช้ shell เดียวกัน */ - if (usesCrownLobbyShellPlay() && gauntletCrownPregamePhase !== 'live') return; - /* ทุกคนตกขอบหมด → เข้า flow gameover ทันที (ไม่รอ scroll เดินเข้าเส้นชัย) — server ก็จบเกม all_dead พร้อมกัน */ - if (playBotsEnabled() && gauntletCrownRunwayBgMapEligiblePlay() - && !gauntletCrownRunwayClientMissionShown && gauntletCrownAllEntitiesEliminatedPlay()) { - gauntletCrownRunwayClientMissionShown = true; - gauntletCrownRunwayWalkActive = false; - const mg2AllDeadMission = gauntletCrownHeistBuildLocalCrownMissionPlay(); - beginSimpleResultFlashThenGcmPlay(mg2AllDeadMission, 'result-gameover.png'); - try { draw(); } catch (_d) { /* ignore */ } - return; - } - if (gauntletCrownRunwayBgFinishLatched) { gauntletSnapLivingToFinishPlay(); return; } - - /* เฟส "เดินเข้าเส้นชัย" — ค่อยๆ เดินทุกคนมาที่เส้นแทน snap กระพริบ แล้วค่อยเข้า freeze→จบ */ - if (gauntletCrownRunwayWalkActive) { - const forceDone = (Date.now() - gauntletCrownRunwayWalkStartMs) >= GAUNTLET_FINISH_WALK_MAX_MS; - const arrived = gauntletWalkLivingToFinishStepPlay(dtSec); - if (arrived || forceDone) { - gauntletCrownRunwayWalkActive = false; - gauntletSnapLivingToFinishPlay(); /* จัดเป๊ะที่เส้นชัยครั้งเดียว */ - gauntletCrownRunwayBgStripFreezeSinceMs = Date.now(); - } - try { syncGauntletCrownJumpButton(); } catch (_sj) { /* ignore */ } - try { draw(); } catch (_d) { /* ignore */ } - return; - } - - if (gauntletCrownRunwayBgStripFreezeSinceMs > 0) { - gauntletCrownRunwayMaintainFinishScrollPlay(); - gauntletSnapLivingToFinishPlay(); /* ตรึงทุกคนที่ยังไม่ตายไว้ที่เส้นชัยทุกเฟรม กันบอทไหลหลุด */ - 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; - } - /* ตามกฎใหม่: เข้าเส้นชัย/รอด ≥1 คน (ไม่ถูกดันตกขอบ) = result-complete · ตายหมด = result-gameover (ไม่มี timeup) */ - const mg2LocalMission = gauntletCrownHeistBuildLocalCrownMissionPlay(); - const mg2LocalWin = missionSurvivorsPlay(mg2LocalMission) >= 1 || missionAnyScorePlay(mg2LocalMission); - beginSimpleResultFlashThenGcmPlay(mg2LocalMission, mg2LocalWin ? 'result-complete.png' : 'result-gameover.png'); - } catch (_e) { /* ignore */ } - try { - draw(); - } catch (_e2) { /* ignore */ } - }, 340); - } - } - return; - } - - if (gauntletCrownRunwayBgMapEligiblePlay() && gauntletCrownRunwayScrollStripStopZoneReachedPlay()) { - gauntletCrownRunwayBeginWalkToFinishPlay(); /* เริ่มเดินค่อยๆ เข้าเส้นชัย (แทน snap+freeze ทันที) */ - try { - draw(); - } catch (_d) { /* ignore */ } - return; - } - /* G1: scroll = base + speed × (serverNow − anchor) → ทุกเครื่องเลื่อนตรงกัน ไม่ขึ้น resolution/frame-rate/host - (เดิม scrollPx += speed×dt นับ local จากเวลาเริ่มของตัวเอง → เลื่อนไม่ตรงกัน + host หลุด/resize เพี้ยน) - fallback เป็นนับ local ถ้ายังไม่มี anchor (กันค้าง) */ - const gcrSpeed = gauntletCrownRunwayBgSpeedPxPerSec || 48; - let gcrTarget; - if (gauntletCrownRunwayLiveAnchorServerMs > 0) { - gcrTarget = gauntletCrownRunwayBaseScrollPx + gcrSpeed * Math.max(0, (serverNow() - gauntletCrownRunwayLiveAnchorServerMs)) / 1000; - } else { - gcrTarget = gauntletCrownRunwayBgScrollPx + gcrSpeed * dtSec; - } - /* ถ้า "จะถึง/เลย" จุดหยุด FINISH → หยุดเป๊ะตรงนั้น แล้วเริ่มจบเกมทันที */ - const gcrLiveStopPx = gauntletCrownRunwayFinishStopScrollPxPlay(); - if (gauntletCrownRunwayBgMapEligiblePlay() && gcrLiveStopPx != null - && gcrLiveStopPx >= gauntletCrownRunwayBgScrollPx - 0.01 - && gcrTarget >= gcrLiveStopPx) { - gauntletCrownRunwayBgScrollPx = gcrLiveStopPx; - gauntletCrownRunwayBeginFinishStopPlay(); - return; - } - /* monotonic — เลื่อนหน้าเท่านั้น (กัน clock jitter ดึงถอย) */ - if (gcrTarget > gauntletCrownRunwayBgScrollPx) gauntletCrownRunwayBgScrollPx = gcrTarget; - } - - /** 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(); - /** ===== MG3 terminal log (เกมจริง) — hacking-terminal ตามสเปก #mg3-terminal-log ===== */ - let mg3TermLog = []; - let mg3TermLineNo = 0; - const MG3_TERM_MAX = 240; - function mg3TermPush(lines) { - const arr = Array.isArray(lines) ? lines : [lines]; - for (let i = 0; i < arr.length; i++) mg3TermLog.push(String(arr[i] == null ? '' : arr[i])); - while (mg3TermLog.length > MG3_TERM_MAX) mg3TermLog.shift(); - } - function mg3TermReset() { mg3TermLog = []; mg3TermLineNo = 0; } - function mg3TermBoot() { - mg3TermLog = []; mg3TermLineNo = 0; - mg3TermPush([ - '[INIT] Booting decryption sequence...', - '[SYS] Locating target server...', - '[SYS] Connection established. Status: SECURE.', - '[HACK] Bypassing "Vault Lockdown" protocol...', - '[HACK] Disabling Cyber Security Check...', - '[LOAD] Injecting Decryption_Core.exe...', - '[READY] Awaiting manual code block alignment...', - '>>> STACKING SEQUENCE START <<<', - ]); - } - function mg3TermOnPlace(perfect, progressPct) { - mg3TermLineNo += 1; - const n = mg3TermLineNo; - if (n >= 100 || (Number.isFinite(Number(progressPct)) && Number(progressPct) >= 99.5)) { - mg3TermPush([ - '>>> Compile Line [100]: SUCCESS. Maximum stack reached.', - '[DATA]: Decryption progress: 100%. Master key generated.', - '[HACK]: "Vault Lockdown" bypassed. Firewall disabled.', - '[SYS]: Evidence files unlocked. Initiating secure download...', - '>>> MISSION ACCOMPLISHED. Disconnecting safely. <<<', - ]); - } else if (perfect) { - mg3TermPush('>>> Compile Line [' + n + ']: PERFECT. Synergy bonus!'); - } else { - mg3TermPush('>>> Compile Line [' + n + ']: SUCCESS. Stack stable.'); - } - } - function mg3TermOnMiss(attemptsRemaining) { - if (attemptsRemaining <= 0) { - mg3TermPush([ - '[CRITICAL]: Packet loss detected. Compile FAILED. (Attempts remaining: 0)', - '[ALERT]: Cyber Security Check triggered! Intruder traced.', - '[SYS]: Initiating "Vault Lockdown" protocol...', - '>>> FATAL ERROR: Connection Terminated. SYSTEM LOCKED. <<<', - ]); - } else { - mg3TermPush('[WARNING]: Packet loss detected. Compile FAILED. (Attempts remaining: ' + attemptsRemaining + '/3)'); - } - } - - /** 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 ด้วยจำนวนเฟรมที่โหลดได้แล้ว (ไม่งั้นเฟรมกลางหาย/ลูปสั้นลง) */ - /** true เมื่ออยู่ในเกมวิ่งหลบ (gauntlet runner) ช่วงเล่นจริง — ใช้เร่งจังหวะขาให้ดูเหมือน "วิ่ง" */ - function gauntletRunnerAnimActivePlay() { - try { - return isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live' && gauntletCrownRunwayAvatarRunAllowedPlay(); - } catch (e) { return false; } - } - - function walkAnimPhaseIndex(now, isWalking) { - if (!isWalking) return 0; - const t = (typeof now === 'number' ? now : Date.now()); - /* gauntlet วิ่งหลบ: เร่งจังหวะขาให้ "วิ่ง" — 0.40 (80ms/เฟรม) เร็วสุดที่ยังลื่น ไม่กระตุก - (sprite มีแค่ 4 เฟรม ถ้าเร็วกว่านี้ เช่น 0.27 เฟรมจะสลับถี่เกินจนดูสั่น/กระตุก) */ - const frameMs = gauntletRunnerAnimActivePlay() ? CHARACTER_ANIM_FRAME_MS * 0.40 : CHARACTER_ANIM_FRAME_MS; - return Math.floor(t / frameMs) % 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; - /* ใช้ธีมจาก server (กันสีซ้ำอยู่แล้ว) — ถ้าไม่มี ตกไปธีมแบบ "กระจายตามดัชนี" (ไม่สุ่ม hash ที่ซ้ำได้) - กัน bot สีเดียวกัน: bot N → ธีม ((N % 8)+1), สกิน ((N % 3)+1) */ - /* 1) สีที่ lobby บันทึกไว้ (ให้ตรงกับ lobby เป๊ะ) — ไม่เขียนทับ */ - try { - const savedMap = JSON.parse(sessionStorage.getItem(lobbyBotTintsStorageKeyPlay()) || '{}'); - const saved = savedMap['__lobby_bot_' + idx]; - if (saved && (saved.colorTheme || saved.colorSkin)) { - const st = playTintFromLobbyRgb(saved.colorTheme, saved.colorSkin); - if (st) return st; - } - } catch (e) { /* ignore */ } - /* 2) ไม่มีค่าจาก lobby → ใช้ธีม server / กระจายตามดัชนี แล้วบันทึกไว้ */ - const slot = playLobbyBotThemes[idx] || { themeIndex: (idx % 8) + 1, skinToneIndex: (idx % 3) + 1 }; - 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; - } - } - - /** แปลง hex/rgb → {r,g,b} (กันค่าเพี้ยน) */ - function hexOrRgbToRgbPlay(v) { - const s0 = String(v || '').trim(); - const m = s0.match(/rgba?\(\s*(\d+)\D+(\d+)\D+(\d+)/i); - if (m) return { r: +m[1], g: +m[2], b: +m[3] }; - let s = s0.replace('#', ''); - if (s.length === 3) s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2]; - if (!/^[0-9a-fA-F]{6}$/.test(s)) return null; - const n = parseInt(s, 16); - return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; - } - function nearestIndexFromHexPlay(value, palette) { - const asIdx = Math.floor(Number(value)); - if (asIdx >= 1 && asIdx <= palette.length) return asIdx; /* เก็บเป็น index อยู่แล้ว */ - const t = hexOrRgbToRgbPlay(value); - if (!t) return null; - let best = -1; let bestD = Infinity; - for (let i = 0; i < palette.length; i++) { - const c = hexOrRgbToRgbPlay(palette[i]); - if (!c) continue; - const d = (c.r - t.r) * (c.r - t.r) + (c.g - t.g) * (c.g - t.g) + (c.b - t.b) * (c.b - t.b); - if (d < bestD) { bestD = d; best = i; } - } - return best >= 0 ? best + 1 : null; - } - /** theme/skin index ที่ผู้เล่นเลือกไว้ (จาก localStorage) — ส่งให้ server เพื่อกันบอทใช้สีเดียวกับเรา */ - function getMyChosenLobbyColorIndicesPlay() { - let body = ''; - let skin = ''; - try { - const raw = localStorage.getItem(LOBBY_PLAYER_TINT_KEY); - if (raw) { const j = JSON.parse(raw); if (j) { body = j.body || j.themeRgb || ''; skin = j.skinRgb || j.skin || ''; } } - } catch (e) { /* ignore */ } - try { - if (!body) body = localStorage.getItem('lobbyThemeColor') || ''; - if (!skin) skin = localStorage.getItem('lobbySkinTone') || ''; - } catch (e2) { /* ignore */ } - return { - themeIndex: nearestIndexFromHexPlay(body, PLAY_LOBBY_THEME_HEX), - skinIndex: nearestIndexFromHexPlay(skin, PLAY_LOBBY_SKIN_HEX), - }; - } - - 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 || ''); - /* ตัวเราเอง: ยึด "สีที่เลือกไว้" (localStorage) ก่อนทุกอย่าง — กัน peerRow/theme-index จาก server มาทับ (สีกระพริบ) */ - if (myId != null && sid === String(myId)) { - const meChosen = loadPlayTintFromLobbyPlayerStorage(); - if (meChosen) return meChosen; - } - 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 sid = peerId != null ? String(peerId) : ''; - /* server-assigned theme index (อาจถูก reassign ตอนเลือกสีชนกัน) — เก็บไว้ใช้ลงสี "block MG3" ให้ตรงกันทุกจอ - (เดิม self ใช้สีจาก localStorage แต่ others ใช้ index จาก server → block สีไม่ตรง 2 จอ) */ - const tiNum = parsePlayLobbyThemeIndex(themeIndex); - if (myId != null && sid === String(myId)) { - if (tiNum) me.lobbyThemeIndexSrv = tiNum; - /* ตัวเราเอง: ใช้ "สีจาก server index" (authoritative) เป็นหลัก — ให้สีเรา "ตรงกันทุกจอ" + block MG3 ตรงกับ avatar - เดิม override ด้วย localStorage → จอเราเห็นสีที่เลือก แต่จออื่น+block MG3 ใช้ server index = ไม่ตรง - (เคส server reassign สีตอนเลือกชนกัน → server index คือสีจริง ต้องยึดอันนี้) */ - const tintMe = (tiNum) ? playTintFromLobbyThemeIndices(themeIndex, skinIndex) : null; - if (tintMe) { me.playTint = tintMe; return; } - const meChosen = loadPlayTintFromLobbyPlayerStorage(); - if (meChosen) me.playTint = meChosen; - return; - } - const oEarly = others.get(peerId) || others.get(sid); - if (oEarly && tiNum) oEarly.lobbyThemeIndexSrv = tiNum; - const tint = playTintFromLobbyThemeIndices(themeIndex, skinIndex); - if (!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 */ } - } - - /** วินิจฉัยสีในเกม: รันใน console บนแต่ละเครื่อง → เทียบสีที่ render ของทุกตัว */ - window.__colorDebug = function () { - var rows = []; - rows.push({ who: '*ME*', id: String(myId == null ? '' : myId).slice(0, 6), renderTint: JSON.stringify(me && me.playTint) }); - others.forEach(function (o, id) { - rows.push({ who: isPreviewBotId(id) ? 'BOT' : 'peer', id: String(id).slice(0, 10), name: o.nickname || '', renderTint: JSON.stringify(o.playTint) }); - }); - try { console.table(rows); } catch (e) { console.log(JSON.stringify(rows, null, 2)); } - var lsTheme = '?'; var lsTint = '?'; - try { lsTheme = localStorage.getItem('lobbyThemeColor'); lsTint = localStorage.getItem(LOBBY_PLAYER_TINT_KEY); } catch (e) { /* ignore */ } - console.log('[color-dbg/game] myId=' + myId + ' playLobbyBotThemes=' + JSON.stringify(playLobbyBotThemes) + - ' LS.lobbyThemeColor=' + lsTheme + ' LS.PLAYER_TINT=' + lsTint); - return rows; - }; - - 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) - // เครื่องช้า/เปิดหลายหน้าต่าง manifest มาช้า >3.6วิ → เดิมยอมแพ้ 8 ครั้ง = char ไม่ warm (avatar ก้อนเขียวค้าง) - // ขยายเป็น ~15วิ (30×500ms) ให้เครื่องช้าโหลด avatar สำเร็จ - const t = _tries || 0; - if (t < 30) setTimeout(function () { preloadPlayCharacterLayers(charId, t + 1); }, 500); - 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) { - /* 'Chatest' = placeholder ที่ไม่มีไฟล์ sprite จริง → อย่าโหลด (กัน 404 รัว) ใช้ default avatar generated แทน */ - const validId = characterId && characterId !== LEGACY_PLACEHOLDER_CHARACTER_ID; - const img = validId ? 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) { - /* placeholder 'Chatest' ไม่มี sprite/เลเยอร์จริง → ปฏิบัติเป็นว่าง (ใช้ default avatar) กัน 404 รัวทุกเส้นทางวาด */ - if (characterId === LEGACY_PLACEHOLDER_CHARACTER_ID) characterId = ''; - 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(); - } - - /** playerKey ของผู้เล่น (ผูกบัญชีเหรียญ/คะแนน) — สร้างถ้ายังไม่มี เหมือน Main-Lobby */ - function getJdPlayerKey() { - try { - let k = (localStorage.getItem('jdPlayerKey') || '').trim(); - if (!k || k.length < 8) { - k = 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14); - localStorage.setItem('jdPlayerKey', k); - } - return k; - } catch (e) { - return 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14); - } - } - - /** บอทพรีวิว: เลือกรหัสคนละตัวจาก 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; - /* time-based movement: เดิม step=MOVE_SPEED ต่อเฟรม → จอ 120fps เดินเร็ว 2 เท่า (คน/บอทวิ่งเร็วช้าไม่เท่ากัน) - playDtMul = dt*60 (clamp ≤3 กัน tunneling) คูณเข้ากับ step ทุกที่ → ความเร็วเท่ากันทุก FPS */ - let playDtMul = 1; - let playTickLastTs = 0; - /** Anti-stuck (MG1/MG4): ดันชน "เพื่อน" กี่เฟรมก่อนปลดล็อกเดินผ่าน + ช่วงเวลาที่ผ่านได้ต่อเนื่อง */ - const ANTI_STUCK_PEER_TICKS = 13; - const ANTI_STUCK_WINDOW_MS = 650; - /** 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) * playDtMul; - } - - 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; - /** โค้งกระโดด local ของ "เรา" — เล่นเต็มอาร์ค (ขึ้น-ลง) เสมอเมื่อกดกระโดด - (server เซ็ต jumpTicks=0 ทันทีตอนข้ามสำเร็จ = ตัดอาร์ค + สัญญาณลอยหายพร้อมตอนดัน +1 → ดูเป็น "วาปข้าม") - ระหว่างอาร์คนี้ vis คุมเอง + glide แนวนอนช้า → เห็นเป็นกระโดดข้ามจริง ไม่ใช่วาป */ - let meGauntletJumpAnimStart = 0; - let meGauntletJumpAnimDur = 0; - function isMeGauntletJumpAnimActivePlay() { - return meGauntletJumpAnimStart > 0 && (Date.now() - meGauntletJumpAnimStart) < meGauntletJumpAnimDur; - } - /** peer อื่น: เริ่ม "อาร์คกระโดด" เมื่อ server ส่ง gauntletJumpSeq เพิ่มขึ้น (นับครั้งเริ่มกระโดด) - แก้บั๊ก "คนอื่นกระโดดแล้วเราไม่เห็น": gauntletJumpTicks ถูกตัด 0 ใน tick เดียวกันตอนข้ามสำเร็จ - → peer ไม่เคยเห็นค่า >0 → ไม่เด้ง · seq ไม่ถูกรีเซ็ต จึงจับ "เริ่มกระโดด" ได้เสมอ (mirror อาร์ค local) */ - function notePeerGauntletJumpFromSync(o, seqRaw) { - if (!o) return; - const seq = Number(seqRaw) || 0; - if (!Object.prototype.hasOwnProperty.call(o, 'gauntletJumpSeq')) { o.gauntletJumpSeq = seq; return; } - if (seq > (o.gauntletJumpSeq || 0)) { - o.gauntletJumpSeq = seq; - o.gauntletJumpAnimStart = Date.now(); - o.gauntletJumpAnimDur = Math.max(360, (gauntletRuntimeJumpTicks || 6) * (gauntletRuntimeTickMs || 220)); - if (window.jdSound) { try { window.jdSound.playSfx('jump', 'mg2'); } catch (e) { /* ignore */ } } /* peer/bot กระโดด = ทุกคนได้ยินเสียง (seq เพิ่ม = กระโดดใหม่จริง ไม่ยิงซ้ำ) */ - } - } - function isPeerGauntletJumpAnimActive(o) { - return !!(o && o.gauntletJumpAnimStart > 0 && (Date.now() - o.gauntletJumpAnimStart) < o.gauntletJumpAnimDur); - } - /** ซิงก์จากเซิร์ฟเวอร์ (gauntlet-sync / GET /api/game-timing) */ - let gauntletRuntimeTickMs = 220; - let gauntletRuntimeJumpTicks = 6; - let gauntletRuntimeJumpHeightMult = 0.52; /* MG2 ความสูงกระโดด (×tile) — รับจาก server game-timing (Admin ปรับได้) */ - /** 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; - /** ดาเมจต่อการยิงโดนบอส (ลบเลือดบอส/นัด) — Admin Minigame-7 · แยกจากคะแนน · default 10 */ - let playBalloonBossDamagePerHit = 10; - let playBalloonBossBossImageUrl = ''; - let playBalloonBossPlayerBalloonImageUrls = ['', '', '', '', '', '', '', '']; /* [2026-07-17] 8 ช่อง (theme 8 สี) — เดิม 6 ตัด balloon-7 (ชมพู) ทิ้ง */ - 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 = {}; - /** จำนวนผู้เล่น (มนุษย์) ที่ server คาดว่าจะเข้าเล่นรอบนี้ — host เริ่มได้ต่อเมื่อมาครบ + กด Ready ครบ */ - let missionExpectedActiveCount = 0; - 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; - /** MG1: อยู่ในช่วง "พักระหว่างข้อ" (หลัง quiz-result ก่อน quiz-phase ถัดไป) → แผงคำถามโชว์ "รอคำถามข้อต่อไป" */ - let quizBetweenActive = false; - /** MG4: อยู่ในช่วง "พักระหว่างข้อ" (หลัง quiz-carry-result ก่อน quiz-carry-phase ถัดไป) */ - let quizCarryBetweenActive = false; - /** MG4: แบนเนอร์ "🎉 X ตอบถูก!" โชว์ก่อนเข้าช่วงพัก (ให้ทุกคนเห็นว่าใครตอบถูก) */ - let quizCarryResultBanner = ''; - let quizCarryResultBannerUntil = 0; - let quizCarryResultBannerTimer = 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 = {}; - /** MG1: กันสั่งจบซ้ำเมื่อตรวจพบ "ตายหมด" (รวมบอท ฝั่ง client) */ - let mg1AllDeadEndScheduled = false; - /** MG1: ตรวจว่าทุกคน(เรา+บอท+ผู้เล่นอื่น)ถูกล็อกตอบผิดหมด → จบเกมทันที (เฉพาะโหมดสืบสวน) */ - function checkMg1AllDeadAndEnd() { - if (mg1AllDeadEndScheduled || previewMode) return; - if (!isQuiz() || !isDetectiveMinigamePlay()) return; - const meLocked = !!(playQuizPlayerLocal && playQuizPlayerLocal.cannotTrue && playQuizPlayerLocal.cannotFalse); - if (!meLocked) return; - let anyAlive = false; - others.forEach(function (o, id) { - if (!o || o.bannedSpectator) return; - if (!playBotsEnabled() && isPreviewBotId(id)) return; - if (!(o.quizCannotTrue && o.quizCannotFalse)) anyAlive = true; - }); - if (anyAlive) return; - mg1AllDeadEndScheduled = true; - /* ทุกคนตอบผิดหมด → host ขอให้ server จบเกม → server ส่ง quiz-ended → โชว์ผล/รอกดรับการ์ด (เหมือนเกมอื่น) - เดิม: auto tryFinish = รับผล/การ์ดเองทันที ไม่รอกด */ - setTimeout(function () { - try { - if (isMePlayHost() && socket && socket.connected) socket.emit('quiz-end-request'); - /* non-host: ไม่ต้องทำอะไร รอ server ส่ง quiz-ended มาโชว์ผล */ - } catch (e) { /* ignore */ } - }, 1600); - } - /** ผีตามตัวเมื่อตอบผิด — ใช้เฉพาะแมป 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; - } - /** ===== โซนผี (quizGhostArea): ตอบผิด→วาร์ปมาขังในโซน เดินออกไม่ได้ — gated: ทำงานเฉพาะเมื่อแมปมีการวาดโซนผี ===== */ - function quizGhostAreaGridPlay() { - const g = mapData && mapData.quizGhostArea; - if (!Array.isArray(g)) return null; - for (let y = 0; y < g.length; y++) { if (g[y] && g[y].some((v) => v === 1)) return g; } - return null; - } - function quizMeIsGhostPlay() { - return !!(isQuiz() && playQuizPlayerLocal && (playQuizPlayerLocal.cannotTrue || playQuizPlayerLocal.cannotFalse)); - } - function quizGhostConfineActivePlay() { - return quizMeIsGhostPlay() && !!quizGhostAreaGridPlay(); - } - /** footprint ของผู้เล่นที่ (x,y) อยู่ในโซนผีครบทุกช่องไหม */ - function quizGhostFootprintInsidePlay(x, y) { - const g = quizGhostAreaGridPlay(); - if (!g) return true; - for (const k of quizTilesFootprintPlay(x, y)) { - const p = k.split(','); const tx = +p[0], ty = +p[1]; - if (!g[ty] || g[ty][tx] !== 1) return false; - } - return true; - } - /** วาร์ป me เข้าโซนผี (ช่องที่ footprint ลงได้ ใกล้กลางโซนสุด) — คืน true ถ้ามีโซนผี */ - function warpMeIntoQuizGhostZonePlay() { - const g = quizGhostAreaGridPlay(); - if (!g) return false; - if (quizGhostFootprintInsidePlay(me.x, me.y)) return true; - const xs = [], ys = []; - for (let y = 0; y < g.length; y++) { const row = g[y] || []; for (let x = 0; x < row.length; x++) { if (row[x] === 1) { xs.push(x); ys.push(y); } } } - if (!xs.length) return false; - const cx = Math.round((Math.min.apply(null, xs) + Math.max.apply(null, xs)) / 2); - const cy = Math.round((Math.min.apply(null, ys) + Math.max.apply(null, ys)) / 2); - const cand = []; - for (let i = 0; i < xs.length; i++) cand.push([xs[i], ys[i], Math.abs(xs[i] - cx) + Math.abs(ys[i] - cy)]); - cand.sort((a, b) => a[2] - b[2]); - let tx = xs[0], ty = ys[0]; - for (const c of cand) { if (quizGhostFootprintInsidePlay(c[0], c[1])) { tx = c[0]; ty = c[1]; break; } } - me.x = tx; me.y = ty; me.tx = tx; me.ty = ty; - try { if (socket && socket.connected) socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } catch (e) { /* ignore */ } - return true; - } - - /** สุ่มช่องในโซนผีที่ footprint ลงได้ (ใช้คุมบอทผีให้อยู่ในโซน) — คืน {x,y} หรือ null */ - function randomGhostZoneCellPlay() { - const g = quizGhostAreaGridPlay(); - if (!g) return null; - const cells = []; - for (let y = 0; y < g.length; y++) { - const row = g[y] || []; - for (let x = 0; x < row.length; x++) { - if (row[x] === 1 && quizGhostFootprintInsidePlay(x, y)) cells.push({ x: x, y: y }); - } - } - if (!cells.length) { - for (let y = 0; y < g.length; y++) { - const row = g[y] || []; - for (let x = 0; x < row.length; x++) if (row[x] === 1) cells.push({ x: x, y: y }); - } - } - return cells.length ? cells[Math.floor(Math.random() * cells.length)] : null; - } - - /** ตรวจว่า 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; - /* ผีเล็กลง 25% จากความสูง avatar (คงสัดส่วนภาพ · เท้ายังตรงพื้น) */ - const hPx = (Math.max(24, Number(heightPx) || 0)) * 0.75; - 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 เท่านั้น */ - /** row ล่างสุดของโซนผี (cache บน mapData) — ใช้ clamp ภาพผีให้อยู่ในโซน */ - function quizGhostZoneMaxRowPlay() { - if (!mapData) return null; - if (mapData._ghostMaxRow !== undefined) return mapData._ghostMaxRow; - const g = mapData.quizGhostArea; - let maxY = null; - if (Array.isArray(g)) { - for (let y = 0; y < g.length; y++) { - const row = g[y] || []; - for (let x = 0; x < row.length; x++) { if (Number(row[x]) === 1) maxY = y; } - } - } - mapData._ghostMaxRow = maxY; - return maxY; - } - function _quizGhostScreenAnchorPlay(axTile, ayTile, w2s, zDrawIn) { - const md = mapData; - const { cw, ch } = getCharacterFootprintWH(md); - const cxWorld = (axTile + cw * 0.5) * tileSize; - /* anchor เท้าผีตามปกติ (ayTile + ch) แต่ clamp ไม่ให้ต่ำกว่าขอบล่างโซนผี (maxRow + 0.6) - → ภาพผีอยู่ "ในโซนผี" พอดี: ไม่ลอยสูงเกิน (เคส ayTile) และไม่ห้อยลงทับ SAFE/SCAM (เคส ayTile+ch) */ - let bottomTile = ayTile + ch; - const maxRow = quizGhostZoneMaxRowPlay(); - if (maxRow != null) { - const clampBottom = maxRow + 0.6; - if (bottomTile > clampBottom) bottomTile = clampBottom; - } - const cyBottomWorld = bottomTile * 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; - /** server-auth (เกมจริง): epoch สิ้นสุดนับ 3-2-1 บนโต๊ะ ช่วง read phase (>0 = กำลังโชว์ countdown) */ - let quizCarrySrvReadCountdownEndAt = 0; - /** MG4 server-auth: ไอคอน ✓/✗ ลอยเหนือหัวตอนตอบ (key = entity id → { ok, until }) ทุกจอเห็นพร้อมกัน */ - const quizCarryEntityFeedbackFx = Object.create(null); - const QUIZ_CARRY_FEEDBACK_FX_MS = 1300; - /** หมดเวลาตอบรอบนี้แล้ว — โชว์ 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; - /** MG4 server-auth (detective): กัน schedule กลับ LobbyB ซ้ำเมื่อ quiz-carry-ended มาหลายครั้ง */ - let quizCarryDetectiveReturnNavDone = 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; - /* เวลา server (epoch ms) ที่ live เริ่มจริง — ใช้คำนวณ scroll/เวลานับถอยหลังจาก serverNow() ร่วมกัน - ทุกเครื่องเห็นแพลตฟอร์มเลื่อนเท่ากัน + จบพร้อมกัน (เดิมสะสม dt local → เลื่อน/จบไม่ตรงกัน = desync) */ - let jumpSurviveLiveAnchorServerMs = 0; - let jumpSurvivePlatformScrollPx = 0; - let jumpSurviveVy = 0; - let jumpSurviveLastTickMs = 0; - let jumpSurviveJumpQueued = false; - let jumpSurviveOnGround = false; - let jumpSurviveLastEmitT = 0; - let jumpSurviveHiddenTimer = null; /* แท็บ hidden → rAF หยุด → แจ้ง server ให้จำลองการตกแทน (คนอื่นเห็นลื่น) */ - let jumpSurviveHiddenLastMs = 0; - let jumpSurviveHiddenSent = false; /* แจ้ง server แล้วว่า hidden (กันส่งซ้ำทุก tick) */ - let jumpSurviveServerAuthPlay = false; /* true เมื่อรับ jump-survive-bot-move/ended ครั้งแรก — server เป็นเจ้าของบอท/รอบ */ - let spaceShooterServerAuthPlay = false; /* MG6: true เมื่อรับ space-shooter-bot-move — server เป็นเจ้าของบอท/รอบ */ - let balloonBossServerAuthPlay = false; /* MG7: true เมื่อรับ balloon-boss-bot-move — server เป็นเจ้าของบอท/รอบ/HP บอส */ - /** 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; - /* เวลา server ที่ live เริ่ม + ตัวนับลำดับก้อนที่เกิดแล้ว — ใช้ทำ deterministic spawn ให้ทุกเครื่องตรงกัน */ - let spaceShooterLiveAnchorServerMs = 0; - let spaceShooterSpawnCount = 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; - if (window.jdSound) { try { window.jdSound.playSfx('bomb', 'mg6'); } catch (e) { /* ignore */ } } - 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; - /* ปุ่ม SHOOT บนจอ (MG7) กด 1 ครั้ง = ยิง 1 นัด — ตั้ง flag ให้ tick ยิงตามมุมลูกศร เหมือนกด Space */ - let balloonBossFireBtnQueued = false; - let balloonBossGameEnded = false; - let balloonBossHitFx = []; - /** Mega Virus — ป๊อปดาเมจต่อบอส (ข้อความ +2) เมื่อกระสุนโดนบอส */ - let balloonBossScorePopups = []; - let balloonBossBossFireAcc = 0; - /* anchor เวลา server + ตัวนับกระสุนบอส — deterministic boss fire ให้ทุกเครื่องยิงทิศ/เวลาเดียวกัน */ - let balloonBossLiveAnchorServerMs = 0; - let balloonBossBossShotCount = 0; - /* MG7 host-authoritative boss HP: host broadcast ดาเมจรวมเป็นระยะ (balloon-boss-sync) - → non-host แสดง HP บอสตามค่า host เป๊ะ (null = ยังไม่ได้รับ → fallback ผลรวมท้องถิ่น) */ - let balloonBossHostBossDmg = null; - let balloonBossLastSyncEmit = 0; - /** quiz_battle — โดม MCQ กด E (ข้อจาก battleQuizMcq) */ - let quizBattleMcqPool = []; - let quizBattleAnsweredComps = new Set(); - let quizBattleModalCompId = null; - let qbAutoLastComp = null; // โดมล่าสุดที่ trigger อัตโนมัติ (กันเด้งซ้ำขณะยืนค้าง) - /** ประวัติคำตอบต่อโดม (สำหรับ MY RESULT): compId → { qIndex, firstTry(bool), correct(bool), submittedIdx, q } */ - let quizBattleAnswerLog = new Map(); - /** เวลาเริ่มเล่นห้อง quiz_battle (ms) — สำหรับ Total time */ - let quizBattleSessionStartMs = 0; - let quizBattleScoreSubmittedAt = 0; - /** ล็อกตัวเลือกระหว่างโชว์ผลถูก/ผิด (กันกดซ้ำ + กันเด้งซ้ำ) */ - let quizBattleChoiceLocked = false; - /** โชว์ popup Completed แล้วหรือยัง (กันเด้งซ้ำเมื่อตอบครบทุกโดม) */ - let quizBattleCompletedShown = false; - /** ถึงจุดจบแล้ว — ล็อกตำแหน่ง ไม่ให้ echo/restore เด้งกลับ start หรือจุดเดิม */ - let quizBattleFinishLock = false; - /** โดมที่ "ผ่านแล้ว" (ตอบถูกหรือผิดก็ผ่าน) — ใช้ปลดล็อกเส้นทาง + จำว่าเคยตอบ ไม่ต้องตอบซ้ำ */ - let quizBattlePassedComps = new Set(); - /** ซูมที่ผู้เล่นปรับเองในฉาก quiz_battle (ล้อเมาส์ / ปุ่ม +−) */ - let quizBattleUserZoomMul = 1; - const SPACE_SHOOTER_SHIP_COLORS = ['#50fa7b', '#ff79c6', '#ff5555', '#f1fa8c', '#bd93f9', '#8be9fd']; - /** ยานยิงอุกาบาต (MG6) สีตาม lobby theme 1-8 — Rocket-1..8 ตรงกับ PLAY_LOBBY_THEME_HEX */ - const SPACE_SHOOTER_ROCKET_THEME_URLS = [1, 2, 3, 4, 5, 6, 7, 8].map(function (n) { return BASE + '/img/ViolentCrime/Rocket-' + n + '.png'; }); - /** theme index (1-8) ของ entity จากสี body ที่ย้อม (playTint) — ใช้แมป asset สีตามผู้เล่น */ - function playLobbyThemeIndexForEntityPlay(ent) { - const tint = ent && ent.playTint; - const body = (tint && typeof tint.body === 'string') ? tint.body : null; - const idx = body ? nearestIndexFromHexPlay(body, PLAY_LOBBY_THEME_HEX) : null; - return (idx >= 1 && idx <= 8) ? idx : null; - } - - 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'); - } - /* Quiz Battle: ใช้ HUD ใหม่ (#qb-game-hud) — ซ่อนกล่อง "คะแนน" เดิม */ - if (isQuizBattle()) { - 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 = {}; mg1AllDeadEndScheduled = false; } - 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; - if (window.jdSound) { try { window.jdSound.playSfx(mine.right ? 'correct' : 'wrong', 'mg1'); } catch (e) { /* ignore */ } } - /* item 4 — ทุกคนได้ยินผลของเพื่อนด้วย (คาสเคดสั้น ๆ หลังผลตัวเอง) เดิม MG1 เงียบสำหรับผลคนอื่น */ - if (window.jdSound && Array.isArray(r.results)) { - let _pk = 0; - for (let i = 0; i < r.results.length && _pk < 6; i++) { - const row = r.results[i]; - if (!row || row.id === myId) continue; - _pk++; - (function (right, order) { - setTimeout(function () { try { window.jdSound.playSfx(right ? 'correct' : 'wrong', 'mg1'); } catch (e) { /* ignore */ } }, 150 * order); - })(!!row.right, _pk); - } - } - 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(); - if (window.jdSound) { try { window.jdSound.playSfx(isCorrect ? 'correct' : 'wrong', 'mg4'); } catch (e) { /* ignore */ } } - 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)); - /* เสียงนับถอยหลัง (เล่นครั้งเดียวต่อเลข กัน re-render ยิงซ้ำ) */ - if (window.jdSound && window.__jdLastCountN !== d) { window.__jdLastCountN = d; try { window.jdSound.playSfx('time'); } catch (e) { /* ignore */ } } - 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 = isQuizQuestionMissionRenderLockPlay() ? 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; - /* พักระหว่างข้อ → เอาคำถามออก โชว์ "รอคำถามข้อต่อไป" (MG1 + MG4) */ - const _qBetween = (isQuiz() && !isQuizCarry() && quizBetweenActive) || (isQuizCarry() && quizCarryBetweenActive); - const _qWinnerBanner = isQuizCarry() && quizCarryResultBannerUntil > Date.now() && quizCarryResultBanner; - if (_qWinnerBanner) textEl.textContent = quizCarryResultBanner; /* MG4: โชว์ใครตอบถูกก่อน */ - else if (_qBetween) textEl.textContent = 'รอคำถามข้อต่อไป'; - 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; - } - } - panel.classList.toggle( - 'quiz-map-question-panel--tf-mock-qz', - !!(isQuiz() && isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'live' && isMissionMockHudPlay()), - ); - 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'); - } - } - /* sub-label ใต้คำถาม: ช่วงอ่านคำถาม → "📖 อ่านคำถาม" (สีทอง) · ช่วงตอบ/พัก → ซ่อน (MG1 + MG4) */ - const subEl = document.getElementById('quiz-map-question-subphase'); - if (subEl) { - /* MG4 เท่านั้น: label "📖 อ่านคำถาม" ใต้คำถามในแผง (MG1 ย้ายไปใต้ TIME แล้ว) */ - const _readNow = !_qBetween && quizCarryReadFreezeActive(); - if (_readNow) { - subEl.textContent = '📖 อ่านคำถาม'; - subEl.style.color = '#ffd34d'; - subEl.classList.remove('is-hidden'); - subEl.setAttribute('aria-hidden', 'false'); - } else { - subEl.textContent = ''; - subEl.style.removeProperty('color'); - subEl.classList.add('is-hidden'); - subEl.setAttribute('aria-hidden', 'true'); - } - } - 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 - serverNow()) / 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; - /* เกมจริง (ห้องจริงทุกแบบ: คนล้วน/มีบอท): server เป็นเจ้าของตำแหน่ง/เลนของทุกตัวผ่าน gauntlet-sync - → ห้าม client จัดเลนเอง เพราะ layout นี้สแนป me ไป pos[0] (เลนบนสุด, จุดสตาร์ท) เสมอ - ทำให้ตอน host หลุด me (host ใหม่) ไปทับบอท takeover ที่กินเลนบนสุด - เฉพาะ editor preview (preview=1, ไม่มี server ขับ) เท่านั้นที่ client จัดเลนเอง */ - if (!previewMode) 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; - } - - /** ป้ายชื่อบอท "บอท N (tier)" — เรียงตาม id (เหมือนทุกเครื่อง) ใช้ o.botTier (host เป็นเจ้าของ ผ่าน fill-bot-state) */ - function relabelPreviewBotsPlay() { - 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; - }); - } - - /* MG5/6/7 server-auth: server ขับ entity บอทที่ "client นี้ไม่เคยสร้าง" (นับ human ไม่ตรง server / - race LobbyB→play / reconnect / ghost) → fill-bot-state ถูกปิดสำหรับเกม server-auth จึงไม่มี self-heal - → *-bot-move ทุก packet ถูกดรอป (if(!o)return) = บอทตัวนั้นค้าง/หายเฉพาะ client นี้ (= อาการ MG6). - สร้าง preview-bot entity เดี๋ยวนี้ + กำหนด avatar/ป้ายตาม index เรียง (deterministic ทุกเครื่อง). - คืน null ถ้าเป็น takeover id (=socket-id ของคนที่ออก) ที่ entity ยังไม่มา → caller รอ join/snapshot. */ - function ensureServerAuthBotEntityPlay(id, dir) { - if (id == null) return null; - let o = others.get(id); - if (o) return o; - if (!isPreviewBotId(id)) return null; - o = { - x: 1, y: 1, tx: 1, ty: 1, - direction: dir || 'down', - nickname: 'บอท', - characterId: null, - playTint: playTintForPreviewBotId(id) || resolvePlayTintForPeer(id), - botTier: 'avg', - spaceShooterScore: 0, spaceShooterHits: 0, spaceShooterEliminated: false, - balloonBossScore: 0, balloonBossBossDmg: 0, balloonBossEliminated: false, - jumpSurviveEliminated: false, - }; - try { o.balloonBossBalloons = balloonBossBalloonsStartPlay(); } catch (e) { o.balloonBossBalloons = 5; } - others.set(id, o); - /* avatar ตาม index เรียง (เหมือน rebalancePreviewBots 5967-5969) → ทุกเครื่องได้ชุด avatar เดียวกัน */ - [...others.keys()].filter(isPreviewBotId).sort().forEach((bid, bi) => { const bo = others.get(bid); if (bo) bo.characterId = pickPreviewBotCharacterId(bi); }); - try { relabelPreviewBotsPlay(); } catch (e) { /* ignore */ } - try { if (o.characterId) preloadPlayTintForAvatar(o.characterId, o.playTint); } catch (e) { /* ignore */ } - return o; - } - - function rebalancePreviewBots() { - if (!playBotsEnabled() || !mapData) return; - const human = countPlayHumans(); - let wantBots = Math.max(0, playBotTargetHeadcount() - human); - wantBots = Math.min(wantBots, Math.max(0, 6 - human)); /* total ≤ 6 เสมอ (กัน over-count เช่น reconnect หลัง bot-takeover) */ - /* Card 3 Ban — บอทที่ถูกแบนรอบนี้ (slot N) ไม่ลงเล่น: ลบทิ้ง + ลดเป้าจำนวนลง 1 + ข้าม slot นั้นตอนสร้าง */ - if (playBannedBotSlot >= 0) { - others.delete(PREVIEW_BOT_PREFIX + playBannedBotSlot); - wantBots = Math.max(0, wantBots - 1); - } - [...others.keys()].filter(isPreviewBotId).forEach((bid) => { - const o = others.get(bid); - if (!o) return; - o.playTint = playTintForPreviewBotId(bid) || resolvePlayTintForPeer(bid); - if (o.botTier == null) { - /* [2026-07-17] เดิมเป็น Math.random() = "สุ่มใหม่ทุกเครื่อง" → ป้าย (ฉลาด/กลาง/พลาดบ่อย) ของบอทตัว - เดียวกันไม่ตรงกันข้ามจอ (user เจอทั้ง MG2 และ MG7). path host-auth adopt rb.tier ทับให้ทีหลังอยู่แล้ว - แต่ path server-auth (MG5/6/7) ไม่เคยส่ง tier ลงมา → ค่าสุ่มนี้อยู่ยาวจนจบเกม. - แก้ที่ต้นทาง: derive จาก slot ใน botId (`__pv_bot_` เหมือนกันทุกเครื่อง) ด้วย "สูตรเดียวกับ - gauntletBotTierForSlot ของ server (server.js:8209)" → ทุกเครื่องได้ค่าเท่ากัน และตรงกับ tier จริง - ที่ server ใช้เดินบอท MG2 ด้วย (เดิม client สุ่มเอง = ป้ายไม่ตรงพฤติกรรมตั้งแต่แรก) */ - const slot = parseInt(String(bid).slice(PREVIEW_BOT_PREFIX.length), 10); - const tierRoll = Number.isFinite(slot) ? ((slot * 73 + 17) % 100) / 100 : 0.5; - 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 === playBannedBotSlot) 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; - } - /* [2026-07-17] ห้าม Math.random() — นี่คือ path ที่ "สร้าง" บอทจริง (ตัว backfill ที่ 6013 แทบไม่เคย - ถูกเรียกเพราะตอนสร้างเซ็ต tier ให้เสมอ) → สุ่มตรงนี้ = ป้าย 'บอท N (ฉลาด)' ไม่ตรงกันข้ามเครื่อง. - nextSlot ข้างบน deterministic อยู่แล้ว (slot ว่างเล็กสุด 0,1,2… ทุกเครื่องได้ชุดเดียวกัน — เป็น - ข้อกำหนดเดิมอยู่แล้วเพราะ idx ต้องตรง playLobbyBotThemes ให้สีตรง lobby) → derive tier จากมัน - ด้วยสูตรเดียวกับ server gauntletBotTierForSlot (server.js:8209) */ - const tierRoll = ((nextSlot * 73 + 17) % 100) / 100; - 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), - }); - /* preload เลเยอร์สี + tint ของบอททันทีตอนสร้าง — กัน score HUD บน host โชว์ avatar บอท "ไม่ย้อมสี" - (เดิม preloadPlayOccupantsTints รันตอน scene-load ก่อนบอทถูกสร้าง → เลเยอร์ยังไม่โหลด) */ - try { - const bo = others.get(id); - if (bo && bo.characterId) preloadPlayTintForAvatar(bo.characterId, bo.playTint || resolvePlayTintForPeer(id)); - } catch (ePB) { /* ignore */ } - botIds.push(id); - } - relabelPreviewBotsPlay(); - [...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; - if (detectiveCaseFillBots) return; /* เกมจริง: server ตัดสินคำตอบบอทเอง */ - 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 qt = mapData.quizTrueArea; - const qf = mapData.quizFalseArea; - /* จับที่ "แถวเท้า" (bottom row ของ footprint) เท่านั้น — ให้ตรงกับ server resolveQuizRound - (หัว/ตัวโผล่เข้าโซนแต่เท้ายังไม่ถึง = ไม่นับ) */ - const { cw, ch } = getCharacterFootprintWH(mapData); - const baseTx = Math.floor(ox); - const feetTy = Math.floor(oy) + ch - 1; - let inT = false, inF = false; - for (let dx = 0; dx < cw; dx++) { - const ftx = baseTx + dx; - if (!inT && quizCellOnPlay(qt, ftx, feetTy)) inT = true; - if (!inF && quizCellOnPlay(qf, ftx, feetTy)) inF = true; - } - 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; - if (!warpMeIntoQuizGhostZonePlay()) { - 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; - quizBetweenActive = false; /* คำถามใหม่มา → ออกจากช่วงพัก */ - 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'; } - /* net-ready badge: emit client-ready ครั้งเดียวเมื่อฉากพร้อม */ - let playClientReadyEmitted = false; - /* วาด badge "กำลังโหลด…" เหนือหัว peer ที่ยังโหลดไม่เสร็จ (netReady=false) — คนอื่นรู้ว่าไม่ได้ค้าง */ - function drawNetLoadingBadgePlay(axTile, ayTile, w2s, zDrawIn) { - try { - const fw = getCharacterFootprintWH(mapData); - const cxw = (axTile + (fw.cw || 1) * 0.5) * tileSize; - const topYw = ayTile * tileSize; - const scr = w2s(cxw, topYw); - const sx = scr[0], sy = scr[1]; - const now = Date.now(); - const spin = ['◐', '◓', '◑', '◒'][Math.floor(now / 180) % 4]; - const txt = spin + ' กำลังโหลด…'; - ctx.save(); - ctx.font = '700 13px Kanit, system-ui, sans-serif'; - ctx.textAlign = 'center'; - const tw = ctx.measureText(txt).width + 12; - const by = sy - 46, bh = 20; - ctx.fillStyle = 'rgba(10,16,30,0.82)'; - if (ctx.roundRect) { ctx.beginPath(); ctx.roundRect(sx - tw / 2, by, tw, bh, 6); ctx.fill(); } else ctx.fillRect(sx - tw / 2, by, tw, bh); - ctx.fillStyle = '#ffd34d'; ctx.textBaseline = 'middle'; - ctx.fillText(txt, sx, by + bh / 2 + 1); - ctx.restore(); - } catch (e) { /* ignore */ } - } - /* ===== Sound (jdSound) — scene + end-sfx helpers ===== */ - var JD_SCENE_MAP = { quiz: 'mg1', gauntlet: 'mg2', stack: 'mg3', quiz_carry: 'mg4', jump_survive: 'mg5', space_shooter: 'mg6', balloon_boss: 'mg7' }; - function jdPlayScene() { var gt = mapData && mapData.gameType; return (gt && JD_SCENE_MAP[gt]) || 'mg1'; } - /* เล่นเสียงจบเกมจากชื่อไฟล์แฟลชผล (complete/victory=ชนะ · timeup=หมดเวลา · gameover=แพ้) + หยุด BGM */ - function jdEndSfxFromFile(f) { - if (!window.jdSound) return; - f = String(f || ''); var s = jdPlayScene(); - if (f.indexOf('victory') >= 0 || f.indexOf('complete') >= 0) window.jdSound.playSfx('win', s); - else if (f.indexOf('timeup') >= 0) window.jdSound.playSfx('timeup', s); - else if (f.indexOf('gameover') >= 0) window.jdSound.playSfx('lose', s); - try { window.jdSound.stopBgm(); } catch (e) { /* ignore */ } - } - /* เสียงคลิกปุ่ม (เฉพาะ element ที่เป็นปุ่มจริง) */ - document.addEventListener('click', function (e) { - if (!window.jdSound) return; - var t = (e.target && e.target.closest) ? e.target.closest('button, .btn, [role="button"], [class*="btn-"]') : null; - if (t) { try { window.jdSound.playSfx('button', jdPlayScene()); } catch (err) { /* ignore */ } } - }, true); - 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; - /* fit ทั้งแมปให้ขอบแมปอยู่ในเฟรมเสมอ → ยาน/บอทที่ clamp อยู่ในขอบแมป [22, mw-22] จะไม่หลุดออกนอกจอ - (เดิม balloon_boss ซูมเข้าหา content-window ทำให้ขอบแมปหลุดนอกจอ = ลมกระโชกดันยาน/บอทลอยหายนอกเฟรม) - BG วาด cover เต็มจอแล้ว (draw ~25633) ขอบดำซ้าย-ขวาจึงไม่โผล่แม้ซูมออก แค่ world rect เล็กลงเล็กน้อย */ - zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96; - } else if (isJumpSurvive()) { - /* MG5 (jump_survive) — เกมเดียวที่ตกไปใช้ zoom ดิบ 1.4 (ไม่ fit จอ = เหลือขอบซ้าย-ขวา) - → fit ความกว้างเต็มจอ (vertical scroller เลื่อนแนวตั้งด้วย jumpSurviveCamCenterY อยู่แล้ว) */ - const mwPx = w * tileSize; - if (mwPx > 0) zDraw = canvas.width / mwPx; - } else if (isQuizQuestionMissionRenderLockPlay()) { - const qmc = getQuizQuestionMissionMapCenterWorldPxPlay(); - if (qmc) { - /* ซูมออกเล็กน้อยให้เห็นพื้นหลัง/โซน SAFE-SCAM ครบแบบ mock (0.96 เคย crop ขอบล่างบนจอแคบ) */ - zDraw = Math.min(canvas.width / qmc.mwPx, canvas.height / qmc.mhPx) * 0.90; - } - } else if (isQuizCarry()) { - const mwPx = w * tileSize; - const mhPx = h * tileSize; - zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96; - } else if (isQuizBattle()) { - zDraw *= quizBattleUserZoomMul; - } else if (isGauntletCrownHeistMapPlay() && !(previewMode && editorEmbedReturn)) { - /* #1 map fit: gauntlet เกมจริง — fit "ความสูงแมป (ลู่ทั้งหมด)" ให้พอดีจอ resolution-adaptive - (เดิมใช้ zoom ดิบ ไม่ปรับตาม resolution = ไม่พอดี) · fit สูงอย่างเดียว เพราะ runway เลื่อนแนวนอน */ - const mhPx = h * tileSize; - const fitZ = (canvas.height / mhPx) * 1.0; /* เต็มจอบน-ล่าง (เดิม 0.96 = มีขอบดำบน-ล่าง) */ - if (Number.isFinite(fitZ) && fitZ > 0) zDraw = fitZ; - } - 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; - /* rejoin กลางเกมที่ live แล้ว (server ส่ง res.missionLive=true) → ข้าม how-to เข้าเล่นต่อทันที - กันเคส refresh กลางเกม แล้วค้างหน้า how-to กดต่อไม่ได้ (handshake ready→countdown ผ่านไปแล้ว) */ - let playRejoinLiveResume = false; - /** ปิด how-to/countdown overlay ทั้งหมด (ใช้ตอน resume เข้าเกม live หลัง reconnect) */ - function hideHowtoOverlaysForLiveResumePlay() { - try { - const ho = document.getElementById('gauntlet-crown-howto-overlay'); - if (ho) ho.classList.add('is-hidden'); - const cd = document.getElementById('gauntlet-crown-countdown'); - if (cd) cd.classList.add('is-hidden'); - gauntletCrownHowtoVisible = false; - } catch (_e) { /* ignore */ } - } - 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; - /* แกน Y ตามผู้เล่นเครื่องตัวเอง (ไม่เฉลี่ยทั้งกลุ่ม) → เลนของเราคงที่ใต้กล้อง, host/คนอื่นอยู่เลนของเขาเอง - (เดิมเฉลี่ย Y ทั้งกลุ่ม → ทุกคนถูกเลื่อนจากเลนจริง ทำให้ host ดูมาซ้อนเลนของ client) ; ตายแล้ว → fallback เฉลี่ยกลุ่ม */ - let py = sumCy / ents.length; - if (!me.gauntletEliminated && Number.isFinite(me.y)) { - py = (me.y + ch * 0.5) * tileSizeRef; - } - 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) - */ - /* runner แนวนอน: แมปแคบกว่าจอ → เดิมจับ "กลางจอ" (px=mapWpx*0.5) = เกิดพื้นดำขนาบซ้าย = "ช่องโหว่ข้างหลัง". - เปลี่ยนเป็น "ชิดซ้าย" (ขอบซ้าย grid = ขอบซ้ายจอ, worldMinX=0) → ไม่มีดำซ้าย + strip scroll (S) เลื่อนขึ้น - ทำให้พื้นดำก่อนแถบหลุดออกนอกจอซ้าย + อุปสรรคหายที่ขอบซ้ายจอพอดี. (ผู้เล่นเลือก: คงสเกล+ชิดซ้าย) */ - if (viewW >= mapWpx) px = minCamX; - 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() || !isMePlayHost() || !isGauntletCrownHeistMapPlay() || !mapData) return; - if (detectiveCaseFillBots) return; /* MG2 เกมจริง: server ตัดสินการตาย/คัดออกของบอทเอง */ - 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'; } - /* MG5 server-auth: server เป็นเจ้าของบอท/รอบ → host เลิก sim บอทเอง, ทุกเครื่องรับตำแหน่งบอทจาก server */ - function jumpSurviveServerAuthActive() { - return isJumpSurvive() && (jumpSurviveServerAuthPlay || isDetectiveMinigamePlay()); - } - /* MG6 server-auth: server เป็นเจ้าของบอท/รอบ → host เลิก sim บอท, ทุกเครื่องรับตำแหน่งบอทจาก server */ - function spaceShooterServerAuthActive() { - return isSpaceShooter() && (spaceShooterServerAuthPlay || isDetectiveMinigamePlay()); - } - /* MG7 server-auth: server เป็นเจ้าของบอท/รอบ/HP บอส → host เลิก sim บอท, ทุกเครื่องรับตำแหน่ง/ดาเมจบอทจาก server */ - function balloonBossServerAuthActive() { - return isBalloonBoss() && (balloonBossServerAuthPlay || isDetectiveMinigamePlay()); - } - - 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'; - } - - /** Mega Virus mnq1eml7 (MG7) — ช่วงเล่นจริง: ใช้ HUD/score-row แบบเดียวกับ MG6 (qm-mock) - กันคะแนน/ชื่อใช้ layout เก่าที่ text ทับ/ซ่อน */ - function isBalloonBossMissionHudActivePlay() { - return isMegaVirusMissionShellMapPlay() && balloonBossSessionStartMs > 0 && !balloonBossGameEnded; - } - - 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 = 2.7; - const lw = dw * lp; - const lh = dh * lp; - const lnx = lightImg.naturalWidth; - const lny = lightImg.naturalHeight; - /* จัดแสงให้ตรงกลางไอคอน ×2 (ไอคอนวาดเยื้องซ้าย cx - dw*0.48 → กลางไอคอน ≈ cx + dw*0.02) */ - const iconCx = cx + dw * 0.02; - ctx.save(); - ctx.globalAlpha = Math.min(0.98, pulse + 0.34); - ctx.globalCompositeOperation = 'screen'; - try { - ctx.drawImage(lightImg, 0, 0, lnx, lny, iconCx - 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(); - } - } - - /** สร้างเศษบล็อกตกจากบล็อกที่พลาด (ใช้ตำแหน่ง/หน้าตาเดิมของ stackFall) */ - function spawnStackMissDebrisPlay(sf) { - if (!sf || !stackMini) return; - const lh = stackMini.layerWorldH || Math.max(14, tileSize * 0.3); - const cxW = sf.xLeft1 + sf.tw / 2; - const landOff = sf.hit && sf.hit.landOffsetTiles ? sf.hit.landOffsetTiles : 0; - const dir = landOff >= 0 ? 1 : -1; - stackMissDebris.push({ - cxW, - topY: sf.y1, - w: sf.tw, - h: lh, - vx: dir * (tileSize * (1.4 + Math.random() * 0.9)), - vy: -tileSize * (0.4 + Math.random() * 0.5), - rot: sf.tiltMax || 0, - vrot: dir * (2.0 + Math.random() * 1.8), - seat: sf.actorSeat, - heavy: !!sf.actorHeavy, - hue: (stackMini.layers.length * 47) % 360, - }); - while (stackMissDebris.length > 6) stackMissDebris.shift(); - _stackDebrisLastT = 0; - } - /** อัปเดต+วาดเศษบล็อกที่ตก (gravity + หมุน + ลอยออกข้าง) จนหลุดขอบจอล่าง */ - function drawStackMissDebrisPlay(ctx, worldToScreen, zoom, floorWorldY) { - if (!stackMissDebris.length) return; - const now = performance.now(); - let dt = _stackDebrisLastT ? (now - _stackDebrisLastT) / 1000 : 0.016; - _stackDebrisLastT = now; - if (dt > 0.05) dt = 0.05; - const g = tileSize * 34; - const chCv = canvas && canvas.height ? canvas.height : 720; - const offLimit = floorWorldY + chCv / Math.max(0.2, zoom) + tileSize * 5; - for (let i = stackMissDebris.length - 1; i >= 0; i--) { - const d = stackMissDebris[i]; - d.vy += g * dt; - d.topY += d.vy * dt; - d.cxW += d.vx * dt; - d.rot += d.vrot * dt; - if (d.topY > offLimit) { stackMissDebris.splice(i, 1); continue; } - const sp = worldToScreen(d.cxW - d.w / 2, d.topY); - const drawW = d.w * zoom; - const drawH = d.h * zoom; - ctx.save(); - ctx.translate(sp[0] + drawW / 2, sp[1] + drawH / 2); - ctx.rotate(d.rot); - drawStackBlockSpriteOrHue(ctx, -drawW / 2, -drawH / 2, drawW, drawH, d.seat, d.heavy, d.hue, { missTint: true, missOverlay: true }); - 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'); - } - } - - /** Space Shooter (mnpz6rkp): โหลด life HUD จาก /img/ViolentCrime/ แล้วคืน image set ให้ drawStackTowerLifeIntegrityBarPlay */ - function ensureViolentCrimeLifeHudImagesPlay() { - if (!violentCrimeLifeBarImg) { - violentCrimeLifeBarImg = new Image(); - violentCrimeLifeBarImg.decoding = 'async'; - violentCrimeLifeBarImg.src = violentCrimeAssetUrl('life-bar.png'); - } - if (!violentCrimeLifeHitImg) { - violentCrimeLifeHitImg = new Image(); - violentCrimeLifeHitImg.decoding = 'async'; - violentCrimeLifeHitImg.src = violentCrimeAssetUrl('life-hit.png'); - } - if (!violentCrimeLifeFullImg) { - violentCrimeLifeFullImg = new Image(); - violentCrimeLifeFullImg.decoding = 'async'; - violentCrimeLifeFullImg.src = violentCrimeAssetUrl('life-full.png'); - } - return { bar: violentCrimeLifeBarImg, full: violentCrimeLifeFullImg, hit: violentCrimeLifeHitImg }; - } - - /** - * สัดส่วนสูงของ 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, opts) { - const imgs = (opts && opts.images) - ? opts.images - : (ensureStackTowerLifeHudImagesPlay(), { bar: stackTowerLifeBarImg, full: stackTowerLifeFullImg, hit: stackTowerLifeHitImg }); - const bar = imgs.bar; - const fullIm = (imgs.full && imgs.full.complete && imgs.full.naturalWidth > 0) - ? imgs.full - : ensureStackTowerHeartMinusImgPlay(); - const hitIm = imgs.hit && imgs.hit.complete && imgs.hit.naturalWidth > 0 - ? imgs.hit - : 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.globalCompositeOperation = 'source-over'; - /* เติมเหลืองทับบล็อกให้เด่น */ - ctx.globalAlpha = Math.min(0.6, 0.3 + pulse * 0.32); - ctx.fillStyle = 'rgba(255, 224, 60, 0.55)'; - ctx.fillRect(sx, sy, drawW, drawH); - /* กรอบเหลืองหนา "รอบ" บล็อก (เลยขอบออกมา) + เรืองแสงออกนอก → เห็นชัด */ - const pad = Math.max(3, Math.min(12, drawW * 0.05)); - const sw = Math.max(3, Math.min(9, drawW * 0.055)); - ctx.shadowColor = 'rgba(255, 196, 24, 0.98)'; - ctx.shadowBlur = Math.max(10, Math.min(30, 14 + pulse * 16)); - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - ctx.globalAlpha = Math.min(1, 0.62 + pulse * 0.38); - ctx.strokeStyle = 'rgba(255, 238, 110, 1)'; - ctx.lineWidth = sw; - ctx.strokeRect(sx - pad + sw * 0.5, sy - pad + sw * 0.5, drawW + pad * 2 - sw, drawH + pad * 2 - sw); - /* เส้นในขาวอมเหลือง เพิ่มความคม */ - ctx.shadowBlur = 0; - ctx.shadowColor = 'transparent'; - ctx.globalAlpha = Math.min(0.95, 0.55 + pulse * 0.4); - ctx.strokeStyle = 'rgba(255, 252, 214, 0.96)'; - ctx.lineWidth = Math.max(1.5, sw * 0.42); - ctx.strokeRect(sx - pad + sw, sy - pad + sw, drawW + pad * 2 - sw * 2, drawH + pad * 2 - sw * 2); - 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'); - } - } - - /** กันภาพผลโชว์ "ซ้ำ" — บางเกมจบ 2 path (เช่น MG2 local finish + server gauntlet-ended) ยิง flash 2 รอบผลต่างกัน - → ตัวที่ 2 ถูกบล็อก (เก็บตัวแรกไว้). reset เมื่อเริ่มมินิเกมรอบใหม่ (resetMissionResultFlashGuardPlay) */ - let _missionResultFlashAt = 0; - function missionResultFlashGuardPlay() { - const now = Date.now(); - if (now - _missionResultFlashAt < 9500) return false; /* เพิ่งโชว์ไป → บล็อกตัวซ้ำ */ - _missionResultFlashAt = now; - return true; - } - function resetMissionResultFlashGuardPlay() { _missionResultFlashAt = 0; } - - /** มีใครได้คะแนน ≥1 ไหม (จาก mission.ranked) — ตัดสิน complete/gameover (MG1 quiz / MG4 quiz_carry) */ - function missionAnyScorePlay(mission) { - const r = (mission && Array.isArray(mission.ranked)) ? mission.ranked : []; - return r.some(function (x) { return x && (Number(x.baseScore) || Number(x.finalScore) || Number(x.score) || 0) > 0; }); - } - /** จำนวนผู้รอด (ไม่ eliminated) — ตัดสิน complete/gameover (MG2 เข้าเส้นชัย / MG5 / MG6) */ - function missionSurvivorsPlay(mission) { - if (mission && typeof mission.survivorCount === 'number') return mission.survivorCount; - const r = (mission && Array.isArray(mission.ranked)) ? mission.ranked : []; - return r.filter(function (x) { return x && !x.eliminated; }).length; - } - /** จำนวนผู้เข้าร่วมทั้งหมด (คน+บอท) */ - function missionParticipantsPlay(mission) { - if (mission && typeof mission.participantCount === 'number' && mission.participantCount > 0) return mission.participantCount; - return (mission && Array.isArray(mission.ranked)) ? mission.ranked.length : 0; - } - /** "รอดทั้งหมด" — ทุกคนรอด ไม่มีใคร eliminated (MG5/MG6) */ - function missionAllSurvivedPlay(mission) { - const total = missionParticipantsPlay(mission); - return total > 0 && missionSurvivorsPlay(mission) >= total; - } - /** แฟลชรูปผลรูปเดียว (5s) แล้วเปิด GCM — เกมที่เดิมไม่มีแฟลช (MG1/MG2). asset ตาม skin ผ่าน gcmMissionMockAssetUrl */ - function beginSimpleResultFlashThenGcmPlay(mission, file) { - if (!missionResultFlashGuardPlay()) return; /* กันโชว์ซ้ำ (MG2: local finish + server gauntlet-ended ยิงทั้งคู่) */ - hideStackTowerResultFlashDomOnlyPlay(); - const ov = document.getElementById('stack-tower-result-flash'); - const imgEl = document.getElementById('stack-tower-result-flash-img'); - if (!mission || !ov || !imgEl) { showGauntletCrownMissionOverlay(mission); return; } - const finishToGcm = function () { hideStackTowerResultFlashDomOnlyPlay(); showGauntletCrownMissionOverlay(mission); }; - imgEl.onerror = function () { imgEl.onerror = null; finishToGcm(); }; - jdEndSfxFromFile(file); - imgEl.src = gcmMissionMockAssetUrl(mission, file); - ov.classList.remove('is-hidden'); - ov.setAttribute('aria-hidden', 'false'); - stackTowerResultFlashTimer = setTimeout(function () { stackTowerResultFlashTimer = null; finishToGcm(); }, STACK_TOWER_RESULT_FLASH_MS); - } - - /** โชว์รูปผล Tower จาก TowerBlock 5 วิ แล้วเปิด GCM สรุปเดิม */ - function beginStackTowerResultFlashThenGcm(mission) { - if (!missionResultFlashGuardPlay()) return; /* กันโชว์ผลซ้ำ */ - 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(); - }; - jdEndSfxFromFile(file); - 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: มีคนรอด ≥1 = complete · ไม่รอดเลย = gameover (เกรดไม่เกี่ยว — ตามกฎใหม่) */ - function jumpSurviveMissionOutcomeImageFile(mission) { - /* รอดทั้งหมด = complete · มีคนตาย/ตายหมด = gameover (ตามกฎใหม่) */ - return missionAllSurvivedPlay(mission) ? 'result-complete.png' : 'result-gameover.png'; - } - - /** - * Jumper ภารกิจ: ใช้ #stack-tower-result-flash เหมือน Tower — time_up = timeup แล้ว outcome; - * all_dead = ข้าม timeup แล้ว outcome อย่างเดียว แล้วค่อย GCM - */ - function beginJumpSurviveMissionResultFlashSequenceThenGcm(mission, endKind) { - if (!missionResultFlashGuardPlay()) return; /* กันโชว์ผลซ้ำ */ - 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(); - }; - jdEndSfxFromFile(file); - 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 (!missionResultFlashGuardPlay()) return; /* กันโชว์ผลซ้ำ */ - 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(); - }; - jdEndSfxFromFile(file); - 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(); - /* หมดเวลา: รอดทั้งหมด = complete · มีคนตาย = gameover (ตามกฎใหม่) */ - showOneFlash(missionAllSurvivedPlay(mission) ? 'result-complete.png' : 'result-gameover.png', finishToGcm); - }); - } else { - showOneFlash('result-gameover.png', finishToGcm); /* all_dead */ - } - } - - /** - * Mega Virus (balloon_boss shell): แฟลชรูปเดียวแล้วค่อย GCM - * — แพ้ (ทุกคนตาย / หมดเวลา) = result-gameover.png (TowerBlock เหมือนภารกิจอื่น — รูป2 flow) - * — ชนะ = end-victory-2.png ใน MegaVirus - */ - function beginBalloonBossMegaVirusResultFlashSequenceThenGcm(mission, endReason) { - if (!missionResultFlashGuardPlay()) return; /* กันโชว์ผลซ้ำ */ - 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 finishToGcm = function () { - hideStackTowerResultFlashDomOnlyPlay(); - showGauntletCrownMissionOverlay(mission); - }; - const showOneFlash = function (url, onDone) { - imgEl.onerror = function () { imgEl.onerror = null; onDone(); }; - jdEndSfxFromFile(url); - imgEl.src = url; - ov.classList.remove('is-hidden'); - ov.setAttribute('aria-hidden', 'false'); - stackTowerResultFlashTimer = setTimeout(function () { stackTowerResultFlashTimer = null; onDone(); }, STACK_TOWER_RESULT_FLASH_MS); - }; - if (endReason === 'victory') { - if (window.jdSound) { try { window.jdSound.playSfx('boss-elim', 'mg7'); } catch (e) { /* ignore */ } } - /* ฆ่าบอส HP=0 → end-victory-2.png (MegaVirus) */ - showOneFlash(megaVirusAssetUrl('end-victory-2.png'), finishToGcm); - } else if (endReason === 'time') { - /* หมดเวลา บอสยังไม่ตาย → result-timeup → result-gameover (MegaVirus ไม่มี timeup ใช้ TowerBlock) */ - showOneFlash(stackTowerAssetUrl('result-timeup.png'), function () { - hideStackTowerResultFlashDomOnlyPlay(); - showOneFlash(stackTowerAssetUrl('result-gameover.png'), finishToGcm); - }); - } else { - /* ตายหมด → result-gameover */ - showOneFlash(stackTowerAssetUrl('result-gameover.png'), finishToGcm); - } - } - - 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() { - /* ยึดเวลาเริ่มจริงของ server (playMissionLiveBeginsAt) + serverNow() → ทุกจอเห็น TIME ตรงกัน - แม้เข้า/reconnect ช้า (เดิมใช้ performance.now() local = จอที่เข้าทีหลังนับเวลาเริ่มใหม่ → TIME 90 vs 8) */ - if (playMissionLiveBeginsAt > 0) { - const el = (serverNow() - playMissionLiveBeginsAt) / 1000; - if (Number.isFinite(el) && el >= 0) return el; - } - 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; - if (playRejoinLiveResume) { stackTowerMissionPhase = 'live'; hideHowtoOverlaysForLiveResumePlay(); 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; - applyGauntletCrownReadyButtonPlay(st, btn); - } - - /** 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; - applyGauntletCrownReadyButtonPlay(st, btn); - } - - /** 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; - applyGauntletCrownReadyButtonPlay(st, btn); - } - - 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; - hideReadyAvatarsFloat(); /* ซ่อนแถวหน้าตัวละคร ready ตอนเริ่มเกม (กันค้างบนจอระหว่างเล่น) */ - 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'); - syncGauntletCrownCountdownMg2ModePlay(cd); - runAlignedCountdownPlay( - function (n) { setCountdown321QuestionAssetGraphic(numEl, n); }, - runFinish, - function (id) { stackTowerMissionCountdownTimer = id; } - ); - } - - 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) return; - if (entry.kind === 'bot' && entry.botId) { - 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 (entry.kind === 'human' && entry.id != null && String(entry.id) !== String(myId)) { - /* คนจริงคนอื่น — ใส่ในสรุปด้วย ใช้คะแนนเฉพาะตัว (เดิมมีแต่ me+bots) */ - const oh = others.get(entry.id) || others.get(String(entry.id)); - pushEnt(String(entry.id), oh ? (oh.nickname || String(entry.id).slice(0, 6)) : String(entry.id).slice(0, 6), oh ? oh.characterId : null, oh ? (oh.stackPreviewHumanPts || 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, 6); - 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, 6); - 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; - if (stackServerAuthPlay) return; /* server-auth: server ตัดสินจบ + ส่ง stack-over */ - 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; - let outcome = 'time_up'; - if (pct >= 100) outcome = 'decrypt_complete'; - else if (missesOut) outcome = 'misses_exhausted'; - /* เล่นจริง: host เป็นคนตัดสินจบ + broadcast → ทุกเครื่องจบพร้อมกัน (เดิมต่างคนต่างจับเวลา local → จบไม่พร้อม) */ - if (detectiveCaseFillBots && !isMePlayHost()) return; /* non-host รอ stack-over จาก host */ - if (detectiveCaseFillBots && socket && myId != null) { - try { socket.emit('stack-over', { outcome: outcome }); } catch (e) { /* ignore */ } - } - finishStackTowerMissionLocalPlay(outcome); - } - - /** จบรอบ MG3 จริง (host เรียกเอง / non-host เรียกตอนรับ stack-over) */ - function finishStackTowerMissionLocalPlay(outcome) { - if (stackTowerMissionEndedOnce || !isStackTowerMissionUiMapPlay()) return; - stackTowerMissionEndedOnce = true; - stackTowerMissionPhase = 'ended'; - applyStackTowerMissionPanelImages(); - beginStackTowerResultFlashThenGcm(stackTowerMissionBuildPayload({ outcome: outcome || 'time_up' })); - } - - 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(); - } - - /* [2026-07-17] MG5 ช่วงท้ายเลื่อนเร็วขึ้น ×2 (user request) — 15 วิสุดท้าย. - ⚠️ ต้องตรงกับ server เป๊ะ (server.js:jsScrollPxAt · ค่าคงที่ชุดเดียวกัน) ไม่งั้นแท่นคนละที่ = ชนไม่ตรงกัน */ - const JS_ENDGAME_BOOST_SEC = 15; - const JS_ENDGAME_BOOST_MUL = 2; - function jsBoostStartSecPlay() { - const total = jumpSurviveTimeLimitSecForMission(); - return Math.max(0, (Number(total) || 0) - JS_ENDGAME_BOOST_SEC); - } - function jsScrollPxAtPlay(elapsedSec, risePerSec) { - const total = jumpSurviveTimeLimitSecForMission(); - const t0 = jsBoostStartSecPlay(); - if (!(Number(total) > 0) || elapsedSec <= t0) return elapsedSec * risePerSec; - return (t0 * risePerSec) + ((elapsedSec - t0) * risePerSec * JS_ENDGAME_BOOST_MUL); - } - /** ความเร็วเลื่อน "ณ ตอนนี้" — ใช้กับคนที่ยืนบนแท่น (ต้องจมตามแท่น ไม่งั้นพอ boost แท่นหนีลงไปคนลอยค้าง) */ - function jsRiseVelNowPlay(md) { - const v = Number(md && md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42; - if (!(jumpSurviveLiveAnchorServerMs > 0)) return v; - const el = Math.max(0, (serverNow() - jumpSurviveLiveAnchorServerMs) / 1000); - return el > jsBoostStartSecPlay() ? v * JS_ENDGAME_BOOST_MUL : v; - } - 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; - /* นับเวลาจาก anchor server (serverNow) → จบพร้อมกันทุกเครื่อง; fallback local ถ้ายังไม่มี anchor */ - const elapsed = jumpSurviveLiveAnchorServerMs > 0 - ? (serverNow() - jumpSurviveLiveAnchorServerMs) / 1000 - : (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; - if (playRejoinLiveResume) { jumpSurviveMissionPhase = 'live'; hideHowtoOverlaysForLiveResumePlay(); 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; - hideReadyAvatarsFloat(); /* ซ่อนแถวหน้าตัวละคร ready ตอนเริ่มเกม (กันค้างบนจอระหว่างเล่น) */ - 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; - /* anchor เวลาเริ่ม live = liveBeginsAt ของ server (ถ้ามี) → scroll/timer ของทุกเครื่องอ้างอิงจุดเดียวกัน */ - jumpSurviveLiveAnchorServerMs = playMissionLiveBeginsAt > 0 ? playMissionLiveBeginsAt : serverNow(); - jumpSurvivePlatformScrollPx = 0; - applyJumpSurvivePreviewSpawnLayout(false); - }; - if (!cd || !numEl) { - runFinish(); - return; - } - cd.classList.remove('is-hidden'); - syncGauntletCrownCountdownMg2ModePlay(cd); - runAlignedCountdownPlay( - function (n) { setCountdown321QuestionAssetGraphic(numEl, n); }, - runFinish, - function (id) { jumperMissionCountdownTimer = id; } - ); - } - - /** 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, 6); - 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 applyGauntletCrownHeistMissionPanelImages() { - const howtoOv = document.getElementById('gauntlet-crown-howto-overlay'); - if (howtoOv) { - const tfHowto = isGauntletCrownHeistMapPlay(); - howtoOv.classList.toggle('gch-quiz-tf-mock', tfHowto); - howtoOv.classList.toggle('gch-mg2-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 = missionMockHudAssetUrl('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 && isGauntletCrownHeistMapPlay()) { - resBg.src = missionMockHudAssetUrl('popup-result.png'); - resBg.onerror = function () { - this.onerror = null; - this.src = BASE + '/img/gauntlet-assets/popup-result.png'; - }; - } - } - - 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; - if (playRejoinLiveResume) { spaceShooterMissionPhase = 'live'; hideHowtoOverlaysForLiveResumePlay(); 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; - hideReadyAvatarsFloat(); /* ซ่อนแถวหน้าตัวละคร ready ตอนเริ่มเกม (กันค้างบนจอระหว่างเล่น) */ - 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; - spaceShooterSpawnCount = 0; - /* anchor = liveBeginsAt ของ server (ทุกเครื่องได้ค่าเดียวกัน) → spawn/timer อ้างอิงจุดเดียวกัน */ - spaceShooterLiveAnchorServerMs = playMissionLiveBeginsAt > 0 ? playMissionLiveBeginsAt : serverNow(); - 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'); - syncGauntletCrownCountdownMg2ModePlay(cd); - runAlignedCountdownPlay( - function (n) { setCountdown321QuestionAssetGraphic(numEl, n); }, - runFinish, - function (id) { spaceShooterMissionCountdownTimer = id; } - ); - } - - 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, 6); - const ranked = top.map(function (row, idx) { - const pos = idx + 1; - /* โบนัสอยู่รอดจนจบเกม = +20 (เฉพาะคนไม่ตาย) — รวมใน finalScore เหมือน rankBonus ของ MG2 */ - const surviveBonus = row.eliminated ? 0 : 20; - 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: surviveBonus, - finalScore: row.baseScore + surviveBonus, - }; - }); - const totalParts = ranked.map(function (r) { return r.finalScore; }); - 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, 6); - 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, 6); - /* โบนัสจบเกม: ฆ่าบอส(+100)→ทุกคนที่ร่วมเล่น · รอดชีวิต(+10)→เฉพาะคนไม่ตาย (รวมใน finalScore, clamp ≥0) */ - const endRBonus = mission && mission.balloonBossEndReason; - const killBonusAll = endRBonus === 'victory' ? 100 : 0; - const ranked = top.map(function (row, idx) { - const pos = idx + 1; - const bs = Math.max(0, Number(row.baseScore) || 0); - const bonus = killBonusAll + (row.eliminated ? 0 : 10); - 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: bonus, - finalScore: Math.max(0, bs + bonus), - }; - }); - const totalParts = ranked.map(function (r) { return r.finalScore; }); - 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, 6); - /* โบนัสจบเกม: ฆ่าบอส(+100)→ทุกคนที่ร่วมเล่น · รอดชีวิต(+10)→เฉพาะคนไม่ตาย (รวมใน finalScore, clamp ≥0) */ - const killBonusAll = reason === 'victory' ? 100 : 0; - const ranked = top.map((row, idx) => { - const pos = idx + 1; - const bs = Math.max(0, Number(row.baseScore) || 0); - const bonus = killBonusAll + (row.eliminated ? 0 : 10); - 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: bonus, - finalScore: Math.max(0, bs + bonus), - }; - }); - const totalParts = ranked.map((r) => r.finalScore); - 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, - }; - } - - /* host เป็นผู้ตัดสินจบ MG6 + แนบคะแนน/สถานะสุดท้าย → ทุกเครื่องจบพร้อมกันและอันดับตรงกัน */ - function triggerSpaceShooterOverPlay(kind) { - if (spaceShooterGameEnded) return; - const k = (kind === 'all_dead') ? 'all_dead' : 'time_up'; - try { - if (socket && myId != null && isMePlayHost()) { - const scores = buildStableShipParticipantRefsPlay().map((e) => ({ - id: e.id, - score: Math.max(0, (e.ref && (e.ref.spaceShooterScore | 0)) || 0), - hits: Math.max(0, Number(e.ref && e.ref.spaceShooterHits) || 0), - eliminated: !!(e.ref && e.ref.spaceShooterEliminated), - })); - socket.emit('space-shooter-over', { kind: k, scores: scores }); - } - } catch (e) { /* ignore */ } - endSpaceShooterMissionRound(k); - } - - function applySpaceShooterOverScoresPlay(scores) { - if (!Array.isArray(scores)) return; - scores.forEach((s) => { - if (!s || s.id == null) return; - const sc = Math.max(0, Number(s.score) || 0); - const hits = Math.max(0, Number(s.hits) || 0); - const ent = (myId != null && String(s.id) === String(myId)) ? me : (others.get(String(s.id)) || others.get(s.id)); - if (!ent) return; - ent.spaceShooterScore = sc; - ent.spaceShooterHits = hits; - if (s.eliminated) ent.spaceShooterEliminated = true; - }); - } - - 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) { - if (isMissionMockHudPlay()) return missionMockHudAssetUrl(file); - return questionMissionAssetUrl(file); - } - - /** สกิน HUD แบบ mock Minigame 1–7 (fix 1920×1080 + score-bar / profile) */ - function getMissionMockHudSkinPlay() { - if (isQuizQuestionMissionUiMapPlay()) return 'question_mission'; - if (isStackTowerMissionUiMapPlay()) return 'stack_tower'; - if (isJumpSurviveMissionUiMapPlay()) return 'jumper'; - if (isSpaceShooterMissionUiMapPlay()) return 'violent_crime'; - if (isMegaVirusMissionShellMapPlay()) return 'mega_virus'; - if (isQuizCarry() && quizCarryEmbedMissionFlowActive()) return 'quiz_carry'; - if (isGauntletCrownHeistMapPlay()) return 'gauntlet_crown'; - return ''; - } - - function isMissionMockHudPlay() { - return !!getMissionMockHudSkinPlay(); - } - - function missionMockHudAssetUrl(file) { - const skin = getMissionMockHudSkinPlay(); - const f = String(file || '').replace(/^\//, ''); - if (skin === 'jumper') return jumperAssetUrl(f); - if (skin === 'violent_crime') return violentCrimeAssetUrl(f); - if (skin === 'mega_virus') return megaVirusAssetUrl(f); - if (skin === 'stack_tower') return stackTowerAssetUrl(f); - if (skin === 'quiz_carry') return BASE + '/img/quiz-carry/' + f; - if (skin === 'gauntlet_crown') return BASE + '/img/gauntlet-assets/' + f; - return questionMissionAssetUrl(f); - } - - function applyMissionMockHudCssVarsPlay(root) { - if (!root) return; - if (!isMissionMockHudPlay()) { - root.style.removeProperty('--mmh-score-bar'); - root.style.removeProperty('--mmh-score-label'); - root.style.removeProperty('--mmh-score-avatar'); - root.style.removeProperty('--mmh-mic-on'); - root.style.removeProperty('--mmh-btn-ready'); - root.style.removeProperty('--mmh-btn-start'); - delete root.dataset.missionMockSkin; - return; - } - const skin = getMissionMockHudSkinPlay(); - root.dataset.missionMockSkin = skin; - if (skin === 'gauntlet_crown') { - root.style.removeProperty('--mmh-score-bar'); - root.style.setProperty('--mmh-score-label', "url('" + missionMockHudAssetUrl('score.png') + "')"); - const crownHead = document.getElementById('play-cyber-crown-score-head'); - const crownImg = document.getElementById('play-cyber-crown-score-img'); - if (crownHead && crownImg) { - const scoreSrc = missionMockHudAssetUrl('score.png'); - crownHead.classList.remove('is-hidden'); - crownHead.setAttribute('aria-hidden', 'false'); - if (crownImg.getAttribute('src') !== scoreSrc) crownImg.setAttribute('src', scoreSrc); - crownImg.alt = 'SCORE :'; - crownImg.classList.add('mg2-score-label'); - } - } else { - root.style.removeProperty('--mmh-score-label'); - root.style.setProperty('--mmh-score-bar', "url('" + missionMockHudAssetUrl('score-bar.png') + "?v=3')"); - } - root.style.setProperty('--mmh-score-avatar', "url('" + missionMockHudAssetUrl('score-avartar.png') + "')"); - root.style.setProperty('--mmh-mic-on', "url('" + missionMockHudAssetUrl('btn-mic_on.png') + "')"); - root.style.setProperty('--mmh-btn-ready', "url('" + missionMockHudAssetUrl('btn-ready.png') + "')"); - root.style.setProperty('--mmh-btn-start', "url('" + missionMockHudAssetUrl('btn-start.png') + "')"); - const micImg = document.getElementById('play-cyber-mic-img'); - if (micImg) { - micImg.classList.remove('is-hidden'); - micImg.setAttribute('aria-hidden', 'false'); - const micSrc = missionMockHudAssetUrl('btn-mic_on.png'); - if (micImg.getAttribute('src') !== micSrc) micImg.setAttribute('src', micSrc); - } - const frameOverlay = document.getElementById('play-cyber-profile-frame-overlay'); - if (frameOverlay) { - const frameSrc = missionMockHudAssetUrl('Avartar.png'); - if (frameOverlay.getAttribute('src') !== frameSrc) frameOverlay.setAttribute('src', frameSrc); - } - } - - function isQuizQuestionMissionHudActivePlay() { - return isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'live'; - } - /* [2026-07-17] เฉพาะ "การเรนเดอร์" (กล้อง+zoom) ของ MG1 — ต้องค้างเท่าตอนเล่นต่อไปแม้จบเกมแล้ว - (user: "ตอนจบเกม ฉากเกมไม่เท่าเดิมกับตอนเล่น"). ต้นเหตุ: zoom/กล้องเดิมผูกกับ - isQuizQuestionMissionHudActivePlay ที่เป็น phase==='live' → พอจบ phase='ended' branch หลุด - → zDraw ตกไปใช้ `zoom` ดิบ + กล้องเด้งกลับไปตาม me = ฉากเปลี่ยนขนาดทันที. - เกมอื่นไม่มีอาการนี้เพราะ zoom ผูกกับ "แมป" ไม่ใช่ "เฟส". - แยกตัวนี้ออกมาเพราะ predicate เดิมคุม HUD/อินพุตอีก ~18 ที่ ซึ่ง "ต้องดับตอนจบ" ตามเดิม. - ล็อกแค่ live|ended → howto/countdown คงพฤติกรรมเดิมเป๊ะ (ไม่มีใครบ่นถึง) */ - function isQuizQuestionMissionRenderLockPlay() { - return isQuizQuestionMissionUiMapPlay() - && (quizQuestionMissionPhase === 'live' || quizQuestionMissionPhase === 'ended'); - } - - /** ภารกิจคำถาม 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) { - /* MG1 ใช้ popup-Howtominigame1.png (รูป SAFE/SCAM) — popup-Howto.png เดิมเป็นดีไซน์ MG4 ใส่ผิดที่ */ - howtoBg.src = questionMissionAssetUrl('popup-Howtominigame1.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; - if (playRejoinLiveResume) { quizQuestionMissionPhase = 'live'; hideHowtoOverlaysForLiveResumePlay(); 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; - hideReadyAvatarsFloat(); /* ซ่อนแถวหน้าตัวละคร ready ตอนเริ่มเกม (กันค้างบนจอระหว่างเล่น) */ - 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'); - syncGauntletCrownCountdownMg2ModePlay(cd); - runAlignedCountdownPlay( - function (n) { setCountdown321QuestionAssetGraphic(numEl, n); }, - runFinish, - function (id) { quizQuestionMissionCountdownTimer = id; } - ); - } - - 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, 6); - 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'; - /* เกรด F (= โชว์ Lose stamp) เฉพาะ "ไม่มีใครได้คะแนนเลย" — ดูจาก playLiveQuizScores เต็ม (ครบทุกคน) - ไม่ใช่ totalSum ของ ranked (me+others) ที่อาจหายคนตอบถูกที่ไม่อยู่ใน others → ขัดกับ flash complete */ - const mg1FullScoreSum = Object.keys(playLiveQuizScores).reduce(function (s, k) { return s + Math.max(0, playLiveQuizScores[k] | 0); }, 0); - if (mg1FullScoreSum <= 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, 6); - 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'; } - /** MG4: ช่วง "อ่านคำถาม" (ก่อนตัวเลือกขึ้น) → freeze ทุกคน+บอท อยู่ตรงไหนตรงนั้น */ - function quizCarryReadFreezeActive() { - return !!(isQuizCarry() && quizCarryServerAuthActive() && !quizCarryBetweenActive - && quizCarryOptionRevealAt && Date.now() < quizCarryOptionRevealAt); - } - - /* MG4 server-authoritative: เกมจริง (detective) เท่านั้น — server เป็นเจ้าของ คำถาม/เฟส/หยิบ-ส่ง/คะแนน/บอท - editor preview (เล่นเดี่ยว) ยังเป็น client-sim เดิม (gate เดียวกับ server: space.detectiveMinigameActive) */ - let quizCarryServerAuthPlay = false; /* ตั้ง true เมื่อรับ quiz-carry-phase (srv) ครั้งแรก — กัน edge ก่อน flag */ - function quizCarryServerAuthActive() { - return isQuizCarry() && (quizCarryServerAuthPlay || isDetectiveMinigamePlay()); - } - - /** พรีวิว 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 quizCarryWrapLinesFit(ctx, text, maxWidth) { - const raw = String(text || '').trim(); - if (!raw) return []; - const lines = []; - let cur = ''; - for (const chr of raw) { - if (chr === '\n') { lines.push(cur); cur = ''; continue; } - const test = cur + chr; - if (cur === '' || ctx.measureText(test).width <= maxWidth) cur = test; - else { lines.push(cur); cur = chr; } - } - if (cur) 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; - let fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); - let maxW = Math.max(96, tileSpanX * ts * 1.5 - 14); - ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; - /* คำตอบไทยมักเป็น "คำเดียวยาว" (ไม่มีช่องว่าง → wrap ไม่ได้) → ย่อฟอนต์ให้พอดีกว้าง + ปล่อยป้ายกว้างได้ กันโดนตัด "…" */ - while (fontPx > 8 && ctx.measureText(line).width > maxW) { - fontPx -= 1; - ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; - } - 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); - } - - /* ผู้ถูกแบนรอบนี้ (Card 3 Ban) เข้ามาดูเฉยๆ ไม่ต้อง Ready → ไม่นับใน gate เริ่มเกม - (ไม่งั้น host จะค้าง N/N+1 เพราะ banned ไม่เคย ready) */ - function isQuizCarryPregameBannedId(id) { - const o = others.get(id) || others.get(String(id)); - return !!(o && o.bannedSpectator); - } - - function quizCarryPregameHumanIds() { - const set = new Set(); - if (myId != null && !isPreviewBotId(myId) && !playBannedSpectator) set.add(String(myId)); - /* นับจาก readyMap ของ server (authoritative + ตัด banned แล้ว) + ตัวเราเท่านั้น - → host/client นับ "จำนวนผู้เล่น" ตรงกันเสมอ (เดิมวน others ซึ่ง membership ต่างกันชั่วคราว → ready ไม่ตรง) - การรอให้ client เข้าครบใช้ expectedActive (gauntletCrownPregameExpectedHumans) แทน */ - [quizCarryLobbyReadyMap, gauntletCrownLobbyReadyMap].forEach((rm) => { - if (rm && typeof rm === 'object') { - Object.keys(rm).forEach((id) => { - if (id && !isPreviewBotId(id) && !isQuizCarryPregameBannedId(id)) set.add(String(id)); - }); - } - }); - return [...set]; - } - - 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; /* fallback (load-bearing): host พึ่งตอน null — อย่าเปลี่ยน (เคยทำ MG3 ready ค้าง 4/5 + MG2 spawn ทับ) */ - return String(playHostId) === String(myId); - } - - /* บอทเติมช่อง (fill-bot) ใน 7 มินิเกมต้องเดิน/เล่น/คะแนนตรงกันทุก client: - host เป็นผู้จำลอง bot ทั้งหมด (AI/ฟิสิกส์/คะแนน) แล้ว broadcast state — client อื่นปิด local sim แล้ว render ตาม - (เดิม: ทุกเครื่องสุ่ม Math.random() แยกกัน → บอทตัวเดียวกันอยู่คนละที่/คนละคะแนนบนแต่ละจอ) */ - function fbNum(v) { return (typeof v === 'number' && Number.isFinite(v)) ? Math.round(v * 100) / 100 : undefined; } - function broadcastFillBotState() { - if (!playBotsEnabled() || !isMePlayHost() || !mapData) return; - if (isQuiz() && detectiveCaseFillBots) return; /* quiz เกมจริง: server เป็นเจ้าของบอท (quiz-bot-move) → ห้าม host ส่งทับ = บอทวาป/ผีหลุด area */ - if (isGauntlet() && detectiveCaseFillBots) return; /* MG2 เกมจริง: server เป็นเจ้าของบอท (gauntlet-sync) → ห้าม host ส่งทับ */ - if (quizCarryServerAuthActive()) return; /* MG4 เกมจริง: server เป็นเจ้าของบอท (quiz-carry-bot-move) → ห้าม host ส่งทับ */ - if (jumpSurviveServerAuthActive()) return; /* MG5 เกมจริง: server เป็นเจ้าของบอท (jump-survive-bot-move) → ห้าม host ส่งทับ */ - if (spaceShooterServerAuthActive()) return; /* MG6 เกมจริง: server เป็นเจ้าของบอท (space-shooter-bot-move) → ห้าม host ส่งทับ */ - if (balloonBossServerAuthActive()) return; /* MG7 เกมจริง: server เป็นเจ้าของบอท (balloon-boss-bot-move) → ห้าม host ส่งทับ */ - const arr = []; - others.forEach((o, id) => { - if (!isPreviewBotId(id) || !o) return; - const row = { id: id, x: fbNum(o.x), y: fbNum(o.y), direction: o.direction || 'down', walking: !!o.botIsWalking }; - if (o.characterId != null) row.cid = o.characterId; /* ให้ client สร้างบอทที่ขาดได้ → ชุดบอท host-authoritative */ - if (o.botTier != null) row.tier = o.botTier; /* tier (ฉลาด/กลาง/พลาดบ่อย) — host เป็นเจ้าของ ให้ label/พฤติกรรมตรงกันทุกเครื่อง (เดิมสุ่ม local → ป้ายไม่ตรง) */ - if (o.gauntletScore != null) row.gScore = o.gauntletScore; - if (o.gauntletEliminated != null) row.gElim = !!o.gauntletEliminated; - if (o.gauntletJumpTicks != null) row.gJump = o.gauntletJumpTicks; - if (o.gauntletJumpVis != null) row.gJumpVis = fbNum(o.gauntletJumpVis); - if (o.jumpSurviveEliminated != null) row.jsElim = !!o.jumpSurviveEliminated; - if (o.spaceShooterCx != null) row.ssCx = fbNum(o.spaceShooterCx); - if (o.spaceShooterCy != null) row.ssCy = fbNum(o.spaceShooterCy); - if (o.spaceShooterScore != null) row.ssScore = o.spaceShooterScore; - if (o.spaceShooterEliminated != null) row.ssElim = !!o.spaceShooterEliminated; - if (o.balloonBossCx != null) row.bbCx = fbNum(o.balloonBossCx); - if (o.balloonBossCy != null) row.bbCy = fbNum(o.balloonBossCy); - if (o.balloonBossScore != null) row.bbScore = o.balloonBossScore; - if (o.balloonBossBossDmg != null) row.bbDmg = o.balloonBossBossDmg; - if (o.balloonBossBalloons != null) row.bbBalloons = o.balloonBossBalloons; - if (o.balloonBossEliminated != null) row.bbElim = !!o.balloonBossEliminated; - if (o.quizCarryHeld !== undefined && !(isQuizCarry() && quizCarryServerAuthActive())) row.qcHeld = o.quizCarryHeld; - /* MG1 quiz: สถานะล็อก (ตอบผิด=ผี) + คะแนนบอท → host เป็นเจ้าของ ส่งให้ทุกเครื่องตรงกัน - (เดิมไม่ส่งเลย → non-host เห็นบอทล็อก/respawn/คะแนนคนละแบบ) */ - if (isQuiz()) { - if (o.quizCannotTrue != null) row.qct = !!o.quizCannotTrue; - if (o.quizCannotFalse != null) row.qcf = !!o.quizCannotFalse; - } - /* คะแนนบอท MG1 (quiz) + MG4 (quiz_carry) → host เป็นเจ้าของ ส่งให้ทุกเครื่องตรงกัน - (เดิม MG4 ไม่ส่ง → non-host sim บอทเอง ด้วย RNG ต่างกัน = คะแนนบอทไม่ตรง 2 จอ) */ - if (isQuiz() || isQuizCarry()) { - const qsc = playLiveQuizScores[id]; - if (qsc != null) row.qsc = Math.max(0, qsc | 0); - } - /* สีบอท (host เป็นเจ้าของ) — แนบไปด้วยเพื่อให้ทุก client แสดงสีตรงกัน - (เดิมแต่ละเครื่องคำนวณสีเองจาก theme/sessionStorage → สีบอทไม่ตรงกันเมื่อ sync ธีมมาช้า) */ - if (o.playTint && o.playTint.body) { - row.tint = { head: o.playTint.head, hair: o.playTint.hair, body: o.playTint.body }; - } - arr.push(row); - }); - /* รายชื่อ "คนจริง" ที่ host เห็น (= myId(host) + others ที่ไม่ใช่บอท) — ให้ non-host force ลบผีที่ host ไม่มี - (humans ไม่ถูก sync แบบบอท → ผี reconnect ค้างบน client; id-based ไม่ชนชื่อ default) */ - const humanRoster = []; - if (myId != null) humanRoster.push(String(myId)); - others.forEach((o, id) => { if (!isPreviewBotId(id) && o) humanRoster.push(String(id)); }); - if (arr.length) socket.emit('fill-bot-state', { bots: arr, humans: humanRoster }); - } - setInterval(broadcastFillBotState, 80); /* ~12.5Hz — self-gate ถ้าไม่ใช่ host หรือไม่มีบอท */ - - /* ===== Server-time sync + เวลาเริ่มเล่นจริงร่วมกัน (liveBeginsAt) ===== - แก้ปัญหา "เวลาในเกมไม่เท่ากัน เริ่ม/จบไม่พร้อมกัน": เดิมแต่ละเครื่องนับ 3-2-1 เองด้วย setTimeout - และจับเวลารอบด้วย performance.now() ของตัวเอง → GO และจุดจบไม่ตรงกัน - แก้: sync นาฬิกากับ server (serverNow) แล้วทุกคนนับไปหา "liveBeginsAt" (server epoch) จุดเดียวกัน - เมื่อ GO พร้อมกัน → นาฬิการอบ (performance.now ตั้งแต่ GO) ก็ sync กันเองอัตโนมัติ */ - let playClockOffset = 0; /* serverTime - clientTime (ms) */ - let playClockBestRtt = null; - let playClockSyncInterval = null; - let playClockEstablished = false; /* sync ครั้งแรกแล้วหรือยัง — ครั้งแรก hard-set, re-sync ถัดไป blend (กันกระโดด) */ - function serverNow() { return Date.now() + playClockOffset; } - function syncPlayClock(round) { - round = round || 0; - if (!socket || !socket.connected) return; - /* เริ่ม burst ใหม่ทุกครั้ง → รีเซ็ต best RTT เพื่อ "re-establish" offset ใหม่ทั้งหมด - ไม่งั้น best RTT ที่ค้างไว้จะทำให้ re-sync ครั้งหลังถูกข้าม แก้ clock drift ไม่ได้ - (เวลาในเกม MG6/MG7 จะเพี้ยนสะสมเพราะ serverNow ของแต่ละเครื่องไม่ตรงกัน) */ - if (round === 0) playClockBestRtt = null; - const t0 = Date.now(); - socket.emit('time-sync', (res) => { - const t1 = Date.now(); - if (res && Number.isFinite(res.t)) { - const rtt = t1 - t0; - if (playClockBestRtt == null || rtt <= playClockBestRtt) { - playClockBestRtt = rtt; - const newOffset = res.t + rtt / 2 - t1; /* ชดเชยครึ่งหนึ่งของ round-trip latency */ - if (!playClockEstablished) { - playClockOffset = newOffset; /* ครั้งแรก: ตั้งตรง ๆ ให้ baseline แม่น */ - playClockEstablished = true; - } else { - /* re-sync ถัดไป: เกลี่ยเข้าค่าใหม่แบบนุ่ม → serverNow ไม่กระโดดทุก 12 วิ (= ไม่ทำเฟส MG3/timer สะดุด). - ก้าวใหญ่ >250ms = สแน็ป (drift จริง/เปลี่ยน server) กัน lag ค้าง; ก้าวเล็ก = blend 25% */ - const dOff = newOffset - playClockOffset; - playClockOffset += (Math.abs(dOff) > 250) ? dOff : dOff * 0.25; - } - } - } - if (round < 5) setTimeout(function () { syncPlayClock(round + 1); }, 300); - }); - } - /* re-sync นาฬิกาเป็นระยะ → กัน clock drift ระหว่างเล่นยาว ๆ ทำให้ serverNow ทุกเครื่องตรงกันเสมอ - (ฐานเวลาเดียวกัน → timer/boss/aim/spawn ของทุก minigame sync กัน) */ - function startPeriodicClockSync() { - if (playClockSyncInterval) return; - playClockSyncInterval = setInterval(function () { - try { if (socket && socket.connected) syncPlayClock(0); } catch (_e) { /* ignore */ } - }, 12000); - } - - /* ─────────────────────────────────────────────────────────────────────── - ตัวบอกสถานะเน็ต (เพิ่มล้วน — ไม่แตะ game logic): ป้าย "เน็ตหน่วง"/"กำลังเชื่อมต่อใหม่" - แสดงมุมขวาบน. วัด RTT เองด้วย time-sync (server มี handler อยู่แล้ว ไม่ต้องแก้ server) - ───────────────────────────────────────────────────────────────────────── */ - const NET_RTT_WARN_MS = 320; /* เกินนี้ = เน็ตหน่วง (เหลือง) */ - const NET_RTT_BAD_MS = 700; /* เกินนี้ = แย่มาก (แดง) */ - let netStatusEl = null; - let netLastRtt = null; - let netReconnecting = false; - let netPingTimer = null; - let netOkHideTimer = null; - /* เน็ตแย่ = กด Ready มินิเกมไม่ได้ (กันเริ่มเกมตอน sync ยังไม่พร้อม) — มี safety กันค้าง */ - let netBadSinceMs = 0; - const NET_READY_BLOCK_SAFETY_MS = 15000; /* เน็ตแย่ต่อเนื่องเกินนี้ → ปลดล็อกให้กดได้ (กัน soft-lock) */ - function isNetBadNowPlay() { - return netReconnecting || (socket && socket.connected === false) || (netLastRtt != null && netLastRtt >= NET_RTT_BAD_MS); - } - /** true = เน็ตแย่จน "ยังไม่ควรให้กด Ready" (แต่ถ้าแย่ต่อเนื่องเกิน safety → คืน false ปลดล็อก) */ - function isNetTooBadToReadyPlay() { - if (!isNetBadNowPlay()) return false; - if (!netBadSinceMs) return true; /* เพิ่งแย่ */ - return (Date.now() - netBadSinceMs) < NET_READY_BLOCK_SAFETY_MS; - } - let _netWaitToastAt = 0; - function showNetWaitToastPlay() { - const now = Date.now(); - if (now - _netWaitToastAt < 1500) return; /* กันสแปม */ - _netWaitToastAt = now; - try { showCrownStartBlockedNoticePlay({ error: 'กำลังรอสัญญาณให้เสถียร… อีกครู่กดพร้อมได้' }); } catch (_e) { /* ignore */ } - try { setNetStatus('bad', 'กำลังปรับสัญญาณให้ลื่นขึ้น — รอสักครู่ค่อยกดพร้อม', true); } catch (_e2) { /* ignore */ } - try { gamelogPost(gameLogCatForPlay(), 'net-ready-block', '[TC175] เน็ตแย่ → บล็อกกด Ready (block+safety 15วิ) · rtt=' + (netLastRtt == null ? '?' : Math.round(netLastRtt)) + ' reconn=' + netReconnecting); } catch (_e3) { /* ignore */ } - } - function ensureNetStatusEl() { - if (netStatusEl) return netStatusEl; - const css = document.createElement('style'); - css.textContent = '' - + '#play-net-status{position:fixed;top:10px;right:12px;z-index:9400;display:none;align-items:center;gap:7px;' - + 'padding:7px 13px;border-radius:11px;font-family:inherit;font-weight:700;font-size:14px;line-height:1;' - + 'color:#fff;box-shadow:0 4px 16px rgba(0,0,0,.42);pointer-events:none;backdrop-filter:blur(2px);' - + 'transition:opacity .25s;letter-spacing:.2px;}' - + '#play-net-status.is-warn{background:linear-gradient(135deg,#c9831a,#e0a52b);}' - + '#play-net-status.is-bad{background:linear-gradient(135deg,#b3231d,#d84038);}' - + '#play-net-status.is-ok{background:linear-gradient(135deg,#1b9e4b,#13c06a);}' - + '#play-net-status .pns-dot{width:9px;height:9px;border-radius:50%;background:#fff;opacity:.92;}' - + '#play-net-status.is-bad .pns-spin{width:13px;height:13px;border:2px solid rgba(255,255,255,.4);' - + 'border-top-color:#fff;border-radius:50%;animation:pnsSpin .8s linear infinite;}' - + '@keyframes pnsSpin{to{transform:rotate(360deg);}}'; - document.head.appendChild(css); - const el = document.createElement('div'); - el.id = 'play-net-status'; - el.innerHTML = ''; - document.body.appendChild(el); - netStatusEl = el; - return el; - } - function setNetStatus(kind, text, withSpin) { - const el = ensureNetStatusEl(); - if (netOkHideTimer) { clearTimeout(netOkHideTimer); netOkHideTimer = null; } - if (kind === 'hidden') { el.style.display = 'none'; return; } - el.className = 'is-' + kind; - const icon = el.querySelector('.pns-icon'); - if (icon) icon.innerHTML = withSpin ? '' : ''; - const t = el.querySelector('.pns-text'); - if (t) t.textContent = text; - el.style.display = 'flex'; - el.style.opacity = '1'; - if (kind === 'ok') { - netOkHideTimer = setTimeout(function () { el.style.opacity = '0'; setTimeout(function () { if (netStatusEl) netStatusEl.style.display = 'none'; }, 280); }, 1600); - } - } - function refreshNetStatus() { - /* จับ/ปล่อยเวลาเริ่มแย่ (ใช้คุม safety ปลดล็อกปุ่ม Ready) + refresh ปุ่ม how-to ให้ปลดล็อกเองเมื่อเน็ตกลับมา */ - const badNow = isNetBadNowPlay(); - if (badNow && !netBadSinceMs) netBadSinceMs = Date.now(); - else if (!badNow && netBadSinceMs) netBadSinceMs = 0; - try { if (typeof syncGauntletCrownJumpButton === 'function') { /* no-op guard */ } if (typeof refreshGauntletCrownHowtoReadyButtonsPlay === 'function') refreshGauntletCrownHowtoReadyButtonsPlay(); } catch (_e) { /* ignore */ } - if (netReconnecting || (socket && socket.connected === false)) { - setNetStatus('bad', 'กำลังเชื่อมต่อใหม่…', true); - return; - } - if (netLastRtt != null && netLastRtt >= NET_RTT_BAD_MS) { - setNetStatus('bad', 'เน็ตหน่วงมาก (' + Math.round(netLastRtt) + ' ms)', false); - } else if (netLastRtt != null && netLastRtt >= NET_RTT_WARN_MS) { - setNetStatus('warn', 'เน็ตหน่วง (' + Math.round(netLastRtt) + ' ms)', false); - } else { - setNetStatus('hidden'); - } - } - function netPingOnce() { - if (!socket || !socket.connected) return; - const t0 = Date.now(); - let done = false; - try { - socket.emit('time-sync', function () { - if (done) return; done = true; - netLastRtt = Date.now() - t0; - refreshNetStatus(); - }); - } catch (_e) { /* ignore */ } - setTimeout(function () { if (!done) { done = true; netLastRtt = Math.max(netLastRtt || 0, NET_RTT_BAD_MS); refreshNetStatus(); } }, 2500); - } - function setupNetStatusIndicator() { - if (netPingTimer) return; - try { - socket.on('disconnect', function () { netReconnecting = true; refreshNetStatus(); }); - socket.on('connect', function () { - if (netReconnecting) { netReconnecting = false; setNetStatus('ok', 'เชื่อมต่อแล้ว', false); } - else { netReconnecting = false; } - netPingOnce(); - }); - if (socket.io && socket.io.on) { - socket.io.on('reconnect_attempt', function () { netReconnecting = true; refreshNetStatus(); }); - socket.io.on('reconnect', function () { netReconnecting = false; setNetStatus('ok', 'เชื่อมต่อแล้ว', false); }); - } - } catch (_e) { /* ignore */ } - netPingTimer = setInterval(netPingOnce, 4000); - netPingOnce(); - } - - /* #20 แสดงเลข version มุมขวาล่าง — อ่านจาก ?v= ของ - +
v —
- - - - -
กำลังโหลดตัวละคร…
- - - - - -
-
- - อนุญาตให้เข้าถึงไมโครโฟน เพื่อให้คุณสามารถสื่อสารและบันทึกเสียงได้อย่างคมชัด โปรดอนุญาตให้แอปเข้าถึงไมโครโฟนของอุปกรณ์ - -
-
- - - -
- - -
-

ช่องเขียว · กด F / แป้นไทย โต้ตอบ

-
-

PLAYERS : 0/10

- -
- -
-
-
- ตัวละครของคุณ -
- -
-
-
- - -
-
- Room ID : - - -
-
- - -
-
-
-
-
- - - - -
-
- - - - - -
- -
-
- - -
- - - - - - - - - - - - -
v —
- - + + + + + + โถงรอ — Game + + + + + + + + + + + + + + +
กำลังโหลดตัวละคร…
+ + + + + +
+
+ + อนุญาตให้เข้าถึงไมโครโฟน เพื่อให้คุณสามารถสื่อสารและบันทึกเสียงได้อย่างคมชัด โปรดอนุญาตให้แอปเข้าถึงไมโครโฟนของอุปกรณ์ + +
+
+ + + +
+ + +
+

ช่องเขียว · กด F / แป้นไทย โต้ตอบ

+
+

PLAYERS : 0/10

+ +
+ +
+
+
+ ตัวละครของคุณ +
+ +
+
+
+ + +
+
+ Room ID : + + +
+
+ + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ +
+
+ + +
+ + + + + + + + + + + + +
v —
+ + diff --git a/www/html/Game/public/video/intro-trailer.mp4 b/www/html/Game/public/video/intro-trailer.mp4 new file mode 100644 index 0000000..2c02144 Binary files /dev/null and b/www/html/Game/public/video/intro-trailer.mp4 differ diff --git a/www/html/Game/public/video/minigame-transition.mp4 b/www/html/Game/public/video/minigame-transition.mp4 new file mode 100644 index 0000000..cb0a70b Binary files /dev/null and b/www/html/Game/public/video/minigame-transition.mp4 differ diff --git a/www/html/Game/server.js b/www/html/Game/server.js index 262fbc9..2eebd69 100644 --- a/www/html/Game/server.js +++ b/www/html/Game/server.js @@ -1,13843 +1,14243 @@ -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' }, -]; -/** โหมดเทสต์: บังคับเกมต่อการ์ด 3 ใบ — array[3], แต่ละช่อง '' = สุ่ม หรือ key จาก DETECTIVE_MINIGAME_POOL · รับ array หรือ key เดี่ยว (เก่า) */ -function normalizeForcedMinigameKeys(v) { - let arr; - if (Array.isArray(v)) arr = v; - else if (v != null && String(v).trim()) arr = [v, v, v]; - else arr = []; - const one = (k) => { - const s = (k == null ? '' : String(k)).trim(); - return s && DETECTIVE_MINIGAME_POOL.some((e) => e.key === s) ? s : ''; - }; - return [one(arr[0]), one(arr[1]), one(arr[2])]; -} -const LOBBY_SLOT_TOTAL = 6; - -/* เวลานับถอยหลัง 3-2-1 ก่อนเริ่มเล่นจริง (ms) — server กำหนด liveBeginsAt = now + ค่านี้ - ให้ทุก client นับไปหาเวลาเดียวกัน → GO/เริ่ม/จบ ตรงกันทุกเครื่อง (เผื่อ buffer ให้ทุกคนรับ event ทัน) */ -const LIVE_COUNTDOWN_MS = 3500; -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 ตามลำดับช่อง — ไม่ซ้ำกับผู้เล่น - taken = สีของ "คนจริง" เท่านั้น แล้วสะสมสีบอทที่จัดไปแล้ว (ไม่รวม pin ของ slot ตัวเอง) - → pin เดิมที่ valid + ไม่ชนคน/บอทอื่น = คงเดิม (ไม่เปลี่ยนสีมั่วทุก sync); ชนเมื่อไหร่ค่อยเลือกใหม่ - แก้ 2 อย่าง: (ก) เดิม getTakenLobbyThemeIndices รวม pin ของ slot ตัวเอง → re-pick เสมอ = สีบอท churn ทุก sync - (ข) "เงาสะท้อนของ user" = บอทสีตรงคน (เช่น bot ทดแทนคนที่ออก pin สีคนนั้นไว้) พอคนนั้นกลับมา → บอท re-pick พ้นสีคน */ -function syncLobbyBotThemeSlots(space) { - if (!space) return []; - const botCount = effectiveBotSlotCount(space); - if (!space.lobbyBotThemeBySlot || typeof space.lobbyBotThemeBySlot !== 'object') { - space.lobbyBotThemeBySlot = {}; - } - const assigned = new Set(); - space.peers.forEach((p) => { - const ti = parseInt(p.lobbyColorThemeIndex, 10); - if (ti >= 1 && ti <= LOBBY_THEME_COUNT) assigned.add(ti); - }); - const pickFree = () => { - for (let t = 1; t <= LOBBY_THEME_COUNT; t++) { if (!assigned.has(t)) return t; } - return (assigned.size % LOBBY_THEME_COUNT) + 1; - }; - const out = []; - for (let i = 0; i < botCount; i++) { - const slot = space.lobbyBotThemeBySlot[i]; - let themeIndex = parseInt(slot && slot.themeIndex, 10); - if (!(themeIndex >= 1 && themeIndex <= LOBBY_THEME_COUNT) || assigned.has(themeIndex)) { - themeIndex = pickFree(); - } - assigned.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), - /* ส่งจำนวนบอทมาด้วย — ให้ client ที่นั่งอยู่ใน LobbyB อยู่แล้ว (ไม่ได้ re-join) เห็นบอทที่มาแทนคนออกทันที */ - botSlotCount: effectiveBotSlotCount(space), - }); -} - -/* ===== Game Log (รวม 7 มินิเกม + LobbyA/B) → /var/www/html/gamelog/data/log.jsonl อ่านที่ /gamelog ===== */ -const GAMELOG_DIR = path.join(__dirname, '..', 'gamelog', 'data'); -const GAMELOG_FILE = path.join(GAMELOG_DIR, 'log.jsonl'); -try { fs.mkdirSync(GAMELOG_DIR, { recursive: true }); } catch (eMk) { /* ignore */ } -function gameLogCategoryOf(space) { - if (!space) return 'อื่นๆ'; - try { - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - const gt = md && md.gameType; - const m = { quiz: 'MG1', gauntlet: 'MG2', stack: 'MG3', quiz_carry: 'MG4', jump_survive: 'MG5', space_shooter: 'MG6', balloon_boss: 'MG7' }; - if (gt && m[gt]) return m[gt]; - if (serverMapIsPostCaseLobbyB(space)) return 'LobbyB'; - return 'LobbyA'; - } catch (e) { return 'อื่นๆ'; } -} -function glog(spaceId, space, ev, detail, who) { - try { - const rec = { - t: Date.now(), - cat: gameLogCategoryOf(space), - room: String((space && space.spaceName) || spaceId || '').slice(0, 64), - who: String(who || '').slice(0, 48), - ev: String(ev || '').slice(0, 48), - detail: detail == null ? '' : String(detail).slice(0, 500), - src: 'server', - }; - fs.appendFile(GAMELOG_FILE, JSON.stringify(rec) + '\n', () => {}); - } catch (e) { /* ignore */ } -} - -/** เก็บกวาด peer ซ้ำ (ghost / "เงาสะท้อนของ user") ออกจาก space.peers ทั้งก้อน — กันกรณี dedup ต่อ-join พลาด/race - จน peer คนเดิมค้าง 2 ตัว → snapshot ส่งไปทุกจอ = เห็นตัวซ้ำ + MG3 คิวตามี seat ผี = เกมค้าง. - เทียบ playerKey ก่อน (เชื่อถือสุด ข้าม reconnect) แล้วชื่อ custom; เก็บตัวที่อยู่ท้าย Map (เพิ่ง join/active ล่าสุด) */ -function pruneDuplicateSpacePeers(spaceId, space) { - if (!space || !space.peers || space.peers.size < 2) return false; - const validKey = (p) => (p && typeof p.playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(p.playerKey)) ? p.playerKey : ''; - const isCustom = (nn) => nn && nn !== 'ผู้เล่น' && nn.indexOf('ผู้เล่น') !== 0 && nn.indexOf('บอท') !== 0; - const lastByKey = new Map(); - const lastByNickNoKey = new Map(); /* ชื่อ → pid เฉพาะ peer ที่ "ไม่มี playerKey" (ก้ำกึ่งว่าเป็น ghost) */ - space.peers.forEach((p, pid) => { - if (!p) return; - const pk = validKey(p); - if (pk) { lastByKey.set(pk, pid); return; } /* มี key → ไม่เข้า nickname dedup */ - const nn = normalizeLobbyNickname(p.nickname); - if (isCustom(nn)) lastByNickNoKey.set(nn, pid); - }); - const remove = []; - space.peers.forEach((p, pid) => { - if (!p) return; - const pk = validKey(p); - if (pk) { - /* มี playerKey → ลบเฉพาะตัวที่ key เดียวกันแต่ไม่ใช่ตัวล่าสุด (= ghost reconnect จริง) - ⚠️ ไม่ลบด้วยชื่อ: 2 คนตั้งชื่อเหมือนกัน (เช่น "MONE") = คนละ playerKey = คนละคน ต้องเก็บทั้งคู่ */ - if (lastByKey.get(pk) !== pid) remove.push(pid); - return; - } - /* ไม่มี playerKey → dedup ชื่อ custom ได้ (เทียบเฉพาะกลุ่มไม่มี key ด้วยกัน) */ - const nn = normalizeLobbyNickname(p.nickname); - if (isCustom(nn) && lastByNickNoKey.get(nn) !== pid) remove.push(pid); - }); - if (!remove.length) return false; - remove.forEach((pid) => { - const gp = space.peers.get(pid); - space.peers.delete(pid); - if (space.quizCarryLobbyReady) delete space.quizCarryLobbyReady[pid]; - if (space.gauntletCrownLobbyReady) delete space.gauntletCrownLobbyReady[pid]; - io.to(spaceId).emit('user-left', { id: pid }); - glog(spaceId, space, 'ghost-prune', 'ลบตัวซ้ำ id=' + String(pid).slice(0, 6), (gp && gp.nickname) || ''); - }); - return true; -} - -/** จำนวน 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; - /* เลือกผู้เล่น ≥ 4 คนตอนสร้างห้อง → ไม่เติมบอท (รอคนจริง) — เว้นแต่ host ตั้งบอทเองภายหลัง (n>0) */ - if (n === 0 && maxP >= 4) return 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'); -const SOUND_SETTINGS_PATH = path.join(__dirname, 'data', 'sound-settings.json'); -const SOUND_SETTINGS_DEFAULT = { masterVol: 1.0, musicVol: 0.35, sfxVol: 0.75 }; -function loadSoundSettings() { - try { - if (fs.existsSync(SOUND_SETTINGS_PATH)) { - const j = JSON.parse(fs.readFileSync(SOUND_SETTINGS_PATH, 'utf8')); - const clamp = (v, d) => { const n = Number(v); return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : d; }; - return { - masterVol: clamp(j.masterVol, SOUND_SETTINGS_DEFAULT.masterVol), - musicVol: clamp(j.musicVol, SOUND_SETTINGS_DEFAULT.musicVol), - sfxVol: clamp(j.sfxVol, SOUND_SETTINGS_DEFAULT.sfxVol), - }; - } - } catch (e) { console.error('loadSoundSettings', e.message); } - return Object.assign({}, SOUND_SETTINGS_DEFAULT); -} -function saveSoundSettings(s) { - try { fs.writeFileSync(SOUND_SETTINGS_PATH, JSON.stringify(s, null, 2)); } catch (e) { console.error('saveSoundSettings', e.message); } -} - -/* ===== Quiz Battle global leaderboard (เก็บคะแนนสูงสุดต่อ room/หมวด ข้ามเซสชัน) ===== */ -const QB_LEADERBOARD_PATH = path.join(__dirname, 'data', 'quiz-battle-leaderboard.json'); -/** @type {{[roomId:string]: Array<{name:string,score:number,characterId:string,ts:number}>}} */ -let qbLeaderboardStore = {}; -function loadQbLeaderboard() { - try { - if (fs.existsSync(QB_LEADERBOARD_PATH)) { - const j = JSON.parse(fs.readFileSync(QB_LEADERBOARD_PATH, 'utf8')); - qbLeaderboardStore = (j && typeof j === 'object') ? j : {}; - } - } catch (e) { qbLeaderboardStore = {}; } -} -function saveQbLeaderboard() { - try { - const dir = path.dirname(QB_LEADERBOARD_PATH); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(QB_LEADERBOARD_PATH, JSON.stringify(qbLeaderboardStore), 'utf8'); - } catch (e) { /* ignore */ } -} -function qbLeaderboardSubmit(roomId, name, score, characterId) { - roomId = String(roomId || '').trim(); - name = String(name || '').trim().slice(0, 24); - score = Math.max(0, Math.floor(Number(score) || 0)); - if (!roomId || !name) return; - if (!Array.isArray(qbLeaderboardStore[roomId])) qbLeaderboardStore[roomId] = []; - const list = qbLeaderboardStore[roomId]; - const ex = list.find((e) => e && e.name === name); - if (ex) { - if (score > (ex.score || 0)) { ex.score = score; ex.characterId = String(characterId || ''); ex.ts = Date.now(); } - } else { - list.push({ name, score, characterId: String(characterId || ''), ts: Date.now() }); - } - list.sort((a, b) => (b.score - a.score) || (a.ts - b.ts)); - if (list.length > 500) list.length = 500; - saveQbLeaderboard(); -} -function qbLeaderboardGet(roomId, name) { - roomId = String(roomId || '').trim(); - const list = Array.isArray(qbLeaderboardStore[roomId]) ? qbLeaderboardStore[roomId] : []; - const top = list.slice(0, 50).map((e, i) => ({ rank: i + 1, name: e.name, score: e.score || 0, characterId: e.characterId || '' })); - let me = null; - const nm = String(name || '').trim(); - if (nm) { - const idx = list.findIndex((e) => e && e.name === nm); - if (idx >= 0) me = { rank: idx + 1, name: nm, score: list[idx].score || 0, characterId: list[idx].characterId || '' }; - } - return { top, me, total: list.length }; -} -loadQbLeaderboard(); - -/* ===== System stats (CPU / RAM) สำหรับหน้า Admin ===== */ -const os = require('os'); -let __prevCpuSample = null; -function __cpuSample() { - const cpus = os.cpus() || []; - let idle = 0, total = 0; - for (const c of cpus) { - for (const k in c.times) total += c.times[k]; - idle += c.times.idle; - } - return { idle, total }; -} -/** CPU% เฉลี่ยช่วงเวลาระหว่างสองครั้งที่เรียก (admin poll ~ทุก 5 วิ) — ครั้งแรกคืน null */ -function __cpuPercent() { - const cur = __cpuSample(); - const prev = __prevCpuSample; - __prevCpuSample = cur; - if (!prev) return null; - const idleD = cur.idle - prev.idle; - const totalD = cur.total - prev.total; - if (totalD <= 0) return null; - return Math.max(0, Math.min(100, Math.round((1 - idleD / totalD) * 100))); -} -__cpuSample(); // เก็บ baseline แรก -function getDiskStats() { - try { - if (typeof fs.statfsSync !== 'function') return null; - const s = fs.statfsSync(__dirname); - const total = s.blocks * s.bsize; - const free = s.bfree * s.bsize; // ว่างจริงทั้งหมด - const avail = s.bavail * s.bsize; // ว่างที่ผู้ใช้ทั่วไปใช้ได้ - const used = total - free; - if (!(total > 0)) return null; - return { total, free: avail, used, usedPercent: Math.round((used / total) * 100) }; - } catch (e) { return null; } -} -/* จำนวนผู้เล่น/ห้อง/socket ออนไลน์ตอนนี้ — โชว์ในหน้า Admin เทียบกับเพดานความจุ */ -function getOnlineStats() { - let players = 0, rooms = 0; - try { - for (const [, sp] of spaces) { - if (sp && sp.peers) { players += sp.peers.size; rooms++; } - } - } catch (e) { /* ignore */ } - let sockets = 0; - try { sockets = (io && io.engine && io.engine.clientsCount) || 0; } catch (e) { /* ignore */ } - return { sockets, players, rooms }; -} - -function getSystemStats() { - const totalMem = os.totalmem(); - const freeMem = os.freemem(); - const usedMem = totalMem - freeMem; - const load = os.loadavg(); - const cores = (os.cpus() || []).length || 1; - const cpuPct = __cpuPercent(); - const mu = process.memoryUsage(); - return { - ok: true, - cpu: { - percent: cpuPct, // 0-100 หรือ null (ครั้งแรก) - cores, - load1: Math.round(load[0] * 100) / 100, - load5: Math.round(load[1] * 100) / 100, - load15: Math.round(load[2] * 100) / 100, - loadPercent: Math.max(0, Math.min(100, Math.round((load[0] / cores) * 100))), - }, - mem: { - total: totalMem, - free: freeMem, - used: usedMem, - usedPercent: Math.round((usedMem / totalMem) * 100), - }, - disk: getDiskStats(), - node: { - rss: mu.rss, - heapUsed: mu.heapUsed, - rssPercent: Math.round((mu.rss / totalMem) * 100), - version: process.version, - uptime: Math.round(process.uptime()), - }, - uptime: { os: Math.round(os.uptime()), process: Math.round(process.uptime()) }, - /* ผู้ใช้ออนไลน์ + เพดานความจุ (ประเมินจาก 2 vCPU / socket fan-out): สบาย ≤300 · เริ่มหนัก ≥500 */ - online: getOnlineStats(), - capacity: { comfortable: 300, strain: 500 }, - hostname: os.hostname(), - platform: os.platform(), - ts: Date.now(), - }; -} - -/** ช่องตัวเลือกบนกริด 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({}); - /* ชุดคำถามพิเศษผูกกับ "เลขคดีสัมบูรณ์ 1-15" (ไม่ผูกกับโฟลเดอร์/level ที่อาจถูกจัดกลุ่มใหม่) - key เดิมในแอดมิน = L{ceil(case/3)}_C{((case-1)%3)+1} ต่อคดี → คงความเข้ากันได้ */ - const cid = Math.max(1, Math.min(15, Number(caseId) || 1)); - const L = Math.ceil(cid / 3); - const C = ((cid - 1) % 3) + 1; - return sets[specialQuizSetKey(L, C)] || sets[specialQuizSetKey(1, 1)] || { questions: [], specialCard: sanitizeSpecialCard({}) }; -} - -/* ===== Special Quiz ในเกม — ไอคอนสุ่มในฉาก + เซสชันถาม-ตอบ multiplayer (ทุกคนต้องตอบถูก) ===== */ -/** เฉพาะ 4 มินิเกมนี้ (ตอนสืบสวน) ที่ไอคอนพิเศษจะสุ่มโผล่ */ -/** คำถามพิเศษขึ้นได้ "ทุกเกม" (7 มินิเกม) — รวม stack(mnn93hpi)+balloon(mnq1eml7) ที่จับด้วยคลิก/แตะไอคอน */ -const SPECIAL_QUIZ_ELIGIBLE_MAP_IDS = ['mng8a80o', 'mno9kb07', 'mnn93hpi', 'mnorwqx1', 'mnptfts2', 'mnpz6rkp', 'mnq1eml7']; -const SPECIAL_QUIZ_ICON_TYPES = ['lawyer', 'police']; -/** โอกาสโผล่ = 1 ใน 3 ของการเล่นแต่ละรอบ */ -/** โอกาสขึ้นคำถามพิเศษต่อรอบ (เล่นจริง) — 1/3 · โหมดเทสต์บังคับการ์ดต่อเกมจะ force ขึ้นเสมอแยกต่างหาก */ -const SPECIAL_QUIZ_SPAWN_CHANCE = 1 / 3; -/** หลุดกลางมินิเกม → ให้เวลา reconnect (วิ) ก่อน bot เข้าเล่นแทน (roster คดี/LobbyB) - ⚠️ ห้ามตั้ง 0: เปลี่ยนหน้า lobbyB→play→lobbyB (socket หลุดชั่วคราว) จะถูกถอดจาก roster → rejoin ไม่ได้ ("ห้องนี้เริ่มคดีแล้ว") - หมายเหตุ: bot takeover "ในเกม MG1" (sess.bots) เป็นทันทีอยู่แล้ว แยกจาก grace นี้ - ตั้ง 22 (เดิม 10) — จาก log 6 คน join ใช้เวลา 10-16 วิ (โหลดหนัก) → grace 10 หมดก่อน = bot takeover ผิด → "ตัวละครซ้ำ 2 ตัว" (บอท+คนที่กลับมาช้า) + คนหลุด */ -const DISCONNECT_GRACE_SEC = 22; - -/** โหมดเทสต์ (admin): บังคับ "การ์ดพิเศษใบไหน → เกมไหน" — { mapId: cardId(1-7) } · เกมที่ตั้งไว้จะ force ขึ้นเสมอด้วยการ์ดนั้น */ -function normalizeTestSpecialCardByMap(v) { - const out = {}; - if (v && typeof v === 'object') { - SPECIAL_QUIZ_ELIGIBLE_MAP_IDS.forEach((mid) => { - const c = Math.floor(Number(v[mid])); - if (c >= 1 && c <= 7) out[mid] = c; - }); - } - return out; -} -/** เวลาให้ตอบต่อข้อ (มิลลิวิ) — ค่าคงที่เริ่มต้น (ย้ายไป 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 obj = md.objects && md.objects[y]; - if (obj && obj[x] === 1) return false; - /* ต้องไม่ใช่ช่อง blockPlayer ด้วย — ไม่งั้นไอคอนไปโผล่บนกำแพง/บล็อกที่ผู้เล่นเดินเข้าไม่ถึง (เช่น MG1 quiz มี 140 ช่อง) */ - const bp = md.blockPlayer && md.blockPlayer[y]; - if (bp && bp[x] === 1) return false; - return true; -} - -/** หลีกเลี่ยงโซน 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, occupiedLaneCount) { - const w = md.width || 20; - const h = md.height || 15; - /* quiz_carry (แบกคำตอบ): โต๊ะ/เคาน์เตอร์ถูกวาดใน "ภาพพื้นหลัง" แต่ไม่ได้มาร์คใน blockPlayer → - ช่องว่างกลางระหว่าง hub กับโต๊ะ interactive ดูเหมือนเดินได้แต่จริง ๆ มีโต๊ะบัง เก็บไอคอนไม่ได้ - → เกิดไอคอน "ใกล้จุดที่ผู้เล่นเกิด/เดิน" + เว้น buffer 2 ช่องรอบ hub/interactive/option (กันโต๊ะที่วาดเลยกริด) */ - if (md.gameType === 'quiz_carry') { - const nearMarkedTable = (x, y) => { - for (const g of [md.quizCarryHubArea, md.interactive, md.quizCarryOptionArea]) { - if (!Array.isArray(g)) continue; - for (let dy = -2; dy <= 2; dy++) { - const gy = g[y + dy]; - if (!gy) continue; - for (let dx = -2; dx <= 2; dx++) { - if (gy[x + dx]) return true; - } - } - } - return false; - }; - const seeds = (Array.isArray(md.lobbyPlayerSpawns) && md.lobbyPlayerSpawns.length) - ? md.lobbyPlayerSpawns - : [md.spawn || { x: Math.floor(w / 2), y: Math.floor(h / 2) }]; - const qcCands = []; - seeds.forEach((s) => { - const bx = Math.floor(Number(s.x)); - const by = Math.floor(Number(s.y)); - for (let dy = -5; dy <= 5; dy++) { - for (let dx = -5; dx <= 5; dx++) { - const dist = Math.abs(dx) + Math.abs(dy); - if (dist < 2 || dist > 6) continue; - const xx = bx + dx; - const yy = by + dy; - if (specialQuizTileWalkable(md, xx, yy) && !specialQuizTileInGameZone(md, xx, yy) && !nearMarkedTable(xx, yy)) { - qcCands.push({ x: xx, y: yy }); - } - } - } - }); - if (qcCands.length) { - const p = qcCands[Math.floor(Math.random() * qcCands.length)]; - return { x: p.x + 0.5, y: p.y + 0.5 }; - } - } - /* gauntlet (วิ่งหลบ): ผู้เล่นวิ่งบน "แถวเลน" เท่านั้น → ไอคอนต้องอยู่บนแถวเดียวกับ gauntletPlayerSpawns - (ที่ x กลางทาง) ผู้เล่นวิ่งผ่านเก็บได้/คลิกได้ — ไม่งั้นลอยออกนอกเลนเก็บไม่ได้ */ - if (md.gameType === 'gauntlet' && Array.isArray(md.gauntletPlayerSpawns) && md.gauntletPlayerSpawns.length) { - /* ใช้เฉพาะเลนที่ "มีผู้เล่นจริง" (occupiedLaneCount ตัวแรกตามลำดับ P1..PN) — กันไอคอนเกิดในเลนว่าง */ - const nLanes = (Number.isFinite(occupiedLaneCount) && occupiedLaneCount > 0) - ? Math.min(md.gauntletPlayerSpawns.length, Math.floor(occupiedLaneCount)) - : md.gauntletPlayerSpawns.length; - const usableSpawns = md.gauntletPlayerSpawns.slice(0, nLanes); - const rows = Array.from(new Set(usableSpawns - .map((s) => Math.floor(Number(s.y))) - .filter((n) => Number.isFinite(n) && n >= 0 && n < h))); - const laneCands = []; - const xMin = Math.max(4, Math.floor(w * 0.30)); - const xMax = Math.min(w - 2, Math.floor(w * 0.72)); - rows.forEach((ry) => { - for (let x = xMin; x <= xMax; x++) { - if (specialQuizTileWalkable(md, x, ry) && !specialQuizTileInGameZone(md, x, ry)) laneCands.push({ x, y: ry }); - } - }); - if (laneCands.length) { - const p = laneCands[Math.floor(Math.random() * laneCands.length)]; - return { x: p.x + 0.5, y: p.y + 0.5 }; - } - } - /* stack (Tower block): เล่นแบบหยอดบล็อก เดินไม่ได้ + กล้องเห็นแค่ "หอ" → วางไอคอนใกล้กึ่งกลางโซนปล่อย/ลง - (ที่อยู่ในจอ) ผู้เล่นคลิก/แตะเก็บได้ — ไม่งั้นไปโผล่ขอบแมปนอกจอ */ - if (md.gameType === 'stack') { - const zc = []; - [md.stackReleaseArea, md.stackLandArea].forEach((g) => { - if (Array.isArray(g)) { - for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (g[y] && g[y][x]) zc.push({ x, y }); - } - }); - if (zc.length) { - let cx2 = 0; - let cy2 = 0; - zc.forEach((c) => { cx2 += c.x; cy2 += c.y; }); - cx2 = Math.round(cx2 / zc.length); - cy2 = Math.round(cy2 / zc.length); - for (let rad = 0; rad <= 6; rad++) { - for (let yy = cy2 - rad; yy <= cy2 + rad; yy++) { - for (let xx = cx2 - rad; xx <= cx2 + rad; xx++) { - if (specialQuizTileWalkable(md, xx, yy)) return { x: xx + 0.5, y: yy + 0.5 }; - } - } - } - /* โซนทั้งหมดเป็น object → วางกลางโซนไปเลย (จะคลิกได้แม้ไม่ walkable เพราะ stack ใช้คลิก) */ - return { x: cx2 + 0.5, y: cy2 + 0.5 }; - } - } - /* จุดเกิดที่ "กำหนดเองจาก editor" (specialQuizSpawnArea) — ใช้กับทุกเกมยกเว้น gauntlet/stack - (สองเกมนั้น return ไปแล้วด้านบน) → สุ่ม 1 ช่องที่ paint ไว้ (เลือกช่องเดินได้ก่อน) */ - if (Array.isArray(md.specialQuizSpawnArea) && md.specialQuizSpawnArea.length) { - const painted = []; - const paintedWalkable = []; - for (let yy = 0; yy < h; yy++) { - const row = md.specialQuizSpawnArea[yy]; - if (!row) continue; - for (let xx = 0; xx < w; xx++) { - if (!row[xx]) continue; - painted.push({ x: xx, y: yy }); - if (specialQuizTileWalkable(md, xx, yy)) paintedWalkable.push({ x: xx, y: yy }); - } - } - const pickFrom = paintedWalkable.length ? paintedWalkable : painted; - if (pickFrom.length) { - const p = pickFrom[Math.floor(Math.random() * pickFrom.length)]; - return { x: p.x + 0.5, y: p.y + 0.5 }; - } - } - /* jump_survive (กระโดด mnptfts2): ผู้เล่นยืนบน "แพลตฟอร์ม" — พื้นที่ว่างนับ walkable ผิด (ลอยกลางอากาศเก็บไม่ได้) - ไอคอนคลิก/แตะเก็บได้ทุกมินิเกม (trySpecialQuizClickAt) → วางบนแพลตฟอร์มใกล้จุดเกิด (อยู่ในจอตั้งแต่ต้น คลิกได้) - = แก้ #44 อีเวนต์พิเศษ (ทนาย/เทพ) ไม่ขึ้นเพราะ BFS หา walkable ไม่เจอ → return null */ - if (md.gameType === 'jump_survive' && Array.isArray(md.jumpSurvivePlatformArea) && md.jumpSurvivePlatformArea.length) { - const sp = md.spawn || { x: Math.floor(w / 2), y: Math.floor(h / 2) }; - const bx = Math.floor(Number(sp.x)); - const by = Math.floor(Number(sp.y)); - const plats = []; - for (let y = 0; y < h; y++) { - const row = md.jumpSurvivePlatformArea[y]; - if (!row) continue; - for (let x = 0; x < w; x++) { - if (row[x]) plats.push({ x, y }); - } - } - if (plats.length) { - plats.sort((a, b) => (Math.abs(a.x - bx) + Math.abs(a.y - by)) - (Math.abs(b.x - bx) + Math.abs(b.y - by))); - const near = plats.slice(0, Math.min(8, plats.length)); - const p = near[Math.floor(Math.random() * near.length)]; - const iy = (p.y - 1 >= 0) ? p.y - 1 : p.y; /* เหนือแพลตฟอร์ม 1 ช่อง = ระดับตัวผู้เล่น (ไม่จมในแพลตฟอร์ม) */ - return { x: p.x + 0.5, y: iy + 0.5 }; - } - } - /* 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) { - /* ไม่มี spawnArea/spawn เดินได้ (เช่น stack/balloon) → seed จาก "กลางแมป" วนออก - เพื่อให้ไอคอนอยู่กลางฉากที่มองเห็น/เอื้อมถึง ไม่ไปโผล่มุมนอกจอ */ - const ccx = Math.floor(w / 2); - const ccy = Math.floor(h / 2); - const maxRad = Math.max(w, h); - for (let rad = 0; rad <= maxRad && sx < 0; rad++) { - for (let yy = ccy - rad; yy <= ccy + rad && sx < 0; yy++) { - for (let xx = ccx - rad; xx <= ccx + rad && sx < 0; xx++) { - if (specialQuizTileWalkable(md, xx, yy)) { sx = xx; sy = yy; } - } - } - } - } - 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; - /* เลือกช่องที่อยู่ "ใกล้ผู้เล่น/กลางฉาก" (2-8 ช่องจาก seed) ก่อน → ผู้เล่นเข้าไปจับได้แน่นอน + อยู่ในจอ · - ไม่มีในระยะนั้นค่อย fallback ทั้งหมด */ - const near = cands.filter((c) => { - const d = Math.abs(c.x - sx) + Math.abs(c.y - sy); - return d >= 2 && d <= 8; - }); - const pool = near.length ? near : cands; - const pick = pool[Math.floor(Math.random() * pool.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 || !sq.appeared) return null; - return { iconType: sq.iconType, x: sq.x, y: sq.y, expiresAt: sq.expiresAt || 0 }; -} - -/** เริ่มมินิเกมสืบสวน: สุ่ม 1/3 ว่ารอบนี้จะมีไอคอนพิเศษไหม + เลือกตำแหน่ง (1 ใบต่อครั้งที่ติด — ดีไซน์ "1 ใน 3") - Test Mode (testSpecialCardByMap / forceSpecialQuiz) → ข้าม roll ขึ้นเสมอ */ -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); - /* [2026-07-17] การ์ดมาจาก "สำรับต่อคดี" (3 ใบไม่ซ้ำจาก 7) แจกตาม suspectPickIndex — ไม่ใช่ - set.specialCard ใบเดียวที่ Admin ตั้ง ซึ่งทำให้ 3 เกมได้ใบเดิมเสมอ (user request). - fallback เป็นค่า Admin ถ้าสำรับหาย (คดีที่เริ่มก่อน deploy นี้ / space เก่าค้าง) */ - const deckIdx = (typeof space.suspectPickIndex === 'number' && space.suspectPickIndex >= 0 && space.suspectPickIndex <= 2) ? space.suspectPickIndex : 0; - const deck = Array.isArray(space.specialCardDeck) ? space.specialCardDeck : null; - let card = set && set.specialCard; - if (deck && deck[deckIdx]) { - const dDef = specialCardDef(deck[deckIdx]) || {}; - card = sanitizeSpecialCard({ cardId: Number(deck[deckIdx]), th: dDef.th }); - } - /* โหมดเทสต์ (admin): บังคับการ์ดใบที่เลือกสำหรับเกมนี้ + force ขึ้นเสมอ (ไม่ต้องสุ่ม) */ - const testCardId = (runtimeGameTiming.testSpecialCardByMap || {})[mapId]; - const testForcedCard = (Number(testCardId) >= 1 && Number(testCardId) <= 7); - if (testForcedCard) { - const def = specialCardDef(testCardId) || {}; - card = sanitizeSpecialCard({ cardId: Number(testCardId), th: def.th }); - } - 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 || testForcedCard; - if (forced) { - console.log('[special-quiz] FORCE-spawn (debug)', mapId); - } else { - /* PRE-SCHEDULE ต่อคดี (แทนสุ่ม 1/3 ทุกเกม): ตอนเริ่มคดีสุ่มไว้แล้วว่าผู้ต้องสงสัยแต่ละคน (0-2) - การ์ดพิเศษจะออก "play ครั้งที่เท่าไหร่" (1-3, เล่นได้ 3 ครั้ง/คน). นับ eligible-play ต่อ suspect - แล้วออกการ์ดเฉพาะครั้งที่ตรงกับที่สุ่มไว้ → ได้การ์ดคนละ 1 ใบแน่นอน ตรงเวลาที่กำหนด ไม่ variance */ - const si = (typeof space.suspectPickIndex === 'number' && space.suspectPickIndex >= 0 && space.suspectPickIndex <= 2) ? space.suspectPickIndex : 0; - if (!space.suspectPlayCount || typeof space.suspectPlayCount !== 'object') space.suspectPlayCount = {}; - space.suspectPlayCount[si] = (space.suspectPlayCount[si] || 0) + 1; - const sched = space.specialQuizPlayBySuspect && space.specialQuizPlayBySuspect[si]; - if (!sched || space.suspectPlayCount[si] !== sched) { - console.log('[special-quiz] skip:schedule', mapId, 'suspect=' + si, 'play=' + space.suspectPlayCount[si], 'target=' + (sched || '?')); - return; - } - console.log('[special-quiz] SPAWN-scheduled', mapId, 'suspect=' + si, '@play=' + sched); - } - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md) { console.log('[special-quiz] skip:no-map', mapId); return; } - /* จำนวน "เลนที่มีผู้เล่นจริง" (คนจริง + บอท) — gauntlet เกิดไอคอนเฉพาะเลนที่ใช้จริง ไม่ใช่ทุกเลนที่ config */ - const occupiedLanes = (space.peers ? space.peers.size : 0) + effectiveBotSlotCount(space); - const tile = pickSpecialQuizSpawnTile(md, occupiedLanes); - 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); -} - -/** หน่วงไอคอน 2 วิ ค่อยโผล่ → emit ให้ทั้งห้อง + ตั้งเวลาให้ "หายไป" หลัง N วินาที (ถ้ายังไม่มีใครเก็บ) */ -const SPECIAL_QUIZ_ICON_DELAY_MS = 2000; -function scheduleSpecialQuizIconAppearAndExpiry(sid, space) { - const sq = space && space.specialQuiz; - if (!sq || sq.triggered || sq.done) return; - const tAppear = setTimeout(() => { - const sp = spaces.get(sid); - if (!sp || !sp.specialQuiz || sp.specialQuiz.triggered || sp.specialQuiz.done) return; - const cur = sp.specialQuiz; - const sec = readSpecialQuizIconExpireSec(); - cur.appeared = true; - cur.expiresAt = (sec > 0) ? (Date.now() + sec * 1000) : 0; - emitSpecialQuizIcon(sid, sp); - if (sec > 0) { - const tExpire = setTimeout(() => { - const sp2 = spaces.get(sid); - if (!sp2 || !sp2.specialQuiz || sp2.specialQuiz.triggered || sp2.specialQuiz.done) return; - sp2.specialQuiz = null; - io.to(sid).emit('special-quiz-icon-clear', {}); - console.log('[special-quiz] icon EXPIRED after', sec + 's'); - }, sec * 1000); - if (!Array.isArray(sp.specialQuizTimers)) sp.specialQuizTimers = []; - sp.specialQuizTimers.push(tExpire); - } - }, SPECIAL_QUIZ_ICON_DELAY_MS); - if (!Array.isArray(space.specialQuizTimers)) space.specialQuizTimers = []; - space.specialQuizTimers.push(tAppear); -} - -/** 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: 1, th: 'ทุกคนรับหลักฐานฟรี +1 ใบ' }, -}; - -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 || '', - }; -} - -/** เพดานเวลาที่ Time Dilation สะสมได้ต่อรอบ (กันการ์ดซ้อนยืดเวลาเกินเหตุ) */ -const TIME_DILATION_MAX_PENDING_SEC = 30; - -/** ใช้การ์ดที่ทำงาน "ทันที" (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 ขยายเอง - cap กันการ์ดซ้อนหลายใบจนเวลายืดเกินเหตุ */ - space.timeDilationPendingSec = Math.min(TIME_DILATION_MAX_PENDING_SEC, (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 — server แจกเองผ่าน game-award.php (มี secret + รู้ playerKey จาก join) กัน client ปลอม - noScore: ไม่ให้เหรียญ Fund ไปปั่นคะแนน high-score (เป็นโชค ไม่ใช่ฝีมือ) */ - const coins = def.coins || 10; - const awards = [...space.peers.values()] - .filter((p) => p.playerKey) - .map((p) => ({ playerKey: p.playerKey, nickname: (p.nickname || '').trim() || 'ผู้เล่น', coins: coins, noScore: true })); - submitGameAwards(awards); - io.to(sid).emit('special-card-applied', { - card: specialCardClientPayload(card), - coins: coins, - coinsServerAwarded: true, - }); - consumePendingSpecialCard(space, card.cardId); - return; - } - /* การ์ดที่เหลือ (after_game / pre_trial / trial_vote / trial_revote / lobby_next) - — คงไว้ใน pendingSpecialCard เพื่อใช้ภายหลัง */ -} - -/* ===== คลังการ์ดพิเศษที่สะสมในเคสนี้ (ใช้ได้พร้อมกันทุกใบ ไม่ทับกัน) ===== */ -function dbgCards(space) { - return JSON.stringify((space && space.pendingSpecialCards || []).map((e) => e.cardId + (e.consumed ? 'x' : ''))); -} -function pendingSpecialCardList(space) { - if (!Array.isArray(space.pendingSpecialCards)) space.pendingSpecialCards = []; - return space.pendingSpecialCards; -} -/** เพิ่มการ์ดเข้าคลัง — กันได้การ์ดซ้ำในเกมเดียว (ถ้ามีใบเดิมยังไม่ได้ใช้ จะไม่เพิ่มซ้ำ) */ -function addPendingSpecialCard(space, card) { - if (!card || !card.cardId) return null; - const list = pendingSpecialCardList(space); - const cid = Number(card.cardId); - if (list.some((e) => Number(e.cardId) === cid && !e.consumed)) { console.log('[card-dbg] add skip dup card' + cid + ' list=' + dbgCards(space)); return null; } - const entry = { cardId: cid, card: card, consumed: false }; - list.push(entry); - console.log('[card-dbg] +card' + cid + ' list=' + dbgCards(space)); - return entry; -} -/** หาการ์ดในคลังตาม cardId ที่ยังไม่ถูกใช้ */ -function findPendingSpecialCard(space, cardId) { - const cid = Number(cardId); - return pendingSpecialCardList(space).find((e) => Number(e.cardId) === cid && !e.consumed) || null; -} -function consumePendingSpecialCard(space, cardId) { - const e = findPendingSpecialCard(space, cardId); - if (e) e.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; - console.log('[card-dbg] specialQuiz end: success=' + !!success + ' cardId=' + (sq.card && sq.card.cardId)); - if (success && sq.card && (sq.card.cardId || sq.card.imageUrl)) { - space.specialCardAwardedThisRun = sq.card; - cardOut = specialCardClientPayload(sq.card); - addPendingSpecialCard(space, sq.card); /* สะสมเข้าคลัง — ใช้ได้พร้อมกันทุกใบ ไม่ทับใบเดิม */ - applySpecialCardImmediate(sid, space, sq.card); - } - io.to(sid).emit('special-quiz-ended', { success: !!success, card: cardOut }); - /* รอ client กด «เล่นต่อ» ก่อนค่อยเล่นรอบ quiz ต่อ (กันเวลาเดินตอนค้างอ่านการ์ด) - — มี safety timeout กันค้างถ้าไม่มีใครกด */ - space.specialQuizAwaitContinue = true; - space.specialQuizContinueAcks = new Set(); /* รวบ ack «เล่นต่อ» จากผู้เล่นจริง — ครบทุกคนถึง resume พร้อมกัน */ - /* แจ้งจำนวนเริ่มต้น 0/N ให้ทุก client (โชว์บนปุ่ม) */ - io.to(sid).emit('special-quiz-continue-progress', { ready: 0, total: space.peers.size }); - if (space.specialQuizContinueTimer) clearTimeout(space.specialQuizContinueTimer); - space.specialQuizContinueTimer = setTimeout(() => { - space.specialQuizContinueTimer = null; - finishSpecialQuizContinue(sid, space); /* safety: ไม่ครบใน 32s → ไปต่อทั้งห้อง */ - }, 32000); -} - -/** นับ ack «เล่นต่อ» — ถ้าผู้เล่นจริงที่ยังอยู่กดครบทุกคน → resume พร้อมกัน (ไม่นับบอท) */ -function maybeFinishSpecialQuizContinue(sid, space) { - if (!space || !space.specialQuizAwaitContinue) return; - const acks = space.specialQuizContinueAcks || (space.specialQuizContinueAcks = new Set()); - const expected = [...space.peers.keys()]; /* ผู้เล่นจริงที่ยังอยู่ในห้อง (บอทไม่ต้องกด) */ - const ready = expected.filter((pid) => acks.has(pid)).length; - io.to(sid).emit('special-quiz-continue-progress', { ready, total: expected.length }); - if (expected.length === 0 || expected.every((pid) => acks.has(pid))) { - finishSpecialQuizContinue(sid, space); - } -} - -/** เล่นรอบ quiz ต่อหลังผู้เล่นกด «เล่นต่อ» (เรียกครั้งเดียว / หรือ safety timeout) */ -function finishSpecialQuizContinue(sid, space) { - if (!space || !space.specialQuizAwaitContinue) return; - space.specialQuizAwaitContinue = false; - space.specialQuizContinueAcks = null; - if (space.specialQuizContinueTimer) { - clearTimeout(space.specialQuizContinueTimer); - space.specialQuizContinueTimer = null; - } - /* แจ้งทุก client ให้ปลด freeze + ปิด overlay พร้อมกัน (กดครบ หรือ safety timeout) */ - io.to(sid).emit('special-quiz-resume-all', {}); - resumeQuizAfterSpecialQuiz(sid, space); -} - -/* เกมหยุดชั่วคราวระหว่างคำถามพิเศษ (กำลังตอบ หรือรอกด «เล่นต่อ») - ใช้ร่วมกันทุก minigame ฝั่ง server — เดิมมีแต่ gauntlet เช็ค ทำให้ MG6/MG7 บอทยังเดิน/ยิงตอน pause */ -function isSpecialQuizPausedServer(space) { - if (!space) return false; - return !!(space.specialQuizAwaitContinue || (space.specialQuiz && space.specialQuiz.triggered && !space.specialQuiz.done)); -} - -/* setTimeout ที่ "หยุดนับ" ระหว่างคำถามพิเศษ (special quiz) — ใช้กับรอบ quiz(MG1)/quiz_carry(MG4) ที่ขับด้วย - setTimeout (ไม่มี tick loop ให้ extend endsAt เหมือน MG2/6/7). Poll: ถ้ากำลัง pause → ดัน deadline ตาม - เวลาที่ freeze (คงเวลาที่เหลือ) แล้ว re-poll; ถ้าถึงเวลาแล้ว → ยิง cb. เก็บ timer ใน space.quizTimers */ -function pausableSpecialQuizTimeout(sid, space, cb, ms) { - const d = { at: Date.now() + Math.max(0, ms | 0), last: Date.now() }; - const push = (t) => { const sp = spaces.get(sid); if (sp && Array.isArray(sp.quizTimers)) sp.quizTimers.push(t); }; - const step = () => { - const sp = spaces.get(sid); - if (!sp) return; - const now = Date.now(); - if (isSpecialQuizPausedServer(sp)) d.at += (now - d.last); /* freeze → เลื่อน deadline = คงเวลาที่เหลือ */ - d.last = now; - if (now >= d.at) { try { cb(); } catch (e) { /* ignore */ } return; } - push(setTimeout(step, Math.max(50, Math.min(300, d.at - now)))); - }; - push(setTimeout(step, Math.max(50, Math.min(300, ms | 0)))); -} - -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([ - 'topic1', 'topic2', 'topic3', 'topic4', 'topic5', - 'topic6', 'topic7', 'topic8', 'topic9', 'topic10', -]); - -/** คำถาม 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 = 'topic1'; - } - 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, - /* [2026-07-21] ไอคอนผู้ต้องสงสัย (สี่เหลี่ยมเล็ก) — ใช้เฉพาะหัวหน้าไต่สวน es3 เท่านั้น. - default = รูปเต็มตัว (suspects) → ถ้าไม่ได้ตั้งค่า พฤติกรรมเหมือนเดิมทุกประการ */ - icons: suspects.slice(), - }; -} - -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; -} - -/** [2026-07-21] ข้อความในหน้ารายละเอียดคดี (กรอกจาก Admin) — ตัดแท็ก/อักขระอันตราย + จำกัดความยาว - ไม่ใช่ URL จึงใช้ตัวนี้แทน sanitizeCaseMediaUrl */ -function sanitizeCaseText(v, max) { - if (typeof v !== 'string') return ''; - return v.replace(/[<>]/g, '').replace(/\r/g, '').trim().slice(0, max || 400); -} -function sanitizeCaseTextList(arr) { - const src = Array.isArray(arr) ? arr : []; - return [0, 1, 2].map((i) => sanitizeCaseText(src[i], 240)); -} - -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), - /* [2026-07-21] ไอคอน es3 — fallback เป็น suspects ที่ผ่าน sanitize แล้ว (ไม่ใช่ default ดิบ) - เพื่อให้ "ไม่ตั้ง icons" = ใช้รูปเต็มตัวชุดเดียวกับที่ตั้งไว้จริง */ - icons: sanitizeCaseMediaUrlList(c.icons, sanitizeCaseMediaUrlList(c.suspects, d.suspects)), - /* [2026-07-21] หน้ารายละเอียดแบบใหม่ (ประกอบเอง): ตั้ง pic แล้วหน้าจะสลับไปโหมดใหม่อัตโนมัติ - ยังไม่ตั้ง (ค่าว่าง) = ใช้รูปเดียวเดิม detail (case-detail-N.png) ต่อไป */ - pic: sanitizeCaseMediaUrl(c.pic, ''), - txtCharge: sanitizeCaseText(c.txtCharge, 300), - txtPlace: sanitizeCaseText(c.txtPlace, 300), - txtVictim: sanitizeCaseText(c.txtVictim, 300), - txtSuspects: sanitizeCaseTextList(c.txtSuspects), - txtNote: sanitizeCaseText(c.txtNote, 1500), - }; - } - 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; - /* ทุกเกม: เอาคะแนนของผู้เล่นทุกคนมาเฉลี่ยเป็น % แล้วแปลงเป็นเกรด A(≥80)/B(≥60)/C - quiz ใช้ % สัมบูรณ์ (ถูก/เต็ม), เกมอื่น normalize เทียบคนคะแนนสูงสุดในห้องแล้วเฉลี่ย */ - 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); - } - } - /* Last Light (crown gauntlet): ใช้เกรดเดียวกับ mission payload (รวมบอท) — แหล่งเดียวกับที่แสดง - F (ไม่มีผู้รอด) ไม่มี tier ในหลักฐาน → map เป็น C */ - if (gameType === 'gauntlet' && isGauntletCrownHeistMapBySpace(space) && space.lastCrownMissionGrade) { - const g = space.lastCrownMissionGrade; - return (g === 'A' || g === 'B') ? g : 'C'; - } - const peers = [...space.peers.values()]; - if (!peers.length) return 'C'; - const raw = peers.map((p) => evidencePlayerRawScore(space, p, gameType)); - let avgPct; - if (gameType === 'balloon_boss') { - /* MG7: เอาคะแนนทุกคนมาเฉลี่ย = ระดับความสำเร็จ (0-100) โดยตรง — ไม่ normalize เทียบคนคะแนนสูงสุด - (เดิม normalize ทำให้คะแนนต่ำ เช่น 15 เฟ้อเป็นเกรดสูง → ได้การ์ดชี้คนร้ายผิด) */ - const avgRaw = raw.reduce((s, v) => s + v, 0) / raw.length; - avgPct = Math.min(100, Math.max(0, avgRaw)); - } else { - const max = Math.max(1, ...raw); - avgPct = raw.reduce((s, v) => s + (v / max) * 100, 0) / raw.length; - } - return evidenceGradeFromPct(avgPct); - } catch (e) { return 'C'; } -} - -/* ===== แจกเหรียญตามอันดับ + คะแนนสะสม (high score) แบบ server-authoritative ===== */ -const GAME_AWARD_URL = process.env.GAME_AWARD_URL || 'https://srv1361159.hstgr.cloud/Admin/api/game-award.php'; -const ACHIEVEMENTS_URL = process.env.ACHIEVEMENTS_URL || GAME_AWARD_URL.replace('game-award.php', 'achievements.php'); -const GAME_AWARD_SECRET_PATH = path.join(__dirname, '..', 'Admin', 'private', 'game-award-secret.txt'); -let __gameAwardSecret = null; -function gameAwardSecret() { - if (__gameAwardSecret != null) return __gameAwardSecret; - try { __gameAwardSecret = String(fs.readFileSync(GAME_AWARD_SECRET_PATH, 'utf8')).trim(); } - catch (e) { __gameAwardSecret = ''; } - return __gameAwardSecret; -} -/* เหรียญตามอันดับผู้รอดชีวิต: อันดับ 1→10, 2→8, 3→6, 4→4, 5→2, 6+→0 */ -const MINIGAME_RANK_COINS = [10, 8, 6, 4, 2, 0]; - -/* เพดานคะแนนที่ client รายงานเองได้ (stack/quiz_carry) — sanity cap กัน overflow/ค่าขยะ/spoof สูงเกินจริง - ค่าจริงต่อรอบอยู่หลักร้อย-พัน เพดานนี้กว้างพอไม่กระทบเล่นจริง (ไม่ใช่ anti-cheat สมบูรณ์ — ดูคอมเมนต์ที่ handler) */ -const REPORTED_MINI_SCORE_MAX = 50000; - -/* คะแนน + สถานะรอด/ตาย ของผู้เล่นต่อ gameType ตอนจบมินิเกม */ -function minigamePlayerScoreSurvived(space, p, gameType) { - /* Card 3 Ban — ผู้ถูกแบนเข้ามาดูเฉยๆ ไม่ได้เล่น → ไม่นับเป็นผู้รอด/ไม่ได้คะแนน */ - if (p && p.bannedSpectator) return { score: 0, survived: false }; - switch (gameType) { - case 'quiz': { - const st = space.quizSession && space.quizSession.players ? space.quizSession.players[p.id] : null; - return { score: st ? Math.max(0, Number(st.score) | 0) : 0, survived: st ? !st.eliminated : true }; - } - case 'quiz_carry': { - /* MG4 server-auth: คะแนนจาก session (server เป็นเจ้าของ) — ทุกคนถือว่ารอด (ไม่มีตกรอบ) */ - const sc = space.quizCarrySession && space.quizCarrySession.players ? space.quizCarrySession.players[p.id] : null; - if (sc) return { score: Math.max(0, Number(sc.score) | 0), survived: true }; - return { score: Math.max(0, p.reportedMiniScore | 0), survived: true }; - } - case 'gauntlet': - return { score: Math.max(0, p.gauntletScore | 0), survived: !p.gauntletEliminated }; - case 'jump_survive': { - /* MG5 server-auth: ใช้สถานะตายจาก session (server เป็นเจ้าของ) ถ้ามี — ไม่งั้น fallback ค่าที่ client รายงานตอนจบ */ - const sj = space.jumpSurviveSession; - if (sj && sj.players && Object.prototype.hasOwnProperty.call(sj.players, p.id)) { - const dead = !!(sj.players[p.id] && sj.players[p.id].eliminated); - return { score: dead ? 0 : 100, survived: !dead }; - } - return { score: Math.max(0, p.reportedMiniScore | 0), survived: p.reportedMiniSurvived !== false }; - } - case 'space_shooter': - return { score: Math.max(0, p.spaceShooterScore | 0), survived: true }; - case 'balloon_boss': - return { score: Math.max(0, p.balloonBossScore | 0), survived: !p.balloonBossEliminated }; - default: /* stack, quiz_carry — client รายงานคะแนน/รอดเอง ตอนจบ */ - return { score: Math.max(0, p.reportedMiniScore | 0), survived: p.reportedMiniSurvived !== false }; - } -} - -/* ยิงไป PHP (มี secret) เพื่อบวกเหรียญ+คะแนนเข้าบัญชีผู้เล่นจริง (client ปลอมไม่ได้) */ -function submitGameAwards(awards) { - const secret = gameAwardSecret(); - if (!secret || !Array.isArray(awards) || !awards.length) return; - try { - fetch(GAME_AWARD_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ secret, awards }), - }).then((r) => r.json()).then((d) => { - if (!d || !d.ok) console.error('[game-award] failed', d); - }).catch((e) => console.error('[game-award] error', e && e.message)); - } catch (e) { console.error('[game-award] throw', e && e.message); } -} - -/* บวก progress achievement (server-authoritative ผ่าน achievements.php — secret เดียวกับ game-award) - items: [{ playerKey, id, inc }] — หนึ่ง POST ต่อรายการ (achievements.php รับทีละ id) */ -function submitAchievementProgress(items) { - const secret = gameAwardSecret(); - if (!secret || !Array.isArray(items) || !items.length) return; - items.forEach((it) => { - if (!it || !it.playerKey || !it.id) return; - try { - fetch(ACHIEVEMENTS_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action: 'progress', secret, playerKey: it.playerKey, id: it.id, inc: it.inc || 1 }), - }).then((r) => r.json()).then((d) => { - if (!d || !d.ok) console.error('[achv] failed', it.id, d); - }).catch((e) => console.error('[achv] error', e && e.message)); - } catch (e) { console.error('[achv] throw', e && e.message); } - }); -} - -/* streak achievements (a6/d4/e1) — POST action=streak {playerKey,id,event:'hit'|'miss'} - PHP นับต่อ/รีเซ็ตเอง + set achievement = best streak (ไม่ลดลง) */ -function submitAchievementStreak(items) { - const secret = gameAwardSecret(); - if (!secret || !Array.isArray(items) || !items.length) return; - items.forEach((it) => { - if (!it || !it.playerKey || !it.id || (it.event !== 'hit' && it.event !== 'miss')) return; - try { - fetch(ACHIEVEMENTS_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action: 'streak', secret, playerKey: it.playerKey, id: it.id, event: it.event }), - }).then((r) => r.json()).then((d) => { - if (!d || !d.ok) console.error('[achv] streak failed', it.id, d); - }).catch((e) => console.error('[achv] streak error', e && e.message)); - } catch (e) { console.error('[achv] streak throw', e && e.message); } - }); -} - -/* แจกเหรียญตามอันดับคะแนน (เฉพาะผู้รอดชีวิต ผู้ตาย = 0) + คะแนนสะสมเข้า leaderboard — ครั้งเดียวต่อรอบ */ -function awardMinigameRankCoins(sid, space) { - if (!space || space.coinAwardDoneForRun) return; - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - const gameType = md && md.gameType; - if (!gameType || gameType === 'zep' || gameType === 'quiz_battle' || serverMapIsPostCaseLobbyB(space)) return; - const peers = [...space.peers.values()]; - if (!peers.length) return; - space.coinAwardDoneForRun = true; - - const rows = peers.map((p) => { - const ss = minigamePlayerScoreSurvived(space, p, gameType); - return { id: p.id, playerKey: p.playerKey || '', nickname: (p.nickname || '').trim() || 'ผู้เล่น', score: ss.score, survived: !!ss.survived, coins: 0 }; - }); - /* ผู้รอดชีวิตเรียงคะแนนมาก→น้อย แล้วให้เหรียญตามอันดับ */ - const survivors = rows.filter((r) => r.survived).sort((a, b) => b.score - a.score); - survivors.forEach((r, i) => { r.coins = i < MINIGAME_RANK_COINS.length ? MINIGAME_RANK_COINS[i] : 0; }); - - const caseId = String(space.caseId || space.detectiveLobbyCaseId || ''); - const awards = rows.filter((r) => r.coins > 0 && r.playerKey).map((r) => ({ playerKey: r.playerKey, nickname: r.nickname, coins: r.coins, caseId: caseId })); - submitGameAwards(awards); - - /* Achievement: d1 Minigame Solver — รอดมินิเกมสำเร็จ (เฉพาะผู้รอดชีวิตที่เป็นผู้เล่นจริง) */ - submitAchievementProgress(survivors.filter((r) => r.playerKey).map((r) => ({ playerKey: r.playerKey, id: 'd1_minigame_solver', inc: 1 }))); - - io.to(sid).emit('minigame-coins', { - gameType, - coinsTable: MINIGAME_RANK_COINS, - results: rows.map((r) => ({ id: r.id, nickname: r.nickname, score: r.score, survived: r.survived, coins: r.coins })), - }); -} - -/* 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 = 6; -const GAUNTLET_DEFAULT_JUMP_HEIGHT_MULT = 0.52; /* MG2 ความสูงกระโดด = ×tile (จุดสูงสุดของส่วนโค้ง sin) — ปรับได้ใน Admin */ -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, - gauntletJumpHeightMult: GAUNTLET_DEFAULT_JUMP_HEIGHT_MULT, - gauntletTimeLimitSec: GAUNTLET_DEFAULT_TIME_LIMIT_SEC, - troublesomeOfferSec: 15, - ...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', - /** จำนวนลูกโป่งต่อคน (0 = ใช้ค่าจากแมป หรือ default 3) */ - balloonBossBalloonsPerPlayer: 0, - /** ดาเมจต่อการยิงโดนบอส (ลบเลือดบอส/นัด) — ตั้งใน Admin Minigame-7 · แยกจากคะแนน */ - balloonBossDamagePerHit: 10, - /** โหมดเทสต์: เกมบังคับต่อการ์ด 3 ใบ — ['','',''] = สุ่มหมด, แต่ละช่องใส่ key เพื่อเจาะจงใบนั้น */ - forcedMinigameKeys: ['', '', ''], - /** โหมดเทสต์: บังคับการ์ดพิเศษต่อเกม { mapId: cardId } */ - testSpecialCardByMap: {}, - /** โหมดเทสต์: บังคับเสนอบทตัวป่วน (ปกติเสนอเฉพาะเมื่อมีคนจริง ≥ 4) */ - troublesomeForceOffer: false, - troublesomeRollMaxSec: 8, - /** เวลาที่ไอคอนคำถามพิเศษอยู่บนฉากก่อนหายไป (วินาที) — 0 = ไม่หาย */ - specialQuizIconExpireSec: 10, - /** เวลาโหวตเลือกผู้เล่น (การ์ด Silence/Ban) วินาที */ - pickVoteSec: 25, - /** เวลาโหวตชี้ตัวคนร้าย (พิจารณาคดี) วินาที */ - trialVoteSec: 30, - /** เวลาโชว์ผลโหวตคนร้าย (ถูก/ผิด) ก่อนตัดไปหน้าผลเต็ม — มิลลิวินาที */ - trialResultRevealMs: 7000, - /** ปลายทางหลังจบคดี (โชว์ผล+หน้าความรู้แล้ว): 'mainlobby' = กลับ Main-Lobby (เดิม) · - 'lobbya' = รีเซ็ตห้องเดิมกลับ LobbyA ให้เล่นคดีใหม่ต่อได้เลย */ - postCaseDestination: 'mainlobby', - }; -} - -/** อ่านเวลา expiry ไอคอนคำถามพิเศษ (วินาที) จาก game-timing — clamp 0..120 */ -function readSpecialQuizIconExpireSec() { - const v = Math.floor(Number(runtimeGameTiming && runtimeGameTiming.specialQuizIconExpireSec)); - if (!Number.isFinite(v) || v < 0) return 10; - return Math.min(120, v); -} - -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 < 8; i++) { /* 8 slot ตาม lobby theme (#4) — เดิม 6 */ - 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(1, Math.min(40, Math.floor(v))); -} -function clampGauntletJumpHeightMult(n) { - const v = Number(n); - if (Number.isNaN(v)) return GAUNTLET_DEFAULT_JUMP_HEIGHT_MULT; - return Math.max(0.2, Math.min(1.5, v)); -} -/** เวลารับข้อเสนอ "ตัวป่วน" (วินาที) — admin ปรับได้ใน game-timing; 5–120, default 15 */ -function clampTroublesomeOfferSec(n) { - const v = Number(n); - if (!Number.isFinite(v)) return 15; - return Math.max(5, Math.min(120, Math.floor(v))); -} -/* เวลาสูงสุด (วินาที) ที่ระบบรอให้ทุกคนรายงานพร้อม ก่อน "บังคับสุ่ม" ผู้รับบทตัวป่วน (เดิม hardcode 8000ms) */ -function clampTroublesomeRollMaxSec(n) { - const v = Number(n); - if (!Number.isFinite(v)) return 8; - return Math.max(1, Math.min(30, 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))); -} - -/** แปลง "ภาพ/สี" ของ gauntlet จาก request — field ที่ **ไม่ได้ส่งมา** ต้องคงค่าเดิม (prev) ไว้เสมอ - ห้ามรีเซ็ตเป็น default. เดิมอ่านจาก d อย่างเดียว → PUT บางส่วน (เช่น {postCaseDestination}) - ทำให้ gauntletLaneImageUrls/laser ถูกล้างทั้งชุด = อุปสรรค MG2 "texture หาย" (fallback สี่เหลี่ยมทึบ). - ส่ง [] มาตรง ๆ ยังล้างได้ตามปกติ (hasOwnProperty = ตั้งใจล้าง). */ -function normalizeGauntletVisualsFromRequest(d, prev) { - const def = defaultGauntletVisuals(); - const p = (prev && typeof prev === 'object') ? prev : {}; - const keep = (k) => (p[k] !== undefined ? p[k] : def[k]); - const base = { - gauntletLaneImageUrls: Array.isArray(p.gauntletLaneImageUrls) ? p.gauntletLaneImageUrls.slice() : def.gauntletLaneImageUrls, - gauntletLaserTopUrl: keep('gauntletLaserTopUrl'), - gauntletLaserBottomUrl: keep('gauntletLaserBottomUrl'), - gauntletLaserLineUrl: keep('gauntletLaserLineUrl'), - gauntletLaserFillColor: keep('gauntletLaserFillColor'), - gauntletLaserStrokeColor: keep('gauntletLaserStrokeColor'), - gauntletLaserLineWidthPx: keep('gauntletLaserLineWidthPx'), - }; - if (!d || typeof d !== 'object') return base; - const has = (k) => Object.prototype.hasOwnProperty.call(d, k); - const out = { ...base }; - if (has('gauntletLaneImageUrls')) { - let laneArr = d.gauntletLaneImageUrls; - if (typeof laneArr === 'string') laneArr = laneArr.split(/[\n\r]+/); - if (!Array.isArray(laneArr)) laneArr = []; - const urls = []; - for (let i = 0; i < laneArr.length && urls.length < 24; i++) { - const u = sanitizeGauntletAssetUrl(laneArr[i]); - if (u) urls.push(u); - } - out.gauntletLaneImageUrls = urls; - } - if (has('gauntletLaserTopUrl')) out.gauntletLaserTopUrl = sanitizeGauntletAssetUrl(d.gauntletLaserTopUrl); - if (has('gauntletLaserBottomUrl')) out.gauntletLaserBottomUrl = sanitizeGauntletAssetUrl(d.gauntletLaserBottomUrl); - if (has('gauntletLaserLineUrl')) out.gauntletLaserLineUrl = sanitizeGauntletAssetUrl(d.gauntletLaserLineUrl); - if (has('gauntletLaserFillColor')) out.gauntletLaserFillColor = clampGauntletLaserColor(d.gauntletLaserFillColor, def.gauntletLaserFillColor); - if (has('gauntletLaserStrokeColor')) out.gauntletLaserStrokeColor = clampGauntletLaserColor(d.gauntletLaserStrokeColor, def.gauntletLaserStrokeColor); - if (has('gauntletLaserLineWidthPx')) out.gauntletLaserLineWidthPx = clampGauntletLaserLineWidthPx(d.gauntletLaserLineWidthPx); - return out; -} - -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), - gauntletJumpHeightMult: clampGauntletJumpHeightMult(j.gauntletJumpHeightMult), - gauntletTimeLimitSec: clampGauntletTimeLimitSec(j.gauntletTimeLimitSec), - troublesomeOfferSec: Object.prototype.hasOwnProperty.call(j, 'troublesomeOfferSec') ? clampTroublesomeOfferSec(j.troublesomeOfferSec) : 15, - ...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) - : '', - balloonBossBalloonsPerPlayer: Object.prototype.hasOwnProperty.call(j, 'balloonBossBalloonsPerPlayer') - ? Math.max(0, Math.min(12, Math.floor(Number(j.balloonBossBalloonsPerPlayer)) || 0)) - : 0, - balloonBossDamagePerHit: Object.prototype.hasOwnProperty.call(j, 'balloonBossDamagePerHit') - ? Math.max(1, Math.min(100, Math.floor(Number(j.balloonBossDamagePerHit)) || 10)) - : 10, - forcedMinigameKeys: normalizeForcedMinigameKeys( - j.forcedMinigameKeys != null ? j.forcedMinigameKeys : j.forcedMinigameKey, - ), - testSpecialCardByMap: normalizeTestSpecialCardByMap(j.testSpecialCardByMap), - troublesomeForceOffer: !!j.troublesomeForceOffer, - troublesomeRollMaxSec: clampTroublesomeRollMaxSec(j.troublesomeRollMaxSec), - specialQuizIconExpireSec: Object.prototype.hasOwnProperty.call(j, 'specialQuizIconExpireSec') - ? Math.max(0, Math.min(120, Math.floor(Number(j.specialQuizIconExpireSec)) || 0)) - : 10, - pickVoteSec: Object.prototype.hasOwnProperty.call(j, 'pickVoteSec') - ? Math.max(5, Math.min(120, Math.floor(Number(j.pickVoteSec)) || 25)) - : 25, - trialVoteSec: Object.prototype.hasOwnProperty.call(j, 'trialVoteSec') - ? Math.max(5, Math.min(180, Math.floor(Number(j.trialVoteSec)) || 30)) - : 30, - trialResultRevealMs: Object.prototype.hasOwnProperty.call(j, 'trialResultRevealMs') - ? Math.max(1500, Math.min(20000, Math.floor(Number(j.trialResultRevealMs)) || 7000)) - : 7000, - postCaseDestination: (j.postCaseDestination === 'lobbya') ? 'lobbya' : 'mainlobby', - }; - } - } catch (e) { console.error('loadGameTiming', e.message); } - return defaultGameTiming(); -} - -let runtimeGameTiming = loadGameTiming(); - -/** MG5 jump_survive: pool ของ pattern แพลตฟอร์ม (verify reachability offline แล้ว) — สุ่ม 1 อัน/เกม - เพื่อให้ layout หลากหลาย/เดายาก · pattern[0] = layout เดิม (baseline) · โหลดครั้งเดียวตอน boot */ -let jumpSurvivePatterns = []; -function loadJumpSurvivePatterns() { - try { - const p = path.join(__dirname, 'data', 'jump-survive-patterns.json'); - if (fs.existsSync(p)) { - const j = JSON.parse(fs.readFileSync(p, 'utf8')); - if (j && Array.isArray(j.patterns)) { - jumpSurvivePatterns = j.patterns.filter((x) => x && Array.isArray(x.platformArea) && Array.isArray(x.variantArea)); - } - } - } catch (e) { console.error('loadJumpSurvivePatterns', e.message); jumpSurvivePatterns = []; } -} -loadJumpSurvivePatterns(); -/** เลือก pattern สุ่ม → เก็บบน space (ใช้ทั้ง server bot physics + ส่งให้ client) · คงเดิมถ้าไม่มี pool */ -function pickJumpSurviveLayoutForSpace(space) { - if (!space || !jumpSurvivePatterns.length) return null; - const idx = Math.floor(Math.random() * jumpSurvivePatterns.length); - const pat = jumpSurvivePatterns[idx] || jumpSurvivePatterns[0]; - space.jsLayout = { idx, platformArea: pat.platformArea, variantArea: pat.variantArea }; - space._jsEffMd = null; /* invalidate effMd cache */ - return space.jsLayout; -} -/** md ที่ override platformArea/variantArea ด้วย pattern ที่เลือก (สร้าง 1 ครั้ง/เกม, cache บน space) - ไม่ mutate แมป share (หลายห้องเล่นพร้อมกันได้) */ -function jsMap(space) { - const base = (space && space.mapId && maps.get(space.mapId)) || (space && space.mapData) || null; - if (!base || !space || !space.jsLayout) return base; - if (space._jsEffMd && space._jsEffMd.__base === base) return space._jsEffMd; - space._jsEffMd = Object.assign(Object.create(base), { - __base: base, - jumpSurvivePlatformArea: space.jsLayout.platformArea, - jumpSurvivePlatformVariantArea: space.jsLayout.variantArea, - }); - return space._jsEffMd; -} - -/** จำนวนลูกโป่งต่อคน Mega Virus — แมปกำหนดก่อน, ไม่งั้นใช้ค่ากลางจาก admin, สุดท้าย default 3 (clamp 1-12) */ -function balloonBossBalloonsForMap(md) { - const fromMap = Math.floor(Number(md && md.balloonBossBalloonsPerPlayer)) || 0; - const fromCfg = Math.floor(Number(runtimeGameTiming && runtimeGameTiming.balloonBossBalloonsPerPlayer)) || 0; - return Math.max(1, Math.min(12, fromMap || fromCfg || 3)); -} - -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, prev); /* prev = คงค่าเดิมของ field ที่ไม่ได้ส่งมา */ - 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; - /* จำนวนลูกโป่งต่อคน (0 = ใช้ค่าจากแมป หรือ default 3) */ - const prevBbCount = Object.prototype.hasOwnProperty.call(prev, 'balloonBossBalloonsPerPlayer') - ? Math.max(0, Math.min(12, Math.floor(Number(prev.balloonBossBalloonsPerPlayer)) || 0)) - : 0; - const balloonBossBalloonsPerPlayer = Object.prototype.hasOwnProperty.call(d, 'balloonBossBalloonsPerPlayer') - ? Math.max(0, Math.min(12, Math.floor(Number(d.balloonBossBalloonsPerPlayer)) || 0)) - : prevBbCount; - /* ดาเมจต่อการยิงโดนบอส (แยกจากคะแนน) — Admin Minigame-7 */ - const prevBbDmg = Object.prototype.hasOwnProperty.call(prev, 'balloonBossDamagePerHit') - ? Math.max(1, Math.min(100, Math.floor(Number(prev.balloonBossDamagePerHit)) || 10)) - : 10; - const balloonBossDamagePerHit = Object.prototype.hasOwnProperty.call(d, 'balloonBossDamagePerHit') - ? Math.max(1, Math.min(100, Math.floor(Number(d.balloonBossDamagePerHit)) || 10)) - : prevBbDmg; - 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 = { - /* เช่นเดียวกับ vis: ถ้า request ไม่ส่ง field มา ให้คงค่าเดิม (prev) — ไม่ใช่รีเซ็ตเป็น default */ - gauntletTickMs: clampGauntletTickMs(Object.prototype.hasOwnProperty.call(d, 'gauntletTickMs') ? d.gauntletTickMs : prev.gauntletTickMs), - gauntletJumpTicks: clampGauntletJumpTicks(Object.prototype.hasOwnProperty.call(d, 'gauntletJumpTicks') ? d.gauntletJumpTicks : prev.gauntletJumpTicks), - gauntletJumpHeightMult: clampGauntletJumpHeightMult(Object.prototype.hasOwnProperty.call(d, 'gauntletJumpHeightMult') ? d.gauntletJumpHeightMult : prev.gauntletJumpHeightMult), - gauntletTimeLimitSec: clampGauntletTimeLimitSec(Object.prototype.hasOwnProperty.call(d, 'gauntletTimeLimitSec') ? d.gauntletTimeLimitSec : prev.gauntletTimeLimitSec), - troublesomeOfferSec: Object.prototype.hasOwnProperty.call(d, 'troublesomeOfferSec') ? clampTroublesomeOfferSec(d.troublesomeOfferSec) : ((runtimeGameTiming && runtimeGameTiming.troublesomeOfferSec) || 15), - ...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, - balloonBossBalloonsPerPlayer, - balloonBossDamagePerHit, - forcedMinigameKeys: Object.prototype.hasOwnProperty.call(d, 'forcedMinigameKeys') - ? normalizeForcedMinigameKeys(d.forcedMinigameKeys) - : (Object.prototype.hasOwnProperty.call(d, 'forcedMinigameKey') - ? normalizeForcedMinigameKeys(d.forcedMinigameKey) - : normalizeForcedMinigameKeys(prev.forcedMinigameKeys)), - testSpecialCardByMap: Object.prototype.hasOwnProperty.call(d, 'testSpecialCardByMap') - ? normalizeTestSpecialCardByMap(d.testSpecialCardByMap) - : normalizeTestSpecialCardByMap(prev.testSpecialCardByMap), - troublesomeForceOffer: Object.prototype.hasOwnProperty.call(d, 'troublesomeForceOffer') - ? !!d.troublesomeForceOffer - : !!prev.troublesomeForceOffer, - troublesomeRollMaxSec: Object.prototype.hasOwnProperty.call(d, 'troublesomeRollMaxSec') - ? clampTroublesomeRollMaxSec(d.troublesomeRollMaxSec) - : clampTroublesomeRollMaxSec(prev.troublesomeRollMaxSec), - specialQuizIconExpireSec: Object.prototype.hasOwnProperty.call(d, 'specialQuizIconExpireSec') - ? Math.max(0, Math.min(120, Math.floor(Number(d.specialQuizIconExpireSec)) || 0)) - : (prev.specialQuizIconExpireSec != null ? Math.max(0, Math.min(120, Math.floor(Number(prev.specialQuizIconExpireSec)) || 0)) : 10), - pickVoteSec: Object.prototype.hasOwnProperty.call(d, 'pickVoteSec') - ? Math.max(5, Math.min(120, Math.floor(Number(d.pickVoteSec)) || 25)) - : (prev.pickVoteSec != null ? Math.max(5, Math.min(120, Math.floor(Number(prev.pickVoteSec)) || 25)) : 25), - trialVoteSec: Object.prototype.hasOwnProperty.call(d, 'trialVoteSec') - ? Math.max(5, Math.min(180, Math.floor(Number(d.trialVoteSec)) || 30)) - : (prev.trialVoteSec != null ? Math.max(5, Math.min(180, Math.floor(Number(prev.trialVoteSec)) || 30)) : 30), - trialResultRevealMs: Object.prototype.hasOwnProperty.call(d, 'trialResultRevealMs') - ? Math.max(1500, Math.min(20000, Math.floor(Number(d.trialResultRevealMs)) || 7000)) - : (prev.trialResultRevealMs != null ? Math.max(1500, Math.min(20000, Math.floor(Number(prev.trialResultRevealMs)) || 7000)) : 7000), - postCaseDestination: Object.prototype.hasOwnProperty.call(d, 'postCaseDestination') - ? (d.postCaseDestination === 'lobbya' ? 'lobbya' : 'mainlobby') - : (prev.postCaseDestination === 'lobbya' ? 'lobbya' : 'mainlobby'), - /* DEV: เปิด HUD โหลด LobbyA (Iron Man) — admin toggle. preserve ค่าเดิมถ้า request ไม่ส่งมา */ - lobbyDebugHud: Object.prototype.hasOwnProperty.call(d, 'lobbyDebugHud') - ? !!d.lobbyDebugHud - : !!prev.lobbyDebugHud, - lobbyDebugHudNames: Object.prototype.hasOwnProperty.call(d, 'lobbyDebugHudNames') - ? (Array.isArray(d.lobbyDebugHudNames) ? d.lobbyDebugHudNames.slice(0, 30).map((s) => String(s).slice(0, 40)) : []) - : (Array.isArray(prev.lobbyDebugHudNames) ? prev.lobbyDebugHudNames : []), - }; - 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, - spawnJoinOrder: typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : null, - lobbyColorThemeIndex: p.lobbyColorThemeIndex || null, - lobbySkinToneIndex: p.lobbySkinToneIndex || null, - bannedSpectator: !!p.bannedSpectator, - nr: p.netReady !== false, /* net-ready: false = เพิ่ง join กำลังโหลด (client โชว์ badge) */ - }; - if (extraFields && extraFields.gauntlet) { - row.gauntletJumpTicks = p.gauntletJumpTicks || 0; - row.gauntletScore = p.gauntletScore || 0; - row.gauntletEliminated = !!p.gauntletEliminated; - } - return row; - }); -} - -/* payload ของ quiz-carry-lobby-sync — แนบ peers snapshot ไปด้วยเสมอ - → client (โดยเฉพาะ host) upsert others ที่ขาดหายให้ครบ กัน host ไม่เห็นตัวละคร client - เมื่อ join/reconnect ไม่ทันรอบ (race ตอนเปลี่ยนหน้า lobbyB→play.html) */ -/* true ถ้า peer นี้คือผู้ถูกแบนรอบนี้ (Card 3 Ban) — เข้ามาดูเฉยๆ ไม่ต้อง Ready และไม่นับใน gate เริ่มเกม - (เช็คทั้ง id เดิมตอนโหวต และ flag bannedSpectator ของ peer ที่ reconnect เข้า play.html) */ -/** Card 3 Ban — แปลง target ตอนโหวต (socket.id) เป็น playerKey = "ตัวตนถาวร" - socket.id เปลี่ยนทุกครั้งที่เปลี่ยนหน้า (LobbyB ⇄ play.html) → ผูกกับ id อย่างเดียวจะหาคนโดนแบน - ไม่เจอตอนเริ่มเกมจริง = ตัวละครโผล่ในเกม + ถูกนับใน gate ready จน host กด START ไม่ได้. - บอท (__lobby_bot_N) ไม่มี playerKey → คืน '' แล้วใช้ id เดิมต่อ (บอท id คงที่อยู่แล้ว) */ -function resolveBanTargetPlayerKey(space, targetId) { - if (!space || !targetId) return ''; - if (String(targetId).indexOf('__lobby_bot_') === 0) return ''; - const p = space.peers && space.peers.get(targetId); - const k = p && p.playerKey; - return (k && /^[a-zA-Z0-9_-]{8,128}$/.test(String(k))) ? String(k) : ''; -} - -function isBannedPeerForRun(space, id) { - if (!space) return false; - if (space.bannedThisRunPlayerId && id === space.bannedThisRunPlayerId) return true; - const pp = space.peers && space.peers.get(id); - if (pp && pp.bannedSpectator) return true; - /* เทียบด้วย playerKey — ครอบเคส socket.id เปลี่ยนหลัง reconnect/เปลี่ยนหน้า (id เดิมไม่มีทางแมตช์) */ - if (space.bannedThisRunPlayerKey && pp && pp.playerKey - && String(pp.playerKey) === String(space.bannedThisRunPlayerKey)) return true; - return false; -} - -/* สำเนา ready map โดยตัด id ผู้ถูกแบนออก — กัน client นับ banned เป็นผู้เล่นที่ต้องรอ Ready (host ค้าง N/N+1) */ -function readyMapWithoutBanned(space, srcMap) { - const out = {}; - if (srcMap && typeof srcMap === 'object') { - Object.keys(srcMap).forEach((id) => { - if (!isBannedPeerForRun(space, id)) out[id] = srcMap[id]; - }); - } - return out; -} - -function quizCarryLobbySyncPayload(space) { - return { - readyMap: readyMapWithoutBanned(space, space && space.quizCarryLobbyReady), - peers: buildPeersSnapForSpace(space), - expectedActive: (space && space.minigameExpectedActiveCount) || 0, - }; -} - -/* payload ของ gauntlet-crown-lobby-sync — ตัด banned ออกจาก readyMap เช่นกัน - แนบ peers snapshot ด้วย (เหมือน quiz_carry) → client/host เติม others ที่ขาด เห็นจำนวนผู้เล่นเท่ากัน */ -function crownLobbySyncPayload(space) { - const payload = { - readyMap: readyMapWithoutBanned(space, space && space.gauntletCrownLobbyReady), - expectedActive: (space && space.minigameExpectedActiveCount) || 0, - peers: buildPeersSnapForSpace(space), - }; - /* MG2: ส่งตำแหน่ง spawn ของบอทมาด้วยตอน pregame → client วางบอทที่จุดเกิด (เดิมไม่ส่ง = บอทค้าง (1,1) มุมบน) */ - const gr = space && space.gauntletRun; - if (gr && gr.bots && gr.bots.size) { - const bots = []; - gr.bots.forEach((b, id) => { - if (!b) return; - bots.push({ id, x: b.x, y: b.y, direction: b.dir || 'right', gauntletScore: b.gauntletScore || 0, gauntletEliminated: !!b.gauntletEliminated }); - }); - if (bots.length) payload.bots = bots; - } - return payload; -} - -/* เช็คว่าพร้อมเริ่มมินิเกม crown-lobby ไหม: ยังไม่เริ่ม + เข้าครบ + ทุกคน (ไม่นับ banned) กด Ready */ -function crownLobbyAllReadyToStart(space) { - const gr = space && space.gauntletRun; - if (!gr || !gr.crownRunHeld || gr.lobbyStarted) return false; - const rm = space.gauntletCrownLobbyReady || {}; - const humanIds = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)); - if (humanIds.length === 0) return false; - const expectedActive = space.minigameExpectedActiveCount || 0; - const enough = !(expectedActive > 0 && humanIds.length < expectedActive); - const allReady = humanIds.every((id) => rm[id]); - /* [ban-gate-dbg] วินิจฉัย "แบนแล้วเริ่มเกมไม่ได้" — log สถานะ gate เมื่อมีแบนในรอบ + ยังไม่ผ่าน (throttle 1.5s). - เผยว่า: banned ไม่ถูก flag? playerKey ซ้ำ (duplicate-MONE)? expectedActive ผิด? หรือมีคนยังไม่ ready */ - if (!(enough && allReady) && space.bannedThisRunPlayerId && (Date.now() - (space._banGateLogAt || 0) > 1500)) { - space._banGateLogAt = Date.now(); - const rows = [...space.peers.entries()].map(([id, p]) => String(id).slice(0, 6) + (isBannedPeerForRun(space, id) ? '=BAN' : '') + (rm[id] ? '=R' : '=-') + (p.bannedSpectator ? 'sp' : '') + '#' + String(p.playerKey || '').slice(0, 6)); - console.log('[ban-gate-dbg] enough=' + enough + ' allReady=' + allReady + ' expected=' + expectedActive + ' humans=' + humanIds.length + ' bannedRun=' + String(space.bannedThisRunPlayerId).slice(0, 10) + ' peers=[' + rows.join(' ') + ']'); - } - return enough && allReady; -} - -/* เริ่มมินิเกม crown-lobby (server เป็นคนสั่ง) — idempotent ด้วย gr.lobbyStarted (รีเซ็ตเองทุกมินิเกมเพราะ gr สร้างใหม่) - ใช้ทั้ง host กด START และ auto-start เมื่อทุกคน ready (Track B: MG1 ไม่ต้องพึ่ง host) */ -function performCrownLobbyStart(sid, space) { - const gr = space && space.gauntletRun; - if (!gr || !gr.crownRunHeld || gr.lobbyStarted) return false; - gr.lobbyStarted = true; - const liveBeginsAt = Date.now() + LIVE_COUNTDOWN_MS; - space.missionLiveBeginsAt = liveBeginsAt; - if (isQuizQuestionMissionShellSpace(space)) { - /* quiz: เลื่อน server timer ไปเริ่มที่ liveBeginsAt (ตรงกับตอน client จบ 3-2-1) */ - setTimeout(() => { - const sp = spaces.get(sid); - if (!sp) return; - const md2 = maps.get(sp.mapId) || sp.mapData; - if (md2 && md2.gameType === 'quiz') { - clearSpaceQuizTimers(sp); - startQuizGame(sid, sp, md2); - } - }, LIVE_COUNTDOWN_MS); - } - if (isStackTowerMissionShellSpace(space)) { - /* MG3 stack: เริ่ม server engine ตอน client จบ countdown (ตรง liveBeginsAt) */ - setTimeout(() => { - const sp = spaces.get(sid); - if (!sp) return; - const md2 = maps.get(sp.mapId) || sp.mapData; - if (md2 && md2.gameType === 'stack') startStackTicker(sid, sp); - }, LIVE_COUNTDOWN_MS); - } - if (isJumpSurviveMissionShellSpace(space)) { - /* MG5 jump_survive: เริ่ม server engine (บอท+รอบ) ตอน client จบ countdown (ตรง liveBeginsAt) */ - setTimeout(() => { - const sp = spaces.get(sid); - if (!sp) return; - const md2 = maps.get(sp.mapId) || sp.mapData; - if (md2 && md2.gameType === 'jump_survive') startJumpSurviveGame(sid, sp); - }, LIVE_COUNTDOWN_MS); - } - if (isSpaceShooterMissionShellSpace(space)) { - /* MG6 space_shooter: เริ่ม server engine (บอท+รอบ) ตอน client จบ countdown (ตรง liveBeginsAt) */ - setTimeout(() => { - const sp = spaces.get(sid); - if (!sp) return; - const md2 = maps.get(sp.mapId) || sp.mapData; - if (md2 && md2.gameType === 'space_shooter') startSpaceShooterGame(sid, sp); - }, LIVE_COUNTDOWN_MS); - } - if (isBalloonBossMissionShellSpace(space)) { - /* MG7 balloon_boss: เริ่ม server engine (บอท+รอบ+HP บอส) ตอน client จบ countdown */ - setTimeout(() => { - const sp = spaces.get(sid); - if (!sp) return; - const md2 = maps.get(sp.mapId) || sp.mapData; - if (md2 && md2.gameType === 'balloon_boss') startBalloonBossGame(sid, sp); - }, LIVE_COUNTDOWN_MS); - } - io.to(sid).emit('gauntlet-crown-lobby-started', { liveBeginsAt }); - return true; -} - -/** สุ่ม 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; -} - -/* key ผู้เล่นในคดี — ใช้ playerKey (คงที่ต่อ browser ข้าม reconnect) กันเคส "2 คนตั้งชื่อซ้ำ = ถูกนับเป็นคนเดียว" - fallback เป็นชื่อ (n:) ถ้า peer ไม่มี playerKey ที่ valid */ -function detectiveParticipantKey(p) { - if (!p) return ''; - if (typeof p.playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(p.playerKey)) return 'k:' + p.playerKey; - const n = normalizeLobbyNickname(p.nickname || ''); - return n ? 'n:' + n : ''; -} - -/* นับผู้เล่นในคดี (caseParticipantKeys = playerKey) ที่กลับเข้า LobbyB + โหลดเสร็จ เทียบกับทั้งหมด - ใช้ตรวจว่าครบหรือยังก่อนให้ host เริ่มมินิเกมรอบถัดไป */ -function detectiveLobbyBMissingParticipants(space) { - const roster = space && space.caseParticipantKeys; - if (!roster || roster.size === 0) return { missing: 0, present: 0, total: 0 }; - /* present = participantKey ที่ "มี peer" และ "โหลด LobbyB เสร็จจริง" (lobbyb-loaded) - — กันเคส peer มีอยู่ (socket re-join ช่วงเปลี่ยนหน้า) แต่ client ยังค้างฉากเกมเก่า = นับครบผิด - fallback: ก่อนมีใครส่ง lobbyb-loaded (loadedSet ว่าง) ใช้ peer-count (กัน deadlock ช่วงแรกสุด) */ - const loadedSet = space.lobbyBLoadedKeys; - const useLoaded = !!(loadedSet && loadedSet.size > 0); - const present = new Set(); - space.peers.forEach((p) => { - const k = detectiveParticipantKey(p); - if (!k || !roster.has(k)) return; - if (useLoaded) { if (loadedSet.has(k)) present.add(k); } - else present.add(k); - }); - const total = roster.size; - return { missing: Math.max(0, total - present.size), present: present.size, total }; -} - -/* แจ้งทุกคนใน LobbyB ว่ามีผู้เล่นกลับเข้ามาแล้วกี่คน/ทั้งหมดกี่คน → host ใช้เปิด/ปิดปุ่มเริ่ม + แสดงสถานะรอ */ -function emitDetectiveLobbyBPresence(sid, space) { - if (!sid || !space) return; - if (!serverMapIsPostCaseLobbyB(space)) return; - const info = detectiveLobbyBMissingParticipants(space); - if (info.total <= 0) return; - io.to(sid).emit('detective-lobbyb-presence', { - present: info.present, - total: info.total, - allHere: info.missing === 0, - }); - /* มีการ์ด Ban ค้าง → เปิดโหวตเมื่อ "ทุกคนกลับเข้า LobbyB ครบ" เท่านั้น (เช็คทุกครั้งที่ presence เปลี่ยน) */ - try { maybeOpenPendingBanVote(sid, space); } catch (e) { /* ignore */ } -} - -/* [2026-07-19] ตัดผู้เล่นออกจาก roster คดี "ทันที" + re-emit presence — ใช้ตอน host kick / คนหลุดระหว่างเฟส - เลือกผู้ต้องสงสัย. ต้นตอบั๊ก: TC144 gate (suspect-pick-start) ใช้ total = caseParticipantKeys.size ที่ล็อก - ตอนเริ่มคดี และลดได้ทางเดียว = grace-prune 22วิ ที่ต้องมี playerKey → host kick (ไม่ prune เลย) และ - disconnect ที่ playerKey ว่าง (grace ข้าม) ทำให้ gate ค้าง "รอผู้เล่น" ถาวร host กด Start ไม่ได้. - ตัดทั้ง 'k:'+playerKey และ 'n:'+nick (เหมือน grace-prune ที่ server.js:13186-13195) เพื่อครอบ roster - ทั้งสองแบบ. emitDetectiveLobbyBPresence จะ no-op เองถ้าไม่ได้อยู่ LobbyB (กันเรียกผิดจังหวะ) */ -function pruneDetectiveParticipantNow(sid, space, playerKey, nickname) { - if (!space) return; - const nick = normalizeLobbyNickname(nickname || ''); - try { - if (space.caseParticipantKeys) { - if (playerKey && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) space.caseParticipantKeys.delete('k:' + playerKey); - if (nick) space.caseParticipantKeys.delete('n:' + nick); - } - if (space.caseParticipantNicknames && nick) space.caseParticipantNicknames.delete(nick); - } catch (e) { /* ignore */ } - emitDetectiveLobbyBPresence(sid, space); -} - -/** โหวต Ban เปิดเมื่อผู้เล่นในคดี "กลับเข้า LobbyB ครบทุกคน" เท่านั้น — กัน candidate ไม่ครบ + timer เริ่ม - * ก่อนคนเข้าครบ ("เล่น 4 คนแต่โหวตได้ 3"). safety: รอเกิน BAN_VOTE_MAX_WAIT_MS (บางคนไม่กลับ) → เปิดเลย กัน deadlock */ -const BAN_VOTE_MAX_WAIT_MS = 30000; -function maybeOpenPendingBanVote(sid, space) { - if (!space || !space.pendingBanVoteCard || space.pickVote) return; - if (!serverMapIsPostCaseLobbyB(space)) return; - const info = detectiveLobbyBMissingParticipants(space); - const allHere = info.total > 0 && info.missing === 0; - /* socket ต้อง "นิ่ง" — ไม่มี rejoin ภายใน 2s ล่าสุด. กันเปิดโหวตตอน LobbyB transition ยัง reconnect อยู่: - ถ้าเปิดตอน socket ยังเปลี่ยน → candidate/vote ผูก socket id เก่า + จอโหวตเด้งซ้ำ (re-broadcast) → "คนค้างขยับไม่ได้" */ - const socketsStable = (Date.now() - (space._lastLobbyBRejoinAt || 0)) > 2000; - if (!space._banVoteWaitSince) space._banVoteWaitSince = Date.now(); - const waited = Date.now() - space._banVoteWaitSince; - if ((!allHere || !socketsStable) && waited < BAN_VOTE_MAX_WAIT_MS) { - /* ยังไม่ครบ/ยังไม่นิ่ง + ยังไม่หมดเวลารอ → รอ แล้ว re-check อีก 1s */ - if (!space._banVoteWaitTimer) { - space._banVoteWaitTimer = setTimeout(() => { - const sp = spaces.get(sid); - if (sp) { sp._banVoteWaitTimer = null; maybeOpenPendingBanVote(sid, sp); } - }, 1000); - } - return; - } - if (space._banVoteWaitTimer) { clearTimeout(space._banVoteWaitTimer); space._banVoteWaitTimer = null; } - space._banVoteWaitSince = 0; - const banCard = space.pendingBanVoteCard; - space.pendingBanVoteCard = null; - console.log('[ban-debug] OPEN ban vote (peers=', space.peers.size, 'present=' + info.present + '/' + info.total + (allHere ? '' : ' TIMEOUT') + ')'); - io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(banCard), context: 'ban' }); - startPlayerPickVote(sid, space, 'ban', (targetId) => { - const sp2 = spaces.get(sid); - console.log('[ban-debug] ban vote RESOLVED target=', targetId); - if (sp2) { - sp2.bannedPlayerId = targetId || null; - sp2.bannedPlayerKey = resolveBanTargetPlayerKey(sp2, targetId); /* ตัวตนถาวร — id จะเปลี่ยนก่อนเกมเริ่ม */ - console.log('[ban-debug] ban target key=', sp2.bannedPlayerKey || '(none/bot)'); - } - }); -} - -/* TC166 safety: กัน lobbyb-loaded gate deadlock — ถ้า 25s แล้วยังไม่ครบ ให้ force นับ present จาก peer ที่อยู่จริงใน LobbyB */ -function armLobbyBLoadedSafety(sid, space) { - if (!sid || !space) return; - if (space.lobbyBLoadedTimer) { clearTimeout(space.lobbyBLoadedTimer); space.lobbyBLoadedTimer = null; } - space.lobbyBLoadedTimer = setTimeout(() => { - const sp = spaces.get(sid); - if (!sp || !serverMapIsPostCaseLobbyB(sp)) return; - const roster = sp.caseParticipantKeys; - if (!roster || roster.size === 0) return; - if (!sp.lobbyBLoadedKeys) sp.lobbyBLoadedKeys = new Set(); - let added = 0; - sp.peers.forEach((p) => { - const k = detectiveParticipantKey(p); - if (k && roster.has(k) && !sp.lobbyBLoadedKeys.has(k)) { sp.lobbyBLoadedKeys.add(k); added++; } - }); - if (added) { - try { glog(sid, sp, 'lobbyb-gate', '[TC166] safety 25s → force นับ present จาก peer ที่อยู่จริง (+' + added + ')'); } catch (e) { /* ignore */ } - try { emitDetectiveLobbyBPresence(sid, sp); } catch (e) { /* ignore */ } - } - }, 25000); -} - -/* TC156 story-gate: นับผู้เล่นในคดี "ที่อ่าน story (cutscene) จบแล้ว" เทียบกับคนที่อยู่ในห้องตอนนี้ - allRead = ทุก roster-nick ที่ยัง present ได้ส่ง story-read-done แล้ว (คนที่ left ไม่นับ — presence gate คุมแยก) */ -function detectiveStoryReadStatus(space) { - const roster = space && space.caseParticipantKeys; - if (!roster || roster.size === 0) return { present: 0, total: 0, allRead: true }; - const read = space.storyReadKeys || new Set(); - const presentRosterKeys = new Set(); - space.peers.forEach((p) => { - const k = detectiveParticipantKey(p); - if (k && roster.has(k)) presentRosterKeys.add(k); - }); - let readCount = 0; - presentRosterKeys.forEach((k) => { if (read.has(k)) readCount++; }); - const allRead = !!space.storyReadComplete || (presentRosterKeys.size > 0 && readCount >= presentRosterKeys.size); - return { present: readCount, total: presentRosterKeys.size, allRead }; -} -function emitDetectiveStoryReadPresence(sid, space) { - if (!sid || !space) return; - if (!serverMapIsPostCaseLobbyB(space)) return; - const st = detectiveStoryReadStatus(space); - io.to(sid).emit('detective-story-read-presence', { present: st.present, total: st.total, allRead: st.allRead }); -} - -function assignDetectiveSuspectCardMinigames(space) { - const available = DETECTIVE_MINIGAME_POOL.filter((e) => maps.has(e.mapId)); - /** โหมดเทสต์ (admin game-timing): เลือกเกมต่อการ์ด 3 ใบ — ช่องที่ตั้ง key = เจาะจง, ช่องว่าง = สุ่ม */ - const forcedKeys = normalizeForcedMinigameKeys(runtimeGameTiming && runtimeGameTiming.forcedMinigameKeys); - const randomPool = shuffleQuizQuestions(available.length ? available : [{ key: 'quiz', mapId: SUSPECT_INVESTIGATION_QUIZ_MAP_ID, labelTh: 'QuestionGame' }]); - let ri = 0; - const pick3 = []; - for (let i = 0; i < 3; i++) { - const fk = forcedKeys[i]; - let entry = fk ? available.find((e) => e.key === fk) : null; - if (!entry) { entry = randomPool[ri % randomPool.length]; ri += 1; } - pick3.push(entry); - } - space.suspectCardMinigames = pick3.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) || '', - }; - }); - // ชุดการ์ดใหม่ = ล้างหลักฐานที่ผู้เล่นเก็บได้ (รวม evidenceByNick fallback — กันหลักฐานคดีเก่า leak ข้ามคดี) - space.playerEvidence = {}; - space.evidenceByNick = {}; - space.caseMinigameSuspects = {}; /* [achv b2] มินิเกมที่รันในคดีนี้ (suspectIndex) — เช็ค "เล่นครบทุกรอบ" */ - space.caseCardTargetedKeys = {}; /* [achv d2] playerKey ที่โดนโหวตการ์ด Silence/Ban ในคดีนี้ */ - /* การ์ดพิเศษ — PRE-SCHEDULE ต่อคดี: สุ่มไว้เลยว่าผู้ต้องสงสัยแต่ละคน (0-2) การ์ดจะออก play ครั้งที่เท่าไหร่ - (1-3; ผู้ต้องสงสัย 1 คนเล่นได้ 3 ครั้ง) → ได้การ์ดคนละ 1 ใบแน่นอน แทนสุ่ม 1/3 ทุกเกม (กัน variance) */ - space.specialQuizPlayBySuspect = { 0: 1 + Math.floor(Math.random() * 3), 1: 1 + Math.floor(Math.random() * 3), 2: 1 + Math.floor(Math.random() * 3) }; - space.suspectPlayCount = { 0: 0, 1: 0, 2: 0 }; - /* [2026-07-17] สำรับการ์ดต่อคดี — สุ่ม 3 ใบ "ไม่ซ้ำกัน" จาก 7 ใบ แจกผู้ต้องสงสัยคนละใบ (user request: - "ได้การ์ดเดียวกันสามเกม จริงๆต้องไม่ตรงกัน"). เดิมทุก suspect ได้ set.specialCard ที่ Admin ตั้งไว้ - "ใบเดียวต่อคดี" → 3 เกมได้ใบเดิมเสมอ. แจกตรงนี้เพราะเป็นจุดเริ่มคดี = ล้างพร้อม suspectPlayCount - ไม่ค้างข้ามคดี. ค่า specialCard ใน Admin จะไม่ถูกใช้เป็นตัวเลือกอีก (ยังใช้เป็น fallback ถ้าสำรับหาย) */ - space.specialCardDeck = specialQuizShuffle([1, 2, 3, 4, 5, 6, 7]).slice(0, 3); - console.log('[special-quiz] schedule set (per-case):', JSON.stringify(space.specialQuizPlayBySuspect), 'deck=' + JSON.stringify(space.specialCardDeck)); - /* #6: ความคืบหน้าการสืบต่อผู้ต้องสงสัย (shared — เท่ากันทุกคน) = จำนวนมินิเกมที่เล่นให้คนนั้น (0-3) - ใช้โชว์ช่องหลักฐานใต้การ์ด suspect แทนหลักฐานส่วนตัว (เดิมแต่ละคนเห็นไม่เท่ากัน) */ - space.suspectTeamProgress = [0, 0, 0]; - // สุ่มคนร้ายตัวจริง 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 */ -/* คีย์เก็บหลักฐานต่อผู้เล่น — ใช้ playerKey (unique ต่อ browser) เป็นหลัก กัน "ชื่อซ้ำ" - (เช่น 2-3 คนชื่อ MONE) ใช้ evidence ก้อนเดียวกัน = ถูกแจกซ้ำ/เกิน. fallback ชื่อเฉพาะกรณีไม่มี key */ -function peerNickKey(space, playerId) { - if (!space || !playerId) return ''; - const p = space.peers && space.peers.get ? space.peers.get(playerId) : null; - if (!p) return ''; - if (typeof p.playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(p.playerKey)) return 'k:' + p.playerKey; - if (!p.nickname) return ''; - return 'n:' + 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]); - /* กันเกิน 3 ใบ/ผู้ต้องสงสัย (#1) — defensive ถ้ามีรอบที่ 4 หลุดมา */ - if (list.length >= 3) { - space.playerEvidence[playerId][suspectIndex] = list; - syncEvidenceByNick(space, playerId); - return null; - } - 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; -} - -/* [achv Phase A 2026-07-05] นับหลักฐานที่ผู้เล่นเก็บได้ → achievements - b4_data_miner(รวมทุกใบ ×100) · b1_evidence_collector(common ×20) · a2_sharp_eye(rare/Silver ×10) · b3_hidden_fragment(legendary ครั้งแรก) - เฉพาะผู้เล่นจริง (มี playerKey) — ข้ามบอท + เรียกจาก "จุดแจกจริง" 2 ที่เท่านั้น (ไม่รวม debug hotkey) */ -function trackEvidenceCollectAchv(space, playerId, card) { - if (!card || !card.rarity) return; - const p = space.peers && space.peers.get ? space.peers.get(playerId) : null; - const pkey = p && typeof p.playerKey === 'string' ? p.playerKey : null; - if (!pkey) return; - const achv = [{ playerKey: pkey, id: 'b4_data_miner', inc: 1 }]; - if (card.rarity === 'common') achv.push({ playerKey: pkey, id: 'b1_evidence_collector', inc: 1 }); - else if (card.rarity === 'rare') achv.push({ playerKey: pkey, id: 'a2_sharp_eye', inc: 1 }); - else if (card.rarity === 'legendary') achv.push({ playerKey: pkey, id: 'b3_hidden_fragment', inc: 1 }); - submitAchievementProgress(achv); -} - -function awardDetectiveEvidenceForMinigameEnd(space, suspectIndex, playerIds, grade) { - const out = {}; - /* [achv b2] บันทึกว่า suspect นี้มีมินิเกมรันในคดีนี้ (ใช้เช็ค "เล่นมินิเกมครบทุกรอบ") */ - if (suspectIndex >= 0 && suspectIndex <= 2) { - space.caseMinigameSuspects = space.caseMinigameSuspects || {}; - space.caseMinigameSuspects[suspectIndex] = true; - } - const grantedKeys = new Set(); /* แจก 1 การ์ด/playerKey ต่อเกม — กัน "หลายแท็บ/ชื่อซ้ำ" key เดียวได้เกิน */ - (playerIds || []).forEach((pid) => { - if (!space.peers.has(pid)) return; - const k = peerNickKey(space, pid); - if (k && grantedKeys.has(k)) return; /* key นี้ได้การ์ดรอบนี้แล้ว (evidence เก็บก้อนเดียวกันตาม key) */ - const card = awardRandomEvidenceCardToPlayer(space, pid, suspectIndex, grade); - if (card != null) { out[pid] = card; if (k) grantedKeys.add(k); trackEvidenceCollectAchv(space, 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 = findPendingSpecialCard(space, 7); - if (pend) { - const def = specialCardDef(7); - freeEvidenceCount = (def && def.evidence) || 1; - for (let n = 0; n < freeEvidenceCount; n++) { - const freeKeys = new Set(); /* 1 ใบ/playerKey ต่อรอบ (กันชื่อซ้ำ/หลายแท็บได้เกิน) */ - ids.forEach((pid) => { - if (!space.peers.has(pid)) return; - const k = peerNickKey(space, pid); - if (k && freeKeys.has(k)) return; - const extra = awardRandomEvidenceCardToPlayer(space, pid, suspectIdx, grade); - if (extra != null) { awardedList[pid].push(extra); if (awarded[pid] == null) awarded[pid] = extra; if (k) freeKeys.add(k); trackEvidenceCollectAchv(space, pid, extra); } - }); - } - pend.consumed = true; - } - space.detectiveAwardDoneForRun = true; - space.lastDetectiveInvestigatedSuspect = suspectIdx; - space.lastEvidenceGrant = { suspectIdx, awarded, awardedList, grade, freeEvidenceCount }; - /* [TC182] verify: จำนวนใบต่อ playerKey ที่แจกรอบนี้ — ควรได้ 1 ใบ/key/เกม (กันชื่อซ้ำแจกเกิน) */ - try { - const keyCounts = {}; - Object.keys(awardedList).forEach((pid) => { - const k = peerNickKey(space, pid) || String(pid); - keyCounts[k] = (keyCounts[k] || 0) + (awardedList[pid] ? awardedList[pid].length : 0); - }); - const vals = Object.values(keyCounts); - const over = vals.some((n) => n > 1); - glog(sid, space, 'evidence-grant', '[TC182] แจกหลักฐาน suspect#' + (suspectIdx + 1) + ' → ' + vals.length + ' key · ใบ/key=' + JSON.stringify(vals) + (over ? ' ⚠️เกิน' : ' (1/key · pass)')); - } catch (e) { /* ignore */ } - 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.lobbyBLoadedKeys = new Set(); /* ออกไปเล่นมินิเกม → ล้าง loaded; ตอนกลับ LobbyB ทุกคนต้องส่ง lobbyb-loaded ใหม่ */ - armLobbyBLoadedSafety(sid, space); /* backstop กัน deadlock ถ้า signal ไม่ครบ */ - space.detectiveAwardDoneForRun = false; - space.coinAwardDoneForRun = false; - space.lastCrownMissionGrade = null; - /* รีเซ็ตคะแนนที่ client รายงาน (stack/quiz_carry) ของรอบก่อน */ - space.peers.forEach((p) => { p.reportedMiniScore = 0; p.reportedMiniSurvived = true; }); - /* เริ่มรอบใหม่ → ล้าง over event เก่า (กัน resume ส่ง over ของรอบก่อนให้คน reconnect) */ - space.lastMinigameOver = null; - /* Card 3 Ban — ผู้ที่ถูกแบนไม่ได้เล่นมินิเกมรอบนี้ (1 รอบ) */ - space.bannedThisRunPlayerId = space.bannedPlayerId || null; - space.bannedThisRunPlayerKey = space.bannedPlayerKey || ''; /* ตัวหลักที่ใช้แมตช์จริง (id เปลี่ยนได้) */ - console.log('[ban-debug] startMinigame: bannedThisRunPlayerId=', space.bannedThisRunPlayerId, 'key=', space.bannedThisRunPlayerKey || '(none/bot)'); - space.bannedPlayerId = null; - space.bannedPlayerKey = ''; - /* จำนวนผู้เล่นที่ต้องเข้าเกม + กด Ready ให้ครบก่อน host เริ่มได้ (ไม่นับผู้ถูกแบน) - สแนปจาก peers ใน LobbyB ตอนนี้ — กัน host เริ่มก่อน client โหลด/กด Ready ครบ (เกมไม่ sync/จบไม่พร้อมกัน) */ - space.minigameExpectedActiveCount = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)).length; - 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; - /* MG5: สุ่ม pattern แพลตฟอร์ม 1 อัน/เกม (layout หลากหลาย/เดายาก) — เก็บบน space, ส่งให้ client ผ่าน game-start */ - space.jsLayout = null; space._jsEffMd = null; - if (md && md.gameType === 'jump_survive') pickJumpSurviveLayoutForSpace(space); - glog(sid, space, 'game-start', 'เริ่มมินิเกม: ' + ((md && md.name) || mapId) + ' (การ์ด ' + ((selectedIndex != null ? selectedIndex + 1 : '?')) + ')'); - /* สุ่มไอคอนคำถามพิเศษสำหรับรอบนี้ (1 ใน 3) — ส่งจริงผ่าน joinCb เมื่อ client เข้า play.html */ - maybeSpawnSpecialQuizForRun(space); - /* ไอคอนยังไม่โผล่ตอนนี้ — รอ client ส่ง 'special-quiz-arm' เมื่อเกม "live จริง" (พ้น howto/นับถอยหลัง) แล้วค่อยหน่วง 2 วิโผล่ */ - - space.peers.forEach((p) => { - const sp = pickSpawnForJoin(md, typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : 0); - p.x = sp.x; - p.y = sp.y; - p.direction = 'down'; - p.gauntletJumpTicks = 0; - p.gauntletScore = 0; - p.gauntletJumpPending = false; - p.gauntletEliminated = false; - p.detectiveFinished = false; /* MG2: รอให้ทุกคน(ที่ยังไม่ตาย)เข้าเส้นชัยก่อนจบเกมพร้อมกัน */ - }); - - 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, - bannedPlayerKey: space.bannedThisRunPlayerKey || '', /* client เทียบ playerKey (id เปลี่ยนหลัง reconnect) */ - }); - 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(); - } - /* quiz_carry ใช้ ready map ของตัวเอง (ไม่ใช่ crown) — รีเซ็ตสถานะ Ready ใหม่ทุกคนเริ่มรอบ แล้วแจ้งทุก client - กัน host เห็นจำนวนผู้เล่น/สถานะ ready ไม่ตรง และให้ flow READY→START ทำงานเหมือนเกมอื่น */ - if (md.gameType === 'quiz_carry') { - space.quizCarryLobbyReady = {}; - space.peers.forEach((_p, id) => { space.quizCarryLobbyReady[id] = false; }); - io.to(sid).emit('quiz-carry-lobby-sync', quizCarryLobbySyncPayload(space)); - } - - const gameStartPayload = { - mapId, - peersSnap, - detectiveMinigame: true, - detectiveReturnLobbyB: true, - minigameLabel: cardEntry.labelTh, - bannedPlayerId: space.bannedThisRunPlayerId || null, - bannedPlayerKey: space.bannedThisRunPlayerKey || '', /* client เทียบ playerKey (id เปลี่ยนหลัง reconnect) */ - }; - if (space.jsLayout) gameStartPayload.jsLayout = space.jsLayout; - 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(); - /* แจกเหรียญตามอันดับ + คะแนนสะสม ก่อนล้างสถานะมินิเกม (คะแนน/ผู้รอดยังครบ) */ - awardMinigameRankCoins(sid, space); - clearSpaceQuizTimers(space); - clearSpecialQuizTimers(space); - space.specialQuiz = null; - if (space.quizSession) space.quizSession.active = false; - space.quizSession = null; - space.detectiveMinigameActive = false; - space.gauntletReturnMapId = null; - /* Last Light (MG2): เก็บเกรด mission (รวมบอท server-authoritative) ก่อนล้าง gauntletRun - → ใช้เป็น "แหล่งเดียว" ของเกรดหลักฐาน ให้ตรงกับสรุปภารกิจที่ผู้เล่นเห็น */ - if (space.gauntletRun && isGauntletCrownHeistMapBySpace(space)) { - try { space.lastCrownMissionGrade = buildGauntletCrownMissionPayload(space).grade; } - catch (_e) { space.lastCrownMissionGrade = null; } - } - stopGauntletTicker(space); - space.gauntletRun = null; - /* MG3 stack (server-auth): หยุด engine + ล้าง session (เกรดถูกเก็บไว้ใน lastStackMissionGrade ตอนจบแล้ว) */ - stopStackTicker(space); - if (space.stackSession) space.stackSession.active = false; - space.stackSession = null; - /* MG4 quiz_carry (server-auth): ล้าง session (คะแนนถูกอ่านไปแจกเหรียญใน awardMinigameRankCoins ด้านบนแล้ว) */ - if (space.quizCarrySession) space.quizCarrySession.active = false; - space.quizCarrySession = null; - if (space.quizCarryWatchdog) { clearInterval(space.quizCarryWatchdog); space.quizCarryWatchdog = null; } - /* MG5 jump_survive (server-auth): ล้าง session */ - if (space.jumpSurviveSession) space.jumpSurviveSession.active = false; - space.jumpSurviveSession = null; - /* MG6 space_shooter (server-auth): ล้าง session */ - if (space.spaceShooterSession) space.spaceShooterSession.active = false; - space.spaceShooterSession = null; - /* MG7 balloon_boss (server-auth): ล้าง session */ - if (space.balloonBossSession) space.balloonBossSession.active = false; - space.balloonBossSession = null; - const suspectIdx = resolveDetectiveSuspectAwardIndex(space); - if (suspectIdx >= 0) { - space.lastDetectiveInvestigatedSuspect = suspectIdx; - /* #6: นับความคืบหน้าการสืบ (shared) +1 ต่อการเล่นมินิเกม 1 รอบของผู้ต้องสงสัยนั้น (สูงสุด 3) */ - if (!Array.isArray(space.suspectTeamProgress)) space.suspectTeamProgress = [0, 0, 0]; - if (suspectIdx >= 0 && suspectIdx <= 2) { - space.suspectTeamProgress[suspectIdx] = Math.min(3, (space.suspectTeamProgress[suspectIdx] || 0) + 1); - try { glog(sid, space, 'suspect-progress', '[TC179] สืบผู้ต้องสงสัย #' + (suspectIdx + 1) + ' → team progress ' + space.suspectTeamProgress.join('/') + ' (shared เท่ากันทุกจอ · pass)'); } catch (e) { /* ignore */ } - } - 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 — ผู้ที่ถูกแบนรอบนี้เข้ามาดูเฉยๆ ไม่ได้เล่น จึงไม่ได้รับหลักฐาน - (เช็คทั้ง id เดิมตอนโหวต และ flag bannedSpectator ของ peer ใน play.html ที่ reconnect ใหม่) */ - awardIds = awardIds.filter((id) => { - if (space.bannedThisRunPlayerId && id === space.bannedThisRunPlayerId) return false; - const pp = space.peers.get(id); - if (pp && pp.bannedSpectator) return false; - return true; - }); - /* ไม่มอบการ์ดเมื่อกลับ 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; - /* [TC166-fix] arm backstop ตรงนี้ = "ตอนกลับถึง LobbyB จริง" — เดิม arm ไว้ตั้งแต่ beginDetectiveSuspectMinigame - (25วิ) แต่มินิเกมยาวกว่านั้น → timer ยิงตอนยังอยู่ในมินิเกม → เจอ guard !serverMapIsPostCaseLobbyB แล้ว - return ทิ้ง → พอกลับ LobbyB ไม่เหลือ backstop เลย. ถ้า client ไหนค้างไม่ส่ง lobbyb-loaded (เช่นโดนดึงออก - กลางมินิเกม) gate จะค้าง present { - const sp = pickSpawnForJoin(lb, typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : 0); - 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 || [], - suspectTeamProgress: Array.isArray(space.suspectTeamProgress) ? space.suspectTeamProgress.slice() : [0, 0, 0], /* #6 shared */ - detectiveReturn: true, - }; - emitDetectiveLobbyReturnToPeers(sid, space, message, basePayload); - /* Card 3 Ban (when: lobby_next): ชนะการ์ดแบนรอบนี้ → "โหวตทันทีที่กลับ LobbyB หลังเกมจบ" - ตั้ง bannedPlayerId ให้มินิเกมรอบถัดไปกันไม่ให้คนที่ถูกโหวตเล่น */ - const banPend = findPendingSpecialCard(space, 3); - console.log('[card-dbg] lobby return: ban(3)=' + !!banPend + ' pickVote=' + !!space.pickVote + ' list=' + dbgCards(space)); - if (banPend && !space.pickVote) { - banPend.consumed = true; - /* ตั้ง flag — เริ่มโหวตตอนผู้เล่น "rejoin เข้า room-lobby" (ตอน return ผู้เล่นกำลัง navigate play.html→room-lobby.html อยู่ event จะหาย) */ - space.pendingBanVoteCard = banPend.card; - } -} - -/* ===== ขั้นพิจารณาคดี — โหวตชี้ตัวคนร้าย ===== */ -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 ครั้ง */ - { - /* โหมดเทสต์ (Ctrl+Alt+W): บังคับให้รอบนี้ "นับเป็นโหวตผิด" ครั้งเดียว เพื่อทริกการ์ด 5 */ - const forceWrong = !!space.testForceWrongVote; - space.testForceWrongVote = false; - 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 = forceWrong ? anyVotes : (anyVotes && mostVoted !== culprit); - const pend = findPendingSpecialCard(space, 5); - if (wrong && pend && !space.trialBailUsed) { - pend.consumed = true; - space.trialBailUsed = true; - space.trialRevoteExcluded = mostVoted; /* ผู้ต้องสงสัยที่โหวตไปแล้ว (ผิด) — ห้ามโหวตซ้ำในรอบใหม่ */ - io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'bail' }); - openTrialVotingPhase(sid, space, { revote: true, excludedSuspect: mostVoted }); - 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); - }); - /* ตัวป่วน (ถ้ามีคนรับบท) — หา socket.id ปัจจุบันจาก playerKey/nickname (id เดิมตอนเสนออาจเปลี่ยนแล้ว) */ - let disruptorId = null; - if (space.troublesomeAccept && (space.disruptorPlayerKey || space.disruptorNickname)) { - for (const [pid, p] of space.peers) { - const keyMatch = space.disruptorPlayerKey && p.playerKey === space.disruptorPlayerKey; - const nickMatch = space.disruptorNickname && normalizeLobbyNickname(p.nickname) === space.disruptorNickname; - if (keyMatch || nickMatch) { disruptorId = pid; break; } - } - } - io.to(sid).emit('trial-result', { - culpritIndex: culprit, - counts, - winners, - winnerNames, - cardMinigames: space.suspectCardMinigames || [], - disruptorId, - hasDisruptor: !!disruptorId, - }); - - /* ===== Achievement auto-track ===== */ - try { - const achv = []; - /* Phase B: majority vote (เช็ค lone-wolf) + มีคนโหวตไหม */ - let mvAchv = 0; for (let k = 1; k <= 2; k++) { if (counts[k] > counts[mvAchv]) mvAchv = k; } - const anyVotesAchv = (counts[0] + counts[1] + counts[2]) > 0; - /* a1 First Deduction (target 1) + a5 Truth Hunter (target 20) — ผู้โหวตถูก (ผู้เล่นจริงเท่านั้น) */ - winners.forEach((id) => { - const p = space.peers.get(id); - if (p && p.playerKey) { - achv.push({ playerKey: p.playerKey, id: 'a1_first_deduction', inc: 1 }); - achv.push({ playerKey: p.playerKey, id: 'a5_truth_hunter', inc: 1 }); - /* ===== Phase B (2026-07-05) — เงื่อนไขต่อผู้โหวตถูก ===== */ - /* c5 Lone Wolf: โหวตถูกแต่เสียงส่วนใหญ่ผิด (สวนทางทีม) */ - if (anyVotesAchv && mvAchv !== culprit) achv.push({ playerKey: p.playerKey, id: 'c5_lone_wolf', inc: 1 }); - /* หลักฐานในมือคดีนี้ (ต่อผู้ต้องสงสัย 3 กอง) */ - const evRows = clonePlayerEvidence(space, id); - const evTotal = evRows.reduce((s, row) => s + (Array.isArray(row) ? row.length : 0), 0); - /* c2 High Stakes: โหวตถูกโดยมีหลักฐาน ≤3 ใบ */ - if (evTotal <= 3) achv.push({ playerKey: p.playerKey, id: 'c2_high_stakes', inc: 1 }); - /* c1 Early Accusation: โหวตถูกโดยมีหลักฐาน ≤1 ใบ (ชี้ตัวก่อนเปิดหลักฐานครบ) */ - if (evTotal <= 1) achv.push({ playerKey: p.playerKey, id: 'c1_early_accusation', inc: 1 }); - /* a4 Logic Over Luck: โหวตถูกโดยไม่มีหลักฐาน legendary (ชี้ชัด) เลย */ - const hasLegendary = evRows.some((row) => Array.isArray(row) && row.some((c) => c && c.rarity === 'legendary')); - if (!hasLegendary) achv.push({ playerKey: p.playerKey, id: 'a4_logic_over_luck', inc: 1 }); - /* c4 Clutch Mind: โหวตถูกใน 10 วิสุดท้ายก่อนหมดเวลา */ - const voteAt = space.trialVoteAt && space.trialVoteAt[id]; - if (voteAt && space.trialVoteEndsAt) { - const remain = space.trialVoteEndsAt - voteAt; - if (remain >= 0 && remain <= 10000) achv.push({ playerKey: p.playerKey, id: 'c4_clutch_mind', inc: 1 }); - } - } - }); - /* ตัวป่วน — นับเฉพาะผู้เล่นจริง (มี playerKey) */ - if (disruptorId && space.disruptorPlayerKey) { - const dKey = space.disruptorPlayerKey; - achv.push({ playerKey: dKey, id: 'e2_the_impostor', inc: 1 }); /* target 1 */ - achv.push({ playerKey: dKey, id: 'e4_agent_of_chaos', inc: 1 }); /* target 10 */ - /* ตัวป่วนชนะ = ทีมโหวตผิดคน (ไม่จับคนร้ายตัวจริง) */ - let mv = 0; for (let k = 1; k <= 2; k++) { if (counts[k] > counts[mv]) mv = k; } - const anyV = (counts[0] + counts[1] + counts[2]) > 0; - if (anyV && mv !== culprit) { - achv.push({ playerKey: dKey, id: 'e3_master_of_doubt', inc: 1 }); /* target 1 */ - achv.push({ playerKey: dKey, id: 'e5_slippery_eel', inc: 1 }); /* target 5 */ - } - } - /* ===== Phase C/D (2026-07-06) — ต่อผู้เล่นที่จบคดี (ไม่ใช่แค่ผู้โหวตถูก) ===== */ - const streakItems = []; - /* c3 Quick Draw: ผู้โหวตถูกที่ "โหวตเร็วที่สุด" ในทีม (ใช้ trialVoteAt) */ - { - let fastId = null, fastAt = Infinity; - winners.forEach((id) => { - const at = space.trialVoteAt && space.trialVoteAt[id]; - const wp = space.peers.get(id); - if (at && wp && wp.playerKey && at < fastAt) { fastAt = at; fastId = id; } - }); - if (fastId) { const fp = space.peers.get(fastId); if (fp && fp.playerKey) achv.push({ playerKey: fp.playerKey, id: 'c3_quick_draw', inc: 1 }); } - } - /* วนผู้เล่นจริงทุกคนที่จบคดี (ข้ามบอท/ผู้ถูกแบน spectator) */ - const caseMgSuspects = Object.keys(space.caseMinigameSuspects || {}).map((n) => parseInt(n, 10)); - const targetedKeys = space.caseCardTargetedKeys || {}; - space.peers.forEach((p, pid) => { - if (!p || !p.playerKey || p.bannedSpectator) return; - const evRows = clonePlayerEvidence(space, pid); - const rarSet = {}; - evRows.forEach((row) => { if (Array.isArray(row)) row.forEach((c) => { if (c && c.rarity) rarSet[c.rarity] = true; }); }); - /* a3 Mind Architect: มีหลักฐานครบ 3 ระดับในคดีนี้ (common+rare+legendary) */ - if (rarSet.common && rarSet.rare && rarSet.legendary) achv.push({ playerKey: p.playerKey, id: 'a3_mind_architect', inc: 1 }); - /* b2 Relentless Investigator: เก็บหลักฐานครบทุก suspect ที่มีมินิเกมรันในคดีนี้ (= เล่นครบทุกรอบ) */ - if (caseMgSuspects.length > 0 && caseMgSuspects.every((si) => Array.isArray(evRows[si]) && evRows[si].length > 0)) { - achv.push({ playerKey: p.playerKey, id: 'b2_relentless_investigator', inc: 1 }); - } - /* d2 Silent Guardian: ไม่โดนโหวตการ์ด Silence/Ban ในคดีนี้ */ - if (!targetedKeys[p.playerKey]) achv.push({ playerKey: p.playerKey, id: 'd2_silent_guardian', inc: 1 }); - /* e1 The Observer (streak): จบคดีนี้โดยไม่แตะ legendary = hit, แตะ = รีเซ็ต */ - streakItems.push({ playerKey: p.playerKey, id: 'e1_the_observer', event: rarSet.legendary ? 'miss' : 'hit' }); - }); - /* a6 Unbreakable Logic (โหวตถูกติดกัน) + d4 Flawless Diver (ไม่โหวตผิดติดกัน) — เฉพาะคนที่โหวตจริง */ - Object.keys(votes).forEach((id) => { - const p = space.peers.get(id); - if (!p || !p.playerKey) return; - const ok = votes[id] === culprit; - streakItems.push({ playerKey: p.playerKey, id: 'a6_unbreakable_logic', event: ok ? 'hit' : 'miss' }); - streakItems.push({ playerKey: p.playerKey, id: 'd4_flawless_diver', event: ok ? 'hit' : 'miss' }); - }); - submitAchievementProgress(achv); - submitAchievementStreak(streakItems); - } catch (e) { console.error('[achv] trial-result', e && e.message); } - - space.silencedPlayerId = null; - - /* item1: admin ตั้งปลายทางหลังจบคดี = 'lobbya' → รีเซ็ตห้องเดิมกลับ LobbyA (ครั้งเดียว/คดี, terminal - เท่านั้น — revote คืนก่อนถึงจุดนี้แล้ว). ทำเงียบ (ไม่ broadcast) → client โชว์ผล+หน้าความรู้ต่อได้ - แล้ว reload (room-lobby.html?space=) มาเจอ LobbyA สด */ - try { - if (runtimeGameTiming && runtimeGameTiming.postCaseDestination === 'lobbya' && !space.postCaseLobbyAReset) { - space.postCaseLobbyAReset = true; - resetDetectiveSpaceToLobbyA(sid, space); - } - } catch (e) { console.error('[item1] post-case reset', e && e.message); } -} - -/* item1: รีเซ็ตห้อง detective กลับสภาพ LobbyA สดๆ — สร้าง space object ใหม่ (คง identity/peers/host/config) - เพื่อกัน state คดีค้าง (evidence/ban/trial/suspect/timers). client reload → join-space เจอ LobbyA. */ -function resetDetectiveSpaceToLobbyA(sid, space) { - if (!space) return; - const lobbyMapId = space.lobbyAMapId || space.mapId; - const lobbyMap = (lobbyMapId && maps.get(lobbyMapId)) || space.lobbyAMapData || space.mapData; - /* หยุด timer ทั้งหมดของห้องเดิม (กัน callback ค้างมายุ่งกับ object ใหม่) */ - try { stopGauntletTicker(space); } catch (e) { /* ignore */ } - try { clearSpaceQuizTimers(space); } catch (e) { /* ignore */ } - try { clearTrialVoteTimer(space); } catch (e) { /* ignore */ } - ['storyReadTimer', 'troublesomeDebTimer', 'troublesomeMaxTimer', 'lobbyBLoadedSafetyTimer', 'pickVoteTimer', 'banVoteTimer'].forEach((t) => { - if (space[t]) { try { clearTimeout(space[t]); } catch (e) { /* ignore */ } space[t] = null; } - }); - /* คืน spawn LobbyA + ยกเลิก ready ให้ทุก peer (client reload จะสร้าง peer ใหม่อยู่แล้ว — นี่กันช่วงคาบเกี่ยว) */ - space.peers.forEach((p) => { - try { - const sp = pickSpawnForJoin(lobbyMap, typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : 0); - p.x = sp.x; p.y = sp.y; p.direction = 'down'; - } catch (e) { /* ignore */ } - p.ready = false; - p.bannedSpectator = false; - }); - /* สร้าง space ใหม่แบบ LobbyA สด (เท่ากับห้องที่เพิ่งสร้าง) — คง identity/peers/host/config, ทิ้ง state คดี */ - const fresh = { - mapData: lobbyMap, mapId: lobbyMapId, peers: space.peers, spaceName: space.spaceName, - hostId: space.hostId, hostPlayerKey: space.hostPlayerKey, isPrivate: space.isPrivate, - maxPlayers: space.maxPlayers, createdAt: space.createdAt, roomPassword: space.roomPassword, - previewEditorTest: space.previewEditorTest, botSlotCount: space.botSlotCount, - lobbyAMapId: lobbyMapId, lobbyAMapData: lobbyMap, - suspectCardMinigames: null, suspectPhaseActive: false, suspectPickIndex: 0, detectiveMinigameActive: false, - graceReconnect: space.graceReconnect || {}, graceTimers: space.graceTimers || {}, - /* กันห้องถูกลบตอน client reload กลับเข้า LobbyA (peer=0 ชั่วขณะ) — keep-alive 5 นาที ให้ทุกคน rejoin ทัน */ - postCaseKeepAliveUntil: Date.now() + 300000, - }; - spaces.set(sid, fresh); - try { glog(sid, fresh, 'post-case', '[item1] reset ห้องกลับ LobbyA (postCaseDestination=lobbya) — รอ client reload'); } catch (e) { /* ignore */ } -} - -/* ===== ห้องสรุปหลักฐาน — การไต่สวน (ปากคำ 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 เพิ่มให้ */ -/* เวลาโหวตชี้ตัวคนร้าย (ms) — ตั้งได้ในหน้า admin (game-timing.trialVoteSec) */ -function trialVoteMs() { return Math.max(5, Math.min(180, Math.floor(Number(runtimeGameTiming.trialVoteSec)) || 30)) * 1000; } -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 = {}; - /* ผู้ต้องสงสัยที่ถูกตัดออก (โหวตผิดไปแล้วในรอบจับผิดตัว) — null ถ้าเป็นการโหวตปกติ */ - const excludedSuspect = opts.revote - ? (Number.isInteger(opts.excludedSuspect) ? opts.excludedSuspect - : (Number.isInteger(space.trialRevoteExcluded) ? space.trialRevoteExcluded : null)) - : null; - if (!opts.revote) space.trialRevoteExcluded = null; - autoVoteBotsTrial(space); - clearTrialVoteTimer(space); - /* Card 2 Extension — เพิ่มเวลาโหวต +10 วิ (ใช้ครั้งเดียว) */ - let extraMs = 0; - let extensionCard = false; - const pend = findPendingSpecialCard(space, 2); - if (pend) { - extraMs = TRIAL_VOTE_EXTENSION_MS; - extensionCard = true; - pend.consumed = true; - } - const durMs = trialVoteMs() + extraMs; - space.trialVoteEndsAt = Date.now() + durMs; - space.trialVoteAt = {}; /* [achv c4] รีเซ็ตเวลาโหวตทุกครั้งที่เปิดโหวต (รวม revote) */ - 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, - excludedSuspect: excludedSuspect, - silenced: !!(space.silencedPlayerId && pid === space.silencedPlayerId), - }); - }); - 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 ห้ามเล่น) ===== */ -/* เวลาโหวตเลือกผู้เล่น Silence/Ban (ms) — ตั้งได้ในหน้า admin (game-timing.pickVoteSec) */ -function pickVoteMs() { return Math.max(5, Math.min(120, Math.floor(Number(runtimeGameTiming.pickVoteSec)) || 25)) * 1000; } - -function clearPickVoteTimer(space) { - if (space.pickVoteTimer) { clearTimeout(space.pickVoteTimer); space.pickVoteTimer = null; } -} - -function pickVoteCandidates(space) { - const arr = []; - /* แนบ characterId + color index มาด้วย → client เรนเดอร์ avatar ได้แม้ peer ยังไม่เข้า cache - ท้องถิ่น (race หลังกลับ LobbyB) กัน avatar เขียว-blob บนจอโหวต Silence/Ban */ - space.peers.forEach((p, id) => arr.push({ - id, - nickname: (p && p.nickname) ? p.nickname : String(id).slice(0, 6), - isBot: false, - characterId: (p && p.characterId != null) ? p.characterId : null, - lobbyColorThemeIndex: (p && p.lobbyColorThemeIndex) || null, - lobbySkinToneIndex: (p && p.lobbySkinToneIndex) || null, - })); - testimonyBotIds(space).forEach((bid, i) => arr.push({ id: bid, nickname: 'บอท ' + (i + 1), isBot: true, characterId: null, lobbyColorThemeIndex: null, lobbySkinToneIndex: null })); - return arr; -} - -/* map candidate → payload (รวม char info สำหรับเรนเดอร์ avatar ฝั่ง client) */ -function pickVoteCandidatePayload(c) { - return { - id: c.id, - nickname: c.nickname, - isBot: c.isBot, - characterId: c.characterId != null ? c.characterId : null, - lobbyColorThemeIndex: c.lobbyColorThemeIndex || null, - lobbySkinToneIndex: c.lobbySkinToneIndex || null, - }; -} - -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 PV_MS = pickVoteMs(); - const candidates = pickVoteCandidates(space); - space.pickVote = { purpose, votes: {}, candidates, endsAt: Date.now() + PV_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(pickVoteCandidatePayload), - endsAt: space.pickVote.endsAt, - durationMs: PV_MS, - }); - }); - emitPickVoteUpdate(sid, space); - space.pickVoteTimer = setTimeout(() => { - const sp = spaces.get(sid); - if (!sp || !sp.pickVote || sp.pickVote.resolved) return; - resolvePlayerPickVote(sid, sp); - }, PV_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 = 0; - Object.keys(counts).forEach((tid) => { if (counts[tid] > max) max = counts[tid]; }); - const maxIds = Object.keys(counts).filter((tid) => counts[tid] === max); - /* เสมอ (มากกว่า 1 คนได้สูงสุดเท่ากัน) หรือไม่มีใครโหวต → ไม่มีใครโดน (เป้าหมายปลอดภัย) */ - const target = (max > 0 && maxIds.length === 1) ? maxIds[0] : 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 }); - /* [achv d2] บันทึก playerKey ที่โดนโหวตการ์ด Silence/Ban ในคดีนี้ (→ ไม่ได้ d2_silent_guardian) */ - if (target && (pv.purpose === 'silence' || pv.purpose === 'ban')) { - const tp = space.peers.get(target); - if (tp && tp.playerKey) { - space.caseCardTargetedKeys = space.caseCardTargetedKeys || {}; - space.caseCardTargetedKeys[tp.playerKey] = true; - } - } - 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); - const excluded = Number.isInteger(space.trialRevoteExcluded) ? space.trialRevoteExcluded : -1; - 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); - } - /* รอบจับผิดตัว: ห้ามบอทโหวตผู้ต้องสงสัยที่ถูกตัดออก → เปลี่ยนไปโหวตคนร้ายจริง */ - if (pick === excluded) pick = culprit; - 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 = findPendingSpecialCard(space, 4); - console.log('[card-dbg] trial pre-vote: silence(4)=' + !!pend + ' list=' + dbgCards(space)); - if (pend) { - 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); - /* คะแนนสุดท้าย authoritative (ผู้เล่นจริง) — ส่งไปกับ quiz-ended ให้ทุก client ตัดสินผล complete/gameover ตรงกัน - (เดิมไม่ส่ง → client อ่าน playLiveQuizScores local ที่ quiz-result ข้อสุดท้ายอาจมาไม่ทัน → ผล 2 จอไม่ตรง) */ - const quizFinalScores = {}; - const sessF = space.quizSession; - if (sessF && sessF.players) { - Object.keys(sessF.players).forEach((pid) => { - const st = sessF.players[pid]; - quizFinalScores[pid] = (st && typeof st.score === 'number') ? Math.max(0, st.score) : 0; - }); - } - if (space.quizSession) space.quizSession.active = false; - if (space.detectiveMinigameActive) { - /* MG1: ไม่ auto-return/auto-grant — ส่ง quiz-ended ให้ client โชว์ผล (รอกดปุ่มรับการ์ดเอง เหมือนเกมอื่น) - เดิม returnDetectiveSpaceToLobbyB ที่นี่ = server แจกการ์ด+เด้งกลับเองทันที = "auto รับผล/การ์ด" - การแจกการ์ด/กลับ LobbyB จะเกิดเมื่อ client กดปุ่ม → detective-minigame-finished (เส้นทางเดียวกับ MG2-7) */ - io.to(sid).emit('quiz-ended', { message: message || 'จบเกม', returnToLobbyB: true, scores: quizFinalScores }); - /* คง quizSession (active=false แล้ว) ไว้ → ตอน client กด finish → returnDetectiveSpaceToLobbyB อ่านคะแนน quiz มาคิดเหรียญได้ - (ถ้า null ที่นี่ minigamePlayerScoreSurvived('quiz') = 0 หมด → เหรียญเพี้ยน) ; จะถูกแทนเมื่อเริ่มมินิเกมรอบใหม่ */ - return; - } - io.to(sid).emit('quiz-ended', { message: message || 'จบเกม', scores: quizFinalScores }); - 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, - }); - pausableSpecialQuizTimeout(sid, space, () => scheduleQuizAnswerPhase(sid, space, md), readMs); /* หยุดนับตอนคำถามพิเศษ */ -} - -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); - /* บอท: server ตัดสินคำตอบข้อนี้ + ตั้งเป้าเดินไปโซน (server tick เดินให้ในช่วง answer) */ - const qCur = sess.questions[sess.qIndex]; - if (qCur) quizBotsDecideAnswers(space, sess.quizMapMd || md, !!qCur.answerTrue); - pausableSpecialQuizTimeout(sid, space, () => resolveQuizRound(sid, space, md), answerMs); /* หยุดนับตอนคำถามพิเศษ */ -} - -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++; - /* จับที่ "แถวเท้า" (bottom row ของ footprint) เท่านั้น — หัว/ตัวโผล่เข้าโซนแต่เท้ายังไม่ถึง = ไม่นับ - (ต้องตรงกับ client quizResolveChoiceFromStanding) */ - const { cw: qCw, ch: qCh } = getCharacterFootprintWHForMove(mdLive); - const qBaseTx = Math.floor(Number(p.x)); - const qFeetTy = Math.floor(Number(p.y)) + qCh - 1; - let inT = false, inF = false; - for (let qdx = 0; qdx < qCw; qdx++) { - const qftx = qBaseTx + qdx; - if (!inT && quizCellOn(tGrid, qftx, qFeetTy)) inT = true; - if (!inF && quizCellOn(fGrid, qftx, qFeetTy)) inF = true; - } - 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; - }); - - /* บอท: server เป็นเจ้าของคะแนน (จากคำตอบที่ตัดสินไว้) + คัดออกถ้าผิด (กฎเดียวกับคน "ตอบผิดถูกคัดออก") */ - if (sess.bots) { - sess.bots.forEach((b, botId) => { - if (!b.eliminated) { - if (b.answerRight) { - b.score += QUIZ_TF_POINTS_PER_CORRECT; - } else { - b.eliminated = true; - /* คัดออก → วาร์ปเข้าโซนผีทันที (เหมือนคนจริง) */ - const gc = quizRandomCellInZone(mdLive.quizGhostArea, mdLive); - if (gc) { b.x = gc.x + 0.5; b.y = gc.y + 0.5; } - b.wanderTX = null; b.retargetAt = 0; - } - } - scores[botId] = b.score; - results.push({ - id: botId, nickname: 'บอท', right: !!b.answerRight, choice: b.choice, - eliminated: !!b.eliminated, score: b.score, - }); - b.tx = null; b.ty = null; /* รีเซ็ตเป้า → เดินมั่วจนข้อถัดไป */ - }); - } - - 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, - }; - space.quizSession.bots = createQuizBots(space, md); - scheduleQuizReadPhase(sid, space, md); -} - -/* ===== MG1 quiz bots — server-authoritative (roster + เดิน + ตอบ + คะแนน) ===== - เดิม host เดิน+คิดคะแนนบอทเอง → host หลุด = บอทหยุด + คะแนนเพี้ยน. ย้ายมา server → บอทเดินเอง time-based + คะแนน server เป็นเจ้าของ - id `__pv_bot_` 0..N-1 (N = effectiveBotSlotCount) ตรงกับ client */ -const QUIZ_BOT_PREFIX = '__pv_bot_'; -const QUIZ_BOT_SPEED_PER_SEC = 0.15 * 1.28 * 60; /* ~11.5 u/s ใกล้ความเร็วบอทเดิม */ - -function quizBotTierForSlot(i) { - const r = ((i * 73 + 17) % 100) / 100; /* deterministic per slot */ - if (r < 0.30) return 'sharp'; - if (r < 0.75) return 'avg'; - return 'weak'; -} -function quizBotCorrectProb(tier) { - return tier === 'sharp' ? 0.84 : tier === 'weak' ? 0.22 : 0.52; -} -function quizTileWalkableServer(md, x, y) { - const w = md.width || 20, h = md.height || 15; - const tx = Math.floor(x), ty = Math.floor(y); - if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; - const ob = md.objects && md.objects[ty] ? md.objects[ty][tx] : 0; - return ob !== 1; -} -function quizCellInGrid(grid, x, y) { - if (!Array.isArray(grid)) return false; - const tx = Math.floor(x), ty = Math.floor(y); - return !!(grid[ty] && Number(grid[ty][tx]) === 1); -} -function quizRandomCellInZone(grid, md) { - if (!Array.isArray(grid)) return null; - const w = md.width || 20, 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 && quizTileWalkableServer(md, x + 0.5, y + 0.5)) pool.push({ x, y }); - } - } - if (!pool.length) return null; - return pool[Math.floor(Math.random() * pool.length)]; -} -function createQuizBots(space, md) { - const botCount = effectiveBotSlotCount(space); - const humans = space.peers ? space.peers.size : 0; - const bots = new Map(); - for (let i = 0; i < botCount; i++) { - const id = QUIZ_BOT_PREFIX + i; - const sp = quizWrongAnswerRespawnPosition(md, humans + i); - bots.set(id, { - tier: quizBotTierForSlot(i), - x: Number(sp.x), y: Number(sp.y), dir: 'down', isWalking: false, - score: 0, eliminated: false, choice: null, answerRight: false, - tx: null, ty: null, wanderTX: null, wanderTY: null, retargetAt: 0, - }); - } - return bots; -} -/* ตัดสินคำตอบบอทแต่ละข้อ (สุ่มตาม tier เดิม) + ตั้งเป้าเดินไปโซนนั้น — ข้ามบอทที่ถูกคัดออกแล้ว */ -function quizBotsDecideAnswers(space, md, correctTrue) { - const sess = space.quizSession; - if (!sess || !sess.bots) return; - const tGrid = md.quizTrueArea || [], fGrid = md.quizFalseArea || []; - sess.bots.forEach((b) => { - b.choice = null; b.answerRight = false; b.tx = null; b.ty = null; - if (b.eliminated) return; - if (Math.random() < 0.14) return; /* เดินมั่ว = ไม่ตอบ = ผิด */ - const wantsCorrect = Math.random() < quizBotCorrectProb(b.tier); - const targetTrue = wantsCorrect ? correctTrue : !correctTrue; - b.choice = targetTrue; - b.answerRight = (targetTrue === correctTrue); - const cell = quizRandomCellInZone(targetTrue ? tGrid : fGrid, md); - if (cell) { b.tx = cell.x + 0.5; b.ty = cell.y + 0.5; } - }); -} -function stepQuizBots(sid, space, dt, now) { - const sess = space.quizSession; - if (!sess || !sess.active || !sess.bots || sess.bots.size === 0) return; - const md = sess.quizMapMd; - if (!md) return; - const w = md.width || 20, h = md.height || 15; - const step = QUIZ_BOT_SPEED_PER_SEC * dt; - const answering = sess.phase === 'answer'; - const ghostGrid = md.quizGhostArea; - sess.bots.forEach((b) => { - /* บอทถูกคัดออก (ตอบผิด) = "ผี" → ขังในโซนผี เดินออกไม่ได้ (เหมือนคนจริง) */ - const confineGhost = b.eliminated && Array.isArray(ghostGrid); - const canWalk = (x, y) => quizTileWalkableServer(md, x, y) && (!confineGhost || quizCellInGrid(ghostGrid, x, y)); - /* hard-snap: ถ้าผีอยู่นอกโซนผี (warp พลาด/ติดขอบ) → ดึงกลับเข้าโซนทันทีทุก tick (กันผีหลุดออกสนาม) */ - if (confineGhost && !quizCellInGrid(ghostGrid, b.x, b.y)) { - const gc = quizRandomCellInZone(ghostGrid, md); - if (gc) { b.x = gc.x + 0.5; b.y = gc.y + 0.5; b.wanderTX = null; b.retargetAt = 0; } - } - let tgtX = b.tx, tgtY = b.ty; - if (b.eliminated || !answering || tgtX == null) { - /* ถูกคัดออก / ไม่ใช่ช่วงตอบ / บอทไม่ตอบ → เดินมั่ว (ผี = มั่วในโซนผี) */ - const arrived = (typeof b.wanderTX === 'number') && Math.hypot(b.wanderTX - b.x, b.wanderTY - b.y) < 0.5; - if (typeof b.wanderTX !== 'number' || arrived || now >= b.retargetAt) { - if (confineGhost) { - const c = quizRandomCellInZone(ghostGrid, md); - b.wanderTX = c ? c.x + 0.5 : b.x; - b.wanderTY = c ? c.y + 0.5 : b.y; - } else { - b.wanderTX = 1 + Math.random() * Math.max(1, w - 2); - b.wanderTY = 1 + Math.random() * Math.max(1, h - 2); - } - b.retargetAt = now + 2000 + Math.floor(Math.random() * 3000); - } - tgtX = b.wanderTX; tgtY = b.wanderTY; - } - let dx = tgtX - b.x, dy = tgtY - b.y; - const dist = Math.hypot(dx, dy); - if (dist < 0.05) { b.isWalking = false; return; } - dx /= dist; dy /= dist; - if (Math.abs(dy) > Math.abs(dx)) b.dir = dy > 0 ? 'down' : 'up'; - else b.dir = dx > 0 ? 'right' : 'left'; - const sx = Math.min(step, dist); - const ox = b.x, oy = b.y; - const nx = b.x + dx * sx, ny = b.y + dy * sx; - if (canWalk(nx, ny)) { b.x = nx; b.y = ny; } - else if (canWalk(nx, b.y)) { b.x = nx; } - else if (canWalk(b.x, ny)) { b.y = ny; } - b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5; - }); - const arr = []; - sess.bots.forEach((b, id) => arr.push({ - id, x: Math.round(b.x * 1000) / 1000, y: Math.round(b.y * 1000) / 1000, - direction: b.dir || 'down', isWalking: !!b.isWalking, elim: !!b.eliminated, - })); - if (arr.length) io.to(sid).emit('quiz-bot-move', { bots: arr }); -} - -/* ============================================================ - MG4 quiz_carry — server-authoritative engine - เดิม host-sim (เดิน/หยิบ/ส่ง/คะแนน/บอท บน client host) → ย้ายมา server: - server เป็นเจ้าของ คำถาม/เฟส/หยิบ-ส่ง/คะแนน/จบรอบ(ตอบถูกคนแรก)/บอท(BFS) + disconnect takeover - เปิดเฉพาะเกมจริง (space.detectiveMinigameActive) — editor preview คง client-sim เดิม - ============================================================ */ -const QUIZ_CARRY_POINTS_PER_CORRECT = 10; -const QUIZ_CARRY_BOT_PREFIX = '__pv_bot_'; -const QUIZ_CARRY_BOT_SPEED_PER_SEC = 0.15 * 1.28 * 60; /* ~11.5 u/s ใกล้บอทเดิม */ -const QUIZ_CARRY_BETWEEN_MS = 2500; - -function quizCarryNormalizeQuestionServer(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 == null ? '' : c)).slice(0, QUIZ_CARRY_MAX_OPTION_SLOTS); - let ci = Number(q.correctIndex); - if (!Number.isFinite(ci) || ci < 0 || ci >= choices.length) ci = 0; - if (!text) text = 'เลือกคำตอบที่ถูกต้อง'; - const out = { text, choices, correctIndex: Math.floor(ci) }; - if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) out.choiceImageUrls = q.choiceImageUrls.slice(0, choices.length); - return out; - } - if (!text) return null; - const t = !!q.answerTrue; - return { text, choices: ['จริง', 'เท็จ'], correctIndex: t ? 0 : 1 }; -} - -/** ชุดคำถาม carry — ลำดับ source ตรงกับ client: global carryQuestions → global questions → map.quizQuestions */ -function getQuizCarryQuestionPool(md) { - const g = loadQuizSettings(); - const src = (Array.isArray(g.carryQuestions) && g.carryQuestions.length) ? g.carryQuestions - : (Array.isArray(g.questions) && g.questions.length) ? g.questions - : (Array.isArray(md.quizQuestions) ? md.quizQuestions : []); - const out = []; - for (const q of src) { const n = quizCarryNormalizeQuestionServer(q); if (n) out.push(n); } - return out; -} - -function quizCarryShuffleServer(a) { - for (let i = a.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - const t = a[i]; a[i] = a[j]; a[j] = t; - } - return a; -} - -/** entity (รวม footprint) ยืนที่ tile (tx,ty) ได้ไหม — เดินได้ทุกช่อง + ไม่ทับ hub (hub = กำแพง) */ -function quizCarryEntityCanStandServer(md, tx, ty) { - const keys = quizCarryFootprintTileKeys(md, tx + 0.5, ty + 0.5); - for (const k of keys) { - const [cx, cy] = k.split(',').map(Number); - if (!quizTileWalkableServer(md, cx + 0.5, cy + 0.5)) return false; - if (quizCellOn(md.quizCarryHubArea || [], cx, cy)) return false; - /* โต๊ะ submit กลาง (interactive/เขียว) = สิ่งกีดขวาง ห้ามยืนทับ/เดินทะลุ (ส่งคำตอบจากช่องข้างๆ ได้ — quizCarryCanSubmitAtServer รับ adjacent) */ - if (quizCellOn(md.interactive || [], cx, cy)) return false; - } - return true; -} - -/** option index (0-based) ที่ footprint ที่ (px,py) ยืนทับ (grid เก็บ 1..N) — null=ไม่ทับ */ -function quizCarryOptionIndexAtServer(md, px, py, choiceCount) { - const oa = md.quizCarryOptionArea; - if (!Array.isArray(oa)) return null; - for (const k of quizCarryFootprintTileKeys(md, px, py)) { - const [tx, ty] = k.split(',').map(Number); - const v = oa[ty] && Number(oa[ty][tx]); - if (Number.isFinite(v) && v >= 1 && v <= QUIZ_CARRY_MAX_OPTION_SLOTS) { - const idx = v - 1; - if (!(choiceCount > 0) || idx < choiceCount) return idx; - } - } - return null; -} - -function quizCarryFootprintAdjacentHubServer(md, px, py) { - if (!md || !Array.isArray(md.quizCarryHubArea)) return false; - const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; - for (const k of quizCarryFootprintTileKeys(md, px, py)) { - const [tx, ty] = k.split(',').map(Number); - if (quizCellOn(md.quizCarryHubArea, tx, ty)) continue; - for (const d of dirs) if (quizCellOn(md.quizCarryHubArea, tx + d[0], ty + d[1])) return true; - } - return false; -} -function quizCarryMapHasInteractiveServer(md) { - if (!md || !Array.isArray(md.interactive)) return false; - for (let y = 0; y < md.interactive.length; y++) { - const row = md.interactive[y]; - if (!row) continue; - for (let x = 0; x < row.length; x++) if (row[x] === 1) return true; - } - return false; -} -function quizCarryFootprintOverlapsInteractiveServer(md, px, py) { - if (!md || !Array.isArray(md.interactive)) return false; - for (const k of quizCarryFootprintTileKeys(md, px, py)) { - const [tx, ty] = k.split(',').map(Number); - if (quizCellOn(md.interactive, tx, ty)) return true; - } - return false; -} -function quizCarryFootprintAdjacentInteractiveServer(md, px, py) { - if (!md || !Array.isArray(md.interactive)) return false; - const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; - for (const k of quizCarryFootprintTileKeys(md, px, py)) { - const [tx, ty] = k.split(',').map(Number); - if (quizCellOn(md.interactive, tx, ty)) continue; - for (const d of dirs) if (quizCellOn(md.interactive, tx + d[0], ty + d[1])) return true; - } - return false; -} -/** ส่งคำตอบได้ไหมที่ (px,py) — มีโซน interactive(เขียว) ใช้โซนนั้น ไม่งั้นชิด/ทับ hub(ม่วง) */ -function quizCarryCanSubmitAtServer(md, px, py) { - if (quizCarryMapHasInteractiveServer(md)) { - return quizCarryFootprintOverlapsInteractiveServer(md, px, py) || quizCarryFootprintAdjacentInteractiveServer(md, px, py); - } - return quizCarryFootprintOverlapsHubServer(md, px, py) || quizCarryFootprintAdjacentHubServer(md, px, py); -} - -/** option idx นี้มีคน/บอทถืออยู่แล้วไหม (exclusive lock) — ยกเว้น actor เอง */ -function quizCarryOptionHeldByAnyoneServer(s, optionIdx, exceptId) { - if (optionIdx == null || optionIdx < 0) return false; - for (const id in s.players) { - if (String(id) === String(exceptId)) continue; - if (s.players[id] && s.players[id].held === optionIdx) return true; - } - let found = false; - s.bots.forEach((b, bid) => { - if (found || String(bid) === String(exceptId)) return; - if (b && b.held === optionIdx) found = true; - }); - return found; -} - -/* ช่องที่บอท "เดินถึงได้จริง" (footprint-aware BFS) จากโซน option/spawn — กันบอทสุ่มเกิด/ติดใน - "คอนโซลกลาง" ที่ยืนได้แต่ออกไม่ได้ (footprint 3×4 ลอดช่องแคบ/ทางที่ติดกำแพง+hub+interactive ไม่ได้ - = อาการ "บอทยืนบนโต๊ะ"). cache ต่อ md (โครงสร้างแมปคงที่ต่อรอบเกม) */ -function quizCarryReachableTilesServer(md) { - if (md.__qcReach) return md.__qcReach; - const w = md.width || 20, h = md.height || 15; - const oa = md.quizCarryOptionArea, sa = md.spawnArea; - const seeds = []; - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const isSeed = (Array.isArray(oa) && oa[y] && Number(oa[y][x]) >= 1) || (Array.isArray(sa) && quizCellOn(sa, x, y)); - if (isSeed && quizCarryEntityCanStandServer(md, x, y)) seeds.push([x, y]); - } - } - const reach = new Set(); - if (!seeds.length) { - /* ไม่มี seed (แมปไม่มี option/spawn) → ไม่กรอง: ทุก standable นับว่าถึงได้ */ - for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (quizCarryEntityCanStandServer(md, x, y)) reach.add(x + ',' + y); - md.__qcReach = reach; - return reach; - } - const q = []; - for (const s of seeds) { const k = s[0] + ',' + s[1]; if (!reach.has(k)) { reach.add(k); q.push(s); } } - while (q.length) { - const cur = q.shift(); - const nb = [[cur[0] + 1, cur[1]], [cur[0] - 1, cur[1]], [cur[0], cur[1] + 1], [cur[0], cur[1] - 1]]; - for (const n of nb) { - const k = n[0] + ',' + n[1]; - if (!reach.has(k) && quizCarryEntityCanStandServer(md, n[0], n[1])) { reach.add(k); q.push(n); } - } - } - md.__qcReach = reach; - return reach; -} -function quizCarryTileReachableServer(md, x, y) { - return quizCarryReachableTilesServer(md).has(Math.floor(x) + ',' + Math.floor(y)); -} - -/** รวมรายชื่อช่องว่าง (เดินยืนได้ + ถึงได้จริง + ไม่ใช่ option/interactive) สำหรับเกิดบอท */ -function quizCarrySpawnTilesServer(md) { - const w = md.width || 20, h = md.height || 15; - const free = [], any = []; - const sa = md.spawnArea; - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - if (!quizCarryEntityCanStandServer(md, x, y)) continue; - if (!quizCarryTileReachableServer(md, x, y)) continue; /* ข้ามช่องที่ยืนได้แต่ออกไม่ได้ (คอนโซลกลาง) → บอทไม่เกิดติด */ - any.push({ x, y }); - const isZone = (Array.isArray(md.quizCarryOptionArea) && md.quizCarryOptionArea[y] && Number(md.quizCarryOptionArea[y][x]) >= 1) - || quizCellOn(md.interactive || [], x, y); - if (isZone) continue; - if (Array.isArray(sa) && sa.length) { if (quizCellOn(sa, x, y)) free.push({ x, y }); } - else free.push({ x, y }); - } - } - return free.length ? free : any; -} - -function createQuizCarryBots(space, md) { - const botCount = effectiveBotSlotCount(space); - const bots = new Map(); - const tiles = quizCarrySpawnTilesServer(md); - for (let i = 0; i < botCount; i++) { - const id = QUIZ_CARRY_BOT_PREFIX + i; - const t = tiles.length ? tiles[(i * 3 + 1) % tiles.length] : { x: 1, y: 1 }; - bots.set(id, { - tier: quizBotTierForSlot(i), - x: t.x + 0.5, y: t.y + 0.5, dir: 'down', isWalking: false, - held: null, score: 0, - botPath: [], pathfindAfter: 0, wantIdx: null, - wanderTX: null, wanderTY: null, retargetAt: 0, - }); - } - return bots; -} - -/** BFS หาเส้นทาง tile→tile (entity stand check + หลบ hub) → คืน array tile-center {x,y} (ไม่รวมจุดเริ่ม) */ -function quizCarryPathfindServer(md, sx, sy, gx, gy) { - const w = md.width || 20, h = md.height || 15; - const startX = Math.floor(sx), startY = Math.floor(sy); - const goalX = Math.floor(gx), goalY = Math.floor(gy); - if (startX === goalX && startY === goalY) return []; - const key = (x, y) => y * w + x; - const prev = new Map(); - const seen = new Uint8Array(w * h); - const q = [[startX, startY]]; - seen[key(startX, startY)] = 1; - const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; - let found = false; - while (q.length) { - const [cx, cy] = q.shift(); - if (cx === goalX && cy === goalY) { found = true; break; } - for (const d of dirs) { - const nx = cx + d[0], ny = cy + d[1]; - if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue; - if (seen[key(nx, ny)]) continue; - /* ปลายทางอาจเป็น option/hub-edge ที่ entity ยืนได้; เช็ค stand เฉพาะช่องที่ไม่ใช่ goal */ - if (!(nx === goalX && ny === goalY) && !quizCarryEntityCanStandServer(md, nx, ny)) continue; - if (nx === goalX && ny === goalY && !quizCarryEntityCanStandServer(md, nx, ny)) { - /* goal ยืนไม่ได้ (เช่นชิด hub) — ยอมรับถ้าเดินได้พื้นฐาน */ - if (!quizTileWalkableServer(md, nx + 0.5, ny + 0.5)) continue; - } - seen[key(nx, ny)] = 1; - prev.set(key(nx, ny), key(cx, cy)); - q.push([nx, ny]); - } - } - if (!found) return []; - const path = []; - let cur = key(goalX, goalY); - const startKey = key(startX, startY); - while (cur !== startKey) { - const x = cur % w, y = Math.floor(cur / w); - path.push({ x: x + 0.5, y: y + 0.5 }); - const p = prev.get(cur); - if (p == null) break; - cur = p; - } - path.reverse(); - return path; -} - -/** หยิบ/ส่ง (server เป็นเจ้าของ) — คืน true ถ้ามีการเปลี่ยนสถานะ · clientPos = ตำแหน่งที่ client รายงานตอนกด grab (ใช้ถ้าใกล้ server ≤1.25 tile) */ -function quizCarryServerTryInteract(sid, space, actorId, isBot, clientPos) { - const s = space.quizCarrySession; - if (!s || !s.active || s.ended) return false; - if (s.phase !== 'answer') return false; - const md = s.mapMd; - if (!md || !s.current) return false; - let px, py; - if (isBot) { const b = s.bots.get(actorId); if (!b) return false; px = b.x; py = b.y; } - else { - const p = space.peers.get(actorId); - if (!p) return false; - px = Number(p.x); py = Number(p.y); - /* client รายงานตำแหน่งตอนกด F — ถ้าใกล้ server พอ ใช้ client เพื่อลด desync หยิบไม่ติด/เด้ง */ - if (clientPos && typeof clientPos === 'object') { - const cx = Number(clientPos.x), cy = Number(clientPos.y); - if (Number.isFinite(cx) && Number.isFinite(cy) && Number.isFinite(px) && Number.isFinite(py)) { - if (Math.hypot(cx - px, cy - py) <= 1.25) { px = cx; py = cy; } - } - } - } - if (!Number.isFinite(px) || !Number.isFinite(py)) return false; - const holder = isBot ? s.bots.get(actorId) : s.players[actorId]; - if (!holder) return false; - const choiceCount = (s.current.choices || []).length; - const pickupIdx = quizCarryOptionIndexAtServer(md, px, py, choiceCount); - const canSubmit = quizCarryCanSubmitAtServer(md, px, py); - const held = holder.held; - - /* ถือป้าย + ยืนโซนส่ง → ส่งคำตอบ */ - if (held != null && canSubmit) { - holder.held = null; - const ok = held === s.current.correctIndex; - io.to(sid).emit('quiz-carry-held', { id: actorId, held: null }); - /* แจ้งผลถูก/ผิด "บรอดแคสต์ให้ทุกคน" → โชว์ไอคอน ✓/✗ เหนือหัวทั้งคนจริงและบอท (ทุกจอเห็นพร้อมกัน) */ - io.to(sid).emit('quiz-carry-answer-feedback', { id: actorId, correct: ok }); - if (ok) { - holder.score = (Number(holder.score) || 0) + QUIZ_CARRY_POINTS_PER_CORRECT; - resolveQuizCarryRound(sid, space, actorId); - } - return true; - } - /* ว่างมือ + ยืนโซนตัวเลือก → หยิบ (ถ้าไม่มีคนถือ) */ - if (held == null && pickupIdx != null) { - if (quizCarryOptionHeldByAnyoneServer(s, pickupIdx, actorId)) return false; - holder.held = pickupIdx; - io.to(sid).emit('quiz-carry-held', { id: actorId, held: pickupIdx }); - return true; - } - return false; -} - -function quizCarryScoresSnapshot(space) { - const s = space.quizCarrySession; - const scores = {}; - if (!s) return scores; - for (const id in s.players) scores[id] = Number(s.players[id].score) || 0; - s.bots.forEach((b, bid) => { scores[bid] = Number(b.score) || 0; }); - return scores; -} - -/** จบรอบ (มีคนตอบถูก หรือหมดเวลา) → เคลียร์ held ทุกคน → ข้อถัดไป / จบเซสชัน */ -function resolveQuizCarryRound(sid, space, winnerId) { - const s = space.quizCarrySession; - if (!s || !s.active || s.ended) return; - if (s.roundResolved) return; - s.roundResolved = true; - s.phase = 'between'; /* บอทหยุด interact ระหว่างช่วงพักรอข้อถัดไป */ - clearSpaceQuizTimers(space); - /* เคลียร์ held ทุกคน (รอบจบ) */ - for (const id in s.players) s.players[id].held = null; - s.bots.forEach((b) => { b.held = null; b.botPath = []; b.wantIdx = null; }); - s.roundsCompleted++; - const willEnd = s.sessionLength > 0 && s.roundsCompleted >= s.sessionLength; - /* resolve ชื่อผู้ชนะ (ให้ client โชว์ชื่อจริง ไม่ใช่ 'ผู้เล่น' ตอน churn) + diag: ใครตอบถูก (คน/บอท) */ - let winnerNick = '', winnerIsBot = false; - if (winnerId) { - if (s.bots && s.bots.has(winnerId)) { winnerIsBot = true; } - else { const wp = space.peers.get(winnerId); if (wp && wp.nickname) winnerNick = wp.nickname; } - } - try { glog(sid, space, 'mg4-round', 'resolve ' + s.roundsCompleted + '/' + s.sessionLength + ' q=' + s.qIndex + ' winner=' + (winnerId ? ('[TC169] ' + (winnerNick || (winnerIsBot ? 'BOT ' + winnerId : winnerId))) : '-(หมดเวลา ไม่มีคนถูก)') + ' correctIdx=' + (s.current ? s.current.correctIndex : '?') + ' willEnd=' + willEnd); } catch (e) { /* ignore */ } - io.to(sid).emit('quiz-carry-result', { - questionIndex: s.qIndex + 1, - correctIndex: s.current ? s.current.correctIndex : null, - winnerId: winnerId || null, - winnerNick, - winnerIsBot, - scores: quizCarryScoresSnapshot(space), - willEnd, - }); - if (willEnd) { - const t = setTimeout(() => endQuizCarryGame(sid, space), 900); - space.quizTimers.push(t); - return; - } - const t = setTimeout(() => scheduleQuizCarryReadPhase(sid, space), QUIZ_CARRY_BETWEEN_MS); - space.quizTimers.push(t); -} - -/** หมดเวลาตอบ (ไม่มีใครถูก) → จบรอบเหมือนกัน (ไม่ให้คะแนน) */ -function resolveQuizCarryTimeup(sid, space) { - const s = space.quizCarrySession; - if (!s || !s.active || s.ended || s.roundResolved) return; - resolveQuizCarryRound(sid, space, null); -} - -function scheduleQuizCarryReadPhase(sid, space) { - const s = space.quizCarrySession; - if (!s || !s.active || s.ended) return; - if (!s.pool.length) { endQuizCarryGame(sid, space); return; } - /* server สุ่มคำถามใหม่ (วนชุดถ้าเล่นยาวกว่าจำนวนข้อ) */ - if (s.deck == null || !s.deck.length) s.deck = quizCarryShuffleServer(s.pool.slice()); - s.current = s.deck.shift(); - s.qIndex++; - s.lastAdvanceAt = Date.now(); - try { glog(sid, space, 'mg4-read', 'q=' + s.qIndex + ' deck=' + (s.deck ? s.deck.length : 0) + ' rounds=' + s.roundsCompleted + '/' + s.sessionLength); } catch (e) { /* ignore */ } - s.roundResolved = false; - s.phase = 'read'; - const readMs = Math.max(0, s.carryReadMs | 0); - s.optionRevealAt = Date.now() + readMs; - s.phaseEndsAt = s.optionRevealAt; - io.to(sid).emit('quiz-carry-phase', { - srv: true, - phase: 'read', - questionIndex: s.qIndex, - questionTotal: s.sessionLength || s.pool.length, - text: s.current.text, - choices: s.current.choices, - correctIndex: s.current.correctIndex, - choiceImageUrls: s.current.choiceImageUrls || null, - optionRevealAt: s.optionRevealAt, - endsAt: s.phaseEndsAt, - }); - if (readMs <= 0) { scheduleQuizCarryAnswerPhase(sid, space); return; } - pausableSpecialQuizTimeout(sid, space, () => scheduleQuizCarryAnswerPhase(sid, space), readMs); /* หยุดนับตอนคำถามพิเศษ */ -} - -function scheduleQuizCarryAnswerPhase(sid, space) { - const s = space.quizCarrySession; - if (!s || !s.active || s.ended) return; - s.phase = 'answer'; - s.lastAdvanceAt = Date.now(); - const ansMs = Math.max(1000, s.carryAnswerMs | 0); - s.optionRevealAt = Date.now(); - s.phaseEndsAt = Date.now() + ansMs; - io.to(sid).emit('quiz-carry-phase', { - srv: true, - phase: 'answer', - questionIndex: s.qIndex, - questionTotal: s.sessionLength || s.pool.length, - text: s.current.text, - choices: s.current.choices, - correctIndex: s.current.correctIndex, - choiceImageUrls: s.current.choiceImageUrls || null, - optionRevealAt: s.optionRevealAt, - endsAt: s.phaseEndsAt, - }); - pausableSpecialQuizTimeout(sid, space, () => resolveQuizCarryTimeup(sid, space), ansMs); /* หยุดนับตอนคำถามพิเศษ */ -} - -function startQuizCarryGame(sid, space) { - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'quiz_carry') return; - if (!space.detectiveMinigameActive) return; /* เกมจริงเท่านั้น — editor preview คง client-sim */ - if (space.quizCarrySession && space.quizCarrySession.active) return; - clearSpaceQuizTimers(space); - const settings = loadQuizSettings(); - const pool = getQuizCarryQuestionPool(md); - const players = {}; - space.peers.forEach((_, peerId) => { players[peerId] = { held: null, score: 0 }; }); - let sessionLength = clampCarrySessionLength(settings.carrySessionLength, 0); - /* 0 = ไม่ได้ตั้งค่า → จบเมื่อ "ถามครบทุกข้อในชุด" รอบเดียว (ไม่วนซ้ำ) ตรงกับที่ผู้เล่นคาดหวังว่า - "พอจบทุกคำถาม เกมต้องจบ"; ถ้า pool ว่างค่อย fallback 10 (กันหารด้วยศูนย์/ค้าง) */ - if (!(sessionLength > 0)) sessionLength = Math.max(1, pool.length || 10); - space.quizCarrySession = { - active: true, ended: false, - pool, deck: null, current: null, qIndex: 0, - phase: 'idle', phaseEndsAt: 0, optionRevealAt: 0, - carryReadMs: Math.max(0, Number(settings.carryReadMs) || 3000), - carryAnswerMs: Math.max(1000, Number(settings.carryAnswerMs) || 5000), - sessionLength, roundsCompleted: 0, roundResolved: false, - players, bots: createQuizCarryBots(space, md), mapMd: md, - }; - /* คนหลุดช่วง how-to → seed เป็นบอท takeover (BFS เล่นต่อ) — แทนคนที่ออก ใช้ socket.id เดิม ไม่ชน id บอท AI */ - { - const sC = space.quizCarrySession; - drainPendingMissionTakeovers(space, 'quiz_carry', (rec) => { - sC.bots.set(rec.id, { - tier: 'avg', - x: Number.isFinite(rec.x) ? rec.x : 1, y: Number.isFinite(rec.y) ? rec.y : 1, - dir: rec.direction || 'down', isWalking: false, - held: null, score: 0, - botPath: [], pathfindAfter: 0, wantIdx: null, - wanderTX: null, wanderTY: null, retargetAt: 0, - takeover: true, takeoverPlayerKey: rec.playerKey || '', - }); - }); - } - if (!pool.length) { endQuizCarryGame(sid, space); return; } - /* watchdog กันเกมค้างไม่จบ: เก็บนอก space.quizTimers (เพราะ resolveQuizCarryRound เคลียร์ array นั้นทุกข้อ) - ถ้าไม่มีความคืบหน้า (lastAdvanceAt) นานเกิน → force resolve/จบ — ป้องกัน "เล่นจนครบแต่เกมไม่ยอมจบ" */ - { - const sC = space.quizCarrySession; - sC.lastAdvanceAt = Date.now(); - const perRound = Math.max(0, sC.carryReadMs) + Math.max(1000, sC.carryAnswerMs) + QUIZ_CARRY_BETWEEN_MS + 1500; - const stallMs = perRound + 20000; /* ช่วงเวลาที่ "ยอมรับได้" ต่อข้อ ก่อนถือว่าค้าง */ - if (space.quizCarryWatchdog) clearInterval(space.quizCarryWatchdog); - space.quizCarryWatchdog = setInterval(() => { - const sp = spaces.get(sid); - const ss = sp && sp.quizCarrySession; - if (!sp || !ss || !ss.active || ss.ended) { clearInterval(space.quizCarryWatchdog); space.quizCarryWatchdog = null; return; } - const idle = Date.now() - (ss.lastAdvanceAt || 0); - if (idle > stallMs) { - try { glog(sid, sp, 'mg4-watchdog', 'STALL idle=' + idle + 'ms phase=' + ss.phase + ' round=' + ss.roundsCompleted + '/' + ss.sessionLength); } catch (e) { /* ignore */ } - ss.lastAdvanceAt = Date.now(); - /* ดันให้ไปต่อ: ถ้ายังตอบ/อ่านค้าง → จบรอบนี้ (ไม่ให้คะแนน) แล้วระบบจะเลื่อนข้อหรือจบเองตาม willEnd */ - if (!ss.roundResolved) { try { resolveQuizCarryRound(sid, sp, null); } catch (e) { /* ignore */ } } - else { try { scheduleQuizCarryReadPhase(sid, sp); } catch (e) { /* ignore */ } } - } - }, 5000); - } - scheduleQuizCarryReadPhase(sid, space); -} - -function endQuizCarryGame(sid, space) { - const s = space.quizCarrySession; - if (s) { s.active = false; s.ended = true; } - if (space.quizCarryWatchdog) { clearInterval(space.quizCarryWatchdog); space.quizCarryWatchdog = null; } - try { glog(sid, space, 'mg4-end', 'END rounds=' + (s ? s.roundsCompleted : '?') + '/' + (s ? s.sessionLength : '?')); } catch (e) { /* ignore */ } - clearSpaceQuizTimers(space); - io.to(sid).emit('quiz-carry-ended', { scores: quizCarryScoresSnapshot(space) }); - if (space.detectiveMinigameActive) { - returnDetectiveSpaceToLobbyB(sid, space, 'จบมินิเกม', { allPlayers: true }); - } -} - -/** เดินบอท carry (BFS ไป option → หยิบ → BFS ไปโซนส่ง → ส่ง) + emit ตำแหน่ง/held */ -function stepQuizCarryBots(sid, space, dt, now) { - const s = space.quizCarrySession; - if (!s || !s.active || s.ended || !s.bots || s.bots.size === 0) return; - const md = s.mapMd; - if (!md) return; - const w = md.width || 20, h = md.height || 15; - const step = QUIZ_CARRY_BOT_SPEED_PER_SEC * dt; - const answering = s.phase === 'answer'; - const choiceCount = s.current ? (s.current.choices || []).length : 0; - - /* ก้าวไป (x,y) ได้ไหม: ต้อง "ยืนได้ทั้ง footprint" (canStand = ทุกช่องของตัวละครไม่ใช่กำแพง/ไม่ทับ hub/ไม่ทับโต๊ะ interactive) - — เดิมเช็คแค่ quizTileWalkableServer ที่ "ช่อง anchor จุดเดียว" ทำให้ footprint ขอบตัวเลื่อนคร่อมกำแพงได้ → - ตกไปช่องที่ reachability-BFS (footprint-aware) ถือว่า "ออกไม่ได้" → โดน push-out teleport ทุก tick = อาการ "บอทว๊าป". - ใช้ canStand ให้ตรงกับ pathfind (ที่วางเส้นบน canStand อยู่แล้ว) → บอทไม่เดินเข้าช่องที่จะถูกดันออก */ - const canStepTo = (x, y) => quizCarryEntityCanStandServer(md, Math.floor(x), Math.floor(y)); - - const followPath = (b) => { - if (!b.botPath || !b.botPath.length) return false; - const tgt = b.botPath[0]; - let dx = tgt.x - b.x, dy = tgt.y - b.y; - const dist = Math.hypot(dx, dy); - if (dist < 0.08) { b.botPath.shift(); b.isWalking = b.botPath.length > 0; return true; } - dx /= dist; dy /= dist; - if (Math.abs(dy) > Math.abs(dx)) b.dir = dy > 0 ? 'down' : 'up'; - else b.dir = dx > 0 ? 'right' : 'left'; - const sx = Math.min(step, dist); - const nx = b.x + dx * sx, ny = b.y + dy * sx; - const ox = b.x, oy = b.y; - if (canStepTo(nx, ny)) { b.x = nx; b.y = ny; } - else if (canStepTo(nx, b.y)) { b.x = nx; } - else if (canStepTo(b.x, ny)) { b.y = ny; } - else { b.botPath = []; } - b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5; - return true; - }; - - const wander = (b) => { - const arrived = (typeof b.wanderTX === 'number') && Math.hypot(b.wanderTX - b.x, b.wanderTY - b.y) < 0.5; - if (typeof b.wanderTX !== 'number' || arrived || now >= b.retargetAt) { - b.wanderTX = 1 + Math.random() * Math.max(1, w - 2); - b.wanderTY = 1 + Math.random() * Math.max(1, h - 2); - b.retargetAt = now + 2000 + Math.floor(Math.random() * 3000); - } - let dx = b.wanderTX - b.x, dy = b.wanderTY - b.y; - const dist = Math.hypot(dx, dy); - if (dist < 0.05) { b.isWalking = false; return; } - dx /= dist; dy /= dist; - if (Math.abs(dy) > Math.abs(dx)) b.dir = dy > 0 ? 'down' : 'up'; - else b.dir = dx > 0 ? 'right' : 'left'; - const sx = Math.min(step, dist); - const nx = b.x + dx * sx, ny = b.y + dy * sx; - const ox = b.x, oy = b.y; - if (canStepTo(nx, ny)) { b.x = nx; b.y = ny; } - else if (canStepTo(nx, b.y)) { b.x = nx; } - else if (canStepTo(b.x, ny)) { b.y = ny; } - else { b.wanderTX = null; } - b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5; - }; - - s.bots.forEach((b, bid) => { - /* ช่วงอ่านคำถาม (read) → บอท freeze อยู่ตรงไหนตรงนั้น (ตรงกับผู้เล่นที่ freeze ฝั่ง client) */ - if (s.phase === 'read') { b.isWalking = false; return; } - /* ทับ hub / โต๊ะ submit กลาง (interactive) / หรือ "ติดในคอนโซลกลาง" (ยืนได้แต่เดินออกไม่ได้) → ดันออกไปช่องว่างที่ถึงได้ - หมายเหตุ: หลังแก้ canStepTo → canStand แล้ว บอทไม่ควรเดินเข้าช่องแบบนี้เอง teleport นี้จึงเป็น safety-net (ควรยิงน้อยมาก) - — diag: glog ทุกครั้งที่ teleport จริง เพื่อยืนยันว่า "ว๊าป" ลดลง (ถอด log ออกเมื่อยืนยันแล้ว) */ - if (quizCarryFootprintOverlapsHubServer(md, b.x, b.y) || quizCarryFootprintOverlapsInteractiveServer(md, b.x, b.y) || !quizCarryTileReachableServer(md, b.x, b.y)) { - const tiles = quizCarrySpawnTilesServer(md); - if (tiles.length) { - const fromX = b.x, fromY = b.y; - const t = tiles[(b.x | 0) % tiles.length]; b.x = t.x + 0.5; b.y = t.y + 0.5; b.botPath = []; - try { glog(sid, space, 'mg4-warp', '[TC158] push-out teleport ' + bid + ' (' + fromX.toFixed(2) + ',' + fromY.toFixed(2) + ')->(' + b.x.toFixed(2) + ',' + b.y.toFixed(2) + ') — event นี้ = ยังว๊าป (fail); ไม่มี event ตลอดเกม = pass'); } catch (e) { /* ignore */ } - } - } - /* ยังไม่มีเส้นทาง → ลองหยิบ/ส่งที่จุดยืนก่อน */ - if (!b.botPath || !b.botPath.length) quizCarryServerTryInteract(sid, space, bid, true); - if (!answering || !s.current) { wander(b); return; } - if (b.botPath && b.botPath.length) { followPath(b); return; } - if (now < (b.pathfindAfter || 0)) { wander(b); return; } - const tier = b.tier || 'avg'; - const pCorrect = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.35 : 0.65; - if (b.held == null) { - /* เลือกข้อจะหยิบ (เลี่ยงข้อที่มีคนถือ) */ - let want = Math.random() < pCorrect ? s.current.correctIndex : Math.floor(Math.random() * Math.max(2, choiceCount)); - if (quizCarryOptionHeldByAnyoneServer(s, want, bid)) { - for (let i = 0; i < choiceCount; i++) { if (!quizCarryOptionHeldByAnyoneServer(s, i, bid)) { want = i; break; } } - } - const dest = quizCarryFindOptionTileServer(md, want, b); - if (dest) { b.botPath = quizCarryPathfindServer(md, b.x, b.y, dest.x + 0.5, dest.y + 0.5); b.wantIdx = want; } - b.pathfindAfter = now + 160 + Math.floor(Math.random() * 200); - if (!b.botPath || !b.botPath.length) wander(b); - } else { - const sub = quizCarryFindSubmitTileServer(md, b); - if (sub) b.botPath = quizCarryPathfindServer(md, b.x, b.y, sub.x + 0.5, sub.y + 0.5); - b.pathfindAfter = now + 160 + Math.floor(Math.random() * 200); - if (!b.botPath || !b.botPath.length) wander(b); - } - }); - - const arr = []; - s.bots.forEach((b, id) => arr.push({ - id, x: Math.round(b.x * 1000) / 1000, y: Math.round(b.y * 1000) / 1000, - direction: b.dir || 'down', isWalking: !!b.isWalking, held: b.held == null ? null : b.held, - })); - if (arr.length) io.to(sid).emit('quiz-carry-bot-move', { bots: arr }); -} - -/** หา tile โซน option (เลขช่อง want+1) ที่เดินถึงได้ — สุ่ม */ -function quizCarryFindOptionTileServer(md, optionIndex, b) { - 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 && quizTileWalkableServer(md, x + 0.5, y + 0.5)) pool.push({ x, y }); - } - if (!pool.length) return null; - return pool[Math.floor(Math.random() * pool.length)]; -} -/** หา tile โซนส่งคำตอบ (interactive ถ้ามี ไม่งั้นช่องชิด hub) */ -function quizCarryFindSubmitTileServer(md, b) { - const w = md.width || 20, h = md.height || 15; - const pool = []; - if (quizCarryMapHasInteractiveServer(md)) { - /* ช่องที่ "ยืนได้จริง + ชิดโต๊ะ interactive" (ไม่ใช่ยืนทับโต๊ะ — โต๊ะเป็นสิ่งกีดขวางแล้ว) → ส่งจากข้างๆ */ - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - if (!quizCarryEntityCanStandServer(md, x, y)) continue; - if (quizCarryFootprintAdjacentInteractiveServer(md, x + 0.5, y + 0.5)) pool.push({ x, y }); - } - } - if (pool.length) return pool[Math.floor(Math.random() * pool.length)]; - } - /* ช่องเดินได้ที่ชิด hub (ยืนส่งได้) */ - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - if (!quizCarryEntityCanStandServer(md, x, y)) continue; - if (quizCarryFootprintAdjacentHubServer(md, x + 0.5, y + 0.5)) pool.push({ x, y }); - } - } - if (!pool.length) return null; - return pool[Math.floor(Math.random() * pool.length)]; -} - -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; -} - -/** โดมติดกัน = 1 ข้อ (comp id 1,2,3…) — ตรงกับ play.js */ -function normalizeQuizBattleDomeCompOnMap(m) { - if (!m || m.gameType !== 'quiz_battle') return; - normalizeQuizBattleDomeAreaOnMap(m); - const w = m.width || 20, h = m.height || 15; - const grid = m.quizBattleDomeArea; - 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] || 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] || grid[ny][nx] !== 1 || comp[ny][nx]) continue; - comp[ny][nx] = nextId; - stack.push([nx, ny]); - } - } - } - } - m.quizBattleDomeComp = comp; -} - -/** - * ประตูบนเลน — ช่องเลนที่อยู่ "หลัง" โดม C ต้องตอบถูกโดม 1..C ก่อน - * ยืนบนโดมได้โดยไม่ต้องตอบ (dist เท่ากัน) · ผ่านจุดหลังโดมต้องตอบก่อน - */ -function hasQuizBattleManualPathGates(m) { - const mg = m && m.quizBattlePathGateArea; - if (!mg || !Array.isArray(mg)) return false; - for (let ty = 0; ty < mg.length; ty++) { - const row = mg[ty]; - if (!row) continue; - for (let tx = 0; tx < row.length; tx++) { - if ((row[tx] | 0) > 0) return true; - } - } - return false; -} - -function buildQuizBattlePathGateFromManualOnMap(m) { - const w = m.width || 20, h = m.height || 15; - const path = m.quizBattlePathArea; - const dome = m.quizBattleDomeArea; - const manual = m.quizBattlePathGateArea; - const gate = Array(h).fill(0).map(() => Array(w).fill(0)); - const bestReq = Array(h).fill(0).map(() => Array(w).fill(-1)); - const queue = []; - - function enqueue(tx, ty, req) { - if (tx < 0 || ty < 0 || tx >= w || ty >= h) return; - const onPath = path[ty] && path[ty][tx] === 1; - const onDome = dome && dome[ty] && dome[ty][tx] === 1; - if (!onPath && !onDome) return; - /* เก็บ req ต่ำสุดที่เข้าช่องได้ — กันเส้นทางวนกลับไปยก req ทับ spawn */ - if (bestReq[ty][tx] >= 0 && req >= bestReq[ty][tx]) return; - bestReq[ty][tx] = req; - queue.push({ tx, ty, req }); - } - - const sp = m.spawn || { x: 1, y: 1 }; - const stx = Math.max(0, Math.min(w - 1, Math.floor(Number(sp.x)) || 0)); - const sty = Math.max(0, Math.min(h - 1, Math.floor(Number(sp.y)) || 0)); - enqueue(stx, sty, 0); - if (bestReq[sty][stx] < 0) { - for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) enqueue(stx + dx, sty + dy, 0); - } - const sa = m.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) enqueue(tx, ty, 0); - } - } - } - - while (queue.length) { - const cur = queue.shift(); - const { tx, ty, req } = cur; - if (bestReq[ty][tx] !== req) continue; - const onDome = dome && dome[ty] && dome[ty][tx] === 1; - let enterReq = req; - const mg = manual && manual[ty] && manual[ty][tx] ? (manual[ty][tx] | 0) : 0; - if (onDome) enterReq = 0; - else if (mg > 0) enterReq = Math.max(enterReq, mg); - if (path[ty] && path[ty][tx] === 1) gate[ty][tx] = enterReq; - let outReq = enterReq; - if (mg > 0) outReq = Math.max(outReq, mg); - for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { - enqueue(tx + dx, ty + dy, outReq); - } - } - m.quizBattlePathGateRequired = gate; -} - -function buildQuizBattlePathGateRequiredOnMap(m) { - if (!m || m.gameType !== 'quiz_battle' || !quizBattlePathModeActiveServer(m)) { - if (m) m.quizBattlePathGateRequired = null; - return; - } - if (hasQuizBattleManualPathGates(m)) { - buildQuizBattlePathGateFromManualOnMap(m); - return; - } - normalizeQuizBattleDomeCompOnMap(m); - const w = m.width || 20, h = m.height || 15; - const path = m.quizBattlePathArea; - const dome = m.quizBattleDomeArea; - const domeComp = m.quizBattleDomeComp; - const gate = Array(h).fill(0).map(() => Array(w).fill(0)); - const dist = Array(h).fill(0).map(() => Array(w).fill(-1)); - const queue = []; - const sp = m.spawn || { x: 1, y: 1 }; - const stx = Math.max(0, Math.min(w - 1, Math.floor(Number(sp.x)) || 0)); - const sty = Math.max(0, Math.min(h - 1, Math.floor(Number(sp.y)) || 0)); - - function tryEnqueue(tx, ty, d) { - if (tx < 0 || ty < 0 || tx >= w || ty >= h) return; - if (dist[ty][tx] >= 0) return; - const onPath = path[ty] && path[ty][tx] === 1; - const onDome = dome && dome[ty] && dome[ty][tx] === 1; - if (!onPath && !onDome) return; - dist[ty][tx] = d; - queue.push({ tx, ty, d }); - } - - tryEnqueue(stx, sty, 0); - if (dist[sty][stx] < 0) { - for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) tryEnqueue(stx + dx, sty + dy, 0); - } - const sa = m.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) tryEnqueue(tx, ty, 0); - } - } - } - - const domeMinDist = {}; - let maxComp = 0; - while (queue.length) { - const cur = queue.shift(); - const { tx, ty, d } = cur; - const dc = domeComp[ty] && domeComp[ty][tx] ? domeComp[ty][tx] : 0; - if (dc > 0) { - maxComp = Math.max(maxComp, dc); - if (domeMinDist[dc] == null || d < domeMinDist[dc]) domeMinDist[dc] = d; - } - for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { - const nx = tx + dx, ny = ty + dy; - if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue; - if (dist[ny][nx] >= 0) continue; - const onPath = path[ny] && path[ny][nx] === 1; - const onDome = dome && dome[ny] && dome[ny][nx] === 1; - if (onPath || onDome) tryEnqueue(nx, ny, d + 1); - } - } - - for (let ty = 0; ty < h; ty++) { - for (let tx = 0; tx < w; tx++) { - if (!path[ty] || path[ty][tx] !== 1) continue; - const d = dist[ty][tx]; - if (d < 0) { - gate[ty][tx] = maxComp; - continue; - } - let req = 0; - for (let c = 1; c <= maxComp; c++) { - if (domeMinDist[c] != null && domeMinDist[c] < d) req = Math.max(req, c); - } - gate[ty][tx] = req; - } - } - m.quizBattlePathGateRequired = gate; -} - -function finalizeQuizBattleMapLayersOnMap(m) { - if (!m || m.gameType !== 'quiz_battle') return; - normalizeQuizBattleDomeAreaOnMap(m); - normalizeQuizBattlePathAreaOnMap(m); - normalizeQuizBattleDomeCompOnMap(m); - buildQuizBattlePathGateRequiredOnMap(m); -} - -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 quizBattlePlayerSolvedCompsServer(space, socketId) { - if (!space || !space.qbwSolved) return new Set(); - const set = space.qbwSolved.get(socketId); - return set || new Set(); -} - -function quizBattlePathGateRequiredAtServer(m, px, py) { - const gate = m && m.quizBattlePathGateRequired; - if (!gate || !quizBattlePathModeActiveServer(m)) return 0; - const keys = quizBattleSpriteBoundsTileKeys(m, px, py); - const path = m.quizBattlePathArea; - let maxReq = 0; - for (const k of keys) { - const [tx, ty] = k.split(',').map(Number); - if (!path[ty] || path[ty][tx] !== 1) continue; - maxReq = Math.max(maxReq, gate[ty][tx] || 0); - } - return maxReq; -} - -function quizBattleFootprintOnDomeServer(m, px, py) { - const dome = m && m.quizBattleDomeArea; - if (!dome) return false; - const keys = quizBattleSpriteBoundsTileKeys(m, px, py); - for (const k of keys) { - const [tx, ty] = k.split(',').map(Number); - if (dome[ty] && dome[ty][tx] === 1) return true; - } - return false; -} - -function quizBattlePathGateAllowsServer(m, space, socketId, px, py) { - if (!m || !quizBattlePathModeActiveServer(m)) return true; - if (quizBattleFootprintOnDomeServer(m, px, py)) return true; - const maxReq = quizBattlePathGateRequiredAtServer(m, px, py); - if (maxReq <= 0) return true; - const solved = quizBattlePlayerSolvedCompsServer(space, socketId); - for (let c = 1; c <= maxReq; c++) { - if (!solved.has(c)) 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); - finalizeQuizBattleMapLayersOnMap(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); - finalizeQuizBattleMapLayersOnMap(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.specialQuizSpawnArea) m.specialQuizSpawnArea = []; - 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); - finalizeQuizBattleMapLayersOnMap(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.specialQuizSpawnArea) m.specialQuizSpawnArea = []; - 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); - finalizeQuizBattleMapLayersOnMap(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/system-stats') { - 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(getSystemStats())); - } - res.writeHead(405); - return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' })); - } - 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('?')[0].split('#')[0].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; - /* นับจาก "ว่างตั้งแต่เมื่อไหร่" (emptySince) ไม่ใช่อายุห้อง — ห้องที่อยู่มานานแล้วเพิ่งว่าง (เช่นกด refresh) จะไม่โดนลบทันที */ - const since = s.emptySince || s.createdAt; - const stale = since == null || now - since > SPACE_EMPTY_TTL_MS; - if (stale) spaces.delete(id); - } - const list = [...spaces] - .filter(([, s]) => !s.isPrivate) - .filter(([, s]) => (s.peers && s.peers.size > 0)) - .filter(([, s]) => !s.joinLocked) /* ห้องที่เริ่มคดีแล้ว (ออกจาก LobbyA) → ซ่อนจาก Join Room list ไม่ให้ใครเข้า */ - .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; - let roomPassword = ''; - if (isPrivate) { - /* ห้องส่วนตัว: ใช้ "ชื่อห้อง" เป็น spaceId (canonical = trim+lowercase) → join ด้วยชื่อ+รหัสได้ - (เดิมเป็นรหัสสุ่ม 6 ตัว ทำให้ join ด้วยชื่อไม่เจอ) */ - const canon = String(d.name || '').trim().toLowerCase(); - if (!canon) return res.writeHead(400), res.end(JSON.stringify({ ok: false, error: 'กรุณาใส่ชื่อห้อง' })); - if (spaces.has(canon)) return res.writeHead(409), res.end(JSON.stringify({ ok: false, error: 'ชื่อห้องนี้ถูกใช้แล้ว ลองชื่ออื่น' })); - sid = canon; - roomPassword = String(d.password == null ? '' : d.password).replace(/\D/g, '').slice(0, 4); - } 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(), - roomPassword, - 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; - } - /* เสียง: volume master/music/sfx — GET เปิดสาธารณะ (sound.js ทุก client อ่าน) · PUT เฉพาะแอดมิน */ - if (url === BASE_PATH + '/api/sound-settings' && req.method === 'GET') { - res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); - res.end(JSON.stringify(loadSoundSettings())); - return; - } - if (url === BASE_PATH + '/api/sound-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 = loadSoundSettings(); - const clamp = (v, cur) => { const n = Number(v); return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : cur; }; - if (d.masterVol !== undefined) s.masterVol = clamp(d.masterVol, s.masterVol); - if (d.musicVol !== undefined) s.musicVol = clamp(d.musicVol, s.musicVol); - if (d.sfxVol !== undefined) s.sfxVol = clamp(d.sfxVol, s.sfxVol); - saveSoundSettings(s); - res.writeHead(200); - res.end(JSON.stringify({ ok: true, settings: s })); - } 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]); - // production: nginx เสิร์ฟ /Game/img/* เป็น static ทั้งหมด — local node จึงต้องเสิร์ฟไฟล์ที่มีอยู่จริงในโฟลเดอร์ด้วย - // (ไม่จำกัดเฉพาะไฟล์อัปโหลด gauntlet-) ไม่งั้น asset ดีไซน์ (popup-Howto, Avartar, btn-ready…) จะ 400 เฉพาะตอนรันผ่าน node ตรง ๆ - const fname = safeGauntletStoredFilename(sub) || path.basename(sub); // basename กัน path traversal - const fp = path.join(GAUNTLET_ASSETS_DIR, fname); - if (!fname || !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'; - // ตัด query string / hash ออกก่อน resolve เป็น path ไฟล์ (เช่น /play.html?space=...&map=... → /play.html) - // ไม่ตัด จะหาไฟล์ชื่อ "play.html?space=..." ไม่เจอ → 404 (เข้า node ตรง ๆ พร้อม query) - url = url.split('?')[0].split('#')[0] || '/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; -/** เข้าเส้นชัยแล้ว — หยุด obstacle ทันที แล้วรอ grace นี้ให้ client เดินทุกคนเข้าเส้นครบก่อนค่อยจบ (ms) - ต้อง >= client GAUNTLET_FINISH_WALK_MAX_MS (2600) + เผื่อยืนที่เส้นเล็กน้อย */ -const GAUNTLET_FINISH_WALK_MS = 3000; -/** กัน client ยิง gauntlet-finish-phase เร็วผิดปกติ (ปลอม/desync) — run จริงใช้เวลานานกว่านี้มากเสมอ - server เป็นผู้ตัดสินว่ายังถึงเส้นชัยไม่ได้ ก่อนเวลานี้นับจาก liveStartMs */ -const GAUNTLET_FINISH_MIN_RUN_MS = 4000; -/** G2: หน่วงเริ่มวิ่ง — หลัง 3-2-1 จบ ค้างที่จุด start ให้ผู้เล่นเห็นเส้นเริ่มก่อน (ทั้ง bg+bots/obstacles sync จาก liveStartMs เดียวกัน) */ -const GAUNTLET_PRERUN_HOLD_MS = 1000; -/** ระยะอยู่กลางอากาศ (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); -} - -/** มินิเกม "live แล้ว" (พ้น how-to/นับถอยหลัง — session engine เริ่มแล้ว หรือ gauntlet ปล่อยวิ่งแล้ว)? - ใช้ตอน rejoin: ถ้า live แล้ว client ต้องข้าม how-to แล้วเข้าเล่นต่อทันที (เดิมโชว์ how-to เสมอ → คน reconnect ค้าง) - signal = session.active (สร้างตอน 3-2-1 จบ, ล้างตอนกลับ LobbyB) — เชื่อถือได้กว่า missionLiveBeginsAt (ไม่เคย reset ต่อเกม) */ -function isMinigameLiveForRejoinResume(space) { - if (!space) return false; - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md) return false; - switch (md.gameType) { - case 'gauntlet': return !!(space.gauntletRun && !space.gauntletRun.crownRunHeld); - case 'stack': return !!(space.stackSession && space.stackSession.active); - case 'jump_survive': return !!(space.jumpSurviveSession && space.jumpSurviveSession.active); - case 'space_shooter': return !!(space.spaceShooterSession && space.spaceShooterSession.active); - case 'balloon_boss': return !!(space.balloonBossSession && space.balloonBossSession.active); - case 'quiz': return !!(space.quizSession && space.quizSession.active); - case 'quiz_carry': return !!(space.quizCarrySession && space.quizCarrySession.active); - default: return false; - } -} - -/* ── How-to / pre-live disconnect takeover (MG3–MG7) ─────────────────────────── - มินิเกมเหล่านี้สร้าง session (bots/turnOrder) ตอน 3-2-1 จบเท่านั้น → ถ้าคนหลุดช่วง how-to / - นับถอยหลัง ยังไม่มี session ให้แปลงเป็นบอท เดิม avatar เลยหายไปเลย (ตกไป user-left). - แก้: เก็บ "pending takeover" ไว้บน space + แจ้ง client เก็บ avatar (*-player-to-bot) แล้ว - seed เป็นบอท server ตอน startXxxGame → บอทรับช่วงเล่นต่อทันทีเมื่อเกมเริ่ม เหมือน MG1/MG2 */ -function missionShellGameTypeForSpace(space) { - if (!space) return null; - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - const gt = md && md.gameType; - if (!gt) return null; - if (gt === 'space_shooter' && isSpaceShooterMissionShellSpace(space)) return 'space_shooter'; - if (gt === 'jump_survive' && isJumpSurviveMissionShellSpace(space)) return 'jump_survive'; - if (gt === 'balloon_boss' && isBalloonBossMissionShellSpace(space)) return 'balloon_boss'; - if (gt === 'stack' && isStackTowerMissionShellSpace(space)) return 'stack'; - if (gt === 'quiz_carry') return 'quiz_carry'; - return null; -} -function recordPendingMissionTakeover(space, gameType, socketId, info, playerKey, nick, wasElim) { - if (!space || !gameType || !socketId || !info) return; - if (!Array.isArray(space.pendingMissionTakeovers)) space.pendingMissionTakeovers = []; - space.pendingMissionTakeovers = space.pendingMissionTakeovers.filter((r) => r.id !== socketId); - const themeIdx = parseInt(info.lobbyColorThemeIndex, 10); - const skinIdx = parseInt(info.lobbySkinToneIndex, 10); - space.pendingMissionTakeovers.push({ - gameType, id: socketId, playerKey: playerKey || '', nick: (nick || 'ผู้เล่น'), wasEliminated: !!wasElim, - x: Number(info.x), y: Number(info.y), direction: info.direction || 'down', - characterId: info.characterId != null ? info.characterId : null, - themeIdx: Number.isFinite(themeIdx) ? themeIdx : null, - skinIdx: Number.isFinite(skinIdx) ? skinIdx : null, - spaceShooterScore: Math.max(0, Number(info.spaceShooterScore) || 0), - spaceShooterEliminated: !!info.spaceShooterEliminated, - balloonBossScore: Math.max(0, Number(info.balloonBossScore) || 0), - balloonBossBossDmg: Math.max(0, Number(info.balloonBossBossDmg) || 0), - balloonBossBalloons: Math.max(0, Number(info.balloonBossBalloons) || 0), - balloonBossEliminated: !!info.balloonBossEliminated, - jumpSurviveEliminated: !!info.jumpSurviveEliminated, - }); -} -/** seed บอท takeover จาก pending (เฉพาะ gameType นี้) — makeBot(rec) สร้างบอทใน session แล้วคืน true ถ้าสำเร็จ */ -function drainPendingMissionTakeovers(space, gameType, makeBot) { - if (!space || !Array.isArray(space.pendingMissionTakeovers) || typeof makeBot !== 'function') return 0; - const keep = []; - let n = 0; - for (const rec of space.pendingMissionTakeovers) { - if (rec.gameType !== gameType) { keep.push(rec); continue; } - try { if (makeBot(rec) !== false) n++; } catch (e) { /* ignore */ } - } - space.pendingMissionTakeovers = keep; - return n; -} - -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, - isBot: false, - })); - /* รวมบอท server-authoritative ในอันดับ/เกรด (client เติมชื่อ/อวตารจาก id) — แหล่งเดียว ไม่ให้ client คำนวณซ้ำ */ - const gr = space && space.gauntletRun; - if (gr && gr.bots && gr.bots.size) { - gr.bots.forEach((b, id) => { - rows.push({ - id, - nickname: 'บอท', - characterId: null, - baseScore: b.gauntletEliminated ? 0 : Math.max(0, b.gauntletScore | 0), - eliminated: !!b.gauntletEliminated, - isBot: true, - }); - }); - } - 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, - isBot: !!rows[i].isBot, - 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 survivorCount = rows.filter((r) => !r.eliminated).length; - /* เกรด F = ไม่มีผู้รอด (ตรงกับ client เดิม) → ไม่มีการ์ดรางวัล */ - let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; - if (survivorCount <= 0) grade = 'F'; - const rewardCard = grade === 'F' ? null : rollGauntletCrownRewardCard(grade); - return { - ranked, - totalSum, - averageScore, - grade, - rewardCard, - totalParts: ranked.map((r) => r.finalScore), - participantCount: ranked.length, - survivorCount, - }; -} - -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; -} - -/* ===== MG2 gauntlet bots — server-authoritative (roster + กระโดด + ชน + คะแนน) ===== - เดิม host จำลองบอท __pv_bot_N เอง (AI+ฟิสิกส์+ตายตกขอบ) → host หลุด = บอทหยุด/คะแนนเพี้ยน. - ย้ายมา server: บอทกระโดด/ชน/คะแนนบน server แล้วส่งใน gauntlet-sync.players (isBot) - id `__pv_bot_` 0..N-1 (N = effectiveBotSlotCount) ตรงกับ client (PREVIEW_BOT_PREFIX) */ -const GAUNTLET_BOT_PREFIX = '__pv_bot_'; - -function gauntletBotTierForSlot(i) { - const r = ((i * 73 + 17) % 100) / 100; /* deterministic per slot */ - if (r < 0.28) return 'sharp'; - if (r < 0.62) return 'avg'; - return 'weak'; -} - -/** ความน่าจะกระโดด (ตรง maybePreviewBotGauntletJump ฝั่ง client) — ชนเซลล์เดียวกัน vs คอลัมน์ถัดไป */ -function gauntletBotJumpProbs(tier) { - return { - pSame: tier === 'sharp' ? 0.96 : tier === 'weak' ? 0.62 : 0.86, - pAhead: tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.48 : 0.72, - }; -} - -/** เลเซอร์ครอบแถว py ไหม (ตรงกับ collision ใน runGauntletTick) */ -function gauntletLaserOverlapsRowServer(o, py, h) { - 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); - return py >= lo && py <= hi; -} - -/** ตัดสินใจกระโดดของบอท 1 ตัว (server) — mirror maybePreviewBotGauntletJump */ -function gauntletBotDecideJump(b, obstacles, md, w, h) { - if ((b.gauntletJumpTicks || 0) > 0) return; - let px = Math.floor(Number(b.x)) || 0; - let py = Math.floor(Number(b.y)) || 0; - px = Math.max(0, Math.min(w - 1, px)); - py = Math.max(0, Math.min(h - 1, py)); - const { ch: gauntletCh } = getCharacterFootprintWHForMove(md); - let sameCell = false; - let incomingCol = false; - for (let i = 0; i < obstacles.length; i++) { - const o = obstacles[i]; - if (!o) continue; - if (o.kind === 'lane' && typeof o.y === 'number') { - if (o.x === px && o.y >= py && o.y < py + gauntletCh) sameCell = true; - if (o.x === px + 1 && o.y >= py && o.y < py + gauntletCh) incomingCol = true; - } - if (o.kind === 'laser' && typeof o.x === 'number') { - if (o.x === px && gauntletLaserOverlapsRowServer(o, py, h)) sameCell = true; - if (o.x === px + 1 && gauntletLaserOverlapsRowServer(o, py, h)) incomingCol = true; - } - } - const { pSame, pAhead } = gauntletBotJumpProbs(b.tier || 'avg'); - const roll = Math.random(); - if (sameCell && roll < pSame) { - b.gauntletJumpTicks = getGauntletJumpTicks(); - b.gauntletJumpSeq = (b.gauntletJumpSeq || 0) + 1; - return; - } - if (incomingCol && roll < pAhead) { b.gauntletJumpTicks = getGauntletJumpTicks(); b.gauntletJumpSeq = (b.gauntletJumpSeq || 0) + 1; } -} - -/** ฟิสิกส์บอท 1 ตัว 1 tick (server) — mirror applyGauntletPhysicsToPreviewBot + runGauntletTick peer loop */ -function gauntletBotStepPhysics(b, obstacles, md, w, h, crownHeist) { - if (crownHeist && b.gauntletEliminated) return; - let px = Math.floor(Number(b.x)) || 0; - let py = Math.floor(Number(b.y)) || 0; - px = Math.max(0, Math.min(w - 1, px)); - py = Math.max(0, Math.min(h - 1, py)); - const air = (b.gauntletJumpTicks || 0) > 0; - const { ch: gauntletCh } = getCharacterFootprintWHForMove(md); - let advanceX = false; - let hitBack = false; - for (const o of 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 && gauntletLaserOverlapsRowServer(o, py, h)) { - if (air) advanceX = true; - else hitBack = true; - } - } - if (advanceX) { - px = Math.min(w - 2, px + 1); - b.gauntletJumpTicks = 0; - if (!crownHeist) b.gauntletScore = (b.gauntletScore || 0) + 1; - } else if (hitBack) { - if (crownHeist) { - if (px <= 0) { - b.gauntletEliminated = true; - b.gauntletScore = 0; - } else { - b.gauntletScore = Math.max(0, (b.gauntletScore || 0) - 10); - px = Math.max(0, px - 1); - } - } else { - px = Math.max(0, px - 1); - } - } - if ((b.gauntletJumpTicks || 0) > 0) b.gauntletJumpTicks--; - b.x = px; - b.y = py; -} - -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, - finishPhase: false, - finishEndAt: null, - gameoverAt: null, /* != null = อยู่ในช่วง grace "ตายหมด" (โชว์ game over ก่อนกลับ LobbyB) */ - }; -} - -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', - nickname: (p.nickname || '').trim() || 'ผู้เล่น', - characterId: p.characterId ?? null, - /* สีจาก lobbyB — กัน client ที่ re-create peer ผ่าน gauntlet-sync ได้สี hash สุ่มไม่ตรง */ - lobbyColorThemeIndex: p.lobbyColorThemeIndex || null, - lobbySkinToneIndex: p.lobbySkinToneIndex || null, - gauntletJumpTicks: p.gauntletJumpTicks || 0, - gauntletJumpSeq: p.gauntletJumpSeq || 0, - gauntletScore: p.gauntletScore || 0, - gauntletEliminated: !!p.gauntletEliminated, - })); - /* บอท server-authoritative — ส่งตำแหน่ง/คะแนน/สถานะให้ทุก client (identity/สี client gen จาก id) */ - if (gr.bots && gr.bots.size) { - gr.bots.forEach((b, id) => { - const row = { - id, - x: b.x, - y: b.y, - direction: b.dir || 'right', - isBot: true, - gauntletJumpTicks: b.gauntletJumpTicks || 0, - gauntletJumpSeq: b.gauntletJumpSeq || 0, - gauntletScore: b.gauntletScore || 0, - gauntletEliminated: !!b.gauntletEliminated, - }; - /* บอท takeover (คนหลุด → id เป็น socket.id จริง ไม่ใช่ __pv_bot_) — ส่งชื่อ+"(บอท)"+สี เดิม - ให้ client คงชื่อ/สีเดิมไว้ แม้ avatar ถูก re-create จาก sync (กันชื่อกลับเป็น "ผู้เล่น" + สีเพี้ยน) */ - if (b.takeover) { - row.isTakeover = true; - const baseNick = String(b.nickname || '').trim(); - if (baseNick) row.nickname = baseNick.indexOf('(บอท)') >= 0 ? baseNick : (baseNick + ' (บอท)'); - if (b.characterId != null) row.characterId = b.characterId; - if (b.lobbyColorThemeIndex != null) row.lobbyColorThemeIndex = b.lobbyColorThemeIndex; - if (b.lobbySkinToneIndex != null) row.lobbySkinToneIndex = b.lobbySkinToneIndex; - } - playersOut.push(row); - }); - } - 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, - gauntletLiveStartMs: gr.liveStartMs || null, /* G1: เวลาเริ่ม run (server) → client anchor scroll เดียวกันทุกเครื่อง */ - ...getGauntletVisualsForClient(), - }); -} - -/* ============================================================ - MG3 Stack Tower — server-authoritative engine (mnn93hpi) - server เป็นเจ้าของ: ลำดับตา (turn order), เฟสแกว่ง (swing), ดรอปบอท, คะแนน/ความคืบหน้า, จบเกม - แล้ว emit event เดิม (stack-state/swing/drop/over) ไปทุกเครื่อง (io.to) → client เป็น renderer - (client เลิก host-sim เมื่อได้ stack-state ที่มี srv:true) — บอท avatar ยังถูก spawn ฝั่ง client ตามเดิม - ============================================================ */ -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.5; /* miss เมื่อ overlap < 50% บล็อก (เดิม 30% หลวมไป = ลงเยื้อง 3 ช่องยังไม่ miss หัวใจไม่ลด) — ต้องตรงกับ client */ -const STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND = 0.38; -const STACK_MAX_CENTER_DRIFT_NO_LAND_MULT = 1.0; -const STACK_TOWER_BLOCK_WORLD_SCALE = 1.5; -const STACK_TICK_MS = 60; -const STACK_DROP_BUSY_MS = 1200; /* รอแอนิเมชันบล็อกร่วง/นิ่งบน client ก่อนถึงตาถัดไป (ลดจาก 1500 + swing เดินต่อระหว่าง busy → ไม่ค้าง) */ -const STACK_BOT_THINK_MIN_MS = 560; -const STACK_BOT_THINK_MAX_MS = 1180; -const STACK_SEATS = 6; -/* เฟส client (ตอนกดวาง) ต้องใกล้เฟส server ไม่เกินนี้ (rad) ถึงจะใช้. - เดิม 1.2rad (~0.35s@0.55Hz) → latency/desync เกินนั้น server ทิ้งเฟส client ใช้ s.phase (เลื่อนไปแล้ว) - = "เล็งกลางแต่ลงเยื้อง/ร่วง" (intermittent ตาม latency). ขยายเป็น ~1รอบ (2π ≈ 1.8s@0.55Hz) - = ครอบ latency เล่นได้จริงทั้งหมด → ใช้เฟสที่ผู้เล่นเห็นเสมอ (ลงตรงจุดที่เห็น) เหลือไว้แค่กันค่า garbage. - เกม co-op สร้างตึกร่วมกัน ไม่ต้องกันโกงเฟส */ -const STACK_DROP_PHASE_TOL = Math.PI * 2; - -function stackBoundsServer(area, w, h) { - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity, 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, minY, maxX, maxY, cx: (minX + maxX + 1) / 2, cy: (minY + maxY + 1) / 2 }; -} -function stackProgressBlocksServer() { - const n = Number(runtimeGameTiming.stackTowerProgressBlocks); - return Number.isFinite(n) && n >= 1 ? Math.max(1, Math.min(500, Math.floor(n))) : 50; -} -function stackTimeLimitSecServer() { - const t = Number(runtimeGameTiming.stackTowerMissionTimeSec); - return Number.isFinite(t) && t > 0 ? Math.max(10, Math.min(7200, Math.floor(t))) : 90; -} -function stackTeamMissesServer() { - const m = Math.floor(Number(runtimeGameTiming.stackTeamMissesMax) || 3); - return Math.max(1, Math.min(20, m)); -} -function newStackSessionState(space, md) { - const w = md.width || 20, h = md.height || 15; - const land = stackBoundsServer(md.stackLandArea, w, h); - let towerCX = land ? land.cx : w * 0.5; - let swingAmp = 1.85; - if (land) { - const span = Math.max(1, land.maxX - land.minX + 1); - swingAmp = Math.min(span * 0.44, Math.max(0.6, span * 0.36)); - } - /* rel-boost: ความกว้างสวิงตาม stackReleaseArea (ต้องตรงกับ client play.js ~12177-12180 เป๊ะ) - เดิม server ใช้แค่ land span → client (มี rel-boost) แกว่ง "กว้างกว่า" server: บล็อกห้อยกว้างแต่ตกแคบ - = ไถลเฉียงตอนปล่อย + logical ตกใกล้กลางเสมอจน "ทับ support ตลอด ไม่นับ miss → หัวใจไม่ลด". - ทำให้ 2 ฝั่งใช้สวิงเดียวกัน → ตกตรง (straight) + miss/หัวใจ ทำงานถูก */ - const rel = stackBoundsServer(md.stackReleaseArea, w, h); - if (rel) { - const spanR = Math.max(1, rel.maxX - rel.minX + 1); - swingAmp = Math.max(swingAmp, Math.min(spanR * 0.32, 3.0)); /* ต้องตรงกับ client play.js — ลดจาก 0.42/4.2 (สวิงกว้างเกิน = หน่วงนิ้วนิดเดียวเยื้องเยอะ "ต่อตรงแต่ลงเยื้อง") แต่ยังกว้างพอให้ miss ได้เมื่อเยื้องจริง */ - } - const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2)); - const cfgW = Number(runtimeGameTiming.stackBlockWidthTiles); - const initW = Number.isFinite(cfgW) && cfgW > 0 ? Math.max(0.85, Math.min(3.2, cfgW)) : autoW; - const hz = Number(runtimeGameTiming.stackSwingHz); - return { - active: true, - topCenterX: towerCX, - widthTiles: initW, - initialWidthTiles: initW, - swingAmp, - phase: Math.random() * Math.PI * 2, - phaseSpeed: Number.isFinite(hz) && hz > 0 ? hz : 0.6, - layers: [], - score: 0, - combo: 0, - progressPct: 0, - teamMissesLeft: stackTeamMissesServer(), - turnOrder: [], - turnIndex: 0, - bots: new Map(), /* botId -> { tier, score, takeover?, takeoverPlayerKey? } */ - humanPts: {}, /* peerId -> pts */ - startMs: Date.now(), - busyUntil: 0, - botThinkUntil: 0, - botAimPhase: null, /* เฟสเป้าหมายฝั่งหน้า (ปล่อยตอนแกว่งวิ่งมาถึงเองธรรมชาติ — ไม่สแนป) */ - botAimDeadline: 0, - lastSwingEmit: 0, - lastStateEmit: 0, - ended: false, - timerId: null, - }; -} -/* port computeStackDropResult (gameplay-only) - phaseOverride: ตา "คนจริง" ส่งเฟสที่เห็นตอนกดมาด้วย (client phase) → คำนวณจุดลงตามที่ผู้เล่นเห็น - ไม่ใช่เฟส server ณ ตอน req มาถึง (ช้ากว่า half-RTT = บล็อกลงเยื้อง). บอท/fallback ใช้ s.phase */ -function stackComputeDropServer(s, md, phaseOverride) { - const dropPhase = (typeof phaseOverride === 'number' && Number.isFinite(phaseOverride)) ? phaseOverride : s.phase; - const raw = s.swingAmp * Math.sin(dropPhase); - const w = md.width || 20, h = md.height || 15; - const land = stackBoundsServer(md.stackLandArea, w, h); - const fallW = Math.max(0.85, Math.min(3.2, s.widthTiles != null ? s.widthTiles : s.initialWidthTiles)); - const wMul = STACK_TOWER_BLOCK_WORLD_SCALE; - const fallWEff = fallW * wMul; - let supportCx, supportW; - if (s.layers.length === 0) { - supportCx = land ? land.cx : w * 0.5; - supportW = land ? (land.maxX - land.minX + 1) : Math.max(8, fallW * 2.2); - } else { - const sup = s.layers[s.layers.length - 1]; - supportCx = sup.cx; - supportW = Math.max(0.85, Math.min(3.2, sup.w != null ? sup.w : s.initialWidthTiles)); - } - const c1 = s.topCenterX + raw; - const half1 = fallWEff * 0.5; - const half0 = (s.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 = s.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; - /* MISS = แค่ overlapMiss (บล็อกไม่ทับบล็อกล่างพอ) เท่านั้น. - เดิมมี driftMiss เทียบ placedCx กับ "กลางพื้นดินตายตัว" (anchorCx=land.cx) → ต่อเอียงเป็นบันได - (แต่ละชั้นทับชั้นล่างพอดี) พอ center สะสมห่างจากกลางพื้น >38% ฐาน บล็อกที่วางดีๆ ก็ถูกนับ miss - = อาการ "ตึกโยกตกทั้งที่ไม่ได้อยู่ริมๆ". ตัดออก — overlapMiss คือเงื่อนไขตกที่ถูกต้องอยู่แล้ว. */ - const miss = overlapMiss; - 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 = s.combo || 0; - const mult = !miss && perfect && comboBefore >= 1 ? 2 : 1; - const progPerLayer = 100 / stackProgressBlocksServer(); - const pts = miss ? 0 : (10 * mult); - const progressDelta = miss ? 0 : (progPerLayer * mult); - const supportRatio = fallWEff > 0 ? overlap / fallWEff : 0; - return { miss, perfect, pts, progressDelta, landOffsetTiles: raw, placedW: fallW, placedCx, overlap, supportRatio, delta, missThreshold }; -} -/* port applyStackHitFromDrop (state-only, no FX) */ -function stackApplyHitServer(s, hit, actor) { - if (hit.miss) { - s.combo = 0; - s.teamMissesLeft = Math.max(0, (s.teamMissesLeft != null ? s.teamMissesLeft : stackTeamMissesServer()) - 1); - return; - } - s.layers.push({ w: hit.placedW, cx: hit.placedCx }); - s.score += hit.pts; - if (hit.progressDelta > 0) s.progressPct = Math.min(100, s.progressPct + hit.progressDelta); - s.combo = hit.perfect ? s.combo + 1 : 0; - s.topCenterX = hit.placedCx; - if (actor) { - if (actor.botId) { const b = s.bots.get(actor.botId); if (b) b.score = (b.score || 0) + hit.pts; } - else if (actor.id != null) { s.humanPts[actor.id] = (s.humanPts[actor.id] || 0) + hit.pts; } - } -} -/* port previewBotAlignStackSwingPhasePlay — เลื่อนเฟสให้บอทเล็งก่อนปล่อย (ตาม tier) */ -function stackBotAlignServer(s, md, tier) { - const w = md.width || 20, h = md.height || 15; - const land = stackBoundsServer(md.stackLandArea, w, h); - const fallW = Math.max(0.85, Math.min(3.2, s.widthTiles != null ? s.widthTiles : s.initialWidthTiles)); - let supportCx, supportW; - if (s.layers.length === 0) { supportCx = land ? land.cx : w * 0.5; supportW = land ? (land.maxX - land.minX + 1) : Math.max(8, fallW * 2.2); } - else { const sup = s.layers[s.layers.length - 1]; supportCx = sup.cx; supportW = Math.max(0.85, Math.min(3.2, sup.w != null ? sup.w : s.initialWidthTiles)); } - const errMax = tier === 'sharp' ? 0.016 : tier === 'avg' ? 0.038 : 0.17; - const layerTight = 1 / Math.sqrt(1 + (s.layers.length || 0) * 1.05); - const tri = Math.random() + Math.random() - 1; - let errTiles = tri * errMax * layerTight; - if (s.layers.length >= 2) { const L = s.layers.length; errTiles -= (s.layers[L - 1].cx - s.layers[L - 2].cx) * 0.28; } - let targetCx = supportCx + errTiles; - if (land && s.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 - s.topCenterX; - const amp = Math.max(1e-5, s.swingAmp); - const sNorm = Math.max(-1, Math.min(1, rawWant / amp)); - const phi0 = Math.asin(sNorm); - const phi1 = Math.PI - phi0; - const twoPi = Math.PI * 2; - const nearest = (anchor, p) => { let best = p, bd = Infinity; for (let k = -5; k <= 5; k++) { const c = p + k * twoPi; const dd = Math.abs(c - anchor); if (dd < bd) { bd = dd; best = c; } } return best; }; - const n0 = nearest(s.phase, phi0), n1 = nearest(s.phase, phi1); - s.phase = Math.abs(n0 - s.phase) <= Math.abs(n1 - s.phase) ? n0 : n1; -} -/* เหมือน stackBotAlignServer แต่ "ไม่สแนป" — คืนเฟสเป้าหมายที่อยู่ "ข้างหน้า" (forward) ของเฟสปัจจุบัน - ให้บอทรอแกว่งวิ่งมาถึงเองตามธรรมชาติแล้วค่อยปล่อย → client เห็นแกว่งลื่น ไม่กระตุก (กันบล็อกวาปก่อนวาง) */ -function stackBotComputeAimPhaseServer(s, md, tier) { - const w = md.width || 20, h = md.height || 15; - const land = stackBoundsServer(md.stackLandArea, w, h); - let supportCx; - if (s.layers.length === 0) { supportCx = land ? land.cx : w * 0.5; } - else { supportCx = s.layers[s.layers.length - 1].cx; } - const errMax = tier === 'sharp' ? 0.016 : tier === 'avg' ? 0.038 : 0.17; - const layerTight = 1 / Math.sqrt(1 + (s.layers.length || 0) * 1.05); - const tri = Math.random() + Math.random() - 1; - let errTiles = tri * errMax * layerTight; - if (s.layers.length >= 2) { const L = s.layers.length; errTiles -= (s.layers[L - 1].cx - s.layers[L - 2].cx) * 0.28; } - let targetCx = supportCx + errTiles; - if (land && s.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 - s.topCenterX; - const amp = Math.max(1e-5, s.swingAmp); - const sNorm = Math.max(-1, Math.min(1, rawWant / amp)); - const phi0 = Math.asin(sNorm); - const phi1 = Math.PI - phi0; - const twoPi = Math.PI * 2; - /* หาเฟสรูปแบบ {phi0,phi1}+2πk ที่ "มากกว่า" เฟสปัจจุบันเล็กน้อย (รอบถัดไปตามทิศแกว่ง) */ - const margin = 0.06; - let best = Infinity; - for (const base of [phi0, phi1]) { - let k = Math.ceil((s.phase + margin - base) / twoPi); - let cand = base + k * twoPi; - if (cand < s.phase + margin) cand += twoPi; - if (cand < best) best = cand; - } - return Number.isFinite(best) ? best : s.phase + twoPi * 0.5; -} -/* สี block ของ "คนจริง" = lobbyColorThemeIndex (1-8) → ตรงกับสี avatar ทุกจอ - (เดิมโค้ด stack ใช้ p.lobbyThemeIndex ที่ไม่เคยถูกเซ็ต → null เสมอ; และ (x%8)+1 บนค่า 1-based = เลื่อนสีผิด 1 ช่อง) */ -function normStackTheme8(n) { - const v = Math.floor(Number(n)); - return Number.isFinite(v) ? ((v - 1) % 8 + 8) % 8 + 1 : null; -} -function stackHumanThemeServer(p) { - return p ? normStackTheme8(p.lobbyColorThemeIndex) : null; -} -/* สี block ของบอท AI = สี avatar บอท slot นั้น (lobbyBotThemeBySlot[slot].themeIndex) ไม่ใช่ (slot%8)+1 */ -function stackBotThemeServer(space, slot) { - const m = space && space.lobbyBotThemeBySlot; - const t = m && m[slot] ? normStackTheme8(m[slot].themeIndex) : null; - return t || (((Math.floor(Number(slot)) % 8) + 8) % 8 + 1); -} -/* สร้างลำดับตา: humans (เรียง spawnJoinOrder) + bots เติมที่ว่าง ≤ 6 ที่นั่ง */ -function buildStackTurnOrderServer(space, s) { - try { syncLobbyBotThemeSlots(space); } catch (_e) { /* ให้ lobbyBotThemeBySlot ตรงกับสีที่ client ได้รับ → สี block บอทตรง avatar */ } - const humans = [...space.peers.entries()].filter(([id]) => !isBannedPeerForRun(space, id)); - humans.sort((a, b) => (((a[1].spawnJoinOrder || 0) - (b[1].spawnJoinOrder || 0)) || (String(a[0]) < String(b[0]) ? -1 : 1))); - const order = []; - let seat = 1; - for (const [id, p] of humans) { - if (seat > STACK_SEATS) break; - order.push({ kind: 'human', seat, id, botId: null, theme: stackHumanThemeServer(p) }); - seat++; - } - /* บอท takeover (คนหลุดช่วง how-to) ได้ seat ต่อจากคน ก่อนบอท AI — คง avatar/ตำแหน่งลำดับเดิมไว้ - ส่ง characterId/สี/ชื่อ ของคนเดิมไปด้วย: client ลบ peer ทิ้งไปแล้วตอน 'user-left' (play.js:21769) - และ botId = socket.id เดิมของเขา → others.get(botId) = null → เดิมวาดเป็นอวตารเขียว + ไม่รู้ว่าใคร */ - for (const [botId, b] of s.bots) { - if (!b || !b.takeover) continue; - if (seat > STACK_SEATS) break; - order.push({ - kind: 'bot', seat, id: null, botId, - theme: (b.theme != null ? b.theme : ((seat % 8) + 1)), - characterId: b.characterId != null ? b.characterId : null, - lobbyColorThemeIndex: b.colorThemeIdx != null ? b.colorThemeIdx : null, - lobbySkinToneIndex: b.skinToneIdx != null ? b.skinToneIdx : null, - nickname: b.nick || '', - takeover: true, /* client: บอทที่มาจากคนหลุด (ไม่ใช่บอท AI) → โชว์ชื่อเดิม + "(บอท)" */ - }); - seat++; - } - const botCount = Math.max(0, Math.min(STACK_SEATS - order.length, effectiveBotSlotCount(space))); - let bannedBotSlot = -1; - const bannedId = space.bannedThisRunPlayerId; - if (typeof bannedId === 'string' && bannedId.indexOf('__lobby_bot_') === 0) { - const n = parseInt(bannedId.slice('__lobby_bot_'.length), 10); - if (Number.isFinite(n) && n >= 0) bannedBotSlot = n; - } - const want = bannedBotSlot >= 0 ? Math.max(0, botCount - 1) : botCount; - let created = 0, slot = 0; - while (created < want && slot < STACK_SEATS + botCount && seat <= STACK_SEATS) { - if (slot === bannedBotSlot) { slot++; continue; } - const botId = GAUNTLET_BOT_PREFIX + slot; - if (!s.bots.has(botId)) s.bots.set(botId, { tier: gauntletBotTierForSlot(slot), score: 0 }); - order.push({ kind: 'bot', seat, id: null, botId, theme: stackBotThemeServer(space, slot) }); - created++; slot++; seat++; - } - s.turnOrder = order; - if (s.turnIndex >= order.length) s.turnIndex = 0; -} -function stackCurrentTurnEntry(s) { - if (!s.turnOrder || !s.turnOrder.length) return null; - const n = s.turnOrder.length; - return s.turnOrder[((s.turnIndex % n) + n) % n] || null; -} -function emitStackStateServer(sid, space) { - const s = space.stackSession; - if (!s) return; - const lthemes = s.layers.map((l) => (l.theme && l.theme >= 1 && l.theme <= 8 ? l.theme : 0)); - io.to(sid).emit('stack-state', { - srv: true, - /* ต้องส่ง characterId/สี/ชื่อ ของบอท takeover ต่อด้วย — ถ้า map ทิ้งตรงนี้ ที่ใส่ไว้ใน - buildStackTurnOrderServer ก็สูญเปล่า (client ได้แต่ theme → หน้าเขียว) */ - order: s.turnOrder.map((e) => ({ - kind: e.kind, seat: e.seat, id: e.id, botId: e.botId, theme: e.theme, - characterId: e.characterId != null ? e.characterId : null, - lobbyColorThemeIndex: e.lobbyColorThemeIndex != null ? e.lobbyColorThemeIndex : null, - lobbySkinToneIndex: e.lobbySkinToneIndex != null ? e.lobbySkinToneIndex : null, - nickname: e.nickname || '', - takeover: !!e.takeover, - })), - idx: s.turnIndex, - hostId: null, - lthemes, - /* full-sync หัวใจ + decrypt% → client ที่พลาด packet stack-drop (ช้า/churn) self-correct - (เดิมส่งเฉพาะตอน drop → พลาดแล้ว "หัวใจไม่ลด/% ไม่ตรง" ค้าง) */ - teamMissesLeft: s.teamMissesLeft, - progressPct: s.progressPct, - }); -} -function stackAdvanceTurnServer(s) { - if (!s.turnOrder || !s.turnOrder.length) return; - s.turnIndex = (s.turnIndex + 1) % s.turnOrder.length; - s.botThinkUntil = 0; - s.botAimPhase = null; -} -function stackSessionMissionGrade(space) { - const s = space.stackSession; - if (!s) return 'C'; - const scores = []; - s.turnOrder.forEach((e) => { - if (e.kind === 'bot' && e.botId) { const b = s.bots.get(e.botId); scores.push(Math.max(0, (b && b.score) | 0)); } - else if (e.id != null) scores.push(Math.max(0, s.humanPts[e.id] | 0)); - }); - const n = scores.length || 1; - const avg = Math.floor(scores.reduce((a, b) => a + b, 0) / n); - let grade = avg >= 80 ? 'A' : avg >= 60 ? 'B' : 'C'; - return grade; -} -function stackSessionEnd(sid, space, outcome) { - const s = space.stackSession; - if (!s || s.ended) return; - s.ended = true; - stopStackTicker(space); - /* คะแนนผู้เล่นจริง → reportedMiniScore (ใช้ทางเดิมของ coin/evidence) */ - s.turnOrder.forEach((e) => { - if (e.kind === 'human' && e.id != null) { - const p = space.peers.get(e.id); - if (p) { p.reportedMiniScore = Math.max(0, s.humanPts[e.id] | 0); p.reportedMiniSurvived = true; } - } - }); - /* [TC183] verify คะแนน MG3: team score + คะแนนต่อคน (ดูว่าสูงผิดปกติ/ซ้ำจากชื่อซ้ำไหม) */ - try { - const per = s.turnOrder.map((e) => { - if (e.kind === 'bot' && e.botId) { const b = s.bots.get(e.botId); return 'bot=' + Math.max(0, (b && b.score) | 0); } - if (e.id != null) { const p = space.peers.get(e.id); return ((p && p.nickname) || e.id).toString().slice(0, 6) + '=' + Math.max(0, s.humanPts[e.id] | 0); } - return '?'; - }); - glog(sid, space, 'stack-score', '[TC183] จบ MG3 outcome=' + outcome + ' team=' + (s.score | 0) + ' layers=' + s.layers.length + ' progress=' + Math.floor(s.progressPct || 0) + '% · ต่อคน: ' + per.join(' ')); - } catch (e) { /* ignore */ } - try { space.lastStackMissionGrade = stackSessionMissionGrade(space); } catch (_e) { space.lastStackMissionGrade = null; } - const overPayload = { outcome: typeof outcome === 'string' ? outcome.slice(0, 32) : 'time_up' }; - io.to(sid).emit('stack-over', overPayload); - space.lastMinigameOver = { event: 'stack-over', payload: overPayload, atMs: Date.now() }; -} -/* [mg3] เดินเฟสแกว่งตาม "เวลาจริง" ที่ผ่านไป (ไม่ใช่ก้าว STACK_TICK_MS คงที่ ที่สมมติ tick สม่ำเสมอ) → - s.phase ตรงกับ t:now ที่ emit เสมอ → client extrapolate (serverNow-t)*speed แล้วตรงพอดี = - ไม่มี re-anchor กระโดดทุก packet = ตัวห้อยแกว่งลื่น ไม่กระตุก. clamp กัน tick หายนาน (วาป) */ -function advanceStackSwingPhase(s, now) { - const last = s.lastPhaseAdvanceAt; - const dtSec = (last && now > last) ? Math.min(0.2, (now - last) / 1000) : (STACK_TICK_MS / 1000); - s.lastPhaseAdvanceAt = now; - s.phase += s.phaseSpeed * dtSec * Math.PI * 2; -} - -function runStackTick(sid, space) { - const s = space.stackSession; - if (!s || !s.active || s.ended) return; - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'stack') { stopStackTicker(space); return; } - const now = Date.now(); - /* คำถามพิเศษ → หยุดเกม: เลื่อน startMs ตาม tick (คงเวลาที่เหลือ) + ไม่แกว่ง/ไม่เช็คจบ (เหมือน MG2/6/7) */ - /* [2026-07-17] เวลาจริง ไม่ใช่ก้าว STACK_TICK_MS คงที่ — ก้าวคงที่ชดเชยไม่ครบเมื่อ tick จริงเดินช้ากว่า - ที่ตั้งไว้ (บทเรียน MG5 07-16: กู้คืนได้แค่ 68%) */ - if (isSpecialQuizPausedServer(space)) { - const nowS = Date.now(); - s.startMs += Math.max(0, Math.min(1000, nowS - (s.lastPauseShiftAt || nowS))); - s.lastPauseShiftAt = nowS; - return; - } - s.lastPauseShiftAt = Date.now(); - /* เงื่อนไขจบเกม (server ตัดสินแหล่งเดียว) */ - if (s.progressPct >= 100) { stackSessionEnd(sid, space, 'decrypt_complete'); return; } - if (s.teamMissesLeft != null && s.teamMissesLeft <= 0) { stackSessionEnd(sid, space, 'misses_exhausted'); return; } - if ((now - s.startMs) >= stackTimeLimitSecServer() * 1000) { stackSessionEnd(sid, space, 'time_up'); return; } - if (!s.turnOrder.length) return; - /* บล็อกก่อนหน้ากำลังร่วง (busy) — ยังดรอปไม่ได้ (drop handler เช็ค busyUntil) แต่ให้ "บล็อกถัดไปแกว่งต่อ" - (advance phase + emit swing) → ตาถัดไปไม่เห็นบล็อกห้อยนิ่ง = ไม่ค้าง · client มี guard (!stackFall) ไม่ให้กระตุกตอนยังร่วง */ - if (now < s.busyUntil) { - advanceStackSwingPhase(s, now); - if (now - s.lastSwingEmit > 50) { - s.lastSwingEmit = now; - io.to(sid).emit('stack-swing', { phase: s.phase, speed: s.phaseSpeed, swingAmp: s.swingAmp, topCenterX: s.topCenterX, t: now }); - } - return; - } - const entry = stackCurrentTurnEntry(s); - if (!entry) return; - if (entry.kind === 'bot' && entry.botId) { - const bot = s.bots.get(entry.botId); - const tier = bot ? bot.tier : 'avg'; - if (s.botThinkUntil === 0) { - s.botThinkUntil = now + STACK_BOT_THINK_MIN_MS + Math.floor(Math.random() * (STACK_BOT_THINK_MAX_MS - STACK_BOT_THINK_MIN_MS)); - } else { - /* เลื่อนเฟสตามเวลา (ให้ client เห็นแกว่ง) */ - advanceStackSwingPhase(s, now); - /* ส่ง swingAmp+topCenterX ด้วย → client วาดบล็อกที่ห้อยตรงกับโมเดล server เป๊ะ (drop landOffset = swingAmp*sin(phase)) ไม่วาปตอนปล่อย */ - if (now - s.lastSwingEmit > 50) { s.lastSwingEmit = now; io.to(sid).emit('stack-swing', { phase: s.phase, speed: s.phaseSpeed, swingAmp: s.swingAmp, topCenterX: s.topCenterX, t: now }); } - if (now >= s.botThinkUntil) { - /* คิดเสร็จ → ล็อกเฟสเป้าหมาย "ข้างหน้า" ครั้งเดียว แล้วรอแกว่งวิ่งมาถึงเองธรรมชาติ - (ไม่สแนปเฟส = ไม่กระตุก) ปล่อยเมื่อถึงเป้า หรือเลย deadline กันค้าง */ - if (s.botAimPhase == null) { - s.botAimPhase = stackBotComputeAimPhaseServer(s, md, tier); - /* deadline ต้องยาวพอให้สวิง "ถึงเป้า" จริง = เวลาแกว่งจาก phase ปัจจุบันไปถึง aimPhase + buffer. - เดิม 2600ms ตายตัว → ตอน hz ต่ำ (สวิงช้า) กว่าจะถึงเป้าใช้ >2.6s → deadline เตะก่อน = บอทปล่อยผิด phase (เฉียง/miss) */ - const _gap = Math.max(0, s.botAimPhase - s.phase); - const _radPerSec = Math.max(1e-4, s.phaseSpeed * Math.PI * 2); - s.botAimDeadline = now + Math.min(15000, Math.ceil((_gap / _radPerSec) * 1000) + 1500); - } - if (!(s.phase >= s.botAimPhase || now >= s.botAimDeadline)) return; - s.botAimPhase = null; - const hit = stackComputeDropServer(s, md); - stackApplyHitServer(s, hit, { botId: entry.botId }); - io.to(sid).emit('stack-drop', { - placedCx: hit.placedCx, placedW: hit.placedW, miss: hit.miss, perfect: hit.perfect, - pts: hit.pts, progressDelta: hit.progressDelta, landOffsetTiles: hit.landOffsetTiles, - overlap: hit.overlap, supportRatio: hit.supportRatio, delta: hit.delta, - seat: entry.seat, heavy: false, theme: entry.theme || null, - actorId: entry.botId, isBot: true, - phase: s.phase, - teamMissesLeft: s.teamMissesLeft, - }); - s.busyUntil = now + STACK_DROP_BUSY_MS; - if (s.teamMissesLeft != null && s.teamMissesLeft <= 0) { - stackSessionEnd(sid, space, 'misses_exhausted'); - return; - } - stackAdvanceTurnServer(s); - emitStackStateServer(sid, space); - } - } - } else { - /* ตาคนจริง — server แกว่งให้ + emit ให้ทุกเครื่อง (รวมเจ้าของตา) เห็นเฟสเดียวกัน รอ stack-drop-req */ - advanceStackSwingPhase(s, now); - if (now - s.lastSwingEmit > 50) { s.lastSwingEmit = now; io.to(sid).emit('stack-swing', { phase: s.phase, speed: s.phaseSpeed, swingAmp: s.swingAmp, topCenterX: s.topCenterX, t: now }); } - } - /* broadcast stack-state เป็นระยะ (กัน late-join / desync) */ - if (now - s.lastStateEmit > 700) { s.lastStateEmit = now; emitStackStateServer(sid, space); } -} -function startStackTicker(sid, space) { - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'stack') return; - if (!space.detectiveMinigameActive) return; /* เกมจริงเท่านั้น — editor preview (เล่นเดี่ยว) คง client-sim เดิม */ - if (space.stackSession && space.stackSession.timerId) return; - space.stackSession = newStackSessionState(space, md); - /* คนหลุดช่วง how-to → seed เป็นบอท takeover ก่อนจัดลำดับตา (buildStackTurnOrderServer จะใส่เป็น seat ให้) */ - { - const s3 = space.stackSession; - drainPendingMissionTakeovers(space, 'stack', (rec) => { - s3.bots.set(rec.id, { - tier: 'avg', score: 0, takeover: true, takeoverPlayerKey: rec.playerKey || '', - theme: normStackTheme8(rec.themeIdx), - /* หน้า/สี/ชื่อ ของคนที่หลุด — recordPendingMissionTakeover เก็บไว้ให้ครบแล้ว (7982-7984) แต่เดิม - ตรงนี้หยิบมาแค่ theme → order row ไม่มี characterId → client วาดหน้าไม่ได้ = "อวตารตัวเขียว" - (setCyberHudScoreAvatarImg: ไม่มี cid → defaultAvatarImg). คอมเมนต์ที่ buildStackTurnOrderServer - เขียนไว้เองว่า "คง avatar เดิมไว้" — ตอนนี้ทำจริงตามนั้น */ - characterId: rec.characterId != null ? rec.characterId : null, - colorThemeIdx: rec.themeIdx != null ? rec.themeIdx : null, - skinToneIdx: rec.skinIdx != null ? rec.skinIdx : null, - nick: rec.nick || '', - }); - }); - } - buildStackTurnOrderServer(space, space.stackSession); - emitStackStateServer(sid, space); - space.stackSession.timerId = setInterval(() => { runStackTick(sid, space); }, STACK_TICK_MS); -} -function stopStackTicker(space) { - if (space && space.stackSession && space.stackSession.timerId) { - clearInterval(space.stackSession.timerId); - space.stackSession.timerId = null; - } -} - -/* ============================================================ - MG5 Jump Survive — server-authoritative bots + รอบ/เกรด + disconnect takeover (mnptfts2) - ผู้เล่นจริง sim ฟิสิกส์เองในเครื่อง (ลื่น/ไม่หน่วง) แล้วรายงานตำแหน่ง+สถานะตาย ผ่าน 'move'; - server เป็นเจ้าของ: บอท (port ฟิสิกส์ pixel-space ด้วย ts=md.tileSize คงที่), จับเวลา/จบรอบ, ตัดสินรอด/เกรด, - และแปลงคนหลุดเป็นบอท server. emit jump-survive-bot-move / jump-survive-player-to-bot / jump-survive-ended - (client เป็น renderer — เลิก host-sim บอทเมื่อ server-auth) — บอทใช้ id __pv_bot_N (ตรงกับ avatar ฝั่ง client) - ============================================================ */ -const JS_BOT_PREFIX = '__pv_bot_'; -const JS_TICK_MS = 33; /* ~30fps server sim — client lerp tx/ty นุ่มอยู่แล้ว */ -const JS_BOT_EMIT_MS = 60; -function jsTileSize(md) { const t = Math.floor(Number(md && md.tileSize)); return Number.isFinite(t) && t > 0 ? t : 32; } -function jsFootprint(md) { - if (!md) return { cw: 1, ch: 1 }; - let cw = Math.floor(Number(md.characterCellsW)); - let ch = Math.floor(Number(md.characterCellsH)); - if (Number.isFinite(cw) && cw >= 1 && Number.isFinite(ch) && ch >= 1) { - 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 jsPlatformPeriodPx(md, ts) { return (md.height || 15) * ts; } -function jsOverlapsObjectSolids(md, ts, left, top, right, bottom) { - const w = md.width || 20, h = md.height || 15; - const x0 = Math.floor(left / ts), x1 = Math.floor((right - 1e-6) / ts); - const y0 = Math.floor(top / ts), 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; - if (ty >= h) continue; /* แพลตฟอร์มวนแนวตั้ง — ไม่ถือ ty>=h เป็นซีลพื้น */ - const ob = md.objects && md.objects[ty] && md.objects[ty][tx]; - if (ob === 1) return true; - } - } - return false; -} -function jsOverlapsObjectWall(md, ts, left, top, right, bottom) { - const w = md.width || 20, h = md.height || 15; - const x0 = Math.floor(left / ts), x1 = Math.floor((right - 1e-6) / ts); - const y0 = Math.floor(top / ts), 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 jsOverlapsPlatforms(md, ts, left, top, right, bottom, scrollPx) { - const w = md.width || 20, h = md.height || 15; - const pa = md.jumpSurvivePlatformArea; - if (!pa) return false; - const period = jsPlatformPeriodPx(md, ts); - 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); - 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, tileRight = (tx + 1) * ts; - if (platBot > top && platTop < bottom && right > tileLeft && left < tileRight) return true; - } - } - } - return false; -} -function jsOverlapsHazardArea(md, ts, left, top, right, bottom) { - const ha = md && md.jumpSurviveHazardArea; - if (!ha) return false; - const pa = md && md.jumpSurvivePlatformArea; - 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 jsOverlapsSolid(md, ts, left, top, right, bottom, scrollPx) { - if (jsOverlapsObjectSolids(md, ts, left, top, right, bottom)) return true; - return jsOverlapsPlatforms(md, ts, left, top, right, bottom, scrollPx); -} -function jsOverlapsSolidForHorzMove(md, ts, left, top, right, bottom, scrollPx) { - if (jsOverlapsObjectSolids(md, ts, left, top, right, bottom)) return true; - const footSlop = ts * 0.16; - const platformBottom = bottom - footSlop; - if (platformBottom <= top + ts * 0.05) return false; - return jsOverlapsPlatforms(md, ts, left, top, right, platformBottom, scrollPx); -} -/* port jumpSurvivePhysicsIntegrate (mission map เสมอ → moveX*1.35, jump mult 1.5) */ -function jsPhysicsIntegrate(md, ts, scrollPx, opts) { - const { dt, ch, pxc0, feetY0, vy0, wasOnGround, jumpQueued, moveLeft, moveRight } = opts; - const w = md.width || 20, h = md.height || 15; - const bw = ts * 0.34; - const bh = ts * 0.9; - let pxc = pxc0, feetY = feetY0, vy = vy0, 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 = 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; - moveX *= 1.35; /* mission map */ - if (jumpQueued && onGround) { - vy = jumpV0; onGround = false; - } else if (wasOnGround) { - const platformRise = (Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42) * dt; - feetY += platformRise; - } - 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; - if (!jsOverlapsSolidForHorzMove(md, ts, nx - bw, headY0, nx + bw, feetY, scrollPx)) pxc = nx; - feetY += vy * dt; - let headTop = feetY - bh; - if (vy > 0 && jsOverlapsSolid(md, ts, pxc - bw, headTop, pxc + bw, feetY, scrollPx)) { - 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)); - let cleared = false; - for (let s = 0; s < maxSteps; s++) { - feetY -= step; headTop = feetY - bh; - if (!jsOverlapsSolid(md, ts, pxc - bw, headTop, pxc + bw, feetY, scrollPx)) { cleared = true; break; } - } - if (cleared) { - let lo = feetY, hi = feetY + step; - for (let i = 0; i < 20; i++) { - const mid = (lo + hi) * 0.5; - if (jsOverlapsSolid(md, ts, pxc - bw, mid - bh, pxc + bw, mid, scrollPx)) hi = mid; else lo = mid; - } - feetY = lo; headTop = feetY - bh; - } - if (feetY - bh < ts) { - feetY = ts + bh + ts; vy = Math.max(vy, ts * 6); onGround = false; headTop = feetY - bh; - } else { vy = 0; onGround = true; } - } else if (vy < 0 && jsOverlapsObjectSolids(md, ts, pxc - bw, headTop, pxc + bw, feetY)) { - 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 (!jsOverlapsObjectSolids(md, ts, pxc - bw, headTop, pxc + bw, feetY)) { vy = 0; break; } - } - } else { onGround = false; } - headTop = feetY - bh; - const headBandBottom = headTop + bh * 0.42; - if (vy < 0 && jsOverlapsSolid(md, ts, pxc - bw, headTop, pxc + bw, feetY, scrollPx) - && jsOverlapsObjectWall(md, ts, pxc - bw, headTop, pxc + bw, headBandBottom)) vy = 0; - headTop = feetY - bh; - if (headTop < ts) { feetY = ts + bh; if (vy < 0) vy = ts * 6; headTop = feetY - bh; } - headTop = feetY - bh; - if (jsOverlapsHazardArea(md, ts, pxc - bw, headTop, pxc + bw, feetY)) return { pxc, feetY, vy, onGround, died: true }; - if (feetY > h * ts + ts * 8) return { pxc, feetY, vy, onGround, died: true }; - return { pxc, feetY, vy, onGround, died: false }; -} -function jsBotHasFloorAhead(md, ts, pxc, feetY, wishDir, scrollPx) { - if (!wishDir) return true; - const bw = ts * 0.34; - const probeX = pxc + wishDir * (bw + ts * 0.82); - const pad = ts * 0.24; - return jsOverlapsPlatforms(md, ts, probeX - pad, feetY - ts * 0.45, probeX + pad, feetY + ts * 8, scrollPx); -} -function jsCollectPlatformCells(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 jsGridPosStandingOnPlatform(md, ts, tx, ty, scrollPx) { - const h = md.height || 15; - const period = jsPlatformPeriodPx(md, ts); - const { cw, ch } = jsFootprint(md); - const rawTop = ty * ts + scrollPx; - const hintY = h * ts * 0.72; /* server ไม่มีกล้อง → ใช้ hint กลางค่อนล่างเหมือน client ตอนยังไม่มี camCenter */ - const k = Math.round((hintY - rawTop - ts * 0.35) / period); - const platTop = rawTop + k * period; - return { x: (tx + 0.5) - cw * 0.5, y: platTop / ts - ch }; -} -/* [2026-07-17] MG5 ช่วงท้ายเลื่อนเร็วขึ้น ×2 (user request) — 15 วิสุดท้ายของรอบ. - ⚠️ สูตรนี้ต้อง "เหมือนกันเป๊ะ" กับฝั่ง client (play.js:jsScrollPxAtPlay / jsRiseVelNowPlay) - ไม่งั้นแท่นจะอยู่คนละที่ระหว่าง server กับที่ผู้เล่นเห็น = ชน/ไม่ชนไม่ตรงกัน - (บทเรียนทั้งวันนี้: สูตรที่คำนวณสองฝั่งต้องแก้พร้อมกันเสมอ) - ตำแหน่งต้องต่อเนื่อง: ใช้ผลรวมของสองช่วง (t₀·v + (t−t₀)·2v) ไม่ใช่คูณทั้งก้อน ไม่งั้นฉากจะกระโดด */ -const JS_ENDGAME_BOOST_SEC = 15; -const JS_ENDGAME_BOOST_MUL = 2; -function jsScrollPxAt(elapsedSec, risePerSec, totalSec) { - const t0 = Math.max(0, (Number(totalSec) || 0) - JS_ENDGAME_BOOST_SEC); - if (!(Number(totalSec) > 0) || elapsedSec <= t0) return elapsedSec * risePerSec; - return (t0 * risePerSec) + ((elapsedSec - t0) * risePerSec * JS_ENDGAME_BOOST_MUL); -} -function jsScrollPx(s, md) { - const risePerSec = Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42; - const elapsed = Math.max(0, (Date.now() - (s.liveBeginsAt || s.startMs)) / 1000); - return jsScrollPxAt(elapsed, risePerSec, jsTimeLimitSec(md)); -} -function jsTimeLimitSec(md) { - /* ตรงกับ client jumpSurviveTimeLimitSecForMission: md.jumpSurviveTimeSec → game-timing.jumpSurviveMissionTimeSec → 60 */ - const t = Number(md.jumpSurviveTimeSec); - if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); - const rt = Number(runtimeGameTiming && runtimeGameTiming.jumpSurviveMissionTimeSec); - return Number.isFinite(rt) && rt > 0 && rt < 7200 ? Math.floor(rt) : 60; -} -function jsBannedBotSlot(space) { - const bannedId = space.bannedThisRunPlayerId; - if (typeof bannedId === 'string' && bannedId.indexOf('__lobby_bot_') === 0) { - const n = parseInt(bannedId.slice('__lobby_bot_'.length), 10); - if (Number.isFinite(n) && n >= 0) return n; - } - return -1; -} -function createJumpSurviveBots(space, md) { - const s = space.jumpSurviveSession; - const ts = jsTileSize(md); - const cells = jsCollectPlatformCells(md); - cells.sort((a, b) => (b.y - a.y) || (a.x - b.x)); /* ล่างก่อน (ใกล้พื้นเริ่ม) */ - const sc = 0; - const pool = cells.length - ? cells.map((c) => jsGridPosStandingOnPlatform(md, ts, c.x, c.y, sc)) - : [jsGridPosStandingOnPlatform(md, ts, Number(md.spawn && md.spawn.x) || 1, Number(md.spawn && md.spawn.y) || 1, sc)]; - const humanCount = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)).length; - /* บอท takeover (จาก how-to) ที่ seed ไว้แล้ว = นับเป็น "คน" กันรวมเกิน 6 + เลื่อนช่อง spawn บอท AI */ - const existing = s.bots.size; - const botCount = Math.max(0, Math.min(6 - Math.min(humanCount + existing, 6), effectiveBotSlotCount(space))); - const bannedSlot = jsBannedBotSlot(space); - let poolIdx = 1 + humanCount + existing; - let created = 0, slot = 0; - while (created < botCount && slot < 64) { - if (slot === bannedSlot) { slot++; continue; } - const id = JS_BOT_PREFIX + slot; - const p = pool[poolIdx % pool.length] || pool[0]; - poolIdx++; - s.bots.set(id, { - tier: 'avg', - x: p.x + (Math.random() - 0.5) * 0.08, - y: p.y + (Math.random() - 0.5) * 0.08, - vy: 0, onGround: true, dir: 'down', walking: false, eliminated: false, - wishX: 0, wishNext: Date.now() + 200, nextJumpAi: Date.now() + 300 + Math.floor(Math.random() * 500), - takeover: false, takeoverPlayerKey: '', - }); - created++; slot++; - } -} -function startJumpSurviveGame(sid, space) { - const md = jsMap(space); /* ใช้ pattern ที่สุ่มไว้ (effMd) — bot/spawn ตรงกับ client */ - if (!md || md.gameType !== 'jump_survive') return; - if (!space.detectiveMinigameActive) return; /* เกมจริงเท่านั้น — editor preview คง client-sim */ - if (space.jumpSurviveSession && space.jumpSurviveSession.active) return; - const now = Date.now(); - const liveBeginsAt = space.missionLiveBeginsAt && space.missionLiveBeginsAt > now ? space.missionLiveBeginsAt : now; - space.jumpSurviveSession = { - active: true, ended: false, - startMs: liveBeginsAt, liveBeginsAt, - /* +grace: client (anchored serverNow) จบ time_up ก่อนเสมอ; server เป็น backstop เผื่อ host หลุด/ไม่มีใครจบ */ - endsAt: liveBeginsAt + jsTimeLimitSec(md) * 1000 + 2000, - bots: new Map(), - players: {}, /* peerId -> { eliminated } */ - lastEmit: 0, - }; - /* คนหลุดช่วง how-to → seed บอท takeover ก่อนสร้างบอท AI */ - seedJumpSurviveTakeoverBots(space, md); - createJumpSurviveBots(space, md); -} -function seedJumpSurviveTakeoverBots(space, md) { - const s = space.jumpSurviveSession; - if (!s) return; - const now = Date.now(); - drainPendingMissionTakeovers(space, 'jump_survive', (rec) => { - s.bots.set(rec.id, { - tier: 'avg', - x: Number.isFinite(rec.x) ? rec.x : 1, y: Number.isFinite(rec.y) ? rec.y : 1, - vy: 0, onGround: false, dir: rec.direction || 'down', walking: false, - eliminated: !!rec.jumpSurviveEliminated, - wishX: 0, wishNext: now + 200, nextJumpAi: now + 200, - takeover: true, takeoverPlayerKey: rec.playerKey || '', - }); - }); -} -function endJumpSurviveGame(sid, space, reason) { - const s = space.jumpSurviveSession; - if (!s || s.ended) return; - s.ended = true; s.active = false; - /* นับผู้รอด (คนจริงจาก players + บอท server ที่ยังไม่ตาย) → เกรดจากจำนวนรอด */ - let survivors = 0; - space.peers.forEach((p, id) => { - const st = s.players[id]; - const dead = st && st.eliminated; - if (!dead) survivors++; - if (p) { p.reportedMiniSurvived = !dead; p.reportedMiniScore = dead ? 0 : 100; } - }); - s.bots.forEach((b) => { if (b && !b.eliminated) survivors++; }); - const grade = survivors >= 6 ? 'A' : survivors === 5 ? 'B' : survivors === 4 ? 'C' : survivors === 3 ? 'D' : survivors === 2 ? 'E' : 'F'; - space.lastJumpSurviveGrade = grade; - io.to(sid).emit('jump-survive-ended', { reason: reason || 'time_up', grade, survivors }); -} -function stepJumpSurviveBots(sid, space, dt, now) { - const s = space.jumpSurviveSession; - if (!s || !s.active || s.ended) return; - const md = jsMap(space); /* ใช้ pattern ที่สุ่มไว้ (effMd) */ - if (!md || md.gameType !== 'jump_survive') return; - /* ขยับบอทเฉพาะหลังนับถอยหลัง 3-2-1 จบ (liveBeginsAt) — ก่อนหน้านี้บอทเริ่มทันทีที่ session active - ทำให้บอทวิ่ง/กระโดดก่อน "1" จบ → กันด้วยการ return จนถึงเวลาจริง (เหมือน scroll ที่อิง liveBeginsAt อยู่แล้ว) */ - if (now < (s.liveBeginsAt || s.startMs || 0)) return; - /* คำถามพิเศษเปิดอยู่ → หยุดบอท + เลื่อน endsAt ไม่ให้เวลาเดิน (เหมือน MG2/MG3/MG6/MG7) - pt8 (07-11) audit ข้าม MG5 ไปเกมเดียว: วันนั้นแก้แต่ฝั่ง client (jumpSurviveTickFrame early-return - ตอน specialQuizFreeze) โดยคิดว่า "scroll/timer อิง anchor อยู่แล้ว" — แต่ s.endsAt ฝั่ง server เป็น - เวลาสัมบูรณ์ ไม่มีใครเลื่อน → รอบจบ time_up ได้ทั้งที่ผู้เล่นยังตอบคำถามค้างอยู่ (วัดได้: pause 35 วิ - แต่เกมจบที่ 63 วิ = เท่า deadline ที่ไม่ pause). - ⚠️ เลื่อนด้วย "เวลาจริงที่ผ่านไป" ไม่ใช่ก้าว JS_TICK_MS คงที่แบบ MG6/MG7: sweep นี้เป็น - setInterval(33ms) ตัวเดียวที่วนทุกห้อง — ของจริงเดิน ~48ms ตอนมีโหลด → บวกทีละ 33 = ชดเชยไม่ครบ - (วัดได้: บวกแบบคงที่กู้คืนได้แค่ 24.1 จาก 35.3 วิ = 68% → เกมยังจบก่อนเวลา 11 วิ). - เป็นบทเรียนเดียวกับ advanceStackSwingPhase (07-10): อย่าสมมติว่า tick มาตรงเวลา */ - if (isSpecialQuizPausedServer(space)) { - if (s.endsAt > 0) s.endsAt += Math.max(0, Math.min(1000, now - (s.lastPauseShiftAt || now))); - s.lastPauseShiftAt = now; - return; - } - s.lastPauseShiftAt = now; /* ไม่ pause → คง anchor ไว้ให้สด (กันกระโดดตอนเริ่ม pause ครั้งถัดไป) */ - /* จบรอบ: หมดเวลา หรือทุก entity ตาย */ - if (now >= s.endsAt) { endJumpSurviveGame(sid, space, 'time_up'); return; } - const ts = jsTileSize(md); - const { cw, ch } = jsFootprint(md); - const h = md.height || 15; - const wpx = (md.width || 20) * ts; - const centerPx = wpx * 0.5; - const sc = jsScrollPx(s, md); - const lowDangerY = h * ts * 0.60; - const outBots = []; - s.bots.forEach((b, id) => { - /* บอทตายแล้ว: ยัง emit elim:true ต่อทุก tick (เดิม return ข้าม = ส่ง elim ครั้งเดียว → client พลาด = ค้างนิ่ง) */ - if (b.eliminated) { outBots.push({ id, x: b.x, y: b.y, dir: b.dir || 'down', walking: false, elim: true }); return; } - let pxc = (b.x + cw * 0.5) * ts; - let feetY = (b.y + ch) * ts; - const lowDanger = feetY > lowDangerY; - if (now >= b.wishNext) { - const wMin = lowDanger ? 180 : 380; - const wSpan = lowDanger ? 480 : 700; - b.wishNext = now + wMin + Math.floor(Math.random() * wSpan); - if (lowDanger && Math.random() < 0.55) b.wishX = 0; - else if (pxc < centerPx - ts * 1.8) b.wishX = 1; - else if (pxc > centerPx + ts * 1.8) b.wishX = -1; - else b.wishX = Math.random() < 0.3 ? (Math.random() < 0.5 ? -1 : 1) : 0; - } - let moveLeft = b.wishX < 0, moveRight = b.wishX > 0; - let hazardJump = false; - if (b.onGround) { - if (moveRight && !jsBotHasFloorAhead(md, ts, pxc, feetY, 1, sc)) { b.wishX = Math.random() < 0.65 ? -1 : 0; hazardJump = true; } - else if (moveLeft && !jsBotHasFloorAhead(md, ts, pxc, feetY, -1, sc)) { b.wishX = Math.random() < 0.65 ? 1 : 0; hazardJump = true; } - moveLeft = b.wishX < 0; moveRight = b.wishX > 0; - } - let jumpQ = false; - if (hazardJump) b.nextJumpAi = Math.min(b.nextJumpAi, now); - if (b.onGround && now >= b.nextJumpAi) { - b.nextJumpAi = now + 300 + Math.floor(Math.random() * (hazardJump ? 520 : 1100)); - let jp = 0.36 + 0.12; /* mission */ - if (lowDanger) jp += 0.22; - jp = Math.min(0.9, jp); - if (hazardJump || Math.random() < jp) jumpQ = true; - } - if (jumpQ) b.justJumped = true; /* [mg5] บอทกระโดดรอบนี้ → ส่ง flag ให้ client เล่นเสียง (เคลียร์ตอน emit) */ - const r = jsPhysicsIntegrate(md, ts, sc, { - dt, ch, pxc0: pxc, feetY0: feetY, vy0: b.vy, wasOnGround: b.onGround, jumpQueued: jumpQ, moveLeft, moveRight, - }); - b.x = r.pxc / ts - cw * 0.5; - b.y = r.feetY / ts - ch; - if (r.died) { - b.eliminated = true; b.vy = 0; b.walking = false; - outBots.push({ id, x: b.x, y: b.y, dir: b.dir, walking: false, elim: true }); - return; - } - b.vy = r.vy; b.onGround = r.onGround; - if (moveRight) b.dir = 'right'; else if (moveLeft) b.dir = 'left'; - b.walking = (!r.onGround && Math.abs(r.vy) > 40) || moveLeft || moveRight; - outBots.push({ id, x: b.x, y: b.y, dir: b.dir, walking: b.walking, elim: false, jmp: !!b.justJumped }); - }); - /* ผู้เล่นจริงที่สลับแท็บ (hidden) → จำลองการตกแทน timer ฝั่ง client ที่ถูก throttle → ส่งตำแหน่ง full-rate (คนอื่นเห็นลื่น). - ไม่มี input (ตก/ไหลตามแท่นเฉย ๆ) เหมือนคนปล่อยจอ. ตาย → mark eliminated + ตั้ง p.jumpSurviveEliminated (เข้าเช็ค all_dead/เกรด) */ - if (s.players) { - for (const pid in s.players) { - const st = s.players[pid]; - if (!st || !st.hidden || st.eliminated) continue; - if (!space.peers.has(pid)) { st.hidden = false; continue; } /* หลุดห้องแล้ว → ปล่อยให้ disconnect logic จัดการ */ - const pxc = ((Number.isFinite(st.x) ? st.x : 1) + cw * 0.5) * ts; - const feetY = ((Number.isFinite(st.y) ? st.y : 1) + ch) * ts; - const r = jsPhysicsIntegrate(md, ts, sc, { - dt, ch, pxc0: pxc, feetY0: feetY, vy0: st.vy || 0, wasOnGround: !!st.onGround, - jumpQueued: false, moveLeft: false, moveRight: false, - }); - st.x = r.pxc / ts - cw * 0.5; - st.y = r.feetY / ts - ch; - st.vy = r.vy; st.onGround = r.onGround; - if (r.died) { - st.eliminated = true; st.hidden = false; st.vy = 0; - const p = space.peers.get(pid); if (p) p.jumpSurviveEliminated = true; - outBots.push({ id: pid, x: st.x, y: st.y, dir: st.dir || 'down', walking: false, elim: true }); - } else { - outBots.push({ id: pid, x: st.x, y: st.y, dir: st.dir || 'down', walking: Math.abs(r.vy) > 40, elim: false }); - } - } - } - if (now - s.lastEmit >= JS_BOT_EMIT_MS) { - s.lastEmit = now; - if (outBots.length) io.to(sid).emit('jump-survive-bot-move', { bots: outBots }); - s.bots.forEach((b) => { b.justJumped = false; }); /* [mg5] เคลียร์ flag หลัง emit แล้ว (กันเล่นเสียงซ้ำ/พลาดถ้าอยู่ระหว่าง emit) */ - } - /* ทุก entity ตายหมด → จบรอบ (all_dead) */ - let anyAlive = false; - space.peers.forEach((p, id) => { const st = s.players[id]; if (!(st && st.eliminated)) anyAlive = true; }); - if (!anyAlive) s.bots.forEach((b) => { if (!b.eliminated) anyAlive = true; }); - if (!anyAlive) endJumpSurviveGame(sid, space, 'all_dead'); -} - -/* ============================================================ - MG6 space_shooter — server-authoritative บอท + รอบ + disconnect takeover (mnpz6rkp) - โลก (อุกาบาต deterministic ต่อ client, กระสุนคน relay) คงเดิม → host หลุด asteroid/timer ไม่พัง; - server ขับ "บอท" (เดิน+ยิง+คะแนน), จับเวลา/จบรอบ (backstop), แปลงคนหลุดเป็นบอท. - reuse 'space-shooter-shot' (กระสุนบอท) + emit 'space-shooter-bot-move' + จบรอบด้วย 'space-shooter-over' - ผู้เล่นจริง sim ยาน/อุกาบาต/ชน เอง (รายงานคะแนน/ตาย ผ่าน 'move') - ============================================================ */ -const SS_TICK_MS = 50; -const SS_BOT_EMIT_MS = 60; -const SS_MAX_ASTEROID_HITS = 3; /* ต้องตรงกับ client SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS */ -function ssTimeLimitSec(md) { - const t = Number(md.spaceShooterTimeSec); - if (Number.isFinite(t) && t === 0) return 0; - if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); - const rt = Number(runtimeGameTiming && runtimeGameTiming.spaceShooterMissionTimeSec); - return Number.isFinite(rt) && rt > 0 && rt < 7200 ? Math.floor(rt) : 60; -} -function ssClampCy(cy, mh) { const lo = mh * 0.5; const hi = Math.max(lo + 4, mh - 20); return Math.max(lo, Math.min(hi, cy)); } -function ssShooterSpawnCenter(md, slot, ts) { - const w = md.width || 20, h = md.height || 15; - const g = md.shooterSpawnSlots; - if (!g) return null; - for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { if (g[y] && Number(g[y][x]) === slot) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; } - return null; -} -function createSpaceShooterBots(space, md) { - const s = space.spaceShooterSession; - const ts = jsTileSize(md); - const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; - const humanCount = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)).length; - /* บอท takeover (จาก how-to) ที่ seed ไว้แล้ว = นับเป็น "คน" ในการจำกัด 6 ช่อง กันรวมเกิน 6 */ - const existing = s.bots.size; - const botCount = Math.max(0, Math.min(6 - Math.min(humanCount + existing, 6), effectiveBotSlotCount(space))); - const bannedSlot = jsBannedBotSlot(space); - let created = 0, slot = 0; - while (created < botCount && slot < 64) { - if (slot === bannedSlot) { slot++; continue; } - const id = JS_BOT_PREFIX + slot; - const slotNum = ((humanCount + existing + created) % 6) + 1; - const cen = ssShooterSpawnCenter(md, slotNum, ts); - let cx = cen ? cen.cx : ((created + 1) / (botCount + 1)) * mw; - let cy = cen ? cen.cy : (mh - ts * 1.5); - cx = Math.max(ts * 0.5, Math.min(mw - ts * 0.5, cx)); - cy = ssClampCy(cy, mh); - s.bots.set(id, { - tier: 'avg', cx, cy, dir: 'up', score: 0, eliminated: false, ssHits: 0, - fireCd: 0.3 + Math.random() * 0.4, - wanderVx: Math.random() < 0.5 ? -1 : 1, wanderVy: Math.random() < 0.5 ? -1 : 1, wanderNext: Date.now() + 600, - takeover: false, takeoverPlayerKey: '', - }); - created++; slot++; - } -} -function startSpaceShooterGame(sid, space) { - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'space_shooter') return; - if (!space.detectiveMinigameActive) return; - if (space.spaceShooterSession && space.spaceShooterSession.active) return; - const now = Date.now(); - const liveBeginsAt = space.missionLiveBeginsAt && space.missionLiveBeginsAt > now ? space.missionLiveBeginsAt : now; - const lim = ssTimeLimitSec(md); - space.spaceShooterSession = { - active: true, ended: false, startMs: liveBeginsAt, liveBeginsAt, - endsAt: lim > 0 ? liveBeginsAt + lim * 1000 + 2000 : 0, /* +grace: client จบ time_up ก่อน; server backstop */ - bots: new Map(), players: {}, lastEmit: 0, - }; - /* คนหลุดช่วง how-to → seed เป็นบอท takeover ก่อนสร้างบอท AI (createSpaceShooterBots นับ s.bots.size เป็น human-like) */ - seedSpaceShooterTakeoverBots(space, md); - createSpaceShooterBots(space, md); -} -function seedSpaceShooterTakeoverBots(space, md) { - const s = space.spaceShooterSession; - if (!s) return; - const ts = jsTileSize(md); - const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; - drainPendingMissionTakeovers(space, 'space_shooter', (rec) => { - const cx = Number.isFinite(rec.x) ? Math.max(ts * 0.5, Math.min(mw - ts * 0.5, (rec.x + 0.5) * ts)) : mw * 0.5; - const cy = ssClampCy(Number.isFinite(rec.y) ? (rec.y + 0.5) * ts : mh - ts * 1.5, mh); - s.bots.set(rec.id, { - tier: 'avg', cx, cy, dir: 'up', - score: rec.spaceShooterScore || 0, eliminated: !!rec.spaceShooterEliminated, ssHits: 0, - fireCd: 0.3 + Math.random() * 0.4, - wanderVx: Math.random() < 0.5 ? -1 : 1, wanderVy: Math.random() < 0.5 ? -1 : 1, wanderNext: Date.now() + 500, - takeover: true, takeoverPlayerKey: rec.playerKey || '', - }); - }); -} -function endSpaceShooterGame(sid, space, reason) { - const s = space.spaceShooterSession; - if (!s || s.ended) return; - s.ended = true; s.active = false; - const scores = []; - space.peers.forEach((p, id) => { - const st = s.players[id]; - const dead = st && st.eliminated; - scores.push({ id, score: Math.max(0, p.spaceShooterScore | 0), hits: 0, eliminated: !!dead }); - }); - s.bots.forEach((b, id) => scores.push({ id, score: Math.max(0, b.score | 0), hits: 0, eliminated: !!b.eliminated })); - io.to(sid).emit('space-shooter-over', { kind: reason === 'all_dead' ? 'all_dead' : 'time_up', scores, srv: true }); -} -function stepSpaceShooterBots(sid, space, dt, now) { - const s = space.spaceShooterSession; - if (!s || !s.active || s.ended) return; - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'space_shooter') return; - /* คำถามพิเศษเปิดอยู่ → หยุดบอท (เดิน+ยิง) + เลื่อน endsAt ไม่ให้เวลาเดิน (เหมือน gauntlet) */ - /* [2026-07-17] เวลาจริง ไม่ใช่ก้าว SS_TICK_MS คงที่ (flaw เดียวกับ MG5 ที่ชดเชยได้แค่ 68%) */ - if (isSpecialQuizPausedServer(space)) { - const nowS = Date.now(); - const shift = Math.max(0, Math.min(1000, nowS - (s.lastPauseShiftAt || nowS))); - if (s.endsAt > 0) s.endsAt += shift; - s.lastPauseShiftAt = nowS; - return; - } - s.lastPauseShiftAt = Date.now(); - if (s.endsAt > 0 && now >= s.endsAt) { endSpaceShooterGame(sid, space, 'time_up'); return; } - const ts = jsTileSize(md); - const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; - const padX = ts * 0.6; - const outBots = []; - s.bots.forEach((b, id) => { - /* บอทตายแล้ว: ไม่เดิน/ไม่ยิง แต่ "ยัง emit elim:true ต่อ" — เดิม return ข้ามเลย (ส่ง elim ครั้งเดียวตอนตาย) - ถ้า client พลาด packet นั้น = ไม่รู้ว่าตาย → ยานค้างนิ่งบนจอ. ส่งซ้ำทุก tick → client ซ่อนได้แน่นอน */ - if (!b.eliminated) { - if (now >= b.wanderNext) { - b.wanderNext = now + 400 + Math.floor(Math.random() * 700); - b.wanderVx = Math.random() < 0.5 ? -1 : (Math.random() < 0.5 ? 1 : 0); - b.wanderVy = Math.random() < 0.5 ? -1 : 1; - } - const spd = b.tier === 'sharp' ? 195 : b.tier === 'weak' ? 115 : 155; - b.cx = Math.max(padX, Math.min(mw - padX, b.cx + b.wanderVx * spd * dt)); - b.cy = ssClampCy(b.cy + b.wanderVy * spd * 0.55 * dt, mh); - b.fireCd -= dt; - if (b.fireCd <= 0) { - b.fireCd = b.tier === 'sharp' ? 0.22 : b.tier === 'weak' ? 0.48 : 0.34; - const bX = b.cx, bY = b.cy - 16; - io.to(sid).emit('space-shooter-shot', { x: bX, y: bY, vy: -500, ownerId: id, t0: now }); - /* คะแนนบอท: โอกาสยิงโดน (ตาม tier) → +5 (ทำให้คะแนนสมเหตุผลโดยไม่ต้องจำลอง asteroid ฝั่ง server) */ - const hitProb = b.tier === 'sharp' ? 0.6 : b.tier === 'weak' ? 0.28 : 0.42; - if (Math.random() < hitProb) b.score += 5; - } - } - outBots.push({ id, cx: b.cx, cy: b.cy, dir: 'up', score: b.score, elim: !!b.eliminated, hits: b.ssHits || 0 }); - }); - if (now - s.lastEmit >= SS_BOT_EMIT_MS) { - s.lastEmit = now; - if (outBots.length) io.to(sid).emit('space-shooter-bot-move', { bots: outBots }); - } - /* [2026-07-17] ตายหมด → จบรอบ — บั๊กเดียวกับ MG7 (เดิมมีแค่หมดเวลา). endSpaceShooterGame รองรับ - reason 'all_dead' อยู่แล้ว (emit kind:'all_dead') แค่ไม่เคยมีใครเรียก. เช็คทั้งสองที่เพราะตอนคนตาย - server เซ็ตคู่กัน (:13516 p.spaceShooterEliminated + s.players[id].eliminated) */ - let ssAnyAlive = false; - space.peers.forEach((p, id) => { - const st = s.players[id]; - if (!((st && st.eliminated) || p.spaceShooterEliminated)) ssAnyAlive = true; - }); - if (!ssAnyAlive) s.bots.forEach((b) => { if (!b.eliminated) ssAnyAlive = true; }); - if (!ssAnyAlive) endSpaceShooterGame(sid, space, 'all_dead'); -} - -/* ============================================================ - MG7 balloon_boss — server-authoritative บอท + รอบ + disconnect takeover (mnq1eml7) - โลก (บอส/กระสุนบอส deterministic ต่อ client, กระสุนคน relay) คงเดิม → host หลุดไม่พัง; - server ขับ "บอท" (ลอย+ยิง+คะแนน+ดาเมจบอส), รวม HP บอส (host-independent), จบรอบ, แปลงคนหลุดเป็นบอท. - reuse 'balloon-boss-shot' (กระสุนบอท) + emit 'balloon-boss-bot-move' + จบรอบด้วย 'balloon-boss-over' - ผู้เล่นจริง sim ยาน/ชน/บอส เอง (รายงานคะแนน/ดาเมจ/ลูกโป่ง/ตาย ผ่าน 'move') - ============================================================ */ -const BB_TICK_MS = 50; -const BB_BOT_EMIT_MS = 70; -function bbTimeLimitSec(md) { - const t = Number(md.balloonBossTimeSec); - if (Number.isFinite(t) && t === 0) return 0; - if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); - const rt = Number(runtimeGameTiming && runtimeGameTiming.balloonBossMissionTimeSec); - return Number.isFinite(rt) && rt > 0 && rt < 7200 ? Math.floor(rt) : 120; -} -function bbMaxHp(md) { const n = Number(md.balloonBossMaxHp); return Math.max(20, Math.min(9999, Number.isFinite(n) ? Math.floor(n) : 100)); } -/* ดาเมจต่อการยิงโดนบอส (ลบเลือดบอส/นัด) จาก game-timing (Admin Minigame-7) — แยกจากคะแนน · default 10 */ -function bbDamagePerHit() { const n = Number(runtimeGameTiming && runtimeGameTiming.balloonBossDamagePerHit); return Math.max(1, Math.min(100, Number.isFinite(n) && n > 0 ? Math.floor(n) : 10)); } -function bbBossCenter(md, ts) { - const bx = 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 bbPlayerSpawnCenter(md, slot, ts) { - const g = md.balloonBossPlayerSlots; - if (!g) return null; - const w = md.width || 20, h = md.height || 15; - for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { if (g[y] && Number(g[y][x]) === slot) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; } - return null; -} -function bbClampWorld(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 bbTeamDmg(space) { - let s = 0; - space.peers.forEach((p) => { s += Math.max(0, p.balloonBossBossDmg | 0); }); - const sess = space.balloonBossSession; - if (sess && sess.bots) sess.bots.forEach((b) => { s += Math.max(0, b.dmg | 0); }); - return s; -} -function createBalloonBossBots(space, md) { - const s = space.balloonBossSession; - const ts = jsTileSize(md); - const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; - const startBalloons = balloonBossBalloonsForMap(md); - const humanCount = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)).length; - /* บอท takeover (จาก how-to) ที่ seed ไว้แล้ว = นับเป็น "คน" กันรวมเกิน 6 + เลื่อนช่อง spawn บอท AI */ - const existing = s.bots.size; - const botCount = Math.max(0, Math.min(6 - Math.min(humanCount + existing, 6), effectiveBotSlotCount(space))); - const bannedSlot = jsBannedBotSlot(space); - let created = 0, slot = 0; - while (created < botCount && slot < 64) { - if (slot === bannedSlot) { slot++; continue; } - const id = JS_BOT_PREFIX + slot; - const slotNum = ((humanCount + existing + created) % 6) + 1; - const cen = bbPlayerSpawnCenter(md, slotNum, ts); - let cx = cen ? cen.cx : ((created + 1) / (botCount + 1)) * mw; - let cy = cen ? cen.cy : (mh - ts * 1.4); - const cl = bbClampWorld(cx, cy, mw, mh); - s.bots.set(id, { - tier: 'avg', homeCx: cl.cx, homeCy: cl.cy, cx: cl.cx, cy: cl.cy, - score: 0, dmg: 0, balloons: startBalloons, eliminated: false, - fireCd: 0.8 + Math.random() * 1.0, phase: Math.random() * Math.PI * 2, - takeover: false, takeoverPlayerKey: '', - }); - created++; slot++; - } -} -function startBalloonBossGame(sid, space) { - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'balloon_boss') return; - if (!space.detectiveMinigameActive) return; - if (space.balloonBossSession && space.balloonBossSession.active) return; - const now = Date.now(); - const liveBeginsAt = space.missionLiveBeginsAt && space.missionLiveBeginsAt > now ? space.missionLiveBeginsAt : now; - const lim = bbTimeLimitSec(md); - space.balloonBossSession = { - active: true, ended: false, startMs: liveBeginsAt, liveBeginsAt, - endsAt: lim > 0 ? liveBeginsAt + lim * 1000 + 2000 : 0, - maxHp: bbMaxHp(md), bots: new Map(), lastEmit: 0, - /* [P5 v4] ฐานเวลาวงลูกศร — server เป็นเจ้าของ, เลื่อนตอนคำถามพิเศษ, ส่งลงไปกับ bot-move ทุก 50ms */ - ringAnchorMs: liveBeginsAt, lastPauseShiftAt: 0, - }; - /* คนหลุดช่วง how-to → seed บอท takeover ก่อนสร้างบอท AI */ - seedBalloonBossTakeoverBots(space, md); - createBalloonBossBots(space, md); -} -function seedBalloonBossTakeoverBots(space, md) { - const s = space.balloonBossSession; - if (!s) return; - const ts = jsTileSize(md); - const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; - const startBalloons = balloonBossBalloonsForMap(md); - drainPendingMissionTakeovers(space, 'balloon_boss', (rec) => { - const cx = Number.isFinite(rec.x) ? (rec.x + 0.5) * ts : mw * 0.5; - const cy = Number.isFinite(rec.y) ? (rec.y + 0.5) * ts : mh - ts * 1.4; - const cl = bbClampWorld(cx, cy, mw, mh); - s.bots.set(rec.id, { - tier: 'avg', homeCx: cl.cx, homeCy: cl.cy, cx: cl.cx, cy: cl.cy, - score: rec.balloonBossScore || 0, dmg: rec.balloonBossBossDmg || 0, - balloons: rec.balloonBossBalloons || startBalloons, eliminated: !!rec.balloonBossEliminated, - fireCd: 0.6 + Math.random() * 0.8, phase: Math.random() * Math.PI * 2, - takeover: true, takeoverPlayerKey: rec.playerKey || '', - }); - }); -} -function endBalloonBossGame(sid, space, reason) { - const s = space.balloonBossSession; - if (!s || s.ended) return; - s.ended = true; s.active = false; - const timeUp = reason === 'time'; - const scores = []; - space.peers.forEach((p, id) => { - scores.push({ id, score: Math.max(0, p.balloonBossScore | 0), eliminated: timeUp ? true : !!p.balloonBossEliminated }); - }); - s.bots.forEach((b, id) => scores.push({ id, score: Math.max(0, b.score | 0), eliminated: timeUp ? true : !!b.eliminated })); - io.to(sid).emit('balloon-boss-over', { reason: reason === 'victory' ? 'victory' : (timeUp ? 'time' : 'all_dead'), scores, srv: true }); -} -/* ตำแหน่งบอส "เคลื่อนที่จริง" — port จาก client getBalloonBossBossLiveCenterPlay (anchor=liveBeginsAt เดียวกัน → ตรงกันทุกเครื่อง) - server เดิมคิดบอสเป็นจุดนิ่ง → บอทไม่รู้ว่าบอสขยับมาทับ. มีตัวนี้ บอทหลบบอสที่เคลื่อนที่ได้ + เล็งยิงตรงบอสจริง */ -function bbBossLiveCenter(md, ts, s, now) { - const base = bbBossCenter(md, ts); - const w = md.width || 20, h = md.height || 15; - const mw = w * ts, mh = h * ts; - const anchor = (s && s.liveBeginsAt) ? s.liveBeginsAt : now; - const tSec = Math.max(0, (now - anchor) / 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 bbClampWorld(cx, cy, mw, mh); -} -function stepBalloonBossBots(sid, space, dt, now) { - const s = space.balloonBossSession; - if (!s || !s.active || s.ended) return; - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'balloon_boss') return; - /* คำถามพิเศษเปิดอยู่ → หยุดบอท (ลอย+ยิง) + เลื่อน endsAt ไม่ให้เวลาเดิน (เหมือน gauntlet) - กันกระสุนบอทถูก emit สะสมตอน pause แล้วไหลรัวหลังตอบเสร็จ */ - if (isSpecialQuizPausedServer(space)) { - /* [P5 v4 2026-07-17] เลื่อนด้วย "เวลาจริง" ไม่ใช่ก้าว BB_TICK_MS คงที่ — บทเรียน MG5 (07-16): - ตอนมีโหลด tick จริงเดินช้ากว่าค่าที่ตั้งไว้ → ก้าวคงที่ชดเชยไม่ครบ (MG5 ได้แค่ 68% ของเวลาที่ควรคืน) */ - const shift = Math.max(0, Math.min(1000, now - (s.lastPauseShiftAt || now))); - if (s.endsAt > 0) s.endsAt += shift; - s.ringAnchorMs += shift; /* วงลูกศรหยุดหมุนพร้อมกันทุกเครื่อง */ - s.lastPauseShiftAt = now; - /* ยัง emit ระหว่าง pause → ทุกเครื่อง re-align anchor ต่อเนื่อง. ถ้าเงียบ client จะ extrapolate - perf.now() ของตัวเองต่อ (วงหมุนต่อคนละมุม) แล้วกระตุกตอน emit กลับมา. bots:[] = no-op ฝั่ง client - (handler แค่ forEach ไม่มี logic ลบบอท) — ตำแหน่งบอทไม่ขยับตอน pause อยู่แล้ว */ - if (now - s.lastEmit >= BB_BOT_EMIT_MS) { - s.lastEmit = now; - io.to(sid).emit('balloon-boss-bot-move', { bots: [], t: Date.now(), anchor: s.ringAnchorMs }); - } - return; - } - s.lastPauseShiftAt = now; - /* จบรอบ: ชนะบอส (HP→0) ก่อน, แล้วหมดเวลา */ - if (bbTeamDmg(space) >= s.maxHp) { endBalloonBossGame(sid, space, 'victory'); return; } - if (s.endsAt > 0 && now >= s.endsAt) { endBalloonBossGame(sid, space, 'time'); return; } - const ts = jsTileSize(md); - const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; - const boss = bbBossLiveCenter(md, ts, s, now); /* บอสเคลื่อนที่จริง (หลบ+เล็งตามตัวนี้) */ - const fleeR = Math.max(ts * 3.4, 96); /* หลบบอสเมื่อ home เข้าใกล้ ~3 ช่อง */ - const fleeSpd = 165; /* px/s ดริฟต์ home หนีบอส (เร็วกว่าบอส → ไม่ติด) */ - const sepMin = ts * 2.4; /* ระยะ home ขั้นต่ำระหว่างบอท (หลบกันเอง) */ - const outBots = []; - const alive = []; - s.bots.forEach((b, id) => { - if (b.eliminated) { outBots.push({ id, cx: b.cx, cy: b.cy, score: b.score, dmg: b.dmg, balloons: b.balloons, elim: true }); return; } /* ตายแล้ว: emit elim:true ต่อ (กัน client พลาด packet = ค้างนิ่ง) */ - alive.push(b); - /* หลบบอส: ดริฟต์ "home" ออกจากบอสที่เคลื่อนเข้าใกล้ (แทน clamp ตำแหน่ง = ไม่ติดนิ่งขอบบอส) */ - const hx = b.homeCx - boss.cx, hy = b.homeCy - boss.cy; - const hd = Math.hypot(hx, hy) || 1e-6; - if (hd < fleeR) { b.homeCx += (hx / hd) * fleeSpd * dt; b.homeCy += (hy / hd) * fleeSpd * dt; } - b.phase += dt; - const wob = Math.sin(b.phase * 1.25) * 26; - const wob2 = Math.cos(b.phase * 0.9) * 18; - const cl = bbClampWorld(b.homeCx + wob, b.homeCy + wob2, mw, mh); - b.cx = cl.cx; b.cy = cl.cy; - b.fireCd -= dt; - if (b.fireCd <= 0) { - b.fireCd = 0.9 + Math.random() * 1.1; - const aim = Math.atan2(boss.cy - b.cy, boss.cx - b.cx) + (Math.random() - 0.5) * 0.5; - const bspd = 500 + Math.random() * 90; - const vx = Math.cos(aim) * bspd, vy = Math.sin(aim) * bspd; - const sx = b.cx + Math.cos(aim) * 26, sy = b.cy - 10.5 + Math.sin(aim) * 26; - io.to(sid).emit('balloon-boss-shot', { sx, sy, vx, vy, ownerId: id, t0: now }); - b.score = Math.max(0, (b.score | 0) - 5); /* สเปก: ยิง 1 ครั้ง −5 */ - const hitProb = b.tier === 'sharp' ? 0.55 : b.tier === 'weak' ? 0.26 : 0.4; - if (Math.random() < hitProb) { b.score += 10; b.dmg += bbDamagePerHit(); } /* โดนบอส: +10 คะแนน, −HP บอสตามค่า Admin (default 10) */ - } - outBots.push({ id, cx: b.cx, cy: b.cy, score: b.score, dmg: b.dmg, balloons: b.balloons, elim: false }); - }); - /* หลบกันเอง: ดัน home ที่ใกล้กันเกินให้แยกออก (รอบถัดไปบอทจะ wobble รอบ home ใหม่ → ไม่ซ้อนกัน) */ - for (let i = 0; i < alive.length; i++) { - for (let j = i + 1; j < alive.length; j++) { - const a = alive[i], c = alive[j]; - const dx = a.homeCx - c.homeCx, dy = a.homeCy - c.homeCy; - const d = Math.hypot(dx, dy) || 1e-6; - if (d < sepMin) { const p = (sepMin - d) * 0.5, nx = dx / d, ny = dy / d; a.homeCx += nx * p; a.homeCy += ny * p; c.homeCx -= nx * p; c.homeCy -= ny * p; } - } - } - alive.forEach((b) => { const cl = bbClampWorld(b.homeCx, b.homeCy, mw, mh); b.homeCx = cl.cx; b.homeCy = cl.cy; }); /* กัน home ดริฟต์หลุดขอบ */ - if (now - s.lastEmit >= BB_BOT_EMIT_MS) { - s.lastEmit = now; - /* [P5 v3] แนบนาฬิกา server (Date.now) → ทุก client (รวม host) ใช้เป็นฐานเวลา "วงลูกศรหมุน" แทน - serverNow() ของตัวเอง (playClockOffset ต่างต่อเครื่อง = วงไม่ sync). ในเกมจริง MG7 เป็น server-auth - (host ไม่ส่ง balloon-boss-sync) → ฐานเวลาต้องมาจาก server. bot-move ส่งทุก 50ms = re-align ถี่. - ส่ง "เสมอ" (แม้ไม่มีบอท = ห้อง 6 คนจริง) เพื่อให้ฐานเวลาไหลเสมอ — empty array 20Hz cost น้อยมาก - และอยู่ใน balloon tick อยู่แล้ว (ไม่กระทบมินิเกมอื่น) */ - /* [P5 v4] anchor = ฐานเวลาวงลูกศรของ server (pause-adjusted). v2/v3 ส่งแค่ `t` (นาฬิกา) ซึ่งตรงแล้ว - แต่ client ยังลบด้วย anchor "ของตัวเอง" ที่ shiftSpecialQuizFrozenTimers เลื่อนเองทุกเฟรม = สะสม - ต่อเครื่อง และไม่มี path ไหน adopt กลับในโหมด server-auth (balloon-boss-sync ถูกปิด) → ไม่มีวัน converge */ - io.to(sid).emit('balloon-boss-bot-move', { bots: outBots, t: Date.now(), anchor: s.ringAnchorMs }); - } - /* [2026-07-17] ทุก entity ตายหมด → จบรอบ (user: "mg7 ตายหมดเกมไม่จบ"). เดิม MG7 มีเงื่อนไขจบแค่ - victory (HP บอสหมด) กับหมดเวลา → ตายครบก็นั่งรอจนหมดเวลาเปล่าๆ. ใช้ pattern เดียวกับ MG5 (:9345) - คน: p.balloonBossEliminated (server เซ็ตจาก move payload :13564, sticky) · บอท: b.eliminated */ - let bbAnyAlive = false; - space.peers.forEach((p) => { if (!p.balloonBossEliminated) bbAnyAlive = true; }); - if (!bbAnyAlive) s.bots.forEach((b) => { if (!b.eliminated) bbAnyAlive = true; }); - if (!bbAnyAlive) endBalloonBossGame(sid, space, 'all_dead'); -} - -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 botCount = Math.max(0, Math.min(GAUNTLET_MAX_PLAYERS - list.length, effectiveBotSlotCount(space))); - const pos = gauntletSpawnPositions(md, list.length + botCount); - const crown = isGauntletCrownHeistMapBySpace(space); - /* จัด lane ให้ "ไม่ทับกันแน่นอน": เรียงตาม spawnJoinOrder (id เป็น tiebreak ให้เสถียร) - แล้วแจกช่อง pos แบบ unique — ถ้า ord ชนกัน/เกินจำนวนช่อง (pos[ord] ซ้ำ) ดันไปช่องว่างถัดไป - = แก้ MG2 user1/user2 spawn ทับกัน (เดิม pos[ord] ซ้ำเมื่อ ord ชนหรือ fallback ช่องสุดท้าย) */ - const ordered = list - .map((p, i) => ({ p, ord: (typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : i) })) - .sort((a, b) => (a.ord - b.ord) || (String(a.p.id) < String(b.p.id) ? -1 : 1)); - const usedPosIdx = new Set(); - ordered.forEach((entry, seq) => { - const p = entry.p; - let idx = Number.isFinite(entry.ord) ? Math.max(0, Math.min(entry.ord, pos.length - 1)) : seq; - let guard = 0; - while (usedPosIdx.has(idx) && guard < pos.length) { idx = (idx + 1) % pos.length; guard++; } - usedPosIdx.add(idx); - const pt = pos[idx] || 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); - /* บอท server-authoritative — แจกช่อง spawn ที่เหลือ (ไม่ทับผู้เล่น) - Card Ban: ถ้าแบน "บอท" (__lobby_bot_N) → ข้าม slot N + ลดจำนวน 1 (ตรงกับ client rebalancePreviewBots) */ - if (botCount > 0) { - let bannedBotSlot = -1; - const bannedId = space.bannedThisRunPlayerId; - if (typeof bannedId === 'string' && bannedId.indexOf('__lobby_bot_') === 0) { - const n = parseInt(bannedId.slice('__lobby_bot_'.length), 10); - if (Number.isFinite(n) && n >= 0) bannedBotSlot = n; - } - const want = bannedBotSlot >= 0 ? Math.max(0, botCount - 1) : botCount; - const bots = new Map(); - let created = 0; - let slot = 0; - while (created < want && slot < GAUNTLET_MAX_PLAYERS + botCount) { - if (slot === bannedBotSlot) { slot++; continue; } - let idx = 0; - while (usedPosIdx.has(idx) && idx < pos.length) idx++; - usedPosIdx.add(idx); - const pt = pos[idx] || pos[pos.length - 1] || { x: 1, y: 1 }; - bots.set(GAUNTLET_BOT_PREFIX + slot, { - tier: gauntletBotTierForSlot(slot), - x: pt.x, - y: pt.y, - dir: 'right', - gauntletJumpTicks: 0, - gauntletScore: crown ? 100 : 0, - gauntletEliminated: false, - }); - created++; - slot++; - } - if (bots.size) space.gauntletRun.bots = bots; - } - 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; -} - -/* ===== Lobby authoritative (server เป็นเจ้าของ LobbyA/B) ===================== - เดิม: บอท lobby เดินบน "host client" (frame-based) แล้ว broadcast → บอทวิ่งเร็วช้าต่างกันตาม FPS/host - + ผู้เล่นออกแล้ว client อื่นไม่ได้ user-left = ผีค้าง - ใหม่: server เดินบอทเอง (time-based, ความเร็วเดียวทุกเครื่อง) + ส่ง roster snapshot ให้ client reconcile (ฆ่าผี) - bot id = '__lobby_bot_' (ตรงกับ client LOBBY_BOT_PREFIX) → client map สี/ตัวละครได้ถูกตัว */ -const LOBBY_BOT_PREFIX = '__lobby_bot_'; -const LOBBY_MOVE_SPEED_PER_SEC = 0.15 * 60; /* ตรงกับ client MOVE_SPEED_PER_SEC (0.15/เฟรม × 60fps) */ - -function lobbyMapOfSpace(space) { - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - return (md && md.gameType === 'lobby') ? md : null; -} - -/* จุดเกิดผู้เล่น 6 ช่อง (mirror client parseLobbyPlayerSpawnsFromMapLobby) — บอทเกิดที่ช่องเดียวกับคน */ -function parseLobbyPlayerSpawnsServer(md) { - const w = md.width || 20, 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; -} - -/* เดินได้ไหม — mirror client canWalkLobbyBot เป๊ะ: กำแพง objects===1 เดินไม่ได้ (เดินได้ทุก tile ที่ไม่ใช่กำแพง เหมือนคน); - tile blockPlayer===1 = ห้ามทับ peer/บอทตัวอื่น (tile ปกติทับได้) - หมายเหตุ: ห้ามจำกัดด้วย spawnArea — นั่นคือ "จุดเกิด" ไม่ใช่ "พื้นที่เดิน" (เคยทำให้บอทติดอยู่กับที่) */ -function lobbyTileWalkable(md, space, x, y, selfBotId) { - const w = md.width || 20, h = md.height || 15; - const tx = Math.floor(x), ty = Math.floor(y); - if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; - const ob = md.objects && md.objects[ty] ? md.objects[ty][tx] : 0; - if (ob === 1) return false; - const bp = md.blockPlayer; - if (bp && bp[ty] && bp[ty][tx] === 1) { - for (const p of space.peers.values()) { - if (p && Math.floor(p.x) === tx && Math.floor(p.y) === ty) return false; - } - if (space.lobbyBots) { - for (const [bid, b] of space.lobbyBots) { - if (bid === selfBotId) continue; - if (Math.floor(b.x) === tx && Math.floor(b.y) === ty) return false; - } - } - } - return true; -} - -/* เกิดบอทที่ "ช่องเกิดผู้เล่น" เดียวกับคน (slotOrd = humans + botIndex → ช่องไม่ทับคน, ไม่ทับกันเอง) - mirror client pickLobbySpawnForJoin: slots6 → lobbyPlayerSpawns[j], fixed → md.spawn */ -function pickLobbyBotSpawnServer(md, space, slotOrd) { - const w = md.width || 20, h = md.height || 15; - const mode = md.lobbySpawnMode; - const ord = slotOrd | 0; - if (mode === 'slots6') { - const j = Math.min(Math.max(0, ord), 5); - const slots = parseLobbyPlayerSpawnsServer(md); - const pick = slots[j]; - if (pick) return { x: pick.x, y: pick.y }; - } else if (mode === 'fixed' && md.spawn) { - const fx = Math.floor(Number(md.spawn.x)); - const fy = Math.floor(Number(md.spawn.y)); - if (Number.isFinite(fx) && Number.isFinite(fy)) return { x: Math.max(0, Math.min(w - 1, fx)), y: Math.max(0, Math.min(h - 1, fy)) }; - } - /* fallback: หาช่อง spawn ที่ define ไว้ตัวแรก ๆ แล้วค่อยสุ่ม tile ที่ไม่ใช่กำแพง */ - const slots = parseLobbyPlayerSpawnsServer(md); - const defined = slots.filter(Boolean); - if (defined.length) { const s = defined[Math.min(ord, defined.length - 1)]; return { x: s.x, y: s.y }; } - for (let i = 0; i < 24; i++) { - const x = 1 + Math.floor(Math.random() * Math.max(1, w - 2)); - const y = 1 + Math.floor(Math.random() * Math.max(1, h - 2)); - if (lobbyTileWalkable(md, space, x, y, null)) return { x, y }; - } - const sp = md.spawn; - if (sp && Number.isFinite(Number(sp.x)) && Number.isFinite(Number(sp.y))) return { x: Math.floor(Number(sp.x)), y: Math.floor(Number(sp.y)) }; - return { x: 1, y: 1 }; -} - -/* คอมโพเนนต์เดินได้ "หลัก" (BFS จาก spawn slot) — กันบอทไปติดเกาะแยก/strip ข้างฉาก (isolated walkable) ที่กำแพงล้อม - cache ต่อ space (invalidate เมื่อ mapId เปลี่ยน) */ -function lobbyMainCompServer(md, space) { - const mid = space && space.mapId; - if (space && space._lobbyMainComp && space._lobbyMainCompMap === mid) return space._lobbyMainComp; - const w = md.width || 20, h = md.height || 15; - const isW = (x, y) => x >= 0 && x < w && y >= 0 && y < h && !(md.objects && md.objects[y] && md.objects[y][x] === 1); - const slots = parseLobbyPlayerSpawnsServer(md).filter(Boolean); - let seed = null; - for (const s of slots) { const sx = Math.floor(s.x), sy = Math.floor(s.y); if (isW(sx, sy)) { seed = [sx, sy]; break; } } - if (!seed && md.spawn && isW(Math.floor(md.spawn.x), Math.floor(md.spawn.y))) seed = [Math.floor(md.spawn.x), Math.floor(md.spawn.y)]; - if (!seed) { for (let y = 0; y < h && !seed; y++) for (let x = 0; x < w; x++) { if (isW(x, y)) { seed = [x, y]; break; } } } - const comp = new Set(); - if (seed) { - comp.add(seed[0] + ',' + seed[1]); - const q = [seed]; - while (q.length) { const [a, b] = q.shift(); for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const nx = a + dx, ny = b + dy, k = nx + ',' + ny; if (!comp.has(k) && isW(nx, ny)) { comp.add(k); q.push([nx, ny]); } } } - } - if (space) { space._lobbyMainComp = comp; space._lobbyMainCompMap = mid; } - return comp; -} -/* เดินบอท lobby ของ space เดียว (เรียกจาก sweeper ทุก ~60ms) — time-based, ความเร็วคงที่ */ -function stepLobbyBotsForSpace(sid, space, dt, nowMs) { - const md = lobbyMapOfSpace(space); - const botCount = md ? effectiveBotSlotCount(space) : 0; - const humans = space.peers ? space.peers.size : 0; - if (!md || botCount <= 0 || humans <= 0) { - if (space.lobbyBots) space.lobbyBots = null; - return; - } - if (!space.lobbyBots) space.lobbyBots = new Map(); - const bots = space.lobbyBots; - /* ปรับจำนวนบอทให้ตรง botCount (id ตาม slot) — เกิดที่ช่องผู้เล่น humans+i (ไม่ทับคน/ไม่ทับกัน) */ - for (let i = 0; i < botCount; i++) { - const id = LOBBY_BOT_PREFIX + i; - if (!bots.has(id)) { - const sp = pickLobbyBotSpawnServer(md, space, humans + i); - bots.set(id, { x: sp.x, y: sp.y, direction: 'down', isWalking: false, wTX: null, wTY: null, retargetAt: 0, pauseUntil: 0 }); - } - } - for (const id of [...bots.keys()]) { - const slot = parseInt(id.slice(LOBBY_BOT_PREFIX.length), 10); - if (!(slot >= 0 && slot < botCount)) bots.delete(id); - } - const w = md.width || 20, h = md.height || 15; - const step = LOBBY_MOVE_SPEED_PER_SEC * dt; - const mainComp = lobbyMainCompServer(md, space); - const inMain = (x, y) => !mainComp.size || mainComp.has(Math.floor(x) + ',' + Math.floor(y)); - bots.forEach((b, id) => { - /* กันบอทไปติดเกาะแยก/strip ข้างฉาก (isolated) → ดึงกลับ spawn หลัก (main component) */ - if (!inMain(b.x, b.y)) { - const slot = parseInt(id.slice(LOBBY_BOT_PREFIX.length), 10); - const sp = pickLobbyBotSpawnServer(md, space, slot); - if (inMain(sp.x, sp.y)) { b.x = sp.x + 0.5; b.y = sp.y + 0.5; } - else { const first = mainComp.values().next().value; if (first) { const [mx, my] = first.split(',').map(Number); b.x = mx + 0.5; b.y = my + 0.5; } } - b.wTX = null; b.wTY = null; b.retargetAt = 0; b.pauseUntil = 0; - } - const arrived = (typeof b.wTX === 'number') && Math.hypot(b.wTX - b.x, b.wTY - b.y) < 0.5; - if (typeof b.wTX !== 'number' || arrived || nowMs >= b.retargetAt) { - /* เป้า wander = สุ่มทั้งแมป (เหมือน client เดิม) — การเดินถูกจำกัดด้วยกำแพงอยู่แล้ว */ - b.wTX = 1 + Math.random() * Math.max(1, w - 2); - b.wTY = 1 + Math.random() * Math.max(1, h - 2); - b.retargetAt = nowMs + 3000 + Math.floor(Math.random() * 4500); - b.pauseUntil = (arrived && Math.random() < 0.45) ? nowMs + 500 + Math.floor(Math.random() * 1600) : 0; - } - if (b.pauseUntil && nowMs < b.pauseUntil) { b.isWalking = false; return; } - let dx = b.wTX - b.x, dy = b.wTY - b.y; - const dist = Math.hypot(dx, dy) || 1; - dx /= dist; dy /= dist; - if (Math.abs(dy) > Math.abs(dx)) b.direction = dy > 0 ? 'down' : 'up'; - else b.direction = dx > 0 ? 'right' : 'left'; - const ox = b.x, oy = b.y; - const nx = b.x + dx * step, ny = b.y + dy * step; - if (lobbyTileWalkable(md, space, nx, ny, id)) { b.x = nx; b.y = ny; } - else if (lobbyTileWalkable(md, space, nx, b.y, id)) { b.x = nx; } - else if (lobbyTileWalkable(md, space, b.x, ny, id)) { b.y = ny; } - else { b.retargetAt = 0; } /* ติด → หาเป้าใหม่เฟรมหน้า */ - b.x = Math.max(0, Math.min(w - 0.01, b.x)); - b.y = Math.max(0, Math.min(h - 0.01, b.y)); - b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5; - }); - const arr = []; - bots.forEach((b, id) => arr.push({ - id, - x: Math.round(b.x * 1000) / 1000, - y: Math.round(b.y * 1000) / 1000, - direction: b.direction || 'down', - isWalking: !!b.isWalking, - })); - if (arr.length) io.to(sid).emit('lobby-bot-move', { bots: arr }); - /* roster snapshot (L3 ฆ่าผี) — ทุก ~1.5s ส่ง id ผู้เล่นจริงทั้งหมด ให้ client ตัดตัวที่ไม่อยู่ทิ้ง - (throttle ย้ายเข้าไปใน emitLobbyRosterSync แล้ว — กันยิงซ้ำเมื่อทั้ง sweep นี้และ interval P3 เรียก) */ - emitLobbyRosterSync(sid, space); -} - -/* throttle ~1.5s ต่อห้อง — ปลอดภัยไม่ว่าถูกเรียกจาก bot sweep (ห้องมีบอท) หรือ interval P3 (ทุกห้อง) */ -function emitLobbyRosterSync(sid, space) { - if (!space || !space.peers) return; - const nowMs = Date.now(); - if (space.lobbyRosterSyncTs && nowMs - space.lobbyRosterSyncTs <= 1500) return; - space.lobbyRosterSyncTs = nowMs; - const ids = [...space.peers.keys()]; - io.to(sid).emit('lobby-roster', { ids }); -} - -let lobbyBotSweeperTs = 0; -function lobbyBotSweepTick() { - const now = Date.now(); - let dt = lobbyBotSweeperTs ? (now - lobbyBotSweeperTs) / 1000 : 0.06; - if (!(dt > 0)) dt = 0.06; - if (dt > 0.2) dt = 0.2; - lobbyBotSweeperTs = now; - for (const [sid, space] of spaces) { - try { stepLobbyBotsForSpace(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องพังทั้ง sweeper */ } - } -} -setInterval(lobbyBotSweepTick, 60); - -/* roster heartbeat ระหว่างเล่นมินิเกม (gameType != lobby) → client (play.js) ตัด "ผี" (peer ที่ไม่อยู่ใน roster จริง) - แก้บั๊ก: คนเดียวมี avatar 2 อัน (อันเก่าค้างตอน reconnect/ออก เพราะ others เป็น add-only ไม่เคย prune) */ -setInterval(() => { - for (const [sid, space] of spaces) { - if (!space || !space.peers || space.peers.size === 0) continue; - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType === 'lobby') continue; /* lobby ใช้ lobby-roster แยก (heartbeat ด้านล่าง) */ - try { io.to(sid).emit('play-roster', { ids: [...space.peers.keys()] }); } catch (e) { /* กัน 1 ห้องล้ม */ } - } -}, 1500); - -/* [P3 2026-07-16] roster heartbeat ของ "lobby" — ต้องยิงทุกห้อง lobby ที่มีคน "ไม่ว่ามีบอทหรือไม่". - บั๊ก: เดิม emitLobbyRosterSync ถูกเรียก "ที่เดียว" คือใน stepLobbyBotsForSpace (bot sweep) ซึ่ง - return ก่อนถ้า effectiveBotSlotCount() <= 0 — และค่านั้น = 0 เสมอเมื่อ maxPlayers >= 4 (สร้างห้อง - เลือก ≥4 คน). ผลคือห้อง 4+ คน "ไม่เคยได้ lobby-roster" → ตัวฆ่าผี L3 ไม่ทำงาน → ถ้า user-left - หลุด/มาไม่ถึงเครื่องไหน peer ผีค้างถาวร → peers.size ต่างกัน → เลข PLAYERS ของ host ไม่ตรงคนอื่น - ("ในบาง case" = เฉพาะห้องที่เลือก ≥4 คน). ยืนยันจาก gamelog: ห้อง 'บอท 0' คือห้องที่มีอาการ. - แยกออกมาเป็น interval ของตัวเอง → roster ไม่ผูกกับการมีบอทอีกต่อไป */ -setInterval(() => { - for (const [sid, space] of spaces) { - if (!space || !space.peers || space.peers.size === 0) continue; - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'lobby') continue; /* เฉพาะโถง lobby (มินิเกมใช้ play-roster ด้านบน) */ - try { emitLobbyRosterSync(sid, space); } catch (e) { /* กัน 1 ห้องล้ม */ } - } -}, 1500); - -/* เดินบอท MG1 quiz บน server (time-based) → บอทเดินเองไม่พึ่ง host (host หลุดก็ไม่หยุด) */ -let quizBotSweepTs = 0; -setInterval(() => { - const now = Date.now(); - let dt = quizBotSweepTs ? (now - quizBotSweepTs) / 1000 : 0.06; - if (!(dt > 0)) dt = 0.06; - if (dt > 0.2) dt = 0.2; - quizBotSweepTs = now; - for (const [sid, space] of spaces) { - const sess = space && space.quizSession; - if (!sess || !sess.active || !sess.bots || sess.bots.size === 0) continue; - try { stepQuizBots(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องล้ม */ } - } -}, 60); - -/* เดินบอท MG4 quiz_carry บน server (time-based, BFS) → host หลุดก็ไม่หยุด */ -let quizCarryBotSweepTs = 0; -setInterval(() => { - const now = Date.now(); - let dt = quizCarryBotSweepTs ? (now - quizCarryBotSweepTs) / 1000 : 0.06; - if (!(dt > 0)) dt = 0.06; - if (dt > 0.2) dt = 0.2; - quizCarryBotSweepTs = now; - for (const [sid, space] of spaces) { - const s = space && space.quizCarrySession; - if (!s || !s.active || s.ended || !s.bots || s.bots.size === 0) continue; - try { stepQuizCarryBots(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องล้ม */ } - } -}, 60); - -/* MG5 jump_survive — sweep บอท server + จับเวลา/จบรอบ (server-auth) */ -let jumpSurviveBotSweepTs = 0; -setInterval(() => { - const now = Date.now(); - let dt = jumpSurviveBotSweepTs ? (now - jumpSurviveBotSweepTs) / 1000 : 0.033; - if (!(dt > 0)) dt = 0.033; - if (dt > 0.05) dt = 0.05; /* clamp เหมือน client (กันกระโดดไกลตอน lag) */ - jumpSurviveBotSweepTs = now; - for (const [sid, space] of spaces) { - const s = space && space.jumpSurviveSession; - if (!s || !s.active || s.ended) continue; - try { stepJumpSurviveBots(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องล้ม */ } - } -}, JS_TICK_MS); - -/* MG6 space_shooter — sweep บอท server + จับเวลา/จบรอบ (server-auth) */ -let spaceShooterBotSweepTs = 0; -setInterval(() => { - const now = Date.now(); - let dt = spaceShooterBotSweepTs ? (now - spaceShooterBotSweepTs) / 1000 : 0.05; - if (!(dt > 0)) dt = 0.05; - if (dt > 0.08) dt = 0.08; - spaceShooterBotSweepTs = now; - for (const [sid, space] of spaces) { - const s = space && space.spaceShooterSession; - if (!s || !s.active || s.ended) continue; - try { stepSpaceShooterBots(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องล้ม */ } - } -}, SS_TICK_MS); - -/* MG7 balloon_boss — sweep บอท server + รวม HP บอส/จบรอบ (server-auth) */ -let balloonBossBotSweepTs = 0; -setInterval(() => { - const now = Date.now(); - let dt = balloonBossBotSweepTs ? (now - balloonBossBotSweepTs) / 1000 : 0.05; - if (!(dt > 0)) dt = 0.05; - if (dt > 0.08) dt = 0.08; - balloonBossBotSweepTs = now; - for (const [sid, space] of spaces) { - const s = space && space.balloonBossSession; - if (!s || !s.active || s.ended) continue; - try { stepBalloonBossBots(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องล้ม */ } - } -}, BB_TICK_MS); - -/** หมดเวลา Gauntlet — หยุด tick, คืนแผนที่ lobby, แจ้งไคลเอนต์ */ -function endGauntletGame(sid, space, reason) { - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'gauntlet') return; - try { - const sq = space.specialQuiz; const gr = space.gauntletRun; - console.log('[mg2-end-dbg] reason=' + reason + ' sq={trig:' + (sq && sq.triggered) + ',done:' + (sq && sq.done) + ',sess:' + !!(sq && sq.session) + '} awaitCont=' + !!space.specialQuizAwaitContinue + ' endsAtIn=' + (gr && gr.endsAt != null ? (gr.endsAt - Date.now()) : 'n/a') + 'ms gameoverAt=' + (gr && gr.gameoverAt != null ? (gr.gameoverAt - Date.now()) + 'ms' : 'null')); - } catch (eDbg) { /* ignore */ } - if (space.detectiveMinigameActive) { - const msg = reason === 'time' ? 'หมดเวลา — จบมินิเกม' - : (reason === 'all_dead' ? 'ตกขอบหมดแล้ว — จบมินิเกม' : 'จบมินิเกม'); - 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; - /* หยุด run ระหว่างตอบคำถามพิเศษ (special-quiz) — เลื่อน endsAt ไปด้วยให้เวลาไม่เดิน, ไม่ไหล obstacle/ชน */ - if (isSpecialQuizPausedServer(space)) { - /* [2026-07-17] เดิม `gr.endsAt += getGauntletTickMs()` = ก้าวคงที่ → ชดเชยไม่ครบ (user: "เหมือนยัง - ไม่หยุดวิ่ง" — obstacle หยุดจริงเพราะ return ออกไป แต่ deadline เดินต่อ รอบเลยจบเร็วกว่าที่ควร). - บทเรียนเดียวกับ MG5 (07-16): interval ที่ตั้ง 33ms ของจริงเดิน ~48ms ตอนมีโหลด → บวกทีละ 33 - กู้คืนได้แค่ ~68%. ใช้ "เวลาจริงที่ผ่านไป" clamp 1s กันกระโดดตอน tick หาย */ - const nowG = Date.now(); - const shift = Math.max(0, Math.min(1000, nowG - (gr.lastPauseShiftAt || nowG))); - if (gr.endsAt != null) gr.endsAt += shift; - gr.lastPauseShiftAt = nowG; - emitGauntletSync(sid, space); /* ส่ง gauntletEndsAt ใหม่ทุก tick → ทุก client ยึดเลขเดียวกับ server */ - return; - } - gr.lastPauseShiftAt = Date.now(); /* ไม่ pause → คง anchor ให้สด กันกระโดดตอนเริ่ม pause รอบหน้า */ - ensureGauntletEndsAtIfNeeded(space); - if (gr.endsAt != null && Date.now() >= gr.endsAt) { - endGauntletGame(sid, space, 'time'); - return; - } - - /* ตายหมดแล้ว (all_dead) — อยู่ในช่วง grace: หยุดเกม + ให้เวลา client วาดจอ result-gameover → GCM - ก่อน server ดึงกลับ LobbyB. เดิม all_dead จบ+return ทันที (0 grace) = client ยังไม่ทันวาด game over - ก็โดนดึงกลับ = "ตายหมดแต่ไม่ขึ้น game over". ใช้ grace เท่ากับทางชนะ (GAUNTLET_FINISH_WALK_MS) */ - if (gr.gameoverAt != null) { - if (gr.obstacles && gr.obstacles.length) gr.obstacles = []; - if (Date.now() >= gr.gameoverAt) { - endGauntletGame(sid, space, 'all_dead'); - return; - } - /* ไม่ emit sync ระหว่าง grace — ปล่อย client คุม overlay result เอง (เหมือน finish grace) */ - return; - } - - /* เข้าโซนเส้นชัยแล้ว (client แจ้ง) — หยุด obstacle/collision ทันที แล้วรอ grace ให้ client - เดินทุกคน "ค่อยๆ" เข้าเส้นชัยจนครบ ค่อยสรุปผล (ไม่ snap กระพริบ / ไม่จบทันที) */ - if (gr.finishPhase) { - if (gr.obstacles.length) { gr.obstacles = []; } - /* ตายหมดตั้งแต่ก่อน/ระหว่าง finish-walk grace → เข้า grace game over (แทน result-complete จากทางเส้นชัย) - (race: client ตัวหนึ่ง scroll ถึงเส้นชัยก่อน → set finishPhase → early-return ข้ามเช็ค all_dead ท้าย tick) */ - if (gauntletAllHumansEliminated(space)) { - gr.gameoverAt = Date.now() + GAUNTLET_FINISH_WALK_MS; - gr.obstacles = []; - try { glog(sid, space, 'gauntlet-end', '[TC172] ตายหมด (ระหว่าง finish-phase) → grace game over → กลับ LobbyB (pass)'); } catch (e) { /* ignore */ } - return; - } - if (gr.finishEndAt == null) gr.finishEndAt = Date.now() + GAUNTLET_FINISH_WALK_MS; - if (Date.now() >= gr.finishEndAt) { - endGauntletGame(sid, space, 'finish'); - } - /* ไม่ emitGauntletSync ระหว่าง grace — ปล่อย client คุมการเดินเข้าเส้นเอง (กัน sync ดึงตำแหน่งกลับ) */ - return; - } - - if (gr.crownRunHeld) { - space.peers.forEach((p) => { - p.gauntletJumpPending = false; - }); - emitGauntletSync(sid, space); - return; - } - - /* G2: pre-run hold — หลัง 3-2-1 จบ ค้างที่จุด start ให้เห็นเส้นเริ่มก่อน (ยังไม่เลื่อน/เกิด obstacle/ขยับบอท) - client ค้าง bg เองด้วย max(0, serverNow − liveStartMs) → bg+bots/obstacles sync ที่ start พร้อมกัน */ - if (gr.liveStartMs != null && Date.now() < gr.liveStartMs) { - 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(); - /* นับครั้งเริ่มกระโดด (ไม่รีเซ็ตตอนข้ามสำเร็จ) → ให้ peer เห็นอาร์คกระโดดเต็ม - แม้ gauntletJumpTicks ถูกตัด 0 ใน tick เดียวกันตอน advanceX (peer ไม่เคยเห็นค่า >0) */ - p.gauntletJumpSeq = (p.gauntletJumpSeq || 0) + 1; - } - 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); - }); - /* บอท server-authoritative — แถวเลนของบอทที่ยังเล่นอยู่ (ไม่ต้องพึ่ง gauntlet-preview-rows จาก host) */ - if (gr.bots && gr.bots.size) { - gr.bots.forEach((b) => { - if (b.gauntletEliminated) return; - const lr = gauntletLaneGridRowFromPlayerTop(md, b.y, h); - if (lr != null) laneSpawnRows.add(lr); - }); - } - /* แถวบนที่ client แจ้ง (บอท preview — editor หรือ client เก่า) — แปลงเป็นแถว 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: เลเซอร์ — ทุกแถว */ - let _gauntletLaserThisTick = false; - 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 }); - _gauntletLaserThisTick = true; - } - /* ประเภท 1: แยกเลน — แถวเท้า · กันซ้อนคอลัมน์เดียวกับเลเซอร์ (laser+lane ไม่โผล่คอลัมน์เดียวกัน) */ - if (!_gauntletLaserThisTick) { - 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; - }); - - /* บอท server-authoritative — ตัดสินใจกระโดด + ฟิสิกส์ รอบ tick เดียวกับผู้เล่น (obstacle เลื่อนแล้วข้างบน) */ - if (gr.bots && gr.bots.size) { - gr.bots.forEach((b) => { - if (crownHeist && b.gauntletEliminated) return; - gauntletBotDecideJump(b, gr.obstacles, md, w, h); - gauntletBotStepPhysics(b, gr.obstacles, md, w, h, crownHeist); - }); - } - - /* ผู้เล่น (คนจริง) ตกขอบหมด → เข้า grace game over (ไม่รอหมดเวลา) แล้วค่อย return LobbyB - ส่ง sync ครั้งสุดท้าย (ทุกคน eliminated) ให้ client ทริก overlay result-gameover ก่อนหมด grace */ - if (gauntletAllHumansEliminated(space)) { - gr.gameoverAt = Date.now() + GAUNTLET_FINISH_WALK_MS; - gr.obstacles = []; - try { glog(sid, space, 'gauntlet-end', '[TC172] ผู้เล่นตกขอบหมด → grace game over แล้วกลับ LobbyB (pass)'); } catch (e) { /* ignore */ } - emitGauntletSync(sid, space); - return; - } - - emitGauntletSync(sid, space); -} - -/** MG2 crown/gauntlet: คนจริงที่ "ยังเล่นอยู่" (ไม่นับผู้ถูกแบน/ผู้ชม) ตกขอบหมด = จบเกม all_dead - บอทอยู่ใน gr.bots ไม่ใช่ space.peers → ไม่ถูกนับอยู่แล้ว (server ตัดสินจากคนจริงเท่านั้น) - - นับเฉพาะ crown map · ต้องมีคนเล่นจริง ≥1 คน (กันจบทันทีตอน 0 คน) */ -function gauntletAllHumansEliminated(space) { - if (!isGauntletCrownHeistMapBySpace(space) || !space.peers || space.peers.size === 0) return false; - let active = 0; - let alive = 0; - space.peers.forEach((p) => { - if (!p || p.bannedSpectator) return; /* ผู้ถูกแบน = เข้ามาดู ไม่ได้เล่น → ไม่บล็อกการจบเกม */ - active++; - if (!p.gauntletEliminated) alive++; - }); - return active > 0 && alive === 0; -} - -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 หลังเริ่มคดี (กันช่องว่าง/ยูนิโค้ดต่างรูปแบบ) */ -/** ชื่อผู้เล่นสูงสุด — ต้องตรงกับ MAX_LEN ใน Game/public/js/display-name.js (2026-07-16: 24 → 10). - นับเป็น code unit เหมือน (ไทย: สระ/วรรณยุกต์นับแยก ไม่ใช่ 10 ตัวที่ตาเห็น). */ -const NICKNAME_MAX_LEN = 10; -/** ตัดชื่อให้อยู่ในลิมิต — server เป็นตัวบังคับจริง: client แก้ maxlength อย่างเดียวไม่พอ - (join-space เดิมรับ nickname ดิบ ไม่ cap เลย → ชื่อยาวเท่าไหร่ก็เข้าได้) */ -function clampNickname(n) { - let s = String(n == null ? '' : n).trim(); - try { s = s.normalize('NFKC'); } catch (e) { /* ignore */ } - s = s.replace(/[\x00-\x1f\x7f]/g, ''); - if (s.length > NICKNAME_MAX_LEN) s = s.slice(0, NICKNAME_MAX_LEN); - return s; -} - -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); -} - -/* spawnJoinOrder เสถียรข้ามการเปลี่ยนหน้า (lobbyA→lobbyB→minigame): จำลำดับด้วย playerKey (คงที่ต่อ browser) - ไม่ใช่ peers.size ที่เปลี่ยนตามลำดับ reconnect → ตำแหน่งเกิด / สีผิว / slot ของแต่ละคนสลับกันมั่ว */ -/** เลือก host คนถัดไปเมื่อ host ปัจจุบันหลุด — คนที่อยู่ในห้องนานสุด (spawnJoinOrder ต่ำสุด) - เสถียรข้ามการ reconnect (spawnJoinOrder ผูกกับ playerKey) — แทน Map-first ที่สลับได้ */ -function pickNextHostPeer(space) { - if (!space || !space.peers || space.peers.size === 0) return null; - let best = null; - let bestOrd = Infinity; - space.peers.forEach((p, id) => { - if (!p) return; - const ord = (typeof p.spawnJoinOrder === 'number' && p.spawnJoinOrder >= 0) ? p.spawnJoinOrder : 9999; - if (ord < bestOrd || (ord === bestOrd && (best == null || String(id) < String(best.id)))) { - bestOrd = ord; - best = p; - } - }); - return best; -} - -function resolveSpawnJoinOrder(space, playerKey) { - if (!space.spawnOrderByKey || typeof space.spawnOrderByKey !== 'object') space.spawnOrderByKey = {}; - const key = (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? playerKey : ''; - const used = new Set(); - space.peers.forEach((p) => { - if (typeof p.spawnJoinOrder === 'number' && p.spawnJoinOrder >= 0) used.add(p.spawnJoinOrder); - }); - /* host/ผู้เล่นเดิมกลับเข้ามา → คืนลำดับเดิมถ้ายังว่าง (ไม่มี peer อื่นถืออยู่) */ - if (key && Object.prototype.hasOwnProperty.call(space.spawnOrderByKey, key)) { - const prev = space.spawnOrderByKey[key]; - if (typeof prev === 'number' && prev >= 0 && !used.has(prev)) return prev; - } - let ord = 0; - while (used.has(ord)) ord++; - if (key) space.spawnOrderByKey[key] = ord; - return ord; -} - -/** สุ่มจุดเกิดในช่อง 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; -} - -/** เปิดเฟส "เลือกผู้ต้องสงสัย" ให้ทั้งห้อง — เรียกหลังตอบข้อเสนอตัวป่วน หรือเมื่อข้ามการเสนอ (คน < 4) */ -function openSuspectPhaseForRoom(sid, space, disruptorAccepted) { - if (!space) return; - 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: !!disruptorAccepted, - 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), - }); - }); -} - -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; - /* [TC190] ทางปกติเดินแล้ว → ปลด backstop (guard troublesomeOfferSent กันซ้ำอยู่แล้ว แต่เก็บให้สะอาด - เหมือน deb/max timer ข้างล่าง ไม่ให้ timer ค้างถือ ref ห้องไว้อีก 45 วิ) */ - if (space.troublesomeDeadlockTimer) { clearTimeout(space.troublesomeDeadlockTimer); space.troublesomeDeadlockTimer = null; } - /* เสนอบทตัวป่วนเฉพาะเมื่อมี "คนจริง ≥ 4 คน" — ยกเว้น admin บังคับ (troublesomeForceOffer) */ - const forceOffer = !!(runtimeGameTiming && runtimeGameTiming.troublesomeForceOffer); - if (peerIds.length < 4 && !forceOffer) { - /* ไม่เสนอบทตัวป่วน (คน < 4) → ข้ามไปเปิดหน้าเลือกผู้ต้องสงสัยเลย (ไม่มีตัวป่วน) */ - space.troublesomeOfferSent = true; - space.troublesomeResponseReceived = true; - space.troublesomeAccept = false; - space.disruptorPlayerKey = ''; - space.disruptorNickname = ''; - if (space.troublesomeDebTimer) clearTimeout(space.troublesomeDebTimer); - if (space.troublesomeMaxTimer) clearTimeout(space.troublesomeMaxTimer); - space.troublesomeDebTimer = null; - space.troublesomeMaxTimer = null; - openSuspectPhaseForRoom(sid, space, false); - 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: clampTroublesomeOfferSec(runtimeGameTiming && runtimeGameTiming.troublesomeOfferSec) }); -} - -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) { - const rollMaxMs = Math.max(1000, Math.round(clampTroublesomeRollMaxSec(runtimeGameTiming && runtimeGameTiming.troublesomeRollMaxSec) * 1000)); - space.troublesomeMaxTimer = setTimeout(() => attemptTroublesomeRoll(sid, true), rollMaxMs); - } -} - -/** - * ห้องทดสอบเอดิเตอร์ / 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 global leaderboard (คะแนนสูงสุดต่อ room/หมวด ข้ามเซสชัน) ===== */ - socket.on('qb-leaderboard-submit', (d) => { - if (!d || typeof d !== 'object') return; - qbLeaderboardSubmit(d.roomId, d.name, d.score, d.characterId); - }); - socket.on('qb-leaderboard-get', (d, cb) => { - if (typeof cb !== 'function') return; - try { cb(qbLeaderboardGet(d && d.roomId, d && d.name)); } catch (e) { cb({ top: [], me: null, total: 0 }); } - }); - /* ===== 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, playerKey, desiredLobbyColorThemeIndex, desiredLobbySkinToneIndex, bannedSpectator, roomPassword }, cb) => { - const space = spaces.get(spaceId); - if (!space || !space.mapData) return cb && cb({ ok: false, error: 'ไม่พบห้อง' }); - /* ห้องส่วนตัวที่ตั้งรหัส → ต้องกรอกรหัสให้ตรง ครั้งแรก แล้วจำด้วย playerKey - (กันด่านรหัสพังตอนเปลี่ยนหน้า LobbyA→play→LobbyB ที่ socket.id เปลี่ยน — ไม่ต้องแนบ pass ทุกลิงก์) */ - if (space.isPrivate && space.roomPassword) { - space.privateVerifiedKeys = space.privateVerifiedKeys || new Set(); - const pk = String(playerKey == null ? '' : playerKey); - const provided = String(roomPassword == null ? '' : roomPassword).replace(/\D/g, '').slice(0, 4); - const passOk = provided === space.roomPassword; - const alreadyOk = (pk && space.privateVerifiedKeys.has(pk)) || space.peers.has(socket.id); - if (!passOk && !alreadyOk) { - return cb && cb({ ok: false, error: 'รหัสผ่านห้องไม่ถูกต้อง' }); - } - if (passOk && pk) space.privateVerifiedKeys.add(pk); - } - 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) { - /* rejoin whitelist: เช็ค playerKey เป็นหลัก (กันเคส 2 คนชื่อซ้ำ — ลบ nick คนนึงตอนออก ทำให้อีกคน rejoin ไม่ได้) - fallback เป็นชื่อ (คนที่ไม่มี playerKey / ความเข้ากันได้เดิม) */ - const jk = (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? ('k:' + playerKey) : ''; - const wlKeys = space.caseParticipantKeys; - const wlNicks = space.caseParticipantNicknames; - const okByKey = !!(jk && wlKeys && wlKeys.size > 0 && wlKeys.has(jk)); - const okByNick = !!(joinNickNorm && wlNicks && wlNicks.size > 0 && wlNicks.has(joinNickNorm)); - if (!okByKey && !okByNick) { - return cb && cb({ ok: false, error: 'ห้องนี้เริ่มคดีแล้ว ไม่รับผู้เล่นเพิ่ม' }); - } - } - socket.join(spaceId); - socket.data.spaceId = spaceId; - /* Host เสถียรข้ามการเปลี่ยนหน้า (lobby→play→lobbyB): จำ host ด้วย playerKey (คงที่ต่อ browser) - ไม่ใช่ socket.id (เปลี่ยนทุกครั้งที่โหลดหน้าใหม่ → host สลับไปมาแบบสุ่ม) */ - const joinPlayerKey = (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? playerKey : ''; - const prevHostId = space.hostId; - if (joinPlayerKey && space.hostPlayerKey && joinPlayerKey === space.hostPlayerKey) { - space.hostId = socket.id; /* host ตัวจริงกลับเข้ามา → คืนสถานะ host เสมอ */ - } else if (!space.hostId) { - space.hostId = socket.id; /* ยังไม่มี host → คนนี้เป็น host (ชั่วคราวถ้ามี hostPlayerKey อยู่แล้ว) */ - if (!space.hostPlayerKey && joinPlayerKey) space.hostPlayerKey = joinPlayerKey; - } - /* reconnect/เปลี่ยนหน้า: ผู้เล่นคนเดิม (playerKey เดิม) ได้ socket.id ใหม่ แต่ peer เก่ายังค้างใน space.peers - จนกว่า disconnect เก่าจะมา (อาจช้า) → ทุกคนเห็น "ตัวละครซ้ำ 2 ตัว" (ghost) - แก้: ลบ ghost peer ที่ playerKey ตรงแต่ socket.id ต่าง ก่อนเพิ่มตัวใหม่ + แจ้ง user-left ให้ client ลบทิ้ง */ - if (joinPlayerKey) { - const staleIds = []; - space.peers.forEach((p, pid) => { - if (pid !== socket.id && p && p.playerKey && p.playerKey === joinPlayerKey) staleIds.push(pid); - }); - staleIds.forEach((pid) => { - space.peers.delete(pid); - if (space.quizCarryLobbyReady) delete space.quizCarryLobbyReady[pid]; - if (space.gauntletCrownLobbyReady) delete space.gauntletCrownLobbyReady[pid]; - io.to(spaceId).emit('user-left', { id: pid }); - }); - /* กลับมาทัน grace 10 วิ → ยกเลิก bot takeover + ปิด banner countdown */ - if (space.graceReconnect && space.graceReconnect[joinPlayerKey]) { - delete space.graceReconnect[joinPlayerKey]; - if (space.graceTimers && space.graceTimers[joinPlayerKey]) { clearTimeout(space.graceTimers[joinPlayerKey]); delete space.graceTimers[joinPlayerKey]; } - io.to(spaceId).emit('peer-grace-cancel', { nickname: nickname || 'ผู้เล่น' }); - } - /* MG1 takeover: ผู้เล่นกลับมากลาง quiz (playerKey เดิม) → ถอนบอทที่แปลงจากเขา (sess.bots) คืนสิทธิ์เล่น - กัน "ตัวซ้ำ" (บอท takeover + ตัวที่ rejoin) */ - const sessJ = space.quizSession; - if (sessJ && sessJ.bots) { - const dropBotIds = []; - sessJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropBotIds.push(bid); }); - dropBotIds.forEach((bid) => { - sessJ.bots.delete(bid); - io.to(spaceId).emit('user-left', { id: bid }); /* ลบ avatar บอท takeover เก่า — ตัวจริงจะมาใหม่ผ่าน user-joined */ - }); - } - /* MG2 takeover เช่นกัน: ผู้เล่นกลับมากลาง gauntlet (playerKey เดิม) → ถอนบอทที่แปลงจากเขา (gauntletRun.bots) */ - const grJ = space.gauntletRun; - if (grJ && grJ.bots) { - const dropG = []; - grJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropG.push(bid); }); - dropG.forEach((bid) => { - grJ.bots.delete(bid); - io.to(spaceId).emit('user-left', { id: bid }); - }); - } - /* MG3 takeover เช่นกัน: กลับมากลาง stack (playerKey เดิม) → ถอนบอท + คืน seat ให้คนจริง (inherit คะแนนกลับ) */ - const s3J = space.stackSession; - if (s3J && s3J.bots) { - let restored = false; - const dropS = []; - s3J.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropS.push(bid); }); - dropS.forEach((bid) => { - const inherited = Math.max(0, ((s3J.bots.get(bid) || {}).score) | 0); - s3J.bots.delete(bid); - const entry = (s3J.turnOrder || []).find((e) => e.kind === 'bot' && String(e.botId) === String(bid)); - if (entry) { entry.kind = 'human'; entry.id = socket.id; entry.botId = null; s3J.humanPts[socket.id] = inherited; restored = true; } - io.to(spaceId).emit('user-left', { id: bid }); - }); - if (restored) emitStackStateServer(spaceId, space); - } - /* MG4 takeover เช่นกัน: กลับมากลาง quiz_carry (playerKey เดิม) → ถอนบอท + คืน slot ให้คนจริง (inherit ป้าย/คะแนนกลับ) */ - const sCJ = space.quizCarrySession; - if (sCJ && sCJ.bots) { - const dropC = []; - sCJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropC.push(bid); }); - dropC.forEach((bid) => { - const bb = sCJ.bots.get(bid) || {}; - sCJ.bots.delete(bid); - sCJ.players[socket.id] = { held: (bb.held == null ? null : bb.held), score: Math.max(0, Number(bb.score) || 0) }; - io.to(spaceId).emit('user-left', { id: bid }); - }); - } - /* MG5 takeover เช่นกัน: กลับมากลาง jump_survive (playerKey เดิม) → ถอนบอท + คืน slot คนจริง (inherit สถานะตาย) */ - const sJJ = space.jumpSurviveSession; - if (sJJ && sJJ.bots) { - const dropJ = []; - sJJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropJ.push(bid); }); - dropJ.forEach((bid) => { - const bb = sJJ.bots.get(bid) || {}; - sJJ.bots.delete(bid); - sJJ.players[socket.id] = { eliminated: !!bb.eliminated }; - io.to(spaceId).emit('user-left', { id: bid }); - }); - } - /* MG6 takeover: กลับมากลาง space_shooter (playerKey เดิม) → ถอนบอท + คืน slot คนจริง (inherit คะแนน/ตาย) */ - const sSJ = space.spaceShooterSession; - if (sSJ && sSJ.bots) { - const dropSS = []; - sSJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropSS.push(bid); }); - dropSS.forEach((bid) => { - const bb = sSJ.bots.get(bid) || {}; - sSJ.bots.delete(bid); - sSJ.players[socket.id] = { eliminated: !!bb.eliminated }; - io.to(spaceId).emit('user-left', { id: bid }); - }); - } - /* MG7 takeover: กลับมากลาง balloon_boss (playerKey เดิม) → ถอนบอท + คืน slot คนจริง */ - const sBJ = space.balloonBossSession; - if (sBJ && sBJ.bots) { - const dropBB = []; - sBJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropBB.push(bid); }); - dropBB.forEach((bid) => { sBJ.bots.delete(bid); io.to(spaceId).emit('user-left', { id: bid }); }); - } - /* MG3–MG7 pending takeover (คนหลุดช่วง how-to/นับถอยหลัง — ยังไม่มี session live): กลับมาทันก่อนเกมเริ่ม - → ล้าง pending ที่ playerKey ตรง กัน drainPendingMissionTakeovers seed เป็นบอทซ้ำตอน startXxxGame - ROOT CAUSE "บอทเข้าไปเล่นด้วย MG3/5/7": rejoin เดิมถอนบอทเฉพาะ session ที่ live แล้ว (บล็อกข้างบน) - แต่ shell (MG3-7) สร้าง session ตอน 3-2-1 จบ → ตอน rejoin ยังไม่มี session = ไม่มีอะไรถอน + - pending ค้าง → seed เป็น "บอทผี" คู่กับคนจริงที่ rejoin. emit user-left(id เก่า) ลบ avatar บอท placeholder */ - if (Array.isArray(space.pendingMissionTakeovers) && space.pendingMissionTakeovers.length) { - const cancelled = space.pendingMissionTakeovers.filter((r) => r && r.playerKey && r.playerKey === joinPlayerKey); - if (cancelled.length) { - space.pendingMissionTakeovers = space.pendingMissionTakeovers.filter((r) => !(r && r.playerKey && r.playerKey === joinPlayerKey)); - cancelled.forEach((r) => { if (r.id) io.to(spaceId).emit('user-left', { id: r.id }); }); - glog(spaceId, space, 'join', '[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover ' + cancelled.length + ' (กันบอทผี MG3/5/7 · pass)', nickname); - } - } - } - /* reconnect dedup ชั้น 2: ghost ที่ playerKey block จับไม่ได้ (peer เก่าไม่มี playerKey) - ลบด้วย "ชื่อ custom เดียวกัน" — แต่ **เฉพาะ peer เก่าที่ไม่มี valid playerKey** เท่านั้น - ⚠️ ห้ามลบ peer ที่มี playerKey คนละค่า: 2 คนตั้งชื่อเหมือนกัน (เช่น "MONE") = คนละคน ต้องเก็บทั้งคู่ - (เดิมลบทุกตัวที่ชื่อตรง → client ที่ชื่อซ้ำ host ถูกลบ = "ไม่ได้เล่นเลย") */ - const validPkStr = (p) => (p && typeof p.playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(p.playerKey)) ? p.playerKey : ''; - const newNickNorm = normalizeLobbyNickname(nickname); - const newNickIsCustom = newNickNorm && newNickNorm !== 'ผู้เล่น' - && newNickNorm.indexOf('ผู้เล่น') !== 0 && newNickNorm.indexOf('บอท') !== 0; - if (newNickIsCustom) { - const ghostIds = []; - space.peers.forEach((p, pid) => { - if (pid === socket.id || !p) return; - const ppk = validPkStr(p); - if (ppk && ppk !== joinPlayerKey) return; /* คนละ playerKey = คนละคน (ชื่อซ้ำได้) → ไม่ลบ */ - if (normalizeLobbyNickname(p.nickname) === newNickNorm) ghostIds.push(pid); - }); - ghostIds.forEach((pid) => { - space.peers.delete(pid); - if (space.quizCarryLobbyReady) delete space.quizCarryLobbyReady[pid]; - if (space.gauntletCrownLobbyReady) delete space.gauntletCrownLobbyReady[pid]; - io.to(spaceId).emit('user-left', { id: pid }); - }); - } - if (serverMapIsPostCaseLobbyB(space)) initTroublesomeState(space); - ensurePeerLobbyThemes(space); - const mdJoin = (space.mapId && maps.get(space.mapId)) || space.mapData; - const spawnJoinOrder = resolveSpawnJoinOrder(space, playerKey); - const spawnPt = pickSpawnForJoin(mdJoin, spawnJoinOrder); - const bbStartBalloons = balloonBossBalloonsForMap(mdJoin); - /* ใช้สีที่ 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 = { - /* [2026-07-16] cap ชื่อตรงนี้ด้วย — เดิมรับค่าดิบจาก client: แก้ maxlength ฝั่ง client อย่างเดียว - ไม่ได้บังคับอะไรเลย (join-space เป็นทางเข้าหลักของชื่อ ไม่ใช่หน้าแก้ชื่อ) */ - id: socket.id, x: +spawnPt.x, y: +spawnPt.y, direction: 'down', nickname: clampNickname(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, - playerKey: (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? playerKey : '', - reportedMiniScore: 0, reportedMiniSurvived: true, - bannedSpectator: !!bannedSpectator, - netReady: false, /* เพิ่ง join → กำลังโหลด จนกว่า client ส่ง client-ready (badge ให้คนอื่นเห็นว่า "กำลังโหลด" ไม่ใช่ค้าง) */ - }; - 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); - /* เก็บกวาด peer ซ้ำทั้งก้อน (ghost) — เผื่อ dedup ต่อ-join ด้านบนพลาด/race จนคนเดิมค้าง 2 ตัว */ - pruneDuplicateSpacePeers(spaceId, space); - glog(spaceId, space, 'join', 'สี#' + (peer.lobbyColorThemeIndex || '?') + ' · คน ' + space.peers.size + ' + บอท ' + effectiveBotSlotCount(space) + (joinPlayerKey ? '' : ' · NO-KEY'), nickname || 'ผู้เล่น'); - /* invariant: คน + บอท ≤ 6 — กัน "เงาสะท้อนของ user": คนหลุดแล้ว grace แปลงเป็นบอท (botSlotCount+1, สีตามคนออก) - พอคนนั้นกลับเข้ามา (reconnect หลัง grace) จะมีทั้งคนจริง + บอทสีเดียวกัน = ตัวซ้ำ. ตัดบอทส่วนเกินออกให้รวมไม่เกิน 6 - (มินิเกม cap ที่ 6-humans อยู่แล้ว ~rebalancePreviewBots แต่ LobbyB ไม่ได้ cap → ตัดที่ server ต้นทาง) */ - { - const humansNow = space.peers.size; - const botsNow = effectiveBotSlotCount(space); - if (humansNow + botsNow > LOBBY_SLOT_TOTAL) { - space.botSlotCount = Math.max(0, LOBBY_SLOT_TOTAL - humansNow); - } - /* re-sync สีบอททุกครั้งที่มีคน (re)join + แจ้ง "ทุกจอ" (รวมคนที่อยู่ในมินิเกม/LobbyB อยู่แล้ว): - เคส "เงาสะท้อนของ user" = คนหลุด→บอทใช้สีคนนั้น (pin)→คนนั้นกลับมา. join handler เคลียร์ pin ที่ชนสีคนแล้ว - (7704) + syncLobbyBotThemeSlots เลือกสีใหม่ที่ไม่ชนคน แต่เดิม emit เฉพาะตอน total>6 → จออื่นไม่อัปเดต = บอทยังสีเดิม. - emit ทุก join → บอทเปลี่ยนสีพ้นจากสี user ทันทีทุกจอ */ - emitLobbyTintSync(spaceId, space); - } - /* กลับ LobbyB เฉพาะเมื่อไม่ได้เข้า play ฉากมินิเกมที่กำลังเล่น (เช่น refresh room-lobby) - — แต่ "เฉพาะตอนไม่มีคนอื่นเล่นค้างอยู่": เดิมดึง "ทั้งห้อง" ออกจากมินิเกมเพราะคนเดียวที่ rejoin มา - ด้วย map ไม่ตรง (คนหลุดกลางเกมแล้ว reconnect เข้า room-lobby) → คนที่เหลือโดนตัดกลางเกม, client - ค้างไม่ส่ง lobbyb-loaded → gate ค้าง = ทั้งห้องเล่นต่อไม่ได้ (เคส 2026-07-15 18:48). - ถ้ายังมีคนอื่นเล่นอยู่ → ปล่อยเกมเดินต่อ, คนที่ rejoin รอจนจบรอบแล้ว return ปกติจะดึงเข้า LobbyB เอง */ - if (space.detectiveMinigameActive) { - const clientMap = playMapId != null ? String(playMapId).trim() : ''; - const activeMap = space.mapId != null ? String(space.mapId).trim() : ''; - if (!clientMap || !activeMap || clientMap !== activeMap) { - const othersInRun = Math.max(0, space.peers.size - 1); /* คนอื่นที่ยังอยู่ในรอบมินิเกม (ไม่นับคนที่เพิ่ง join) */ - if (othersInRun === 0) { - returnDetectiveSpaceToLobbyB(spaceId, space, 'กลับ LobbyB'); - } else { - try { glog(spaceId, space, 'lobbyb-gate', '[TC144] rejoin map ไม่ตรง (' + (clientMap || 'none') + '≠' + activeMap + ') แต่ยังมีคนเล่นอยู่ ' + othersInRun + ' คน → ไม่ดึงทั้งห้องออก (pass)', nickname || 'ผู้เล่น'); } catch (e) { /* ignore */ } - } - } - } - 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', quizCarryLobbySyncPayload(space)); - } - if (mdJoin.gameType === 'gauntlet') { - const ord = typeof peer.spawnJoinOrder === 'number' ? peer.spawnJoinOrder : [...space.peers.keys()].indexOf(socket.id); - /* นับช่อง spawn ให้พอกับ "คน + บอท" — เดิมส่งแค่ peers.size: ห้อง 1 คน = 1 ช่อง = pos[0] (เลนบนสุด) เสมอ - แล้วไปทับบอทที่ initGauntletPeersAll วางไว้ที่ pos[0] → คน(host ใหม่)+บอท วิ่งเลนเดียวกันเลนบนสุด (bug ที่เจอซ้ำ) */ - const grJoin = space.gauntletRun; - const botCntJoin = (grJoin && grJoin.bots) ? grJoin.bots.size : 0; - const pos = gauntletSpawnPositions(mdJoin, space.peers.size + botCntJoin); - /* เลี่ยงทับทั้ง "peer อื่น" และ "บอท" ที่อยู่ในเลนแล้ว — ดันไปช่องว่างถัดไป (เดิมเช็คแต่ peer ไม่เช็คบอท) */ - const occupied = new Set(); - space.peers.forEach((q) => { if (q && q.id !== socket.id) occupied.add(Math.floor(q.x) + ',' + Math.floor(q.y)); }); - if (grJoin && grJoin.bots) grJoin.bots.forEach((b) => { if (b) occupied.add(Math.floor(Number(b.x)) + ',' + Math.floor(Number(b.y))); }); - let pidx = Number.isFinite(ord) ? Math.max(0, Math.min(ord, pos.length - 1)) : pos.length - 1; - let pguard = 0; - while (pos[pidx] && occupied.has(Math.floor(pos[pidx].x) + ',' + Math.floor(pos[pidx].y)) && pguard < pos.length) { pidx = (pidx + 1) % pos.length; pguard++; } - const pt = pos[pidx] != null ? pos[pidx] : 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', crownLobbySyncPayload(space)); - } - 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', crownLobbySyncPayload(space)); - } - 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), - suspectTeamProgress: Array.isArray(space.suspectTeamProgress) ? space.suspectTeamProgress.slice() : [0, 0, 0], /* #6 shared */ - botSlotCount: effectiveBotSlotCount(space), - lobbyBotThemes: syncLobbyBotThemeSlots(space), - joinLocked: !!space.joinLocked, - /* [TC154] host กำลังเปิด wizard เลือกระดับ/คดี = set-ready ถูก "ปฏิเสธฝั่ง server" (12391). - เดิมไม่ได้ส่งสถานะนี้ใน join ack → คนที่เพิ่งเข้ามาไม่รู้ว่าโดนล็อก: ปุ่มพร้อมดูกดได้ปกติ - แต่กดแล้ว server ตีกลับเงียบ ๆ = "กดไม่ติด ไม่มีอะไรบอก" (คนละอันกับ relay lobby-preplay-open - ที่ส่งเฉพาะตอน host กด เปิด/ปิด — คนเข้าทีหลังไม่เคยได้ยิน) */ - preplayOpen: !!space.preplayOpen, - missionLiveBeginsAt: space.missionLiveBeginsAt || null, - missionLive: isMinigameLiveForRejoinResume(space), /* rejoin กลางเกม live → client ข้าม how-to เข้าเล่นต่อ */ - }; - if (space.jsLayout) joinCb.jsLayout = space.jsLayout; /* MG5: ส่ง pattern ที่สุ่มไว้ให้ client (initial + late-join) */ - 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); - /* กันค้างตอน reconnect: event "จบมินิเกม" (balloon-boss-over/stack-over/space-shooter-over) - host ยิงครั้งเดียวผ่าน socket.to — ถ้า client หลุดตอนนั้นจะค้างถาวร (ไม่มี replay). - เก็บ over ล่าสุดไว้ → resend ให้ socket ที่เพิ่ง (re)join ภายใน 45s ถ้ายังอยู่ในแมปมินิเกม - (gameType !== 'lobby' = ยังไม่ทรานซิชันกลับ). delay เล็กน้อยให้ client applyMapAndStart เสร็จก่อน */ - const lmo = space.lastMinigameOver; - if (lmo && lmo.event && (Date.now() - lmo.atMs) < 45000 && mapDataOut && mapDataOut.gameType !== 'lobby') { - const targetSid = socket.id; - setTimeout(() => { - try { - const sp = spaces.get(spaceId); - if (sp && sp.lastMinigameOver === lmo && io.sockets.sockets.get(targetSid)) { - io.to(targetSid).emit(lmo.event, lmo.payload); - } - } catch (_e) { /* ignore */ } - }, 400); - } - if (space.hostId !== prevHostId) io.to(spaceId).emit('host-changed', { hostId: space.hostId }); /* host เปลี่ยน (เช่น host ตัวจริงกลับเข้ามา) → sync ทุกคน */ - 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 }); - } - } - /* Card 3 Ban: ผู้เล่นกลับเข้า LobbyB หลังเกมจบ + มีการ์ด Ban ค้าง → เริ่มโหวตแบนทันที - (คน rejoin คนแรกเป็นคนเปิดโหวต; คนที่ rejoin ทีหลังขณะโหวตเปิดอยู่ → ส่งสถานะให้เห็นด้วย) */ - if (serverMapIsPostCaseLobbyB(space)) { - /* จับเวลา rejoin ล่าสุด → maybeOpenPendingBanVote รอ socket "นิ่ง" 2s ก่อนเปิดโหวต (กันเปิดกลาง transition) */ - space._lastLobbyBRejoinAt = Date.now(); - /* ผู้เล่นกลับเข้า LobbyB แล้ว → อัปเดตจำนวนคนที่พร้อมให้ทุกคน (host ใช้เปิดปุ่มเริ่มเมื่อครบ) */ - emitDetectiveLobbyBPresence(spaceId, space); - console.log('[ban-debug] join-space rejoin: postLobbyB=true pendingBan=', !!space.pendingBanVoteCard, 'pickVote=', !!space.pickVote, 'by', socket.id); - if (space.pendingBanVoteCard && !space.pickVote) { - /* เปิดโหวต Ban เฉพาะเมื่อทุกคนกลับเข้า LobbyB ครบ (ไม่ใช่หน่วงเวลาคงที่ตอนคนยังเข้าไม่ครบ) - — maybeOpenPendingBanVote เช็ค present/total + safety timeout เอง */ - maybeOpenPendingBanVote(spaceId, space); - } else if (space.pickVote && !space.pickVote.resolved) { - const pv = space.pickVote; - /* คนเพิ่ง reconnect ระหว่างโหวตยังเปิด (LobbyB transition ยังไม่นิ่งตอนเปิดโหวต) → เดิมไม่อยู่ใน - candidate snapshot = "เล่น 4 คนแต่โหวตได้ 3". rebuild candidates จาก peers ปัจจุบัน แล้ว - broadcast ให้ทุกคนเห็นครบ (client เก็บ selection เดิมไว้เมื่อ purpose เดิม) */ - pv.candidates = pickVoteCandidates(space); - autoVotePickBots(space); - const pvPayload = { purpose: pv.purpose, candidates: pv.candidates.map(pickVoteCandidatePayload), endsAt: pv.endsAt, durationMs: pickVoteMs() }; - space.peers.forEach((_p, pid) => { const sk = io.sockets.sockets.get(pid); if (sk) sk.emit('player-pick-vote-open', pvPayload); }); - emitPickVoteUpdate(spaceId, space); - } - } - }); - - /** ผู้เล่นเดินชนไอคอนคำถามพิเศษ — ใครชนก่อนเป็นคนเปิดเซสชันให้ทั้งห้อง */ - /** 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); - }); - - /** Test Mode (Ctrl+Alt+W): บังคับให้รอบโหวตชี้คนร้ายถัดไป "นับเป็นโหวตผิด" 1 ครั้ง → ใช้เทสต์การ์ด 5 (จับผิดตัว/โหวตใหม่) */ - socket.on('test-force-wrong-vote', () => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || !serverMapIsPostCaseLobbyB(space) || !space.peers.has(socket.id)) return; - space.testForceWrongVote = true; - console.log('[test] force-wrong-vote armed by', socket.id); - }); - - /** client แจ้งว่าเกม "live จริง" แล้ว (พ้น howto/นับถอยหลัง) → ปล่อยไอคอนคำถามพิเศษ (หน่วง 2 วิ + นับถอยหลัง) ครั้งเดียว/รอบ */ - socket.on('special-quiz-arm', () => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || !space.peers.has(socket.id)) return; - const sq = space.specialQuiz; - if (!sq || sq.triggered || sq.done || sq.appeared || sq.armScheduled) return; - sq.armScheduled = true; - scheduleSpecialQuizIconAppearAndExpiry(sid, space); - }); - - 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); - /* [achv b5] คนที่แตะไอคอน ตำรวจ/ทนาย = คนเก็บไอเทมช่วยเหลือ (trigger ครั้งเดียว/ไอคอน) */ - if (started) { - const bp = space.peers.get(socket.id); - if (bp && bp.playerKey) submitAchievementProgress([{ playerKey: bp.playerKey, id: 'b5_deep_scanner', inc: 1 }]); - } - 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 }); - /* รอบจบไปแล้ว (resume แล้ว) — ตอบ ok เฉย ๆ */ - if (!space.specialQuizAwaitContinue) return reply({ ok: true }); - /* เก็บ ack ของผู้เล่นคนนี้ แล้วเช็ค: ครบทุกคน → resume พร้อมกัน (ไม่ resume ทันทีคนเดียว) */ - if (!space.specialQuizContinueAcks) space.specialQuizContinueAcks = new Set(); - space.specialQuizContinueAcks.add(socket.id); - reply({ ok: true }); - maybeFinishSpecialQuizContinue(sid, space); - }); - - /** เปลี่ยนสีเสื้อ/ผิวใน 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; - } - /* ชนกับ "คนจริง" คนอื่น → ปฏิเสธ; ชนกับ "บอท" เท่านั้น → ดันบอทไปสีอื่น (ให้สอดคล้องกับตอน join) - กันอาการ "เปลี่ยนสีไม่ได้" เมื่อบอทยึดสีไว้ */ - let humanTaken = false; - space.peers.forEach((op, oid) => { - if (oid === socket.id) return; - if (parseInt(op.lobbyColorThemeIndex, 10) === themeIndex) humanTaken = true; - }); - if (humanTaken) return reply({ ok: false, error: 'สีเสื้อนี้มีคนใช้แล้ว' }); - if (space.lobbyBotThemeBySlot && typeof space.lobbyBotThemeBySlot === 'object') { - Object.keys(space.lobbyBotThemeBySlot).forEach((k) => { - const slot = space.lobbyBotThemeBySlot[k]; - if (slot && parseInt(slot.themeIndex, 10) === themeIndex) slot.themeIndex = 0; /* syncLobbyBotThemeSlots จะหาสีใหม่ให้ */ - }); - } - peer.lobbyColorThemeIndex = themeIndex; - peer.lobbySkinToneIndex = skinIndex; - emitLobbyTintSync(sid, space); - reply({ - ok: true, - lobbyColorThemeIndex: themeIndex, - lobbySkinToneIndex: skinIndex, - lobbyBotThemes: space.lobbyBotThemeBySlot ? syncLobbyBotThemeSlots(space) : [], - }); - }); - - /* บอท lobby เดินตรงกันทุก client: host เป็นเจ้าของตำแหน่ง → relay ให้คนอื่นในห้อง - (กัน desync จากการที่แต่ละเครื่องสุ่มเดินบอทแยกกัน) */ - /* DEPRECATED: server เป็นเจ้าของบอท lobby เองแล้ว (stepLobbyBotsForSpace/lobbyBotSweepTick) - → ไม่ relay ตำแหน่งบอทจาก client อีก (กัน host เวอร์ชันเก่าฉีดตำแหน่งชนกับ server tick = บอทกระตุก/2 แหล่ง) */ - socket.on('lobby-bot-move', () => { /* no-op: server-authoritative */ }); - - /* fill-bot ใน 7 มินิเกม: host เป็นเจ้าของ state ของบอท → relay ให้คนอื่นในห้อง - (กัน desync จากการที่แต่ละเครื่องจำลองบอทแยกกัน) */ - socket.on('fill-bot-state', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || space.hostId !== socket.id) return; /* เฉพาะ host เท่านั้น */ - if (!data || !Array.isArray(data.bots) || data.bots.length > 16) return; - /* พ่วง hostId — ผู้รับ (non-host เท่านั้น เพราะ socket.to ไม่ส่งกลับผู้ส่ง) ใช้ยืนยันว่า "ตัวเองไม่ใช่ host" - แก้ playHostId ที่ผิดตอน race LobbyB→play (กัน 2 จอคิดว่าเป็น host → sim บอทคนละชุด) */ - const humans = (Array.isArray(data.humans) ? data.humans.filter((h) => h != null).slice(0, 50).map(String) : undefined); - socket.to(sid).emit('fill-bot-state', { bots: data.bots, hostId: space.hostId, humans }); - }); - - /* MG7 balloon_boss — relay กระสุนของผู้เล่น/บอท ให้ทุกคนในห้องเห็นตรงกัน (ทุกคนยิงได้ ไม่จำกัด host) - payload: { sx, sy, vx, vy, ownerId, t0 } — t0 = serverNow ตอนยิง ให้ผู้รับ fast-forward ตำแหน่งให้ตรง */ - socket.on('balloon-boss-shot', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || !data) return; - if (isSpecialQuizPausedServer(space)) return; /* คำถามพิเศษ → ไม่ relay กระสุน (กันค้างยิงรัวหลังตอบ) */ - const ok = ['sx', 'sy', 'vx', 'vy'].every((k) => typeof data[k] === 'number' && Number.isFinite(data[k])); - if (!ok) return; - socket.to(sid).emit('balloon-boss-shot', { - sx: data.sx, sy: data.sy, vx: data.vx, vy: data.vy, - ownerId: data.ownerId, t0: (typeof data.t0 === 'number' ? data.t0 : Date.now()), - /* [P5] forward the shooter's aim so the receiver can pin the arrow ring to it (fixes arrow/bullet - desync on a viewer's screen). Redundant with atan2(vy,vx) today since vx/vy = dir*spd, but - explicit = authoritative and future-proof if the velocity ever stops being a pure direction. */ - aim: (typeof data.aim === 'number' && Number.isFinite(data.aim)) ? data.aim : undefined, - }); - }); - - /* MG7 balloon_boss — host เป็นคนตัดสินจบเกม แล้ว relay ให้ทุกคนจบพร้อมกัน */ - socket.on('balloon-boss-over', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || !data) return; - const reason = (data.reason === 'victory' || data.reason === 'all_dead' || data.reason === 'time') ? data.reason : 'all_dead'; - const scores = Array.isArray(data.scores) - ? data.scores.filter((s) => s && s.id != null).map((s) => ({ id: s.id, score: Math.max(0, Number(s.score) || 0), eliminated: !!s.eliminated })) - : []; - const overPayload = { reason, scores }; - socket.to(sid).emit('balloon-boss-over', overPayload); - /* เก็บไว้ resend ให้คน reconnect (host ยิงครั้งเดียว — หลุดตอนนั้นจะค้างถาวร) */ - space.lastMinigameOver = { event: 'balloon-boss-over', payload: overPayload, atMs: Date.now() }; - }); - - /* MG7 balloon_boss — host เป็นเจ้าของ HP บอส: relay ดาเมจรวม + สถานะผู้เล่น (ลูกโป่ง/ตาย) เป็นระยะ - → ทุกเครื่องแสดง HP บอสตรงกับ host เป๊ะ (เดิมแต่ละเครื่องรวม balloonBossBossDmg จาก move เองทยอยมา = bar เพี้ยน) */ - socket.on('balloon-boss-sync', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || space.hostId !== socket.id || !data) return; /* host เท่านั้น */ - const bossDmg = Math.max(0, Number(data.bossDmg) || 0); - const anchor = Math.max(0, Number(data.anchor) || 0); /* forward anchor host → clients adopt (มุมลูกศรวงกลม sync) */ - const hostT = (typeof data.t === 'number' && Number.isFinite(data.t) && data.t > 0) ? data.t : undefined; /* [P5 v2] นาฬิกา host → clients ใช้เป็นฐานเวลาวงลูกศร (หมุน sync ต่อเนื่อง ไม่พึ่ง serverNow ต่อเครื่อง) */ - const players = Array.isArray(data.players) - ? data.players.filter((p) => p && p.id != null).slice(0, 12).map((p) => ({ - id: p.id, - d: Math.max(0, Number(p.d) || 0), - b: Math.max(0, Number(p.b) || 0), - e: !!p.e, - })) - : []; - socket.to(sid).emit('balloon-boss-sync', { bossDmg, players, anchor, t: hostT }); - }); - - /* MG3 stack (shared tower) — relay ผลการวาง (เจ้าของตา/บอทของ host) ให้ทุกคนวางบล็อกเดียวกัน + เลื่อนตาพร้อมกัน */ - socket.on('stack-drop', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || !data) return; - if (space.stackSession && space.stackSession.active) return; /* server-auth: client ไม่เป็นเจ้าของดรอป */ - const num = (v) => (typeof v === 'number' && Number.isFinite(v)) ? v : 0; - const themeOf = (v) => { const n = Math.floor(Number(v)); return (n >= 1 && n <= 8) ? n : null; }; - socket.to(sid).emit('stack-drop', { - placedCx: num(data.placedCx), placedW: num(data.placedW), - miss: !!data.miss, perfect: !!data.perfect, - pts: num(data.pts), progressDelta: num(data.progressDelta), landOffsetTiles: num(data.landOffsetTiles), - overlap: num(data.overlap), - supportRatio: (data.supportRatio == null ? null : num(data.supportRatio)), - delta: num(data.delta), - seat: Math.max(1, Math.min(8, Math.floor(num(data.seat)) || 1)), heavy: !!data.heavy, - theme: themeOf(data.theme), /* สี block ที่วาง (host เป็นเจ้าของ) — เดิมถูกตัดทิ้ง → client freeze สีตัวเอง = เพี้ยน */ - actorId: data.actorId != null ? String(data.actorId).slice(0, 64) : '', isBot: !!data.isBot, - }); - }); - /* MG1 quiz — host ขอจบเกมก่อนครบข้อ (ทุกคนตอบผิดหมด=ผีหมด) → endQuizGame โชว์ผล/รอกดรับการ์ด (ไม่ auto) */ - socket.on('quiz-end-request', () => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || space.hostId !== socket.id) return; /* host เท่านั้น */ - if (!space.detectiveMinigameActive || !space.quizSession || !space.quizSession.active) return; - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'quiz') return; - endQuizGame(sid, space, 'จบเกม — ทุกคนตอบผิดหมด'); - }); - - /* MG3 stack — relay เฟสแกว่ง (เจ้าของตา → คนอื่น mirror บล็อกที่กำลังแกว่ง) */ - socket.on('stack-swing', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || !data) return; - if (space.stackSession && space.stackSession.active) return; /* server-auth: server แกว่งเอง */ - const ph = Number(data.phase); - if (!Number.isFinite(ph)) return; - socket.to(sid).emit('stack-swing', { phase: ph }); - }); - - /* MG3 stack (server-auth) — รับ "เจตนากดวาง" จากผู้เล่นจริง → server คำนวณ ณ เฟสปัจจุบัน + emit ผลให้ทุกเครื่อง */ - socket.on('stack-drop-req', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space) return; - const s = space.stackSession; - if (!s || !s.active || s.ended) return; - if (Date.now() < s.busyUntil) return; /* กำลังแอนิเมชันดรอปก่อนหน้า */ - const entry = stackCurrentTurnEntry(s); - if (!entry || entry.kind !== 'human' || String(entry.id) !== String(socket.id)) return; /* ต้องเป็นตาของคนนี้ */ - const md = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (!md || md.gameType !== 'stack') return; - /* ใช้เฟสที่ผู้เล่นเห็นจริงตอนกด (client ส่งมา) → บล็อกลงตรงจุดที่เห็นแกว่ง ไม่เยื้องตาม half-RTT/latency. - sanity bound กว้าง (STACK_DROP_PHASE_TOL = 2π) เหลือไว้กันค่า garbage เท่านั้น (ดู const) */ - let dropPhase = s.phase; - const cph = data && Number(data.phase); - const usedClient = Number.isFinite(cph) && Math.abs(cph - s.phase) <= STACK_DROP_PHASE_TOL; - if (usedClient) dropPhase = cph; - const hit = stackComputeDropServer(s, md, dropPhase); - try { console.log('[mg3-drop-dbg] cph=' + (Number.isFinite(cph) ? cph.toFixed(3) : '-') + ' sphase=' + s.phase.toFixed(3) + ' diff=' + (Number.isFinite(cph) ? Math.abs(cph - s.phase).toFixed(3) : '-') + ' used=' + (usedClient ? 'CLIENT' : 'server') + ' raw=' + (s.swingAmp * Math.sin(dropPhase)).toFixed(3) + ' overlap=' + hit.overlap.toFixed(3) + ' thresh=' + (hit.missThreshold != null ? hit.missThreshold.toFixed(3) : '?') + ' miss=' + hit.miss + ' perfect=' + hit.perfect + ' layers=' + s.layers.length + ' amp=' + s.swingAmp.toFixed(2)); } catch (eDbg) { /* ignore */ } - stackApplyHitServer(s, hit, { id: entry.id }); - const peer = space.peers.get(socket.id); - const theme = stackHumanThemeServer(peer) || (entry.theme || null); - io.to(sid).emit('stack-drop', { - placedCx: hit.placedCx, placedW: hit.placedW, miss: hit.miss, perfect: hit.perfect, - pts: hit.pts, progressDelta: hit.progressDelta, landOffsetTiles: hit.landOffsetTiles, - overlap: hit.overlap, supportRatio: hit.supportRatio, delta: hit.delta, - seat: entry.seat, heavy: false, theme, - actorId: entry.id, isBot: false, - phase: dropPhase, - teamMissesLeft: s.teamMissesLeft, - }); - s.busyUntil = Date.now() + STACK_DROP_BUSY_MS; - if (s.teamMissesLeft != null && s.teamMissesLeft <= 0) { - stackSessionEnd(sid, space, 'misses_exhausted'); - return; - } - stackAdvanceTurnServer(s); - emitStackStateServer(sid, space); - }); - /* MG3 stack — host เป็นเจ้าของลำดับตา+index relay ให้ non-host (กัน host/client มองลำดับตาไม่ตรง) */ - socket.on('stack-state', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || space.hostId !== socket.id || !data) return; /* host เท่านั้น */ - if (space.stackSession && space.stackSession.active) return; /* server-auth: server เป็นเจ้าของลำดับตา */ - const themeOf = (v) => { const n = Math.floor(Number(v)); return (n >= 1 && n <= 8) ? n : null; }; - const order = Array.isArray(data.order) - ? data.order.filter((e) => e && (e.kind === 'human' || e.kind === 'bot')).slice(0, 8).map((e) => ({ - kind: e.kind === 'bot' ? 'bot' : 'human', - seat: Math.max(1, Math.min(8, Math.floor(Number(e.seat)) || 1)), - id: e.id != null ? String(e.id).slice(0, 64) : null, - botId: e.botId != null ? String(e.botId).slice(0, 64) : null, - theme: themeOf(e.theme), /* สีต่อ seat (host เป็นเจ้าของ) — เดิมถูกตัดทิ้ง → client คำนวณเอง = เพี้ยน */ - })) - : []; - const idx = Math.max(0, Math.floor(Number(data.idx)) || 0); - /* สีของ block ที่วางทั้งหอ (host authoritative) — ต้อง relay ด้วย ไม่งั้นถูก allowlist ตัดทิ้ง */ - const lthemes = Array.isArray(data.lthemes) - ? data.lthemes.slice(0, 200).map((v) => { const n = Math.floor(Number(v)); return (n >= 1 && n <= 8) ? n : 0; }) - : []; - /* hostId (= host จริงที่ server ยืนยัน) → client ที่เข้าใจผิดว่าตัวเองเป็น host (dual-host) แก้ playHostId แล้วหยุด sim เอง */ - socket.to(sid).emit('stack-state', { order, idx, hostId: socket.id, lthemes }); - }); - - /* MG3 stack — host ตัดสินจบรอบ relay ให้ทุกคนจบพร้อมกัน */ - socket.on('stack-over', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space) return; - if (space.stackSession && space.stackSession.active) return; /* server-auth: server ตัดสินจบเกม */ - const outcome = (data && typeof data.outcome === 'string') ? data.outcome.slice(0, 32) : 'time_up'; - const overPayload = { outcome }; - socket.to(sid).emit('stack-over', overPayload); - space.lastMinigameOver = { event: 'stack-over', payload: overPayload, atMs: Date.now() }; - }); - - /* MG6 space_shooter — host ตัดสินจบเกม + แนบคะแนนสุดท้าย → ทุกคนจบ/อันดับตรงกัน */ - socket.on('space-shooter-over', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || !data) return; - const kind = (data.kind === 'all_dead') ? 'all_dead' : 'time_up'; - const scores = Array.isArray(data.scores) - ? data.scores.filter((s) => s && s.id != null).map((s) => ({ id: s.id, score: Math.max(0, Number(s.score) || 0), hits: Math.max(0, Number(s.hits) || 0), eliminated: !!s.eliminated })) - : []; - const overPayload = { kind, scores }; - socket.to(sid).emit('space-shooter-over', overPayload); - space.lastMinigameOver = { event: 'space-shooter-over', payload: overPayload, atMs: Date.now() }; - }); - - /* MG6 space_shooter — relay กระสุนผู้เล่น/บอท ให้ทุกคนเห็นตรงกัน (host เห็นยังไง client เห็นแบบนั้น) */ - socket.on('space-shooter-shot', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || !data) return; - if (isSpecialQuizPausedServer(space)) return; /* คำถามพิเศษ → ไม่ relay กระสุน (กันค้างยิงรัวหลังตอบ) */ - const ok = ['x', 'y', 'vy'].every((k) => typeof data[k] === 'number' && Number.isFinite(data[k])); - if (!ok) return; - socket.to(sid).emit('space-shooter-shot', { - x: data.x, y: data.y, vy: data.vy, - ownerId: data.ownerId, t0: (typeof data.t0 === 'number' ? data.t0 : Date.now()), - }); - }); - - /* MG6 — host คิดการชน "บอท × อุกาบาต" (asteroid deterministic + ตำแหน่งบอทจาก server) แล้วรายงานจำนวนชน - server เป็นคนตัดสินตายจริง (>= SS_MAX_ASTEROID_HITS) → กันบอททะลุอุกาบาตไม่มีวันตาย และสถานะตายตรงกันทุกเครื่อง */ - socket.on('space-shooter-bot-hit', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || space.hostId !== socket.id || !data || typeof data.id !== 'string') return; /* เชื่อเฉพาะ host */ - const s = space.spaceShooterSession; - if (!s || !s.active || s.ended) return; - if (isSpecialQuizPausedServer(space)) return; - const b = s.bots.get(data.id); - if (!b || b.eliminated) return; - const hits = Math.max(0, Math.min(99, Number(data.hits) || 0)); - b.ssHits = Math.max(b.ssHits || 0, hits); /* monotonic */ - if (b.ssHits >= SS_MAX_ASTEROID_HITS) { - b.eliminated = true; - /* step loop ข้ามบอทที่ตายแล้ว (ไม่ emit ต่อ) → ส่ง bot-move แจ้ง elim เดี๋ยวนี้ครั้งเดียวให้ทุกเครื่องรู้ */ - io.to(sid).emit('space-shooter-bot-move', { bots: [{ id: data.id, cx: b.cx, cy: b.cy, dir: 'up', score: b.score, elim: true, hits: b.ssHits || 0 }] }); - } - }); - - /* MG5 — ผู้เล่นสลับแท็บ (hidden): browser throttle setInterval เหลือ ~1Hz → emit move ~1/วิ = คนอื่นเห็นตกเป็นก้อน. - แก้: client แจ้ง hidden พร้อม state ปัจจุบัน → server จำลองการตก (jsPhysicsIntegrate) แทน ใน stepJumpSurviveBots - แล้ว broadcast ผ่าน jump-survive-bot-move (full rate) → คนอื่นเห็นลื่น. visible → hidden:false → client คุมเอง. */ - socket.on('jump-survive-hidden', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || !data) return; - const s = space.jumpSurviveSession; - if (!s || !s.active || s.ended) return; - if (!s.players[socket.id]) s.players[socket.id] = {}; - const st = s.players[socket.id]; - if (data.hidden) { - if (st.eliminated) return; - st.hidden = true; - st.x = Number.isFinite(data.x) ? data.x : (Number.isFinite(st.x) ? st.x : 1); - st.y = Number.isFinite(data.y) ? data.y : (Number.isFinite(st.y) ? st.y : 1); - st.vy = Number.isFinite(data.vy) ? data.vy : 0; - st.onGround = !!data.onGround; - st.dir = data.direction || st.dir || 'down'; - } else { - st.hidden = false; - } - }); - - /** เปลี่ยนชื่อที่แสดงใน 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: 'ไม่พบผู้เล่น' }); - /* ใช้ clampNickname ตัวเดียวกับ join-space — เดิม cap ที่ 32 ตรงนี้ที่เดียว ไม่ตรงกับ client (24) - และ join-space ไม่ cap เลย = 3 ค่าคนละอัน */ - let safe = clampNickname(nickname); - if (!safe) return reply({ ok: false, error: 'ชื่อว่าง' }); - 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; - const claimPeer = space.peers.get(socket.id); - if (claimPeer && claimPeer.playerKey) space.hostPlayerKey = claimPeer.playerKey; - 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; - /* เก็บตัวตนตัวป่วนแบบถาวร (playerKey/nickname) — socket.id เปลี่ยนหลังเล่นมินิเกม ใช้ id ตรง ๆ ไม่ได้ */ - if (accept) { - const dPeer = space.peers.get(socket.id); - space.disruptorPlayerKey = (dPeer && dPeer.playerKey) ? dPeer.playerKey : ''; - space.disruptorNickname = dPeer ? normalizeLobbyNickname(dPeer.nickname) : ''; - } else { - space.disruptorPlayerKey = ''; - space.disruptorNickname = ''; - } - openSuspectPhaseForRoom(sid, space, accept); - 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 กดเริ่มสืบสวนได้' }); - } - /* ต้องรอผู้เล่นทุกคนในคดีกลับเข้า LobbyB ให้ครบก่อน host ถึงเริ่มมินิเกมรอบถัดไปได้ - (เดิม: หลังจบเกม client บางคนยังโหลด LobbyB ไม่เสร็จ แต่ host กดเริ่มได้เลย → คนนั้นหลุดรอบ) */ - const waitInfo = detectiveLobbyBMissingParticipants(space); - if (waitInfo.missing > 0) { - try { const hp = space.peers.get(socket.id); glog(sid, space, 'lobbyb-gate', '[TC144] BLOCK เริ่มสืบสวน — รอผู้เล่นกลับเข้า LobbyB ครบ (' + waitInfo.present + '/' + waitInfo.total + ') — gate ทำงาน (pass)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } - return reply({ - ok: false, - waitingPlayers: true, - present: waitInfo.present, - total: waitInfo.total, - error: 'รอผู้เล่นกลับเข้าห้องให้ครบก่อน (' + waitInfo.present + '/' + waitInfo.total + ')', - }); - } - /* ต้องโหวต Ban ให้เสร็จก่อนถึงเริ่มมินิเกมรอบถัดไปได้ (มีการ์ด Ban ค้าง = ยังไม่ได้เลือก/โหวตยังไม่จบ) */ - if (space.pendingBanVoteCard || (space.pickVote && space.pickVote.purpose === 'ban' && !space.pickVote.resolved)) { - try { const hp = space.peers.get(socket.id); glog(sid, space, 'lobbyb-gate', '[TC-BAN] BLOCK เริ่มสืบสวน — ต้องโหวตแบนให้เสร็จก่อน (gate ทำงาน)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } - return reply({ ok: false, waitingBanVote: true, error: 'ต้องโหวตแบนผู้เล่นให้เสร็จก่อนเริ่มมินิเกมถัดไป' }); - } - /* TC156: gate จนกว่าทุกคนอ่าน story (cutscene) ครบ — เฉพาะรอบแรก (storyReadComplete เป็น true หลังครบ/รอบถัดไป) */ - if (!space.storyReadComplete) { - const stRead = detectiveStoryReadStatus(space); - if (!stRead.allRead) { - try { const hp = space.peers.get(socket.id); glog(sid, space, 'story-gate', '[TC156] BLOCK เริ่มสืบสวน — รอทุกคนอ่าน story ครบ (' + stRead.present + '/' + stRead.total + ') — gate ทำงาน (pass)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } - return reply({ ok: false, waitingStory: true, present: stRead.present, total: stRead.total, error: 'รอผู้เล่นอ่านเนื้อเรื่องให้ครบก่อน (' + stRead.present + '/' + stRead.total + ')' }); - } - space.storyReadComplete = true; - } - 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: 'ยังไม่ได้สุ่มมินิเกมสำหรับการ์ดนี้' }); - } - /* จำกัดสืบผู้ต้องสงสัยละไม่เกิน 3 ครั้ง (#1) — นับจาก max หลักฐานของผู้ต้องสงสัยนี้ข้ามผู้เล่น - (แต่ละรอบทุกคนได้ +1) → กันเล่นครั้งที่ 4 + กันการ์ดพิเศษโผล่รอบที่ 4 (#2) */ - let timesPlayedThisSuspect = 0; - space.peers.forEach((_p, pid) => { - const prog = playerSuspectProgressFromEvidence(space, pid); - const n = Array.isArray(prog) ? (prog[selectedIndex] | 0) : 0; - if (n > timesPlayedThisSuspect) timesPlayedThisSuspect = n; - }); - if (timesPlayedThisSuspect >= 3) { - return reply({ ok: false, error: 'สืบผู้ต้องสงสัยคนนี้ครบ 3 ครั้งแล้ว — เลือกคนอื่น' }); - } - /* Card 3 Ban — ก่อนเริ่มมินิเกมรอบหน้า โหวตเลือกผู้เล่น 1 คนที่ห้ามเล่น */ - const pend = findPendingSpecialCard(space, 3); - if (pend && !space.pickVote) { - pend.consumed = true; - io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'ban' }); - try { const hp = space.peers.get(socket.id); glog(sid, space, 'vote-gate', '[TC157] เริ่ม vote ban (ผู้เล่นเข้า LobbyB ครบ ' + waitInfo.present + '/' + waitInfo.total + ') — มินิเกมจะเริ่มหลังโหวตจบเท่านั้น (pass)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } - startPlayerPickVote(sid, space, 'ban', (targetId) => { - const sp = spaces.get(sid); - if (!sp) return; - sp.bannedPlayerId = targetId || null; - sp.bannedPlayerKey = resolveBanTargetPlayerKey(sp, targetId); /* ตัวตนถาวร — id จะเปลี่ยนก่อนเกมเริ่ม */ - 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); - try { const hp = space.peers.get(socket.id); glog(sid, space, 'lobbyb-gate', '[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ ' + waitInfo.total + ' คน · ผู้ต้องสงสัย #' + (selectedIndex + 1), (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } - 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: 'ไม่อยู่ในห้อง' }); - } - /* client รายงานคะแนน/รอด ของตัวเอง (ใช้กับเกมที่ server ไม่ได้นับคะแนน เช่น stack/quiz_carry) */ - const finPeer = space.peers.get(socket.id); - if (finPeer && data && typeof data === 'object') { - /* คะแนน client รายงานเอง (stack/quiz_carry) — clamp ด้วย sanity cap กัน overflow/ค่าขยะ/spoof สูงเกินจริง - หมายเหตุ: ยังเป็น client-trusted ภายในเพดาน (anti-cheat สมบูรณ์ต้องจำลองเกมฝั่ง server — เกินสโคปตอนนี้) */ - if (Number.isFinite(Number(data.score))) { - let rs = Math.max(0, Math.floor(Number(data.score))); - if (rs > REPORTED_MINI_SCORE_MAX) { - console.warn('[mini-score] clamp reportedMiniScore', socket.id, rs, '->', REPORTED_MINI_SCORE_MAX); - rs = REPORTED_MINI_SCORE_MAX; - } - finPeer.reportedMiniScore = rs; - } - if (data.survived === false) finPeer.reportedMiniSurvived = false; - } - 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: 'ไม่ได้อยู่ในมินิเกมสืบสวน' }); - } - /* MG2 วิ่งหลบ (gauntlet): ไม่จบเกมตอนคนแรกเข้าเส้นชัย — ให้คนที่เข้าเส้นแล้ว "รอในฉาก" - จนกว่าทุกคนที่ยังไม่ตาย/ไม่ถูกแบนจะเข้าเส้นครบ แล้วค่อยจบพร้อมกัน (returnDetective แจกการ์ด - ทุกคนพร้อมกันตอนทุกคนยังอยู่ในห้อง) · endGauntletGame timer เป็น fallback ถ้าใครไม่เข้าเส้น */ - const finMd = (space.mapId && maps.get(space.mapId)) || space.mapData; - if (finMd && finMd.gameType === 'gauntlet' && space.detectiveMinigameActive) { - if (finPeer) finPeer.detectiveFinished = true; - const allDone = [...space.peers.values()].every((p) => - p.gauntletEliminated || p.bannedSpectator || p.detectiveFinished - ); - if (!allDone) { - /* ยังมีคนวิ่งอยู่ → ให้ client นี้รอในฉาก (ไม่เด้งออก) จะกลับ LobbyB พร้อมกันตอนเกมจบ */ - return reply({ ok: true, holdInScene: true }); - } - /* ครบทุกคนแล้ว → จบเกมให้ทุกคนด้านล่าง */ - } - // เล่นมินิเกม 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 เปิดพิจารณาคดีได้' }); - /* [TC179-followup] ใช้ team progress (ตัวเลขเดียวกับที่การ์ดโชว์ทุกจอ) เป็นหลัก — หลักฐานแจก "รายคน" - คนที่หลุด/โดนบอทแทนตอนมินิเกมของผู้ต้องสงสัยคนนั้นจะไม่มีใบนั้น → host เห็นการ์ด ✓ ครบแต่กด - "ชี้ตัวคนร้าย" ไม่ได้ (เดิมเช็คแต่ prog ส่วนตัว). max(ส่วนตัว, ทีม) = ไม่บล็อกทั้งสองทาง */ - const prog = playerSuspectProgressFromEvidence(space, socket.id); - const teamProg = Array.isArray(space.suspectTeamProgress) ? space.suspectTeamProgress : [0, 0, 0]; - if (![0, 1, 2].every((i) => Math.max((prog[i] || 0), (teamProg[i] || 0)) >= 1)) { - return reply({ ok: false, error: 'ต้องสืบครบทั้ง 3 ผู้ต้องสงสัยก่อน (เก็บหลักฐานอย่างน้อย 1 ใบต่อคน)' }); - } - /* ชี้ตัวคนร้าย = เข้าพิจารณาคดี → ต้องรอผู้เล่นในคดีกลับเข้า LobbyB ครบก่อน (กันคนตกรอบโหวต) */ - const accuseWait = detectiveLobbyBMissingParticipants(space); - if (accuseWait.missing > 0) { - try { const hp = space.peers.get(socket.id); glog(sid, space, 'lobbyb-gate', '[TC144] BLOCK ชี้ตัวคนร้าย — รอผู้เล่นเข้า LobbyB ครบ (' + accuseWait.present + '/' + accuseWait.total + ') — gate ทำงาน (pass)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } - return reply({ ok: false, waitingPlayers: true, present: accuseWait.present, total: accuseWait.total, error: 'รอผู้เล่นกลับเข้าห้องให้ครบก่อน (' + accuseWait.present + '/' + accuseWait.total + ')' }); - } - 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 = {}; - const d3AlreadySubmitted = space.testimonySubmitted[socket.id] !== undefined; - space.testimonySubmitted[socket.id] = picks.slice(0, required); - /* [achv d3] นับหลักฐานที่ "ส่งมอบให้ทีมวิเคราะห์" บนหน้า es3 (ห้องสรุปหลักฐาน) — นับครั้งแรกของรอบนี้เท่านั้น กันนับซ้ำ */ - if (!d3AlreadySubmitted) { - const d3p = space.peers.get(socket.id); - if (d3p && d3p.playerKey && picks.length > 0) { - submitAchievementProgress([{ playerKey: d3p.playerKey, id: 'd3_the_backbone', inc: picks.length }]); - } - } - 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: 'เลือกผู้ต้องสงสัยไม่ถูกต้อง' }); - /* รอบจับผิดตัว: ห้ามโหวตผู้ต้องสงสัยที่โหวตผิดไปแล้ว */ - if (Number.isInteger(space.trialRevoteExcluded) && idx === space.trialRevoteExcluded) { - return reply({ ok: false, error: 'ผู้ต้องสงสัยนี้ถูกโหวตไปแล้ว (ผิด) — เลือกคนอื่น', excluded: true }); - } - space.trialVotes = space.trialVotes || {}; - space.trialVotes[socket.id] = idx; - space.trialVoteAt = space.trialVoteAt || {}; - space.trialVoteAt[socket.id] = Date.now(); /* [achv c4] เวลาโหวต → เช็ค "10 วิสุดท้าย" */ - emitTrialVoteUpdate(sid, space); - const totalTV = trialEligibleVoterTotal(space); - const votedNow = Object.keys(space.trialVotes).length; - try { glog(sid, space, 'trial-vote', '[TC180] ยืนยันโหวต ' + votedNow + '/' + totalTV + (votedNow >= totalTV && totalTV > 0 ? ' — ครบ → reveal (pass)' : ' — รออีก')); } catch (e) { /* ignore */ } - if (votedNow >= 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: 'ไม่พบผู้เล่นในห้อง' }); - } - /* [2026-07-19] เก็บ playerKey/nickname ของคนที่ถูก kick "ก่อน" ลบ peer — ใช้ prune roster คดี */ - const kickedPeer = space.peers.get(tid) || {}; - const kickedKey = (typeof kickedPeer.playerKey === 'string') ? kickedPeer.playerKey : ''; - const kickedNick = kickedPeer.nickname || ''; - 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 }); - /* [2026-07-19] host ดึงคนออก = ตั้งใจถอดถาวร → ตัดออกจาก roster คดีทันที + re-emit presence - ไม่งั้น TC144 gate ตอนเลือกผู้ต้องสงสัยจะค้าง "รอผู้เล่น (x/y)" ตลอด host กด Start ต่อไม่ได้ - (host-kick เดิมไม่เคย prune roster / ไม่ arm grace / ไม่ re-emit presence เลย) */ - pruneDetectiveParticipantNow(sid, space, kickedKey, kickedNick); - cb && cb({ ok: true }); - }); - - /* พร้อมแล้ว — ไม่บังคับจำนวนคนในห้อง */ - socket.on('set-ready', ({ ready }) => { - for (const [sid, space] of spaces) { - if (space.peers.has(socket.id)) { - /* ล็อก: host กำลังเปิด wizard เลือกระดับ/คดี → ปฏิเสธ set-ready (กัน client หลุด flow ตอน host เลือกอยู่) */ - if (space.preplayOpen && space.hostId !== socket.id) { - const p = space.peers.get(socket.id); - io.to(socket.id).emit('peer-ready', { id: socket.id, ready: !!(p && p.ready) }); /* ย้ำสถานะเดิมกลับให้ผู้ส่ง */ - try { glog(sid, space, 'preplay-lock', '[TC154] REJECT set-ready ระหว่าง host เลือกคดี — feature ทำงาน (pass): client ยกเลิกพร้อมไม่ได้', (p && p.nickname) || String(socket.id).slice(0, 6)); } catch (e) { /* ignore */ } - break; - } - const p = space.peers.get(socket.id); - if (p) p.ready = !!ready; - io.to(sid).emit('peer-ready', { id: socket.id, ready: !!ready }); - break; - } - } - }); - - /* LobbyA: host เปิด/ปิด wizard เลือกระดับ/คดี → เก็บ state ที่ server + relay ทั้งห้อง (ล็อกปุ่มพร้อมฝั่ง client) */ - socket.on('lobby-preplay-open', ({ open } = {}) => { - for (const [sid, space] of spaces) { - if (space.peers.has(socket.id) && space.hostId === socket.id) { - space.preplayOpen = !!open; - io.to(sid).emit('lobby-preplay-open', { open: !!open }); - try { const hp = space.peers.get(socket.id); glog(sid, space, 'preplay-lock', '[TC154] ' + (open ? 'host เปิด wizard เลือกคดี → ล็อก set-ready ทั้งห้อง' : 'host ปิด wizard → ปลดล็อก set-ready') + ' (' + space.peers.size + ' คนในห้อง)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } - break; - } - } - }); - - /* TC156: client อ่าน story (cutscene) จบ → บันทึกว่าคนนี้พร้อม · ครบทุกคน → ปลดล็อกเลือกผู้ต้องสงสัย */ - socket.on('story-read-done', () => { - for (const [sid, space] of spaces) { - if (!space.peers.has(socket.id)) continue; - if (!serverMapIsPostCaseLobbyB(space)) break; - const p = space.peers.get(socket.id); - const key = detectiveParticipantKey(p); - const nick = p && p.nickname ? normalizeLobbyNickname(p.nickname) : ''; - if (!key) break; - if (!space.storyReadKeys) space.storyReadKeys = new Set(); - const already = space.storyReadKeys.has(key); - space.storyReadKeys.add(key); - /* อ่าน story จบ = โหลด LobbyB เสร็จแน่นอน → นับ lobbyb-loaded ด้วย (reliable กว่า event lobbyb-loaded เดี่ยว) */ - if (!space.lobbyBLoadedKeys) space.lobbyBLoadedKeys = new Set(); - space.lobbyBLoadedKeys.add(key); - const st = detectiveStoryReadStatus(space); - if (!already) { try { glog(sid, space, 'story-gate', '[TC156] อ่าน story จบ ' + st.present + '/' + st.total, nick); } catch (e) { /* ignore */ } } - if (st.allRead && !space.storyReadComplete) { - space.storyReadComplete = true; - if (space.storyReadTimer) { clearTimeout(space.storyReadTimer); space.storyReadTimer = null; } - try { glog(sid, space, 'story-gate', '[TC156] ทุกคนอ่าน story ครบ (' + st.total + ') → ปลดล็อกเลือกผู้ต้องสงสัย (pass)'); } catch (e) { /* ignore */ } - } - emitDetectiveStoryReadPresence(sid, space); - /* piggyback เพิ่ม lobbyBLoadedKeys แล้ว → ต้อง broadcast lobbyb-presence ใหม่ด้วย - ไม่งั้น client ค้างโชว์ "รอผู้เล่น (2/3)" (ค่าเก่า) → ปุ่ม host ถูก disable ทั้งที่ครบแล้ว */ - emitDetectiveLobbyBPresence(sid, space); - break; - } - }); - - /* client โหลดฉาก LobbyB เสร็จจริง → นับเป็น "present" ได้ (กัน host เริ่มก่อนคนโหลดเสร็จ = ค้างตอนจบ) */ - socket.on('lobbyb-loaded', () => { - for (const [sid, space] of spaces) { - if (!space.peers.has(socket.id)) continue; - if (!serverMapIsPostCaseLobbyB(space)) break; - const p = space.peers.get(socket.id); - const key = detectiveParticipantKey(p); - const nick = p && p.nickname ? normalizeLobbyNickname(p.nickname) : ''; - if (!key) break; - if (!space.lobbyBLoadedKeys) space.lobbyBLoadedKeys = new Set(); - if (!space.lobbyBLoadedKeys.has(key)) { - space.lobbyBLoadedKeys.add(key); - const info = detectiveLobbyBMissingParticipants(space); - try { glog(sid, space, 'lobbyb-gate', '[TC144] โหลด LobbyB เสร็จ ' + info.present + '/' + info.total, nick); } catch (e) { /* ignore */ } - } - emitDetectiveLobbyBPresence(sid, space); - break; - } - }); - - /* client โหลดฉากเสร็จ (ทุกฉาก lobby/minigame) → เอา badge "กำลังโหลด" ออก (คนอื่นเห็นว่าพร้อมแล้ว) */ - socket.on('client-ready', () => { - for (const [sid, space] of spaces) { - if (!space.peers.has(socket.id)) continue; - const p = space.peers.get(socket.id); - if (p && p.netReady !== true) { - p.netReady = true; - io.to(sid).emit('peer-net', { id: socket.id, nr: true }); - } - 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) { - space.preplayOpen = false; /* host เริ่มเกมแล้ว → ปลดล็อก (client เปลี่ยนฉากอยู่แล้ว แต่เคลียร์ state กัน stuck) */ - 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' }); - } - /* detective LobbyA→LobbyB: ไม่บังคับยืนโซนส้ม (หน้าจอเลือกคดีบังแผนที่ ขยับตัวไม่ได้) */ - if (!isLobbyADetectiveStart && !lobbyHostStandingInStartArea(space, socket.id)) { - return reply({ ok: false, error: 'ยืนในพื้นที่เริ่มเกม (สีส้มในเอดิเตอร์) ก่อนกดเริ่ม' }); - } - if (isLobbyADetectiveStart) { - space.lobbyAMapId = space.mapId; /* item1: จำ map LobbyA เดิมไว้ (ก่อนทับด้วย LobbyB) เพื่อ restore ตอน post-case reset */ - space.lobbyAMapData = space.mapData; - space.postCaseLobbyAReset = false; /* เริ่มคดีใหม่ → เปิด guard reset→LobbyA อีกครั้ง */ - assignDetectiveSuspectCardMinigames(space); - space.mapId = POST_CASE_LOBBY_SPACE_ID; - space.mapData = maps.get(POST_CASE_LOBBY_SPACE_ID); - const caseNickWl = new Set(); - const caseKeyWl = new Set(); - space.peers.forEach((p) => { - caseNickWl.add(normalizeLobbyNickname(p.nickname)); /* ชื่อ — ใช้ whitelist rejoin (คงเดิม) */ - const k = detectiveParticipantKey(p); - if (k) caseKeyWl.add(k); /* playerKey — ใช้ presence/story/gate (กันชื่อซ้ำนับเป็นคนเดียว) */ - }); - space.caseParticipantNicknames = caseNickWl; - space.caseParticipantKeys = caseKeyWl; - /* TC156: gate เลือกผู้ต้องสงสัยจนกว่าทุกคนอ่าน story (cutscene) ครบ — reset ที่ initial LobbyA→LobbyB เท่านั้น - (รอบ investigate ถัดไปไม่มี cutscene → storyReadComplete คงเป็น true) · safety timer กัน deadlock ถ้า client ไม่ emit */ - space.storyReadKeys = new Set(); - space.storyReadComplete = false; - space.lobbyBLoadedKeys = new Set(); /* เริ่มคดี → ทุกคนต้องส่ง lobbyb-loaded ใหม่ ก่อนนับ present */ - armLobbyBLoadedSafety(sid, space); /* backstop กัน deadlock */ - if (space.storyReadTimer) clearTimeout(space.storyReadTimer); - space.storyReadTimer = setTimeout(() => { - const sp = spaces.get(sid); - if (sp && !sp.storyReadComplete) { - sp.storyReadComplete = true; - try { glog(sid, sp, 'story-gate', '[TC156] safety-timeout 90s → force ปลดล็อก (client บางคนไม่ส่ง story-read-done)'); } catch (e) { /* ignore */ } - try { emitDetectiveStoryReadPresence(sid, sp); } catch (e) { /* ignore */ } - } - }, 90000); - space.joinLocked = true; - space.troublesomeOfferSent = false; - space.troublesomeTargetId = null; - space.troublesomeResponseReceived = false; - space.troublesomeAccept = false; - space.disruptorPlayerKey = ''; - space.disruptorNickname = ''; - 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); - /* [TC190] backstop กัน deadlock "หน้าเลือกผู้ต้องสงสัยไม่ขึ้น": - openSuspectPhaseForRoom เข้าถึงได้ "ทางเดียว" = client ส่ง troublesome-eligible → - scheduleTroublesomeRoll → timer. ตัว troublesomeMaxTimer (8 วิ) ที่ดูเหมือน safety net - ถูก arm อยู่ "ข้างใน" scheduleTroublesomeRoll ที่สัญญาณนั้นเรียกเอง + สองบรรทัดข้างบนเพิ่งล้าง - timer เป็น null → ถ้าไม่มีใครส่งเลย (ติด overlay how-to/mic, หรือ room-lobby.js:4543 ซ่อน - mic overlay แล้วไม่เรียก tryTroublesomeLobbyGate ซ้ำ) = ห้องค้างถาวร ไม่มีอะไรกู้ - (พิสูจน์แล้ว: justice-mg-all.js --probe eligible-loss → ค้าง 40 วิ ไม่มีวันเปิด). - arm ตรงนี้ = เฟสที่ห้อง "มาถึง LobbyB" จริง (บทเรียน §4: backstop ที่ arm ผิดเฟสจะ no-op เงียบ) - — storyReadTimer ข้างบนรู้บทเรียนนี้อยู่แล้ว, gate นี้แค่ไม่เคยได้. 45 วิ = พ้น worst case - ที่เคยวัดได้ (6 คน join ใช้ 10-16 วิ) เยอะพอจะไม่แย่งทางปกติ. */ - if (space.troublesomeDeadlockTimer) clearTimeout(space.troublesomeDeadlockTimer); - space.troublesomeDeadlockTimer = setTimeout(() => { - const sp = spaces.get(sid); - if (!sp || sp.troublesomeOfferSent) return; /* เปิดไปแล้วตามทางปกติ */ - if (!serverMapIsPostCaseLobbyB(sp)) return; /* ไม่ได้อยู่ LobbyB แล้ว */ - try { glog(sid, sp, 'suspect-gate', '[TC190] safety-timeout 45s → force เปิดเฟสเลือกผู้ต้องสงสัย (ไม่มี client ส่ง troublesome-eligible เลย) — backstop ทำงาน (pass)'); } catch (e) { /* ignore */ } - /* force=true ข้ามเงื่อนไข "ทุกคน eligible"; ถ้า pool ว่าง attemptTroublesomeRoll - fallback เป็น peerIds ทั้งหมดอยู่แล้ว (10638) จึงปลอดภัยเมื่อไม่มีใคร eligible */ - attemptTroublesomeRoll(sid, true); - }, 45000); - space.peers.forEach((p) => { - const sp = pickSpawnForJoin(space.mapData, typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : 0); - 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; - /* MG5: สุ่ม pattern แพลตฟอร์ม 1 อัน/เกม (เหมือน detective starter) */ - space.jsLayout = null; space._jsEffMd = null; - if (md && md.gameType === 'jump_survive') pickJumpSurviveLayoutForSpace(space); - 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 (space.jsLayout) gameStartPayload.jsLayout = space.jsLayout; - 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; - /* เก็บข้อมูลก่อนลบ peer — ใช้ทำ grace 10 วิ (รอ reconnect) + bot takeover ระหว่างมินิเกม */ - const leftPeerInfo = space.peers.get(socket.id) || {}; - const leftPlayerKey = (typeof leftPeerInfo.playerKey === 'string') ? leftPeerInfo.playerKey : ''; - const leftNick = (leftPeerInfo.nickname || '').trim() || 'ผู้เล่น'; - /* คนที่หลุดตาย/ตกรอบไปแล้วหรือยัง (ตอนหลุด) — ใช้ตัดสินว่า bot ที่มาแทนควร spawn ใหม่ไหม (คนตายแล้วไม่ควรฟื้น) */ - let leftWasEliminated = false; - try { - const md8 = (space.mapId && maps.get(space.mapId)) || space.mapData; - const gt8 = md8 && md8.gameType; - leftWasEliminated = minigamePlayerScoreSurvived(space, leftPeerInfo, gt8).survived === false; - } catch (e) { /* ignore */ } - /* สี (ธีม/สกิน) ของคนที่ออก — ใช้ให้ bot ที่มาแทนใช้สีเดียวกัน (b) */ - const leftThemeIndex = parseInt(leftPeerInfo.lobbyColorThemeIndex, 10); - const leftSkinIndex = parseInt(leftPeerInfo.lobbySkinToneIndex, 10); - /* MG1 (quiz live): หลุด → แปลง entity เดิม (socket.id) เป็นบอท server "ในเกมทันที" - เก็บ avatar เดิมไว้ = สี+ตำแหน่งเดิมอัตโนมัติ + inherit คะแนน/สถานะตกรอบ (ผีถ้าตอบผิด) */ - let mg1Takeover = false; - try { - const md9 = (space.mapId && maps.get(space.mapId)) || space.mapData; - const sessT = space.quizSession; - if (sessT && sessT.active && sessT.bots && md9 && md9.gameType === 'quiz') { - const st = (sessT.players && sessT.players[socket.id]) || {}; - const px = Number(leftPeerInfo.x), py = Number(leftPeerInfo.y); - sessT.bots.set(socket.id, { - tier: quizBotTierForSlot(sessT.bots.size), - x: Number.isFinite(px) ? px : 1, y: Number.isFinite(py) ? py : 1, - dir: leftPeerInfo.direction || 'down', isWalking: false, - score: Math.max(0, Number(st.score) || 0), - eliminated: !!(st.cannotTrue && st.cannotFalse), - choice: null, answerRight: false, tx: null, ty: null, wanderTX: null, wanderTY: null, retargetAt: 0, - takeover: true, takeoverPlayerKey: leftPlayerKey || '', - }); - mg1Takeover = true; - } - } catch (e) { /* ignore */ } - /* MG2 (gauntlet live): หลุดกลาง run → แปลง peer เป็นบอท server "ในที่เดิม" (ตำแหน่ง/คะแนน/สถานะเดิม) - เก็บ avatar เดิมไว้ + เล่นต่อเป็นบอท (host หลุดก็ไม่หยุด race) */ - let mg2Takeover = false; - try { - const md9b = (space.mapId && maps.get(space.mapId)) || space.mapData; - const grT = space.gauntletRun; - /* takeover ตราบที่ run ยังมีอยู่ — รวมช่วง how-to / pre-run hold (crownRunHeld) ด้วย เหมือน MG1 - (เดิมข้าม crownRunHeld → host หลุดตอน how-to = avatar หายไปเลย ไม่มีบอทรับช่วง; ตอนนี้แปลงเป็นบอททันที - บอทจะค้างในเลนตอน how-to แล้ววิ่งเองเมื่อเกมเริ่ม — emitGauntletSync ส่งบอท takeover ระหว่าง hold อยู่แล้ว) */ - if (grT && md9b && md9b.gameType === 'gauntlet') { - if (!grT.bots) grT.bots = new Map(); - const px = Number(leftPeerInfo.x), py = Number(leftPeerInfo.y); - const themeIdx = parseInt(leftPeerInfo.lobbyColorThemeIndex, 10); - const skinIdx = parseInt(leftPeerInfo.lobbySkinToneIndex, 10); - grT.bots.set(socket.id, { - tier: 'avg', - /* คงตำแหน่ง float เดิม (อย่า floor — เดิม floor ทำให้บอทสแนปไปทับเลน/ตัวอื่น) เหมือน MG1 */ - x: Number.isFinite(px) ? px : 1, - y: Number.isFinite(py) ? py : 1, - dir: leftPeerInfo.direction || 'right', - gauntletJumpTicks: 0, - gauntletScore: Math.max(0, Number(leftPeerInfo.gauntletScore) || 0), - gauntletEliminated: !!leftPeerInfo.gauntletEliminated, - /* เก็บ identity เดิมไว้ → sync ส่งชื่อ+"(บอท)"+สี ให้ client คงไว้ (กันชื่อ/สีเพี้ยนตอน re-create) */ - nickname: leftNick, - characterId: leftPeerInfo.characterId != null ? leftPeerInfo.characterId : null, - lobbyColorThemeIndex: Number.isFinite(themeIdx) ? themeIdx : null, - lobbySkinToneIndex: Number.isFinite(skinIdx) ? skinIdx : null, - takeover: true, - takeoverPlayerKey: leftPlayerKey || '', - }); - mg2Takeover = true; - } - } catch (e) { /* ignore */ } - /* MG3 (stack live): หลุดกลางรอบ → แปลง turn-order entry เป็นบอท server "ในที่เดิม" (ใช้ socket.id เป็น botId - → client เก็บ avatar เดิมไว้) inherit คะแนนสะสม แล้วบอทเล่นตาแทนต่อ (host หลุดก็ไม่ค้างตา) */ - let mg3Takeover = false; - try { - const md9c = (space.mapId && maps.get(space.mapId)) || space.mapData; - const s3 = space.stackSession; - if (s3 && s3.active && !s3.ended && md9c && md9c.gameType === 'stack') { - const entry = (s3.turnOrder || []).find((e) => e.kind === 'human' && String(e.id) === String(socket.id)); - if (entry) { - const inheritedPts = Math.max(0, s3.humanPts[socket.id] | 0); - s3.bots.set(socket.id, { tier: 'avg', score: inheritedPts, takeover: true, takeoverPlayerKey: leftPlayerKey || '' }); - entry.kind = 'bot'; - entry.botId = socket.id; - entry.id = null; - mg3Takeover = true; - } - } - } catch (e) { /* ignore */ } - /* MG4 (quiz_carry live): หลุดกลางเกม → แปลง entity เดิม (socket.id) เป็นบอท server "ในที่เดิม" - เก็บ avatar เดิม + inherit ป้ายที่ถือ/คะแนน → บอท BFS เล่นต่อ (host หลุดก็ไม่หยุด) */ - let mg4Takeover = false; - try { - const md9d = (space.mapId && maps.get(space.mapId)) || space.mapData; - const sC = space.quizCarrySession; - if (sC && sC.active && !sC.ended && md9d && md9d.gameType === 'quiz_carry') { - const st = sC.players[socket.id] || {}; - const px = Number(leftPeerInfo.x), py = Number(leftPeerInfo.y); - sC.bots.set(socket.id, { - tier: 'avg', - x: Number.isFinite(px) ? px : 1, y: Number.isFinite(py) ? py : 1, - dir: leftPeerInfo.direction || 'down', isWalking: false, - held: (st.held == null ? null : st.held), - score: Math.max(0, Number(st.score) || 0), - botPath: [], pathfindAfter: 0, wantIdx: null, - wanderTX: null, wanderTY: null, retargetAt: 0, - takeover: true, takeoverPlayerKey: leftPlayerKey || '', - }); - delete sC.players[socket.id]; - mg4Takeover = true; - } - } catch (e) { /* ignore */ } - /* MG5 (jump_survive live): หลุดกลางเกม → แปลง entity เดิม (socket.id) เป็นบอท server "ในที่เดิม" - เก็บ avatar/ตำแหน่งเดิม + inherit สถานะตาย → บอทกระโดดเอาตัวรอดต่อ (host หลุดก็ไม่หยุด) */ - let mg5Takeover = false; - try { - const md9e = (space.mapId && maps.get(space.mapId)) || space.mapData; - const sJ = space.jumpSurviveSession; - if (sJ && sJ.active && !sJ.ended && md9e && md9e.gameType === 'jump_survive') { - const st = sJ.players[socket.id] || {}; - const wasDead = !!st.eliminated || !!leftPeerInfo.jumpSurviveEliminated; - const px = Number(leftPeerInfo.x), py = Number(leftPeerInfo.y); - sJ.bots.set(socket.id, { - tier: 'avg', - x: Number.isFinite(px) ? px : 1, y: Number.isFinite(py) ? py : 1, - vy: 0, onGround: false, dir: leftPeerInfo.direction || 'down', walking: false, - eliminated: wasDead, - wishX: 0, wishNext: Date.now() + 200, nextJumpAi: Date.now() + 200, - takeover: true, takeoverPlayerKey: leftPlayerKey || '', - }); - delete sJ.players[socket.id]; - mg5Takeover = true; - } - } catch (e) { /* ignore */ } - /* MG6 (space_shooter live): หลุดกลางเกม → แปลงเป็นบอท server "ในที่เดิม" (คะแนนเดิม) ยิงต่อ */ - let mg6Takeover = false; - try { - const md9f = (space.mapId && maps.get(space.mapId)) || space.mapData; - const sS = space.spaceShooterSession; - if (sS && sS.active && !sS.ended && md9f && md9f.gameType === 'space_shooter') { - const ts = jsTileSize(md9f); - const mh = (md9f.height || 15) * ts; - const cx = Number.isFinite(Number(leftPeerInfo.x)) ? (Number(leftPeerInfo.x) + 0.5) * ts : (md9f.width || 20) * ts * 0.5; - const cy = ssClampCy(Number.isFinite(Number(leftPeerInfo.y)) ? (Number(leftPeerInfo.y) + 0.5) * ts : mh - ts * 1.5, mh); - const st = sS.players[socket.id] || {}; - sS.bots.set(socket.id, { - tier: 'avg', cx, cy, dir: 'up', - score: Math.max(0, Number(leftPeerInfo.spaceShooterScore) || 0), - eliminated: !!st.eliminated || !!leftPeerInfo.spaceShooterEliminated, - fireCd: 0.3 + Math.random() * 0.3, - wanderVx: Math.random() < 0.5 ? -1 : 1, wanderVy: Math.random() < 0.5 ? -1 : 1, wanderNext: Date.now() + 500, - takeover: true, takeoverPlayerKey: leftPlayerKey || '', - }); - delete sS.players[socket.id]; - mg6Takeover = true; - } - } catch (e) { /* ignore */ } - /* MG7 (balloon_boss live): หลุดกลางเกม → แปลงเป็นบอท server "ในที่เดิม" (คะแนน/ดาเมจ/ลูกโป่ง/ตายเดิม) ยิงต่อ */ - let mg7Takeover = false; - try { - const md9g = (space.mapId && maps.get(space.mapId)) || space.mapData; - const sB = space.balloonBossSession; - if (sB && sB.active && !sB.ended && md9g && md9g.gameType === 'balloon_boss') { - const ts = jsTileSize(md9g); - const mw = (md9g.width || 20) * ts, mh = (md9g.height || 15) * ts; - const cx = Number.isFinite(Number(leftPeerInfo.x)) ? (Number(leftPeerInfo.x) + 0.5) * ts : mw * 0.5; - const cy = Number.isFinite(Number(leftPeerInfo.y)) ? (Number(leftPeerInfo.y) + 0.5) * ts : mh - ts * 1.4; - const cl = bbClampWorld(cx, cy, mw, mh); - sB.bots.set(socket.id, { - tier: 'avg', homeCx: cl.cx, homeCy: cl.cy, cx: cl.cx, cy: cl.cy, - score: Math.max(0, Number(leftPeerInfo.balloonBossScore) || 0), - dmg: Math.max(0, Number(leftPeerInfo.balloonBossBossDmg) || 0), - balloons: Math.max(0, Number(leftPeerInfo.balloonBossBalloons) || balloonBossBalloonsForMap(md9g)), - eliminated: !!leftPeerInfo.balloonBossEliminated, - fireCd: 0.6 + Math.random() * 0.8, phase: Math.random() * Math.PI * 2, - takeover: true, takeoverPlayerKey: leftPlayerKey || '', - }); - mg7Takeover = true; - } - } catch (e) { /* ignore */ } - /* MG3–MG7 หลุดช่วง how-to / นับถอยหลัง (ยังไม่มี session live) → เก็บ pending takeover ไว้ - แล้ว seed เป็นบอทตอนเกมเริ่ม + แจ้ง client เก็บ avatar (*-player-to-bot) เหมือน MG1/MG2 - (เดิม: ไม่มี session = ไม่ takeover = avatar หายเลยตอน how-to) */ - let shellTakeoverEvent = null; - try { - if (!mg1Takeover && !mg2Takeover && !mg3Takeover && !mg4Takeover && !mg5Takeover && !mg6Takeover && !mg7Takeover && space.detectiveMinigameActive) { - const gtShell = missionShellGameTypeForSpace(space); - const liveActive = !!( - (gtShell === 'space_shooter' && space.spaceShooterSession && space.spaceShooterSession.active) || - (gtShell === 'jump_survive' && space.jumpSurviveSession && space.jumpSurviveSession.active) || - (gtShell === 'balloon_boss' && space.balloonBossSession && space.balloonBossSession.active) || - (gtShell === 'quiz_carry' && space.quizCarrySession && space.quizCarrySession.active) || - (gtShell === 'stack' && space.stackSession && space.stackSession.active) - ); - if (gtShell && !liveActive) { - recordPendingMissionTakeover(space, gtShell, socket.id, leftPeerInfo, leftPlayerKey, leftNick, leftWasEliminated); - shellTakeoverEvent = - gtShell === 'space_shooter' ? 'space-shooter-player-to-bot' : - gtShell === 'jump_survive' ? 'jump-survive-player-to-bot' : - gtShell === 'balloon_boss' ? 'balloon-boss-player-to-bot' : - gtShell === 'quiz_carry' ? 'quiz-carry-player-to-bot' : - gtShell === 'stack' ? 'stack-player-to-bot' : null; - } - } - } catch (e) { /* ignore */ } - 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', quizCarryLobbySyncPayload(space)); - } - } - if (space.gauntletCrownLobbyReady && typeof space.gauntletCrownLobbyReady === 'object') { - delete space.gauntletCrownLobbyReady[socket.id]; - if (isCrownLobbyShellSpace(space)) { - io.to(sid).emit('gauntlet-crown-lobby-sync', crownLobbySyncPayload(space)); - } - } - if (space.qbwSolved) space.qbwSolved.delete(socket.id); // Quiz Battle (ฉากเดิน) — เอาคะแนนคนออกแล้ว sync ใหม่ด้านล่าง - space.peers.delete(socket.id); - if (space.peers.size === 0) { - stopGauntletTicker(space); - stopStackTicker(space); - space.stackSession = null; - clearSpaceQuizTimers(space); - space.quizSession = null; - if (space.quizCarrySession) space.quizCarrySession.active = false; - space.quizCarrySession = null; - if (space.quizCarryWatchdog) { clearInterval(space.quizCarryWatchdog); space.quizCarryWatchdog = null; } - if (space.jumpSurviveSession) space.jumpSurviveSession.active = false; - space.jumpSurviveSession = null; - if (space.spaceShooterSession) space.spaceShooterSession.active = false; - space.spaceShooterSession = null; - if (space.balloonBossSession) space.balloonBossSession.active = false; - space.balloonBossSession = null; - space.pendingMissionTakeovers = []; /* ห้องว่าง → ล้าง pending takeover ค้าง (กัน seed บอทผีรอบหน้า) */ - space.emptySince = Date.now(); - /* ระหว่างมินิเกมสืบสวน / คดีล็อก — อย่าลบห้องเมื่อเปลี่ยนหน้า room-lobby → play.html (รอ join กลับ) */ - const keepCaseSpace = !!(space.detectiveMinigameActive - || (space.postCaseKeepAliveUntil && Date.now() < space.postCaseKeepAliveUntil) /* item1: ห้อง reset→LobbyA รอ client reload กลับ */ - || (space.joinLocked && space.caseParticipantNicknames && space.caseParticipantNicknames.size > 0)); - /* ห้อง Quiz Battle — เก็บไว้เมื่อว่าง ให้ refresh กลับเข้าห้องเดิมได้ (TTL prune จะเก็บกวาดเมื่อว่างเกิน 45 วิ) */ - const spaceMd = (space.mapId && maps.get(space.mapId)) || space.mapData; - const keepQuizBattleRoom = !!(spaceMd && spaceMd.gameType === 'quiz_battle'); - if (keepCaseSpace || keepQuizBattleRoom) { - space.hostId = null; - return; - } - spaces.delete(sid); - } else { - if (wasHost) { - /* host หลุดกลางเปิด wizard เลือกคดี → ปลดล็อกปุ่มพร้อมให้คนที่เหลือ (กัน stuck lock) */ - if (space.preplayOpen) { space.preplayOpen = false; io.to(sid).emit('lobby-preplay-open', { open: false }); try { glog(sid, space, 'preplay-lock', '[TC154] host หลุดกลางเปิด wizard → auto-ปลดล็อก set-ready (กัน stuck)', leftNick); } catch (e) { /* ignore */ } } - const firstPeer = pickNextHostPeer(space); /* คนอยู่นานสุด (spawnJoinOrder ต่ำสุด) — แทน Map-first */ - if (firstPeer) { - space.hostId = firstPeer.id; /* ย้าย pointer host ชั่วคราวให้คนที่เหลือ (liveness) */ - /* อย่าเขียนทับ hostPlayerKey ถ้ามีอยู่แล้ว — host ตัวจริงต้อง reclaim ได้เมื่อ rejoin จาก play.html - (เปลี่ยนหน้า lobby→play→lobbyB ทำให้ socket disconnect ชั่วคราว ไม่ใช่ออกจากห้องจริง - ถ้าเขียนทับ key → host/client สลับกันถาวรหลังเข้า minigame) */ - if (!space.hostPlayerKey && firstPeer.playerKey) space.hostPlayerKey = firstPeer.playerKey; - io.to(sid).emit('host-changed', { hostId: firstPeer.id }); - glog(sid, space, 'host-change', 'host ใหม่ → ' + ((firstPeer.nickname) || String(firstPeer.id).slice(0, 6)), leftNick); - } - } - if (mg2Takeover) { - /* MG2: ไม่ลบ avatar — แปลงเป็นบอทในที่เดิม (สี/ตำแหน่ง/คะแนนเดิม) ให้ client เก็บ entity ไว้ */ - io.to(sid).emit('gauntlet-player-to-bot', { id: socket.id, nickname: leftNick }); - glog(sid, space, 'leave', 'หลุด MG2 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); - } else if (mg1Takeover) { - /* MG1: ไม่ลบ avatar — แปลงเป็นบอทในที่เดิม (สี/ตำแหน่ง/สถานะเดิม) ให้ client เก็บ entity ไว้ */ - io.to(sid).emit('quiz-player-to-bot', { id: socket.id, nickname: leftNick }); - glog(sid, space, 'leave', 'หลุด MG1 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); - } else if (mg3Takeover) { - /* MG3: ไม่ลบ avatar — แปลง seat เป็นบอท server เล่นตาแทน (สี/ตำแหน่งเดิม) + sync ลำดับตาใหม่ */ - io.to(sid).emit('stack-player-to-bot', { id: socket.id, nickname: leftNick }); - emitStackStateServer(sid, space); - glog(sid, space, 'leave', 'หลุด MG3 → บอทแทนในเกม · เหลือคน ' + space.peers.size, leftNick); - } else if (mg4Takeover) { - /* MG4: ไม่ลบ avatar — แปลงเป็นบอท server (ตำแหน่ง/ป้ายที่ถือ/คะแนนเดิม) BFS เล่นต่อ */ - io.to(sid).emit('quiz-carry-player-to-bot', { id: socket.id, nickname: leftNick }); - glog(sid, space, 'leave', 'หลุด MG4 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); - } else if (mg5Takeover) { - /* MG5: ไม่ลบ avatar — แปลงเป็นบอท server (ตำแหน่ง/สถานะเดิม) กระโดดเอาตัวรอดต่อ */ - io.to(sid).emit('jump-survive-player-to-bot', { id: socket.id, nickname: leftNick }); - glog(sid, space, 'leave', 'หลุด MG5 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); - } else if (mg6Takeover) { - /* MG6: ไม่ลบ avatar — แปลงเป็นบอท server (ตำแหน่ง/คะแนนเดิม) ยิงต่อ */ - io.to(sid).emit('space-shooter-player-to-bot', { id: socket.id, nickname: leftNick }); - glog(sid, space, 'leave', 'หลุด MG6 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); - } else if (mg7Takeover) { - /* MG7: ไม่ลบ avatar — แปลงเป็นบอท server (ตำแหน่ง/คะแนน/ดาเมจ/ลูกโป่งเดิม) ยิงบอสต่อ */ - io.to(sid).emit('balloon-boss-player-to-bot', { id: socket.id, nickname: leftNick }); - glog(sid, space, 'leave', 'หลุด MG7 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); - } else if (shellTakeoverEvent) { - /* how-to/นับถอยหลัง: client เก็บ avatar + ติดป้าย (บอท) — บอท server จะ seed ตอนเกมเริ่ม (pendingMissionTakeovers) */ - io.to(sid).emit(shellTakeoverEvent, { id: socket.id, nickname: leftNick }); - glog(sid, space, 'leave', 'หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน ' + space.peers.size, leftNick); - } else { - io.to(sid).emit('user-left', { id: socket.id }); - glog(sid, space, 'leave', 'ออกจากห้อง · เหลือคน ' + space.peers.size, leftNick); - } - if (space.qbwSolved) qbwBroadcastScores(sid, space); // Quiz Battle ฉากเดิน — อัปเดตอันดับหลังมีคนออก - emitDetectiveLobbyBPresence(sid, space); /* คนออกจาก LobbyB → อัปเดตจำนวนที่พร้อมให้ host */ - /* กำลังรอกด «เล่นต่อ» (special-quiz) แล้วมีคนออก — เอา ack คนนั้นออกแล้วเช็คใหม่ (กันค้างรอคนที่ไม่อยู่แล้ว) */ - if (space.specialQuizAwaitContinue) { - if (space.specialQuizContinueAcks) space.specialQuizContinueAcks.delete(socket.id); - maybeFinishSpecialQuizContinue(sid, space); - } - /* ห้องส่งหลักฐาน (testimony) — คนหลุดระหว่างเฟส → ลบ submit/ready ที่ค้าง แล้ว re-emit สถานะ - เพื่อให้ "จำนวนที่ต้องส่ง/ready" ลดตามจริง (peers.delete ไปแล้วด้านบน) ไม่งั้น host รอคนที่ไม่อยู่ = ไปต่อไม่ได้ */ - if (space.testimonyActive) { - if (space.testimonySubmitted) delete space.testimonySubmitted[socket.id]; - if (space.testimonyReadyNext) delete space.testimonyReadyNext[socket.id]; - emitTestimonyStatus(sid, space); - if (space.testimonyRevealed) { - io.to(sid).emit('testimony-ready-update', { - ready: Object.keys(space.testimonyReadyNext || {}), - totalPlayers: testimonyTotalParticipants(space), - hostId: space.hostId, - }); - } - } - /* หลุดในสาย "คดี" — ครอบทั้ง 3 จังหวะ: - (1) ระหว่างมินิเกม (2) ระหว่าง LobbyB (3) ช่วงเปลี่ยนหน้า minigame→LobbyB (detectiveMinigameActive เพิ่งเป็น false) - ทั้งหมดให้เวลา reconnect 10 วิ (กันเปลี่ยนหน้า lobbyB→play→lobbyB ที่ socket หลุดชั่วคราว) - ครบเวลายังไม่กลับ → ลบออกจาก roster คดี + เติมบอทสีตามคนที่ออก (ไม่งั้น LobbyB ค้าง "รอผู้เล่น x/y" เริ่มเกมต่อไม่ได้) */ - const inCaseFlowForBot = space.detectiveMinigameActive - || serverMapIsPostCaseLobbyB(space) - || (space.joinLocked && space.caseParticipantNicknames && space.caseParticipantNicknames.size > 0); - if (space.suspectPhaseActive) { - /* [2026-07-19] หลุดระหว่างเฟส "เลือกผู้ต้องสงสัย" → ตัดออกจาก roster ทันที (user: เล่นต่อทันที) - ครอบเคส playerKey ว่างด้วย (เดิม grace block ข้ามถ้า leftPlayerKey ว่าง → gate ค้างถาวร). - เฟสนี้อยู่ใน LobbyB ยังไม่เข้าเกม จึงไม่ต้องรอ grace 22วิ/ไม่ต้องมีบอทแทน — แค่ให้ gate ลด - แล้ว host เริ่มต่อได้เลย. ช่วง "เล่นมินิเกมจริง" ยังใช้ grace+บอทเหมือนเดิม (else-if ล่าง) */ - pruneDetectiveParticipantNow(sid, space, leftPlayerKey, leftNick); - } else if (inCaseFlowForBot && leftPlayerKey) { - io.to(sid).emit('peer-disconnect-grace', { id: socket.id, nickname: leftNick, seconds: DISCONNECT_GRACE_SEC }); - if (!space.graceReconnect || typeof space.graceReconnect !== 'object') space.graceReconnect = {}; - if (!space.graceTimers || typeof space.graceTimers !== 'object') space.graceTimers = {}; - if (space.graceTimers[leftPlayerKey]) clearTimeout(space.graceTimers[leftPlayerKey]); - space.graceReconnect[leftPlayerKey] = Date.now(); - space.graceTimers[leftPlayerKey] = setTimeout(() => { - const sp = spaces.get(sid); - if (!sp) return; - if (sp.graceTimers) delete sp.graceTimers[leftPlayerKey]; - /* กลับมาแล้ว (playerKey อยู่ใน peers) → ไม่ต้อง takeover */ - let back = false; - if (sp.peers) sp.peers.forEach((p) => { if (p && p.playerKey === leftPlayerKey) back = true; }); - if (sp.graceReconnect) delete sp.graceReconnect[leftPlayerKey]; - if (back) return; - /* คนนี้กลายเป็นบอทถาวร → เอาออกจาก roster คดี เพื่อ LobbyB ไม่รอคนนี้อีก (เดิมยังนับ → ค้าง "รอผู้เล่น (x/y)") */ - if (sp.caseParticipantNicknames && leftNick) { - try { sp.caseParticipantNicknames.delete(normalizeLobbyNickname(leftNick)); } catch (e) { /* ignore */ } - /* เอา participantKey ออกจาก roster gate ด้วย (playerKey — กัน gate รอคนที่ออกถาวร) */ - try { - if (sp.caseParticipantKeys) { - if (leftPlayerKey && /^[a-zA-Z0-9_-]{8,128}$/.test(leftPlayerKey)) sp.caseParticipantKeys.delete('k:' + leftPlayerKey); - sp.caseParticipantKeys.delete('n:' + normalizeLobbyNickname(leftNick)); - } - } catch (e) { /* ignore */ } - emitDetectiveLobbyBPresence(sid, sp); - } - /* คนออกถาวร → ลด expectedActive (จำนวนคนที่ต้องเข้าครบก่อนเริ่ม) + re-emit lobby-sync - แก้บั๊ก: host หลุดในหน้า how-to ก่อนกด START → host ใหม่กดไม่ได้เพราะค้าง "รอผู้เล่นครบ (x/y)" (y ไม่ลด) */ - if (typeof sp.minigameExpectedActiveCount === 'number' && sp.minigameExpectedActiveCount > 0) { - sp.minigameExpectedActiveCount = Math.max(0, sp.minigameExpectedActiveCount - 1); - } - if (isCrownLobbyShellSpace(sp)) io.to(sid).emit('gauntlet-crown-lobby-sync', crownLobbySyncPayload(sp)); - /* (c) persist + (b) สี: เพิ่มจำนวนบอทถาวร 1 ตัว (อยู่ยาวจนจบคดี: LobbyB → เกมอื่น → vote) - แล้ว pin "สีของคนที่ออก" ให้ slot บอทใหม่ → บอทที่มาแทนสีตรงคนเดิม + ส่งสีให้ทุกเครื่องตรงกัน */ - try { - const oldBotCount = effectiveBotSlotCount(sp); - if (oldBotCount < LOBBY_SLOT_TOTAL) { - sp.botSlotCount = oldBotCount + 1; - if (leftThemeIndex >= 1 && leftThemeIndex <= LOBBY_THEME_COUNT) { - if (!sp.lobbyBotThemeBySlot || typeof sp.lobbyBotThemeBySlot !== 'object') sp.lobbyBotThemeBySlot = {}; - sp.lobbyBotThemeBySlot[oldBotCount] = { - themeIndex: leftThemeIndex, - skinToneIndex: (leftSkinIndex >= 1 && leftSkinIndex <= LOBBY_SKIN_COUNT) ? leftSkinIndex : pickLobbySkinIndexForSlot(oldBotCount), - }; - } - emitLobbyTintSync(sid, sp); - } - } catch (e) { /* ignore */ } - io.to(sid).emit('peer-bot-takeover', { nickname: leftNick, eliminated: leftWasEliminated }); - glog(sid, sp, 'bot-takeover', 'หลุดเกิน ' + DISCONNECT_GRACE_SEC + 'วิ → บอทแทน' + (leftWasEliminated ? ' (ตกรอบแล้ว ไม่ spawn)' : ' (สีตามคนออก)'), leftNick); - }, DISCONNECT_GRACE_SEC * 1000); - } - } - }); - - /* client แจ้งว่าเส้นชัยมาถึงแล้ว (BG ถึง finish) → เข้าโหมดจบ: หยุด obstacle + collision */ - socket.on('gauntlet-finish-phase', () => { - 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; - const gr = space.gauntletRun; - if (!gr) return; - /* server-authority: ยังไม่เริ่ม run (crownRunHeld) หรือยิงเร็วผิดปกติ → ปฏิเสธ (กัน client ปลอม/desync) */ - if (gr.crownRunHeld) return; - if (gr.liveStartMs != null && (Date.now() - gr.liveStartMs) < GAUNTLET_FINISH_MIN_RUN_MS) return; - if (gr.finishPhase) return; - gr.finishPhase = true; - gr.obstacles = []; - /* คนแรกเข้าเส้นชัย → หยุด collision (obstacles ว่าง) + สั่ง client เคลียร์อุปสรรคจากจอ "จังหวะเดียวกัน" - กันอาการ "หายแล้วยังชน" (visual เคลียร์ตรงกับตอน collision หยุด ไม่เร็วไป) */ - io.to(sid).emit('gauntlet-finish-clear'); - emitGauntletSync(sid, space); - }); - - 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; - }); - - /* MG5 jump_survive — relay การกระโดดของคนจริงให้เพื่อนในห้อง (เสียงเท่านั้น ไม่แตะ physics; mg5 ไม่มี jump-seq ใน move sync) */ - socket.on('js-jump', () => { - 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 !== 'jump_survive') return; - socket.to(sid).emit('js-jump', { id: socket.id }); - }); - - 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; - /* G2: เลื่อนเวลาเริ่มจริงไปอนาคต = ค้างที่ start (hold) → ทั้ง bg(client) + obstacle/bot(server) gate ด้วย liveStartMs เดียวกัน */ - gr.liveStartMs = Date.now() + GAUNTLET_PRERUN_HOLD_MS; /* G1/G2: เวลาเริ่ม run จริง (server) — broadcast ให้ทุกเครื่อง anchor scroll เดียวกัน */ - 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', quizCarryLobbySyncPayload(space)); - }); - - 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', quizCarryLobbySyncPayload(space)); - }); - - 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()].filter((id) => !isBannedPeerForRun(space, id)); - /* ต้องเข้าเกมครบก่อน (กัน host เริ่มก่อน client โหลดเสร็จ) */ - const expectedActive = space.minigameExpectedActiveCount || 0; - if (expectedActive > 0 && humanIds.length < expectedActive) { - return reply({ ok: false, waitingPlayers: true, present: humanIds.length, total: expectedActive, error: 'รอผู้เล่นเข้าเกมให้ครบก่อน (' + humanIds.length + '/' + expectedActive + ')' }); - } - const allReady = humanIds.length > 0 && humanIds.every((id) => rm[id]); - if (!allReady) return reply({ ok: false, error: 'ยังมีผู้เล่นที่ยังไม่ Ready' }); - const liveBeginsAt = Date.now() + LIVE_COUNTDOWN_MS; - space.missionLiveBeginsAt = liveBeginsAt; - io.to(sid).emit('quiz-carry-lobby-started', { liveBeginsAt }); - reply({ ok: true }); - /* MG4 server-auth: เกมจริง (detective) → server เริ่ม engine เองหลัง 3-2-1 (เหมือน MG1 ใน performCrownLobbyStart) */ - setTimeout(() => { - const sp = spaces.get(sid); - if (!sp || !sp.detectiveMinigameActive) return; - const md2 = (sp.mapId && maps.get(sp.mapId)) || sp.mapData; - if (md2 && md2.gameType === 'quiz_carry') startQuizCarryGame(sid, sp); - }, LIVE_COUNTDOWN_MS); - }); - - /* host เลือกคำถาม quiz_carry → relay index ให้คนอื่นในห้องใช้ข้อเดียวกัน (กันสุ่มไม่ตรงกัน) - server-auth (เกมจริง): server เป็นเจ้าของคำถามผ่าน quiz-carry-phase → ข้าม relay */ - socket.on('quiz-carry-question', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || space.hostId !== socket.id) return; /* เฉพาะ host */ - if (space.quizCarrySession && space.quizCarrySession.active) return; - const idx = Number(data && data.idx); - if (!Number.isInteger(idx) || idx < 0) return; - socket.to(sid).emit('quiz-carry-question', { idx }); - }); - - /* host จบเซสชัน quiz_carry → ให้คนอื่นโชว์สรุปพร้อมกัน (server-auth ใช้ quiz-carry-ended แทน) */ - socket.on('quiz-carry-session-complete', () => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space || space.hostId !== socket.id) return; - if (space.quizCarrySession && space.quizCarrySession.active) return; - socket.to(sid).emit('quiz-carry-session-complete', {}); - }); - - /* MG4 server-auth: client ส่ง intent "หยิบ/ส่ง" (กด F / ปุ่ม GRAB) → server ตัดสินจากตำแหน่งล่าสุด */ - socket.on('quiz-carry-grab', (data) => { - const sid = socket.data.spaceId; - const space = sid ? spaces.get(sid) : null; - if (!space) return; - const s = space.quizCarrySession; - if (!s || !s.active || s.ended) return; - const changed = quizCarryServerTryInteract(sid, space, socket.id, false, data); - /* กดแล้วไม่เกิดอะไร (หยิบไม่ได้/ส่งไม่ได้) → ยืนยัน held จริงกลับไปให้ "คนกด" เพื่อแก้ optimistic prediction - ที่อาจค้างผิด (เช่น ทำนายว่าหยิบได้แต่ server ปฏิเสธเพราะคนอื่นถืออยู่) */ - if (!changed) { - const hp = s.players[socket.id]; - io.to(socket.id).emit('quiz-carry-held', { id: socket.id, held: hp ? (hp.held == null ? null : hp.held) : null }); - } - }); - - /* ตอบเวลา server ปัจจุบัน → client ใช้คำนวณ clock offset (ชดเชย latency) ให้เวลาในเกมตรงกันทุกเครื่อง */ - socket.on('time-sync', (cb) => { - if (typeof cb === 'function') cb({ t: Date.now() }); - }); - - 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', crownLobbySyncPayload(space)); - }); - - 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', crownLobbySyncPayload(space)); - /* หมายเหตุ: host (ผู้เล่นจริง) เป็นคนกด START เอง — ไม่ auto-start (ตามที่ต้องการให้ host คุมจังหวะเริ่ม) */ - }); - - 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()].filter((id) => !isBannedPeerForRun(space, id)); - /* ต้องเข้าเกมครบก่อน (กัน host เริ่มก่อน client โหลดเสร็จ → เกมไม่ sync/จบไม่พร้อมกัน) */ - const expectedActive = space.minigameExpectedActiveCount || 0; - if (expectedActive > 0 && humanIds.length < expectedActive) { - return reply({ ok: false, waitingPlayers: true, present: humanIds.length, total: expectedActive, error: 'รอผู้เล่นเข้าเกมให้ครบก่อน (' + humanIds.length + '/' + expectedActive + ')' }); - } - const allReady = humanIds.length > 0 && humanIds.every((id) => rm[id]); - if (!allReady) return reply({ ok: false, error: 'ยังมีผู้เล่นที่ยังไม่ Ready' }); - performCrownLobbyStart(sid, space); - 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; - } else if (quizBattlePathModeActiveServer(md) && !quizBattlePathGateAllowsServer(md, space, socket.id, 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; - } - } - let moveForceEmit = false; - if (p && md && md.gameType === 'space_shooter' && data) { - if (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 (data.spaceShooterHits != null) { - const nh = Math.floor(Number(data.spaceShooterHits)); - if (Number.isFinite(nh) && nh >= 0) { - const prevH = Math.max(0, p.spaceShooterHits | 0); - if (nh > prevH) { p.spaceShooterHits = nh; moveForceEmit = true; } - } - } - /* ตาย/รอด มาจากเจ้าของยาน → ต้อง relay แม้ตำแหน่งไม่เปลี่ยน (ยานหยุดตอนตาย) - ไม่งั้นยานคนตายจะค้างนิ่งบนจออีกฝั่ง (ดูเหมือนขยับไม่ได้) + ตายไม่ตรงกัน */ - if (data.spaceShooterEliminated && !p.spaceShooterEliminated) { - p.spaceShooterEliminated = true; - moveForceEmit = true; - const sSm = space.spaceShooterSession; - if (sSm && sSm.active && !sSm.ended) { - if (!sSm.players[socket.id]) sSm.players[socket.id] = {}; - sSm.players[socket.id].eliminated = true; - } - } - } - if (p && md && md.gameType === 'jump_survive' && data) { - /* MG5: การตายมาจากเจ้าตัว → relay แม้ตำแหน่งไม่เปลี่ยน (ตอนตายไม่ขยับ) ไม่งั้นคนอื่นไม่รู้ = "ไม่ยอมตาย" */ - if (data.jumpSurviveEliminated && !p.jumpSurviveEliminated) { - p.jumpSurviveEliminated = true; - moveForceEmit = true; - /* MG5 server-auth: บันทึกการตายของคนจริงเข้า session (server เป็นเจ้าของรอด/เกรด + เช็ค all_dead) */ - const sJm = space.jumpSurviveSession; - if (sJm && sJm.active && !sJm.ended) { - if (!sJm.players[socket.id]) sJm.players[socket.id] = {}; - sJm.players[socket.id].eliminated = true; - } - } - /* isWalking ของเจ้าตัว → relay ให้คนอื่นเห็น anim วิ่ง (เดิม out ไม่ใส่ jsWalking = client อื่นไม่เคยได้ → anim ไม่ขึ้น); - เปลี่ยนสถานะ (เริ่มเดิน/หยุด) → force emit แม้ตำแหน่งไม่เปลี่ยน */ - const jwIn = !!data.jsWalking; - if (jwIn !== !!p.jumpSurviveWalking) moveForceEmit = true; - p.jumpSurviveWalking = jwIn; - } - if (p && md && md.gameType === 'balloon_boss' && data) { - const bbDefaultBalloons = balloonBossBalloonsForMap(md); - 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); - /* เพดานเพิ่ม/แพ็กเก็ต = เผื่อยิงโดนหลายนัดต่อ move (ดาเมจ/นัดตั้งได้ถึง 100) */ - const bbDmgCap = Math.max(8, bbDamagePerHit() * 3); - if (nd <= prevD + bbDmgCap) 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) { p.balloonBossEliminated = true; moveForceEmit = true; } - else if (data.balloonBossEliminated) p.balloonBossEliminated = true; - } - /* server-auth (เกมจริง): server เป็นเจ้าของ held/score ผ่าน quiz-carry-held/result → ไม่รับค่าจาก client (กัน spoof/desync) */ - const quizCarrySrvAuth = !!(space.quizCarrySession && space.quizCarrySession.active); - if (!quizCarrySrvAuth && p && md && md.gameType === 'quiz_carry' && data && data.quizCarryHeld !== undefined) { - /* relay ของที่ผู้เล่น (มนุษย์) ถืออยู่ → client อื่นเห็น "ใครถืออะไร"; force emit แม้ยืนนิ่ง (หยิบ/วางขณะไม่ขยับ) */ - const prevHeld = (p.quizCarryHeld === undefined ? null : p.quizCarryHeld); - const rawHeld = data.quizCarryHeld; - const nh = (rawHeld == null) ? null : Math.floor(Number(rawHeld)); - const newHeld = (nh == null || !Number.isFinite(nh)) ? null : nh; - if (newHeld !== prevHeld) moveForceEmit = true; - p.quizCarryHeld = newHeld; - } - if (!quizCarrySrvAuth && p && md && md.gameType === 'quiz_carry' && data && data.quizCarryScore !== undefined) { - /* คะแนน carry ของผู้เล่น (มนุษย์) → relay ให้ทุกเครื่องเห็นคะแนนตรงกัน */ - const sc = Math.max(0, Math.floor(Number(data.quizCarryScore) || 0)); - if (sc !== (p.quizCarryScore | 0)) moveForceEmit = true; - p.quizCarryScore = sc; - } - /* L2 server clamp (เฉพาะ lobby): จำกัดระยะต่อ move ตามเวลาจริง → กันเครื่องเร็ว/โกงวิ่งเกินความเร็ว - เผื่อ jitter/lag burst ด้วย factor 3 + ฐาน 0.5 (move ปกติ ~12.5Hz เดินแค่ ~0.72u อยู่ใต้เพดานสบาย) - ไม่แตะมินิเกม (ฟิสิกส์ตก/กระโดดเร็วกว่านี้) */ - if (p && md && md.gameType === 'lobby') { - const tnow = Date.now(); - const tlast = (typeof p.lastLobbyMoveTs === 'number') ? p.lastLobbyMoveTs : 0; - let edt = tlast ? (tnow - tlast) / 1000 : 0.1; - if (!(edt > 0)) edt = 0.1; - if (edt > 0.5) edt = 0.5; - p.lastLobbyMoveTs = tnow; - const maxDist = LOBBY_MOVE_SPEED_PER_SEC * edt * 3 + 0.5; - const ddx = Number(nx) - Number(p.x); - const ddy = Number(ny) - Number(p.y); - if (Number.isFinite(ddx) && Number.isFinite(ddy)) { - const dlen = Math.hypot(ddx, ddy); - if (dlen > maxDist && dlen > 0) { - const s = maxDist / dlen; - nx = Number(p.x) + ddx * s; - ny = Number(p.y) + ddy * s; - } - } - } - 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 || moveForceEmit) { - const out = { id: socket.id, x: nx, y: ny, direction: p.direction, characterId: p.characterId, lobbyColorThemeIndex: p.lobbyColorThemeIndex || null, lobbySkinToneIndex: p.lobbySkinToneIndex || null }; - if (md && md.gameType === 'space_shooter') { - out.spaceShooterScore = Math.max(0, p.spaceShooterScore | 0); - out.spaceShooterHits = Math.max(0, p.spaceShooterHits | 0); - out.spaceShooterEliminated = !!p.spaceShooterEliminated; - } - if (md && md.gameType === 'balloon_boss') { - const bbDef = balloonBossBalloonsForMap(md); - 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 (md && md.gameType === 'quiz_carry') { - out.quizCarryHeld = (p.quizCarryHeld === undefined ? null : p.quizCarryHeld); - out.quizCarryScore = Math.max(0, p.quizCarryScore | 0); - } - if (md && md.gameType === 'jump_survive') { - out.jumpSurviveEliminated = !!p.jumpSurviveEliminated; - out.jsWalking = !!p.jumpSurviveWalking; /* anim วิ่งให้คนอื่นเห็น */ - } - 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; -}); +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' }, +]; +/** โหมดเทสต์: บังคับเกมต่อการ์ด 3 ใบ — array[3], แต่ละช่อง '' = สุ่ม หรือ key จาก DETECTIVE_MINIGAME_POOL · รับ array หรือ key เดี่ยว (เก่า) */ +function normalizeForcedMinigameKeys(v) { + let arr; + if (Array.isArray(v)) arr = v; + else if (v != null && String(v).trim()) arr = [v, v, v]; + else arr = []; + const one = (k) => { + const s = (k == null ? '' : String(k)).trim(); + return s && DETECTIVE_MINIGAME_POOL.some((e) => e.key === s) ? s : ''; + }; + return [one(arr[0]), one(arr[1]), one(arr[2])]; +} +const LOBBY_SLOT_TOTAL = 6; + +/* เวลานับถอยหลัง 3-2-1 ก่อนเริ่มเล่นจริง (ms) — server กำหนด liveBeginsAt = now + ค่านี้ + ให้ทุก client นับไปหาเวลาเดียวกัน → GO/เริ่ม/จบ ตรงกันทุกเครื่อง (เผื่อ buffer ให้ทุกคนรับ event ทัน) */ +const LIVE_COUNTDOWN_MS = 3500; +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 ตามลำดับช่อง — ไม่ซ้ำกับผู้เล่น + taken = สีของ "คนจริง" เท่านั้น แล้วสะสมสีบอทที่จัดไปแล้ว (ไม่รวม pin ของ slot ตัวเอง) + → pin เดิมที่ valid + ไม่ชนคน/บอทอื่น = คงเดิม (ไม่เปลี่ยนสีมั่วทุก sync); ชนเมื่อไหร่ค่อยเลือกใหม่ + แก้ 2 อย่าง: (ก) เดิม getTakenLobbyThemeIndices รวม pin ของ slot ตัวเอง → re-pick เสมอ = สีบอท churn ทุก sync + (ข) "เงาสะท้อนของ user" = บอทสีตรงคน (เช่น bot ทดแทนคนที่ออก pin สีคนนั้นไว้) พอคนนั้นกลับมา → บอท re-pick พ้นสีคน */ +function syncLobbyBotThemeSlots(space) { + if (!space) return []; + const botCount = effectiveBotSlotCount(space); + if (!space.lobbyBotThemeBySlot || typeof space.lobbyBotThemeBySlot !== 'object') { + space.lobbyBotThemeBySlot = {}; + } + const assigned = new Set(); + space.peers.forEach((p) => { + const ti = parseInt(p.lobbyColorThemeIndex, 10); + if (ti >= 1 && ti <= LOBBY_THEME_COUNT) assigned.add(ti); + }); + const pickFree = () => { + for (let t = 1; t <= LOBBY_THEME_COUNT; t++) { if (!assigned.has(t)) return t; } + return (assigned.size % LOBBY_THEME_COUNT) + 1; + }; + const out = []; + for (let i = 0; i < botCount; i++) { + const slot = space.lobbyBotThemeBySlot[i]; + let themeIndex = parseInt(slot && slot.themeIndex, 10); + if (!(themeIndex >= 1 && themeIndex <= LOBBY_THEME_COUNT) || assigned.has(themeIndex)) { + themeIndex = pickFree(); + } + assigned.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), + /* ส่งจำนวนบอทมาด้วย — ให้ client ที่นั่งอยู่ใน LobbyB อยู่แล้ว (ไม่ได้ re-join) เห็นบอทที่มาแทนคนออกทันที */ + botSlotCount: effectiveBotSlotCount(space), + }); +} + +/* ===== Game Log (รวม 7 มินิเกม + LobbyA/B) → /var/www/html/gamelog/data/log.jsonl อ่านที่ /gamelog ===== */ +const GAMELOG_DIR = path.join(__dirname, '..', 'gamelog', 'data'); +const GAMELOG_FILE = path.join(GAMELOG_DIR, 'log.jsonl'); +try { fs.mkdirSync(GAMELOG_DIR, { recursive: true }); } catch (eMk) { /* ignore */ } +function gameLogCategoryOf(space) { + if (!space) return 'อื่นๆ'; + try { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + const gt = md && md.gameType; + const m = { quiz: 'MG1', gauntlet: 'MG2', stack: 'MG3', quiz_carry: 'MG4', jump_survive: 'MG5', space_shooter: 'MG6', balloon_boss: 'MG7' }; + if (gt && m[gt]) return m[gt]; + if (serverMapIsPostCaseLobbyB(space)) return 'LobbyB'; + return 'LobbyA'; + } catch (e) { return 'อื่นๆ'; } +} +function glog(spaceId, space, ev, detail, who) { + try { + const rec = { + t: Date.now(), + cat: gameLogCategoryOf(space), + room: String((space && space.spaceName) || spaceId || '').slice(0, 64), + who: String(who || '').slice(0, 48), + ev: String(ev || '').slice(0, 48), + detail: detail == null ? '' : String(detail).slice(0, 500), + src: 'server', + }; + fs.appendFile(GAMELOG_FILE, JSON.stringify(rec) + '\n', () => {}); + } catch (e) { /* ignore */ } +} + +/** เก็บกวาด peer ซ้ำ (ghost / "เงาสะท้อนของ user") ออกจาก space.peers ทั้งก้อน — กันกรณี dedup ต่อ-join พลาด/race + จน peer คนเดิมค้าง 2 ตัว → snapshot ส่งไปทุกจอ = เห็นตัวซ้ำ + MG3 คิวตามี seat ผี = เกมค้าง. + เทียบ playerKey ก่อน (เชื่อถือสุด ข้าม reconnect) แล้วชื่อ custom; เก็บตัวที่อยู่ท้าย Map (เพิ่ง join/active ล่าสุด) */ +function pruneDuplicateSpacePeers(spaceId, space) { + if (!space || !space.peers || space.peers.size < 2) return false; + const validKey = (p) => (p && typeof p.playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(p.playerKey)) ? p.playerKey : ''; + const isCustom = (nn) => nn && nn !== 'ผู้เล่น' && nn.indexOf('ผู้เล่น') !== 0 && nn.indexOf('บอท') !== 0; + const lastByKey = new Map(); + const lastByNickNoKey = new Map(); /* ชื่อ → pid เฉพาะ peer ที่ "ไม่มี playerKey" (ก้ำกึ่งว่าเป็น ghost) */ + space.peers.forEach((p, pid) => { + if (!p) return; + const pk = validKey(p); + if (pk) { lastByKey.set(pk, pid); return; } /* มี key → ไม่เข้า nickname dedup */ + const nn = normalizeLobbyNickname(p.nickname); + if (isCustom(nn)) lastByNickNoKey.set(nn, pid); + }); + const remove = []; + space.peers.forEach((p, pid) => { + if (!p) return; + const pk = validKey(p); + if (pk) { + /* มี playerKey → ลบเฉพาะตัวที่ key เดียวกันแต่ไม่ใช่ตัวล่าสุด (= ghost reconnect จริง) + ⚠️ ไม่ลบด้วยชื่อ: 2 คนตั้งชื่อเหมือนกัน (เช่น "MONE") = คนละ playerKey = คนละคน ต้องเก็บทั้งคู่ */ + if (lastByKey.get(pk) !== pid) remove.push(pid); + return; + } + /* ไม่มี playerKey → dedup ชื่อ custom ได้ (เทียบเฉพาะกลุ่มไม่มี key ด้วยกัน) */ + const nn = normalizeLobbyNickname(p.nickname); + if (isCustom(nn) && lastByNickNoKey.get(nn) !== pid) remove.push(pid); + }); + if (!remove.length) return false; + remove.forEach((pid) => { + const gp = space.peers.get(pid); + space.peers.delete(pid); + if (space.quizCarryLobbyReady) delete space.quizCarryLobbyReady[pid]; + if (space.gauntletCrownLobbyReady) delete space.gauntletCrownLobbyReady[pid]; + io.to(spaceId).emit('user-left', { id: pid }); + glog(spaceId, space, 'ghost-prune', 'ลบตัวซ้ำ id=' + String(pid).slice(0, 6), (gp && gp.nickname) || ''); + }); + return true; +} + +/** จำนวน 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; + /* เลือกผู้เล่น ≥ 4 คนตอนสร้างห้อง → ไม่เติมบอท (รอคนจริง) — เว้นแต่ host ตั้งบอทเองภายหลัง (n>0) */ + if (n === 0 && maxP >= 4) return 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: '' }; +} + +/* [2026-07-23] เดิม catch แล้วเงียบ + handler ตอบ {ok:true} เสมอ → เขียนไฟล์ไม่สำเร็จก็ยังขึ้น + "บันทึกแล้ว" (โกหกผู้ใช้). ทำให้คืนผลจริงเหมือน saver ตัวอื่นในไฟล์นี้ (case-media/game-timing) */ +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'); + return { ok: true }; + } catch (e) { + console.error('saveAiSettings', e.message); + let err = 'บันทึกไม่ได้'; + if (e && e.code === 'EACCES') err = 'ไม่มีสิทธิ์เขียนไฟล์ ai-settings.json (chown เป็น user ที่รันเกม)'; + else if (e && e.message) err = 'บันทึกไม่ได้: ' + e.message; + return { ok: false, error: err }; + } +} + +/* บอกว่าคีย์ที่เก็บไว้เป็นของเจ้าไหน + โชว์แบบปิดบัง — หน้า Admin จะได้ยืนยันได้ว่าเซฟติดจริง + (เดิมหน้าเว็บล้างช่องคีย์ทุกครั้งหลังเซฟ และไม่เคยแสดง hasKey → ดูเหมือน "กดเซฟแล้วไม่เซฟ") */ +function aiKeyInfo(key) { + const k = String(key || ''); + if (!k) return { hasKey: false, provider: '', keyMasked: '' }; + const provider = /^sk-or-/i.test(k) ? 'OpenRouter' : 'OpenAI'; + return { hasKey: true, provider, keyMasked: k.slice(0, 8) + '…' + k.slice(-4) }; +} + +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 Battle: เปิด/ปิดโหมด + วันหมดเขต (เวลาไทย) [2026-07-22] ===== + ไฟล์เขียนโดย Admin/api/quiz-battle-modes.php → ที่นี่อ่านอย่างเดียวและ **hot-reload ตาม mtime** + (แอดมินกดบันทึกแล้วมีผลทันที ไม่ต้อง restart game-zep) */ +const QBM_MODES_PATH = path.join(__dirname, 'data', 'quiz-battle-modes.json'); +const QBM_ROOM_COUNT = 10; +const QBM_TZ = 'Asia/Bangkok'; +const QBM_TZ_OFFSET_MIN = 7 * 60; /* ไทยไม่มี DST — บวก 7 ชม.คงที่ */ +let qbModesCache = null; +let qbModesCacheMtime = 0; +let qbModesCacheAt = 0; + +/** เวลาไทยตอนนี้เป็น 'YYYY-MM-DD HH:MM' */ +function thaiNowString(nowMs) { + const d = new Date((typeof nowMs === 'number' ? nowMs : Date.now()) + QBM_TZ_OFFSET_MIN * 60000); + const p = (n) => String(n).padStart(2, '0'); + return d.getUTCFullYear() + '-' + p(d.getUTCMonth() + 1) + '-' + p(d.getUTCDate()) + + ' ' + p(d.getUTCHours()) + ':' + p(d.getUTCMinutes()); +} + +/** 'YYYY-MM-DD HH:MM' (เวลาไทย) → epoch ms · คืน NaN ถ้ารูปแบบไม่ถูก */ +function thaiStringToEpochMs(s) { + if (typeof s !== 'string') return NaN; + const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/.exec(s.trim()); + if (!m) return NaN; + const utc = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], 0, 0); + return utc - QBM_TZ_OFFSET_MIN * 60000; +} + +const QBM_MAX_PLAYERS_CAP = 50; /* เพดานแข็งของ Quiz Battle — ปรับได้ไม่เกินนี้ */ +/** จำกัดจำนวนคนต่อห้องให้อยู่ 1..50 (ค่าเสีย/ว่าง → 50) */ +function qbClampMaxPlayers(v) { + const n = Math.floor(Number(v)); + if (!Number.isFinite(n) || n < 1) return QBM_MAX_PLAYERS_CAP; + return Math.min(QBM_MAX_PLAYERS_CAP, n); +} + +function qbDefaultModes() { + const modes = {}; + for (let i = 1; i <= QBM_ROOM_COUNT; i++) modes[String(i)] = { title: '', enabled: true, openUntil: '', maxPlayers: QBM_MAX_PLAYERS_CAP }; + return { version: 1, modes }; +} + +function loadQuizBattleModes() { + try { + const st = fs.statSync(QBM_MODES_PATH); + const mt = st.mtimeMs; + if (qbModesCache && mt === qbModesCacheMtime) return qbModesCache; + const j = JSON.parse(fs.readFileSync(QBM_MODES_PATH, 'utf8')); + const out = qbDefaultModes(); + if (j && typeof j === 'object' && j.modes && typeof j.modes === 'object') { + for (let i = 1; i <= QBM_ROOM_COUNT; i++) { + const k = String(i); + const src = j.modes[k]; + if (!src || typeof src !== 'object') continue; + out.modes[k] = { + title: typeof src.title === 'string' ? src.title : '', + enabled: !Object.prototype.hasOwnProperty.call(src, 'enabled') || !!src.enabled, + openUntil: typeof src.openUntil === 'string' ? src.openUntil : '', + maxPlayers: qbClampMaxPlayers(src.maxPlayers), /* [2026-07-23] จำนวนคนต่อห้อง (ปรับได้ ≤50) */ + }; + } + } + qbModesCache = out; + qbModesCacheMtime = mt; + qbModesCacheAt = Date.now(); + return out; + } catch (e) { + /* ไม่มีไฟล์/ไฟล์พัง → เปิดทุกห้อง (fail-open) ดีกว่าปิดเกมทั้งระบบเพราะ config หาย */ + if (!qbModesCache) qbModesCache = qbDefaultModes(); + return qbModesCache; + } +} + +/** ห้อง n เปิดให้เล่นอยู่ไหม (เทียบกับเวลาไทยตอนนี้) */ +function quizBattleRoomState(n, nowMs) { + const num = Math.floor(Number(n)); + if (!(num >= 1 && num <= QBM_ROOM_COUNT)) return { enabled: false, open: false, expired: false, reason: 'ไม่มีห้องนี้' }; + const m = loadQuizBattleModes().modes[String(num)] || {}; + const enabled = m.enabled !== false; + const until = m.openUntil || ''; + let expired = false; + if (until) { + const endMs = thaiStringToEpochMs(until); + if (Number.isFinite(endMs)) expired = (typeof nowMs === 'number' ? nowMs : Date.now()) > endMs; + } + const open = enabled && !expired; + let reason = ''; + if (!enabled) reason = 'โหมดนี้ถูกปิดโดยผู้ดูแลระบบ'; + else if (expired) reason = 'หมดเวลาเล่นแล้ว (ถึง ' + until + ' น. เวลาไทย)'; + return { enabled, open, expired, reason, openUntil: until }; +} + +/** จำนวนคนสูงสุดของห้อง QB n ตาม config (default 50) */ +function quizBattleRoomMaxPlayers(n) { + const num = Math.floor(Number(n)); + if (!(num >= 1 && num <= QBM_ROOM_COUNT)) return QBM_MAX_PLAYERS_CAP; + const m = loadQuizBattleModes().modes[String(num)] || {}; + return qbClampMaxPlayers(m.maxPlayers); +} + +/** ดึงเลขห้องจากชื่อ space "Quiz Battle ห้อง N" หรือจาก mapId "qbroomN" — คืน 0 ถ้าไม่ใช่ QB */ +function quizBattleRoomNumberOf(space, mapId) { + const mid = String((space && space.mapId) || mapId || ''); + const mm = /^qbroom(\d{1,2})$/.exec(mid); + if (mm) { + const n = Number(mm[1]); + if (n >= 1 && n <= QBM_ROOM_COUNT) return n; + } + return 0; +} + +const SOUND_SETTINGS_PATH = path.join(__dirname, 'data', 'sound-settings.json'); +const SOUND_SETTINGS_DEFAULT = { masterVol: 1.0, musicVol: 0.35, sfxVol: 0.75 }; +function loadSoundSettings() { + try { + if (fs.existsSync(SOUND_SETTINGS_PATH)) { + const j = JSON.parse(fs.readFileSync(SOUND_SETTINGS_PATH, 'utf8')); + const clamp = (v, d) => { const n = Number(v); return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : d; }; + return { + masterVol: clamp(j.masterVol, SOUND_SETTINGS_DEFAULT.masterVol), + musicVol: clamp(j.musicVol, SOUND_SETTINGS_DEFAULT.musicVol), + sfxVol: clamp(j.sfxVol, SOUND_SETTINGS_DEFAULT.sfxVol), + }; + } + } catch (e) { console.error('loadSoundSettings', e.message); } + return Object.assign({}, SOUND_SETTINGS_DEFAULT); +} +function saveSoundSettings(s) { + try { fs.writeFileSync(SOUND_SETTINGS_PATH, JSON.stringify(s, null, 2)); } catch (e) { console.error('saveSoundSettings', e.message); } +} + +/* ===== Quiz Battle global leaderboard (เก็บคะแนนสูงสุดต่อ room/หมวด ข้ามเซสชัน) ===== */ +const QB_LEADERBOARD_PATH = path.join(__dirname, 'data', 'quiz-battle-leaderboard.json'); +/** @type {{[roomId:string]: Array<{name:string,score:number,characterId:string,ts:number}>}} */ +let qbLeaderboardStore = {}; +function loadQbLeaderboard() { + try { + if (fs.existsSync(QB_LEADERBOARD_PATH)) { + const j = JSON.parse(fs.readFileSync(QB_LEADERBOARD_PATH, 'utf8')); + qbLeaderboardStore = (j && typeof j === 'object') ? j : {}; + } + } catch (e) { qbLeaderboardStore = {}; } +} +function saveQbLeaderboard() { + try { + const dir = path.dirname(QB_LEADERBOARD_PATH); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(QB_LEADERBOARD_PATH, JSON.stringify(qbLeaderboardStore), 'utf8'); + } catch (e) { /* ignore */ } +} +function qbLeaderboardSubmit(roomId, name, score, characterId) { + roomId = String(roomId || '').trim(); + name = String(name || '').trim().slice(0, 24); + score = Math.max(0, Math.floor(Number(score) || 0)); + if (!roomId || !name) return; + if (!Array.isArray(qbLeaderboardStore[roomId])) qbLeaderboardStore[roomId] = []; + const list = qbLeaderboardStore[roomId]; + const ex = list.find((e) => e && e.name === name); + if (ex) { + if (score > (ex.score || 0)) { ex.score = score; ex.characterId = String(characterId || ''); ex.ts = Date.now(); } + } else { + list.push({ name, score, characterId: String(characterId || ''), ts: Date.now() }); + } + list.sort((a, b) => (b.score - a.score) || (a.ts - b.ts)); + if (list.length > 500) list.length = 500; + saveQbLeaderboard(); +} +function qbLeaderboardGet(roomId, name) { + roomId = String(roomId || '').trim(); + const list = Array.isArray(qbLeaderboardStore[roomId]) ? qbLeaderboardStore[roomId] : []; + const top = list.slice(0, 50).map((e, i) => ({ rank: i + 1, name: e.name, score: e.score || 0, characterId: e.characterId || '' })); + let me = null; + const nm = String(name || '').trim(); + if (nm) { + const idx = list.findIndex((e) => e && e.name === nm); + if (idx >= 0) me = { rank: idx + 1, name: nm, score: list[idx].score || 0, characterId: list[idx].characterId || '' }; + } + return { top, me, total: list.length }; +} +loadQbLeaderboard(); + +/* ===== System stats (CPU / RAM) สำหรับหน้า Admin ===== */ +const os = require('os'); +let __prevCpuSample = null; +function __cpuSample() { + const cpus = os.cpus() || []; + let idle = 0, total = 0; + for (const c of cpus) { + for (const k in c.times) total += c.times[k]; + idle += c.times.idle; + } + return { idle, total }; +} +/** CPU% เฉลี่ยช่วงเวลาระหว่างสองครั้งที่เรียก (admin poll ~ทุก 5 วิ) — ครั้งแรกคืน null */ +function __cpuPercent() { + const cur = __cpuSample(); + const prev = __prevCpuSample; + __prevCpuSample = cur; + if (!prev) return null; + const idleD = cur.idle - prev.idle; + const totalD = cur.total - prev.total; + if (totalD <= 0) return null; + return Math.max(0, Math.min(100, Math.round((1 - idleD / totalD) * 100))); +} +__cpuSample(); // เก็บ baseline แรก +function getDiskStats() { + try { + if (typeof fs.statfsSync !== 'function') return null; + const s = fs.statfsSync(__dirname); + const total = s.blocks * s.bsize; + const free = s.bfree * s.bsize; // ว่างจริงทั้งหมด + const avail = s.bavail * s.bsize; // ว่างที่ผู้ใช้ทั่วไปใช้ได้ + const used = total - free; + if (!(total > 0)) return null; + return { total, free: avail, used, usedPercent: Math.round((used / total) * 100) }; + } catch (e) { return null; } +} +/* จำนวนผู้เล่น/ห้อง/socket ออนไลน์ตอนนี้ — โชว์ในหน้า Admin เทียบกับเพดานความจุ */ +function getOnlineStats() { + let players = 0, rooms = 0; + try { + for (const [, sp] of spaces) { + if (sp && sp.peers) { players += sp.peers.size; rooms++; } + } + } catch (e) { /* ignore */ } + let sockets = 0; + try { sockets = (io && io.engine && io.engine.clientsCount) || 0; } catch (e) { /* ignore */ } + return { sockets, players, rooms }; +} + +function getSystemStats() { + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const usedMem = totalMem - freeMem; + const load = os.loadavg(); + const cores = (os.cpus() || []).length || 1; + const cpuPct = __cpuPercent(); + const mu = process.memoryUsage(); + return { + ok: true, + cpu: { + percent: cpuPct, // 0-100 หรือ null (ครั้งแรก) + cores, + load1: Math.round(load[0] * 100) / 100, + load5: Math.round(load[1] * 100) / 100, + load15: Math.round(load[2] * 100) / 100, + loadPercent: Math.max(0, Math.min(100, Math.round((load[0] / cores) * 100))), + }, + mem: { + total: totalMem, + free: freeMem, + used: usedMem, + usedPercent: Math.round((usedMem / totalMem) * 100), + }, + disk: getDiskStats(), + node: { + rss: mu.rss, + heapUsed: mu.heapUsed, + rssPercent: Math.round((mu.rss / totalMem) * 100), + version: process.version, + uptime: Math.round(process.uptime()), + }, + uptime: { os: Math.round(os.uptime()), process: Math.round(process.uptime()) }, + /* ผู้ใช้ออนไลน์ + เพดานความจุ (ประเมินจาก 2 vCPU / socket fan-out): สบาย ≤300 · เริ่มหนัก ≥500 */ + online: getOnlineStats(), + capacity: { comfortable: 300, strain: 500 }, + hostname: os.hostname(), + platform: os.platform(), + ts: Date.now(), + }; +} + +/** ช่องตัวเลือกบนกริด 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({}); + /* ชุดคำถามพิเศษผูกกับ "เลขคดีสัมบูรณ์ 1-15" (ไม่ผูกกับโฟลเดอร์/level ที่อาจถูกจัดกลุ่มใหม่) + key เดิมในแอดมิน = L{ceil(case/3)}_C{((case-1)%3)+1} ต่อคดี → คงความเข้ากันได้ */ + const cid = Math.max(1, Math.min(15, Number(caseId) || 1)); + const L = Math.ceil(cid / 3); + const C = ((cid - 1) % 3) + 1; + return sets[specialQuizSetKey(L, C)] || sets[specialQuizSetKey(1, 1)] || { questions: [], specialCard: sanitizeSpecialCard({}) }; +} + +/* ===== Special Quiz ในเกม — ไอคอนสุ่มในฉาก + เซสชันถาม-ตอบ multiplayer (ทุกคนต้องตอบถูก) ===== */ +/** เฉพาะ 4 มินิเกมนี้ (ตอนสืบสวน) ที่ไอคอนพิเศษจะสุ่มโผล่ */ +/** คำถามพิเศษขึ้นได้ "ทุกเกม" (7 มินิเกม) — รวม stack(mnn93hpi)+balloon(mnq1eml7) ที่จับด้วยคลิก/แตะไอคอน */ +const SPECIAL_QUIZ_ELIGIBLE_MAP_IDS = ['mng8a80o', 'mno9kb07', 'mnn93hpi', 'mnorwqx1', 'mnptfts2', 'mnpz6rkp', 'mnq1eml7']; +const SPECIAL_QUIZ_ICON_TYPES = ['lawyer', 'police']; +/** โอกาสโผล่ = 1 ใน 3 ของการเล่นแต่ละรอบ */ +/** โอกาสขึ้นคำถามพิเศษต่อรอบ (เล่นจริง) — 1/3 · โหมดเทสต์บังคับการ์ดต่อเกมจะ force ขึ้นเสมอแยกต่างหาก */ +const SPECIAL_QUIZ_SPAWN_CHANCE = 1 / 3; +/** หลุดกลางมินิเกม → ให้เวลา reconnect (วิ) ก่อน bot เข้าเล่นแทน (roster คดี/LobbyB) + ⚠️ ห้ามตั้ง 0: เปลี่ยนหน้า lobbyB→play→lobbyB (socket หลุดชั่วคราว) จะถูกถอดจาก roster → rejoin ไม่ได้ ("ห้องนี้เริ่มคดีแล้ว") + หมายเหตุ: bot takeover "ในเกม MG1" (sess.bots) เป็นทันทีอยู่แล้ว แยกจาก grace นี้ + ตั้ง 22 (เดิม 10) — จาก log 6 คน join ใช้เวลา 10-16 วิ (โหลดหนัก) → grace 10 หมดก่อน = bot takeover ผิด → "ตัวละครซ้ำ 2 ตัว" (บอท+คนที่กลับมาช้า) + คนหลุด */ +const DISCONNECT_GRACE_SEC = 22; + +/** โหมดเทสต์ (admin): บังคับ "การ์ดพิเศษใบไหน → เกมไหน" — { mapId: cardId(1-7) } · เกมที่ตั้งไว้จะ force ขึ้นเสมอด้วยการ์ดนั้น */ +function normalizeTestSpecialCardByMap(v) { + const out = {}; + if (v && typeof v === 'object') { + SPECIAL_QUIZ_ELIGIBLE_MAP_IDS.forEach((mid) => { + const c = Math.floor(Number(v[mid])); + if (c >= 1 && c <= 7) out[mid] = c; + }); + } + return out; +} +/** เวลาให้ตอบต่อข้อ (มิลลิวิ) — ค่าคงที่เริ่มต้น (ย้ายไป 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 obj = md.objects && md.objects[y]; + if (obj && obj[x] === 1) return false; + /* ต้องไม่ใช่ช่อง blockPlayer ด้วย — ไม่งั้นไอคอนไปโผล่บนกำแพง/บล็อกที่ผู้เล่นเดินเข้าไม่ถึง (เช่น MG1 quiz มี 140 ช่อง) */ + const bp = md.blockPlayer && md.blockPlayer[y]; + if (bp && bp[x] === 1) return false; + return true; +} + +/** หลีกเลี่ยงโซน 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, occupiedLaneCount) { + const w = md.width || 20; + const h = md.height || 15; + /* quiz_carry (แบกคำตอบ): โต๊ะ/เคาน์เตอร์ถูกวาดใน "ภาพพื้นหลัง" แต่ไม่ได้มาร์คใน blockPlayer → + ช่องว่างกลางระหว่าง hub กับโต๊ะ interactive ดูเหมือนเดินได้แต่จริง ๆ มีโต๊ะบัง เก็บไอคอนไม่ได้ + → เกิดไอคอน "ใกล้จุดที่ผู้เล่นเกิด/เดิน" + เว้น buffer 2 ช่องรอบ hub/interactive/option (กันโต๊ะที่วาดเลยกริด) */ + if (md.gameType === 'quiz_carry') { + const nearMarkedTable = (x, y) => { + for (const g of [md.quizCarryHubArea, md.interactive, md.quizCarryOptionArea]) { + if (!Array.isArray(g)) continue; + for (let dy = -2; dy <= 2; dy++) { + const gy = g[y + dy]; + if (!gy) continue; + for (let dx = -2; dx <= 2; dx++) { + if (gy[x + dx]) return true; + } + } + } + return false; + }; + const seeds = (Array.isArray(md.lobbyPlayerSpawns) && md.lobbyPlayerSpawns.length) + ? md.lobbyPlayerSpawns + : [md.spawn || { x: Math.floor(w / 2), y: Math.floor(h / 2) }]; + const qcCands = []; + seeds.forEach((s) => { + const bx = Math.floor(Number(s.x)); + const by = Math.floor(Number(s.y)); + for (let dy = -5; dy <= 5; dy++) { + for (let dx = -5; dx <= 5; dx++) { + const dist = Math.abs(dx) + Math.abs(dy); + if (dist < 2 || dist > 6) continue; + const xx = bx + dx; + const yy = by + dy; + if (specialQuizTileWalkable(md, xx, yy) && !specialQuizTileInGameZone(md, xx, yy) && !nearMarkedTable(xx, yy)) { + qcCands.push({ x: xx, y: yy }); + } + } + } + }); + if (qcCands.length) { + const p = qcCands[Math.floor(Math.random() * qcCands.length)]; + return { x: p.x + 0.5, y: p.y + 0.5 }; + } + } + /* gauntlet (วิ่งหลบ): ผู้เล่นวิ่งบน "แถวเลน" เท่านั้น → ไอคอนต้องอยู่บนแถวเดียวกับ gauntletPlayerSpawns + (ที่ x กลางทาง) ผู้เล่นวิ่งผ่านเก็บได้/คลิกได้ — ไม่งั้นลอยออกนอกเลนเก็บไม่ได้ */ + if (md.gameType === 'gauntlet' && Array.isArray(md.gauntletPlayerSpawns) && md.gauntletPlayerSpawns.length) { + /* ใช้เฉพาะเลนที่ "มีผู้เล่นจริง" (occupiedLaneCount ตัวแรกตามลำดับ P1..PN) — กันไอคอนเกิดในเลนว่าง */ + const nLanes = (Number.isFinite(occupiedLaneCount) && occupiedLaneCount > 0) + ? Math.min(md.gauntletPlayerSpawns.length, Math.floor(occupiedLaneCount)) + : md.gauntletPlayerSpawns.length; + const usableSpawns = md.gauntletPlayerSpawns.slice(0, nLanes); + const rows = Array.from(new Set(usableSpawns + .map((s) => Math.floor(Number(s.y))) + .filter((n) => Number.isFinite(n) && n >= 0 && n < h))); + const laneCands = []; + const xMin = Math.max(4, Math.floor(w * 0.30)); + const xMax = Math.min(w - 2, Math.floor(w * 0.72)); + rows.forEach((ry) => { + for (let x = xMin; x <= xMax; x++) { + if (specialQuizTileWalkable(md, x, ry) && !specialQuizTileInGameZone(md, x, ry)) laneCands.push({ x, y: ry }); + } + }); + if (laneCands.length) { + const p = laneCands[Math.floor(Math.random() * laneCands.length)]; + return { x: p.x + 0.5, y: p.y + 0.5 }; + } + } + /* stack (Tower block): เล่นแบบหยอดบล็อก เดินไม่ได้ + กล้องเห็นแค่ "หอ" → วางไอคอนใกล้กึ่งกลางโซนปล่อย/ลง + (ที่อยู่ในจอ) ผู้เล่นคลิก/แตะเก็บได้ — ไม่งั้นไปโผล่ขอบแมปนอกจอ */ + if (md.gameType === 'stack') { + const zc = []; + [md.stackReleaseArea, md.stackLandArea].forEach((g) => { + if (Array.isArray(g)) { + for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (g[y] && g[y][x]) zc.push({ x, y }); + } + }); + if (zc.length) { + let cx2 = 0; + let cy2 = 0; + zc.forEach((c) => { cx2 += c.x; cy2 += c.y; }); + cx2 = Math.round(cx2 / zc.length); + cy2 = Math.round(cy2 / zc.length); + for (let rad = 0; rad <= 6; rad++) { + for (let yy = cy2 - rad; yy <= cy2 + rad; yy++) { + for (let xx = cx2 - rad; xx <= cx2 + rad; xx++) { + if (specialQuizTileWalkable(md, xx, yy)) return { x: xx + 0.5, y: yy + 0.5 }; + } + } + } + /* โซนทั้งหมดเป็น object → วางกลางโซนไปเลย (จะคลิกได้แม้ไม่ walkable เพราะ stack ใช้คลิก) */ + return { x: cx2 + 0.5, y: cy2 + 0.5 }; + } + } + /* จุดเกิดที่ "กำหนดเองจาก editor" (specialQuizSpawnArea) — ใช้กับทุกเกมยกเว้น gauntlet/stack + (สองเกมนั้น return ไปแล้วด้านบน) → สุ่ม 1 ช่องที่ paint ไว้ (เลือกช่องเดินได้ก่อน) */ + if (Array.isArray(md.specialQuizSpawnArea) && md.specialQuizSpawnArea.length) { + const painted = []; + const paintedWalkable = []; + for (let yy = 0; yy < h; yy++) { + const row = md.specialQuizSpawnArea[yy]; + if (!row) continue; + for (let xx = 0; xx < w; xx++) { + if (!row[xx]) continue; + painted.push({ x: xx, y: yy }); + if (specialQuizTileWalkable(md, xx, yy)) paintedWalkable.push({ x: xx, y: yy }); + } + } + const pickFrom = paintedWalkable.length ? paintedWalkable : painted; + if (pickFrom.length) { + const p = pickFrom[Math.floor(Math.random() * pickFrom.length)]; + return { x: p.x + 0.5, y: p.y + 0.5 }; + } + } + /* jump_survive (กระโดด mnptfts2): ผู้เล่นยืนบน "แพลตฟอร์ม" — พื้นที่ว่างนับ walkable ผิด (ลอยกลางอากาศเก็บไม่ได้) + ไอคอนคลิก/แตะเก็บได้ทุกมินิเกม (trySpecialQuizClickAt) → วางบนแพลตฟอร์มใกล้จุดเกิด (อยู่ในจอตั้งแต่ต้น คลิกได้) + = แก้ #44 อีเวนต์พิเศษ (ทนาย/เทพ) ไม่ขึ้นเพราะ BFS หา walkable ไม่เจอ → return null */ + if (md.gameType === 'jump_survive' && Array.isArray(md.jumpSurvivePlatformArea) && md.jumpSurvivePlatformArea.length) { + const sp = md.spawn || { x: Math.floor(w / 2), y: Math.floor(h / 2) }; + const bx = Math.floor(Number(sp.x)); + const by = Math.floor(Number(sp.y)); + const plats = []; + for (let y = 0; y < h; y++) { + const row = md.jumpSurvivePlatformArea[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x]) plats.push({ x, y }); + } + } + if (plats.length) { + plats.sort((a, b) => (Math.abs(a.x - bx) + Math.abs(a.y - by)) - (Math.abs(b.x - bx) + Math.abs(b.y - by))); + const near = plats.slice(0, Math.min(8, plats.length)); + const p = near[Math.floor(Math.random() * near.length)]; + const iy = (p.y - 1 >= 0) ? p.y - 1 : p.y; /* เหนือแพลตฟอร์ม 1 ช่อง = ระดับตัวผู้เล่น (ไม่จมในแพลตฟอร์ม) */ + return { x: p.x + 0.5, y: iy + 0.5 }; + } + } + /* 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) { + /* ไม่มี spawnArea/spawn เดินได้ (เช่น stack/balloon) → seed จาก "กลางแมป" วนออก + เพื่อให้ไอคอนอยู่กลางฉากที่มองเห็น/เอื้อมถึง ไม่ไปโผล่มุมนอกจอ */ + const ccx = Math.floor(w / 2); + const ccy = Math.floor(h / 2); + const maxRad = Math.max(w, h); + for (let rad = 0; rad <= maxRad && sx < 0; rad++) { + for (let yy = ccy - rad; yy <= ccy + rad && sx < 0; yy++) { + for (let xx = ccx - rad; xx <= ccx + rad && sx < 0; xx++) { + if (specialQuizTileWalkable(md, xx, yy)) { sx = xx; sy = yy; } + } + } + } + } + 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; + /* เลือกช่องที่อยู่ "ใกล้ผู้เล่น/กลางฉาก" (2-8 ช่องจาก seed) ก่อน → ผู้เล่นเข้าไปจับได้แน่นอน + อยู่ในจอ · + ไม่มีในระยะนั้นค่อย fallback ทั้งหมด */ + const near = cands.filter((c) => { + const d = Math.abs(c.x - sx) + Math.abs(c.y - sy); + return d >= 2 && d <= 8; + }); + const pool = near.length ? near : cands; + const pick = pool[Math.floor(Math.random() * pool.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 || !sq.appeared) return null; + return { iconType: sq.iconType, x: sq.x, y: sq.y, expiresAt: sq.expiresAt || 0 }; +} + +/** เริ่มมินิเกมสืบสวน: สุ่ม 1/3 ว่ารอบนี้จะมีไอคอนพิเศษไหม + เลือกตำแหน่ง (1 ใบต่อครั้งที่ติด — ดีไซน์ "1 ใน 3") + Test Mode (testSpecialCardByMap / forceSpecialQuiz) → ข้าม roll ขึ้นเสมอ */ +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); + /* [2026-07-17] การ์ดมาจาก "สำรับต่อคดี" (3 ใบไม่ซ้ำจาก 7) แจกตาม suspectPickIndex — ไม่ใช่ + set.specialCard ใบเดียวที่ Admin ตั้ง ซึ่งทำให้ 3 เกมได้ใบเดิมเสมอ (user request). + fallback เป็นค่า Admin ถ้าสำรับหาย (คดีที่เริ่มก่อน deploy นี้ / space เก่าค้าง) */ + const deckIdx = (typeof space.suspectPickIndex === 'number' && space.suspectPickIndex >= 0 && space.suspectPickIndex <= 2) ? space.suspectPickIndex : 0; + const deck = Array.isArray(space.specialCardDeck) ? space.specialCardDeck : null; + let card = set && set.specialCard; + if (deck && deck[deckIdx]) { + const dDef = specialCardDef(deck[deckIdx]) || {}; + card = sanitizeSpecialCard({ cardId: Number(deck[deckIdx]), th: dDef.th }); + } + /* โหมดเทสต์ (admin): บังคับการ์ดใบที่เลือกสำหรับเกมนี้ + force ขึ้นเสมอ (ไม่ต้องสุ่ม) */ + const testCardId = (runtimeGameTiming.testSpecialCardByMap || {})[mapId]; + const testForcedCard = (Number(testCardId) >= 1 && Number(testCardId) <= 7); + if (testForcedCard) { + const def = specialCardDef(testCardId) || {}; + card = sanitizeSpecialCard({ cardId: Number(testCardId), th: def.th }); + } + 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 || testForcedCard; + if (forced) { + console.log('[special-quiz] FORCE-spawn (debug)', mapId); + } else { + /* PRE-SCHEDULE ต่อคดี (แทนสุ่ม 1/3 ทุกเกม): ตอนเริ่มคดีสุ่มไว้แล้วว่าผู้ต้องสงสัยแต่ละคน (0-2) + การ์ดพิเศษจะออก "play ครั้งที่เท่าไหร่" (1-3, เล่นได้ 3 ครั้ง/คน). นับ eligible-play ต่อ suspect + แล้วออกการ์ดเฉพาะครั้งที่ตรงกับที่สุ่มไว้ → ได้การ์ดคนละ 1 ใบแน่นอน ตรงเวลาที่กำหนด ไม่ variance */ + const si = (typeof space.suspectPickIndex === 'number' && space.suspectPickIndex >= 0 && space.suspectPickIndex <= 2) ? space.suspectPickIndex : 0; + if (!space.suspectPlayCount || typeof space.suspectPlayCount !== 'object') space.suspectPlayCount = {}; + space.suspectPlayCount[si] = (space.suspectPlayCount[si] || 0) + 1; + const sched = space.specialQuizPlayBySuspect && space.specialQuizPlayBySuspect[si]; + if (!sched || space.suspectPlayCount[si] !== sched) { + console.log('[special-quiz] skip:schedule', mapId, 'suspect=' + si, 'play=' + space.suspectPlayCount[si], 'target=' + (sched || '?')); + return; + } + console.log('[special-quiz] SPAWN-scheduled', mapId, 'suspect=' + si, '@play=' + sched); + } + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md) { console.log('[special-quiz] skip:no-map', mapId); return; } + /* จำนวน "เลนที่มีผู้เล่นจริง" (คนจริง + บอท) — gauntlet เกิดไอคอนเฉพาะเลนที่ใช้จริง ไม่ใช่ทุกเลนที่ config */ + const occupiedLanes = (space.peers ? space.peers.size : 0) + effectiveBotSlotCount(space); + const tile = pickSpecialQuizSpawnTile(md, occupiedLanes); + 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); +} + +/** หน่วงไอคอน 2 วิ ค่อยโผล่ → emit ให้ทั้งห้อง + ตั้งเวลาให้ "หายไป" หลัง N วินาที (ถ้ายังไม่มีใครเก็บ) */ +const SPECIAL_QUIZ_ICON_DELAY_MS = 2000; +function scheduleSpecialQuizIconAppearAndExpiry(sid, space) { + const sq = space && space.specialQuiz; + if (!sq || sq.triggered || sq.done) return; + const tAppear = setTimeout(() => { + const sp = spaces.get(sid); + if (!sp || !sp.specialQuiz || sp.specialQuiz.triggered || sp.specialQuiz.done) return; + const cur = sp.specialQuiz; + const sec = readSpecialQuizIconExpireSec(); + cur.appeared = true; + cur.expiresAt = (sec > 0) ? (Date.now() + sec * 1000) : 0; + emitSpecialQuizIcon(sid, sp); + if (sec > 0) { + const tExpire = setTimeout(() => { + const sp2 = spaces.get(sid); + if (!sp2 || !sp2.specialQuiz || sp2.specialQuiz.triggered || sp2.specialQuiz.done) return; + sp2.specialQuiz = null; + io.to(sid).emit('special-quiz-icon-clear', {}); + console.log('[special-quiz] icon EXPIRED after', sec + 's'); + }, sec * 1000); + if (!Array.isArray(sp.specialQuizTimers)) sp.specialQuizTimers = []; + sp.specialQuizTimers.push(tExpire); + } + }, SPECIAL_QUIZ_ICON_DELAY_MS); + if (!Array.isArray(space.specialQuizTimers)) space.specialQuizTimers = []; + space.specialQuizTimers.push(tAppear); +} + +/** 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: 1, th: 'ทุกคนรับหลักฐานฟรี +1 ใบ' }, +}; + +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 || '', + }; +} + +/** เพดานเวลาที่ Time Dilation สะสมได้ต่อรอบ (กันการ์ดซ้อนยืดเวลาเกินเหตุ) */ +const TIME_DILATION_MAX_PENDING_SEC = 30; + +/** ใช้การ์ดที่ทำงาน "ทันที" (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 ขยายเอง + cap กันการ์ดซ้อนหลายใบจนเวลายืดเกินเหตุ */ + space.timeDilationPendingSec = Math.min(TIME_DILATION_MAX_PENDING_SEC, (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 — server แจกเองผ่าน game-award.php (มี secret + รู้ playerKey จาก join) กัน client ปลอม + noScore: ไม่ให้เหรียญ Fund ไปปั่นคะแนน high-score (เป็นโชค ไม่ใช่ฝีมือ) */ + const coins = def.coins || 10; + const awards = [...space.peers.values()] + .filter((p) => p.playerKey) + .map((p) => ({ playerKey: p.playerKey, nickname: (p.nickname || '').trim() || 'ผู้เล่น', coins: coins, noScore: true })); + submitGameAwards(awards); + io.to(sid).emit('special-card-applied', { + card: specialCardClientPayload(card), + coins: coins, + coinsServerAwarded: true, + }); + consumePendingSpecialCard(space, card.cardId); + return; + } + /* การ์ดที่เหลือ (after_game / pre_trial / trial_vote / trial_revote / lobby_next) + — คงไว้ใน pendingSpecialCard เพื่อใช้ภายหลัง */ +} + +/* ===== คลังการ์ดพิเศษที่สะสมในเคสนี้ (ใช้ได้พร้อมกันทุกใบ ไม่ทับกัน) ===== */ +function dbgCards(space) { + return JSON.stringify((space && space.pendingSpecialCards || []).map((e) => e.cardId + (e.consumed ? 'x' : ''))); +} +function pendingSpecialCardList(space) { + if (!Array.isArray(space.pendingSpecialCards)) space.pendingSpecialCards = []; + return space.pendingSpecialCards; +} +/** เพิ่มการ์ดเข้าคลัง — กันได้การ์ดซ้ำในเกมเดียว (ถ้ามีใบเดิมยังไม่ได้ใช้ จะไม่เพิ่มซ้ำ) */ +function addPendingSpecialCard(space, card) { + if (!card || !card.cardId) return null; + const list = pendingSpecialCardList(space); + const cid = Number(card.cardId); + if (list.some((e) => Number(e.cardId) === cid && !e.consumed)) { console.log('[card-dbg] add skip dup card' + cid + ' list=' + dbgCards(space)); return null; } + const entry = { cardId: cid, card: card, consumed: false }; + list.push(entry); + console.log('[card-dbg] +card' + cid + ' list=' + dbgCards(space)); + return entry; +} +/** หาการ์ดในคลังตาม cardId ที่ยังไม่ถูกใช้ */ +function findPendingSpecialCard(space, cardId) { + const cid = Number(cardId); + return pendingSpecialCardList(space).find((e) => Number(e.cardId) === cid && !e.consumed) || null; +} +function consumePendingSpecialCard(space, cardId) { + const e = findPendingSpecialCard(space, cardId); + if (e) e.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; + console.log('[card-dbg] specialQuiz end: success=' + !!success + ' cardId=' + (sq.card && sq.card.cardId)); + if (success && sq.card && (sq.card.cardId || sq.card.imageUrl)) { + space.specialCardAwardedThisRun = sq.card; + cardOut = specialCardClientPayload(sq.card); + addPendingSpecialCard(space, sq.card); /* สะสมเข้าคลัง — ใช้ได้พร้อมกันทุกใบ ไม่ทับใบเดิม */ + applySpecialCardImmediate(sid, space, sq.card); + } + io.to(sid).emit('special-quiz-ended', { success: !!success, card: cardOut }); + /* รอ client กด «เล่นต่อ» ก่อนค่อยเล่นรอบ quiz ต่อ (กันเวลาเดินตอนค้างอ่านการ์ด) + — มี safety timeout กันค้างถ้าไม่มีใครกด */ + space.specialQuizAwaitContinue = true; + space.specialQuizContinueAcks = new Set(); /* รวบ ack «เล่นต่อ» จากผู้เล่นจริง — ครบทุกคนถึง resume พร้อมกัน */ + /* แจ้งจำนวนเริ่มต้น 0/N ให้ทุก client (โชว์บนปุ่ม) */ + io.to(sid).emit('special-quiz-continue-progress', { ready: 0, total: space.peers.size }); + if (space.specialQuizContinueTimer) clearTimeout(space.specialQuizContinueTimer); + space.specialQuizContinueTimer = setTimeout(() => { + space.specialQuizContinueTimer = null; + finishSpecialQuizContinue(sid, space); /* safety: ไม่ครบใน 32s → ไปต่อทั้งห้อง */ + }, 32000); +} + +/** นับ ack «เล่นต่อ» — ถ้าผู้เล่นจริงที่ยังอยู่กดครบทุกคน → resume พร้อมกัน (ไม่นับบอท) */ +function maybeFinishSpecialQuizContinue(sid, space) { + if (!space || !space.specialQuizAwaitContinue) return; + const acks = space.specialQuizContinueAcks || (space.specialQuizContinueAcks = new Set()); + const expected = [...space.peers.keys()]; /* ผู้เล่นจริงที่ยังอยู่ในห้อง (บอทไม่ต้องกด) */ + const ready = expected.filter((pid) => acks.has(pid)).length; + io.to(sid).emit('special-quiz-continue-progress', { ready, total: expected.length }); + if (expected.length === 0 || expected.every((pid) => acks.has(pid))) { + finishSpecialQuizContinue(sid, space); + } +} + +/** เล่นรอบ quiz ต่อหลังผู้เล่นกด «เล่นต่อ» (เรียกครั้งเดียว / หรือ safety timeout) */ +function finishSpecialQuizContinue(sid, space) { + if (!space || !space.specialQuizAwaitContinue) return; + space.specialQuizAwaitContinue = false; + space.specialQuizContinueAcks = null; + if (space.specialQuizContinueTimer) { + clearTimeout(space.specialQuizContinueTimer); + space.specialQuizContinueTimer = null; + } + /* แจ้งทุก client ให้ปลด freeze + ปิด overlay พร้อมกัน (กดครบ หรือ safety timeout) */ + io.to(sid).emit('special-quiz-resume-all', {}); + resumeQuizAfterSpecialQuiz(sid, space); +} + +/* เกมหยุดชั่วคราวระหว่างคำถามพิเศษ (กำลังตอบ หรือรอกด «เล่นต่อ») + ใช้ร่วมกันทุก minigame ฝั่ง server — เดิมมีแต่ gauntlet เช็ค ทำให้ MG6/MG7 บอทยังเดิน/ยิงตอน pause */ +function isSpecialQuizPausedServer(space) { + if (!space) return false; + return !!(space.specialQuizAwaitContinue || (space.specialQuiz && space.specialQuiz.triggered && !space.specialQuiz.done)); +} + +/* setTimeout ที่ "หยุดนับ" ระหว่างคำถามพิเศษ (special quiz) — ใช้กับรอบ quiz(MG1)/quiz_carry(MG4) ที่ขับด้วย + setTimeout (ไม่มี tick loop ให้ extend endsAt เหมือน MG2/6/7). Poll: ถ้ากำลัง pause → ดัน deadline ตาม + เวลาที่ freeze (คงเวลาที่เหลือ) แล้ว re-poll; ถ้าถึงเวลาแล้ว → ยิง cb. เก็บ timer ใน space.quizTimers */ +function pausableSpecialQuizTimeout(sid, space, cb, ms) { + const d = { at: Date.now() + Math.max(0, ms | 0), last: Date.now() }; + const push = (t) => { const sp = spaces.get(sid); if (sp && Array.isArray(sp.quizTimers)) sp.quizTimers.push(t); }; + const step = () => { + const sp = spaces.get(sid); + if (!sp) return; + const now = Date.now(); + if (isSpecialQuizPausedServer(sp)) d.at += (now - d.last); /* freeze → เลื่อน deadline = คงเวลาที่เหลือ */ + d.last = now; + if (now >= d.at) { try { cb(); } catch (e) { /* ignore */ } return; } + push(setTimeout(step, Math.max(50, Math.min(300, d.at - now)))); + }; + push(setTimeout(step, Math.max(50, Math.min(300, ms | 0)))); +} + +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([ + 'topic1', 'topic2', 'topic3', 'topic4', 'topic5', + 'topic6', 'topic7', 'topic8', 'topic9', 'topic10', +]); + +/** คำถาม 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 = 'topic1'; + } + 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, + /* [2026-07-21] ไอคอนผู้ต้องสงสัย (สี่เหลี่ยมเล็ก) — ใช้เฉพาะหัวหน้าไต่สวน es3 เท่านั้น. + default = รูปเต็มตัว (suspects) → ถ้าไม่ได้ตั้งค่า พฤติกรรมเหมือนเดิมทุกประการ */ + icons: suspects.slice(), + }; +} + +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; +} + +/** [2026-07-21] ข้อความในหน้ารายละเอียดคดี (กรอกจาก Admin) — ตัดแท็ก/อักขระอันตราย + จำกัดความยาว + ไม่ใช่ URL จึงใช้ตัวนี้แทน sanitizeCaseMediaUrl */ +function sanitizeCaseText(v, max) { + if (typeof v !== 'string') return ''; + return v.replace(/[<>]/g, '').replace(/\r/g, '').trim().slice(0, max || 400); +} +function sanitizeCaseTextList(arr) { + const src = Array.isArray(arr) ? arr : []; + return [0, 1, 2].map((i) => sanitizeCaseText(src[i], 240)); +} + +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), + /* [2026-07-21] ไอคอน es3 — fallback เป็น suspects ที่ผ่าน sanitize แล้ว (ไม่ใช่ default ดิบ) + เพื่อให้ "ไม่ตั้ง icons" = ใช้รูปเต็มตัวชุดเดียวกับที่ตั้งไว้จริง */ + icons: sanitizeCaseMediaUrlList(c.icons, sanitizeCaseMediaUrlList(c.suspects, d.suspects)), + /* [2026-07-21] หน้ารายละเอียดแบบใหม่ (ประกอบเอง): ตั้ง pic แล้วหน้าจะสลับไปโหมดใหม่อัตโนมัติ + ยังไม่ตั้ง (ค่าว่าง) = ใช้รูปเดียวเดิม detail (case-detail-N.png) ต่อไป */ + pic: sanitizeCaseMediaUrl(c.pic, ''), + txtCharge: sanitizeCaseText(c.txtCharge, 300), + txtPlace: sanitizeCaseText(c.txtPlace, 300), + txtVictim: sanitizeCaseText(c.txtVictim, 300), + txtSuspects: sanitizeCaseTextList(c.txtSuspects), + txtNote: sanitizeCaseText(c.txtNote, 1500), + }; + } + 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; + /* ทุกเกม: เอาคะแนนของผู้เล่นทุกคนมาเฉลี่ยเป็น % แล้วแปลงเป็นเกรด A(≥80)/B(≥60)/C + quiz ใช้ % สัมบูรณ์ (ถูก/เต็ม), เกมอื่น normalize เทียบคนคะแนนสูงสุดในห้องแล้วเฉลี่ย */ + 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); + } + } + /* Last Light (crown gauntlet): ใช้เกรดเดียวกับ mission payload (รวมบอท) — แหล่งเดียวกับที่แสดง + F (ไม่มีผู้รอด) ไม่มี tier ในหลักฐาน → map เป็น C */ + if (gameType === 'gauntlet' && isGauntletCrownHeistMapBySpace(space) && space.lastCrownMissionGrade) { + const g = space.lastCrownMissionGrade; + return (g === 'A' || g === 'B') ? g : 'C'; + } + const peers = [...space.peers.values()]; + if (!peers.length) return 'C'; + const raw = peers.map((p) => evidencePlayerRawScore(space, p, gameType)); + let avgPct; + if (gameType === 'balloon_boss') { + /* MG7: เอาคะแนนทุกคนมาเฉลี่ย = ระดับความสำเร็จ (0-100) โดยตรง — ไม่ normalize เทียบคนคะแนนสูงสุด + (เดิม normalize ทำให้คะแนนต่ำ เช่น 15 เฟ้อเป็นเกรดสูง → ได้การ์ดชี้คนร้ายผิด) */ + const avgRaw = raw.reduce((s, v) => s + v, 0) / raw.length; + avgPct = Math.min(100, Math.max(0, avgRaw)); + } else { + const max = Math.max(1, ...raw); + avgPct = raw.reduce((s, v) => s + (v / max) * 100, 0) / raw.length; + } + return evidenceGradeFromPct(avgPct); + } catch (e) { return 'C'; } +} + +/* ===== แจกเหรียญตามอันดับ + คะแนนสะสม (high score) แบบ server-authoritative ===== */ +const GAME_AWARD_URL = process.env.GAME_AWARD_URL || 'https://srv1361159.hstgr.cloud/Admin/api/game-award.php'; +const ACHIEVEMENTS_URL = process.env.ACHIEVEMENTS_URL || GAME_AWARD_URL.replace('game-award.php', 'achievements.php'); +const GAME_AWARD_SECRET_PATH = path.join(__dirname, '..', 'Admin', 'private', 'game-award-secret.txt'); +let __gameAwardSecret = null; +function gameAwardSecret() { + if (__gameAwardSecret != null) return __gameAwardSecret; + try { __gameAwardSecret = String(fs.readFileSync(GAME_AWARD_SECRET_PATH, 'utf8')).trim(); } + catch (e) { __gameAwardSecret = ''; } + return __gameAwardSecret; +} +/* เหรียญตามอันดับผู้รอดชีวิต: อันดับ 1→10, 2→8, 3→6, 4→4, 5→2, 6+→0 */ +const MINIGAME_RANK_COINS = [10, 8, 6, 4, 2, 0]; + +/* เพดานคะแนนที่ client รายงานเองได้ (stack/quiz_carry) — sanity cap กัน overflow/ค่าขยะ/spoof สูงเกินจริง + ค่าจริงต่อรอบอยู่หลักร้อย-พัน เพดานนี้กว้างพอไม่กระทบเล่นจริง (ไม่ใช่ anti-cheat สมบูรณ์ — ดูคอมเมนต์ที่ handler) */ +const REPORTED_MINI_SCORE_MAX = 50000; + +/* คะแนน + สถานะรอด/ตาย ของผู้เล่นต่อ gameType ตอนจบมินิเกม */ +function minigamePlayerScoreSurvived(space, p, gameType) { + /* Card 3 Ban — ผู้ถูกแบนเข้ามาดูเฉยๆ ไม่ได้เล่น → ไม่นับเป็นผู้รอด/ไม่ได้คะแนน */ + if (p && p.bannedSpectator) return { score: 0, survived: false }; + switch (gameType) { + case 'quiz': { + const st = space.quizSession && space.quizSession.players ? space.quizSession.players[p.id] : null; + return { score: st ? Math.max(0, Number(st.score) | 0) : 0, survived: st ? !st.eliminated : true }; + } + case 'quiz_carry': { + /* MG4 server-auth: คะแนนจาก session (server เป็นเจ้าของ) — ทุกคนถือว่ารอด (ไม่มีตกรอบ) */ + const sc = space.quizCarrySession && space.quizCarrySession.players ? space.quizCarrySession.players[p.id] : null; + if (sc) return { score: Math.max(0, Number(sc.score) | 0), survived: true }; + return { score: Math.max(0, p.reportedMiniScore | 0), survived: true }; + } + case 'gauntlet': + return { score: Math.max(0, p.gauntletScore | 0), survived: !p.gauntletEliminated }; + case 'jump_survive': { + /* MG5 server-auth: ใช้สถานะตายจาก session (server เป็นเจ้าของ) ถ้ามี — ไม่งั้น fallback ค่าที่ client รายงานตอนจบ */ + const sj = space.jumpSurviveSession; + if (sj && sj.players && Object.prototype.hasOwnProperty.call(sj.players, p.id)) { + const dead = !!(sj.players[p.id] && sj.players[p.id].eliminated); + return { score: dead ? 0 : 100, survived: !dead }; + } + return { score: Math.max(0, p.reportedMiniScore | 0), survived: p.reportedMiniSurvived !== false }; + } + case 'space_shooter': + return { score: Math.max(0, p.spaceShooterScore | 0), survived: true }; + case 'balloon_boss': + return { score: Math.max(0, p.balloonBossScore | 0), survived: !p.balloonBossEliminated }; + default: /* stack, quiz_carry — client รายงานคะแนน/รอดเอง ตอนจบ */ + return { score: Math.max(0, p.reportedMiniScore | 0), survived: p.reportedMiniSurvived !== false }; + } +} + +/* ยิงไป PHP (มี secret) เพื่อบวกเหรียญ+คะแนนเข้าบัญชีผู้เล่นจริง (client ปลอมไม่ได้) */ +function submitGameAwards(awards) { + const secret = gameAwardSecret(); + if (!secret || !Array.isArray(awards) || !awards.length) return; + try { + fetch(GAME_AWARD_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ secret, awards }), + }).then((r) => r.json()).then((d) => { + if (!d || !d.ok) console.error('[game-award] failed', d); + }).catch((e) => console.error('[game-award] error', e && e.message)); + } catch (e) { console.error('[game-award] throw', e && e.message); } +} + +/* บวก progress achievement (server-authoritative ผ่าน achievements.php — secret เดียวกับ game-award) + items: [{ playerKey, id, inc }] — หนึ่ง POST ต่อรายการ (achievements.php รับทีละ id) */ +function submitAchievementProgress(items) { + const secret = gameAwardSecret(); + if (!secret || !Array.isArray(items) || !items.length) return; + items.forEach((it) => { + if (!it || !it.playerKey || !it.id) return; + try { + fetch(ACHIEVEMENTS_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'progress', secret, playerKey: it.playerKey, id: it.id, inc: it.inc || 1 }), + }).then((r) => r.json()).then((d) => { + if (!d || !d.ok) console.error('[achv] failed', it.id, d); + }).catch((e) => console.error('[achv] error', e && e.message)); + } catch (e) { console.error('[achv] throw', e && e.message); } + }); +} + +/* streak achievements (a6/d4/e1) — POST action=streak {playerKey,id,event:'hit'|'miss'} + PHP นับต่อ/รีเซ็ตเอง + set achievement = best streak (ไม่ลดลง) */ +function submitAchievementStreak(items) { + const secret = gameAwardSecret(); + if (!secret || !Array.isArray(items) || !items.length) return; + items.forEach((it) => { + if (!it || !it.playerKey || !it.id || (it.event !== 'hit' && it.event !== 'miss')) return; + try { + fetch(ACHIEVEMENTS_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'streak', secret, playerKey: it.playerKey, id: it.id, event: it.event }), + }).then((r) => r.json()).then((d) => { + if (!d || !d.ok) console.error('[achv] streak failed', it.id, d); + }).catch((e) => console.error('[achv] streak error', e && e.message)); + } catch (e) { console.error('[achv] streak throw', e && e.message); } + }); +} + +/* แจกเหรียญตามอันดับคะแนน (เฉพาะผู้รอดชีวิต ผู้ตาย = 0) + คะแนนสะสมเข้า leaderboard — ครั้งเดียวต่อรอบ */ +function awardMinigameRankCoins(sid, space) { + if (!space || space.coinAwardDoneForRun) return; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + const gameType = md && md.gameType; + if (!gameType || gameType === 'zep' || gameType === 'quiz_battle' || serverMapIsPostCaseLobbyB(space)) return; + const peers = [...space.peers.values()]; + if (!peers.length) return; + space.coinAwardDoneForRun = true; + + const rows = peers.map((p) => { + const ss = minigamePlayerScoreSurvived(space, p, gameType); + return { id: p.id, playerKey: p.playerKey || '', nickname: (p.nickname || '').trim() || 'ผู้เล่น', score: ss.score, survived: !!ss.survived, coins: 0 }; + }); + /* ผู้รอดชีวิตเรียงคะแนนมาก→น้อย แล้วให้เหรียญตามอันดับ */ + const survivors = rows.filter((r) => r.survived).sort((a, b) => b.score - a.score); + survivors.forEach((r, i) => { r.coins = i < MINIGAME_RANK_COINS.length ? MINIGAME_RANK_COINS[i] : 0; }); + + const caseId = String(space.caseId || space.detectiveLobbyCaseId || ''); + const awards = rows.filter((r) => r.coins > 0 && r.playerKey).map((r) => ({ playerKey: r.playerKey, nickname: r.nickname, coins: r.coins, caseId: caseId })); + submitGameAwards(awards); + + /* Achievement: d1 Minigame Solver — รอดมินิเกมสำเร็จ (เฉพาะผู้รอดชีวิตที่เป็นผู้เล่นจริง) */ + submitAchievementProgress(survivors.filter((r) => r.playerKey).map((r) => ({ playerKey: r.playerKey, id: 'd1_minigame_solver', inc: 1 }))); + + io.to(sid).emit('minigame-coins', { + gameType, + coinsTable: MINIGAME_RANK_COINS, + results: rows.map((r) => ({ id: r.id, nickname: r.nickname, score: r.score, survived: r.survived, coins: r.coins })), + }); +} + +/* 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 = 6; +const GAUNTLET_DEFAULT_JUMP_HEIGHT_MULT = 0.52; /* MG2 ความสูงกระโดด = ×tile (จุดสูงสุดของส่วนโค้ง sin) — ปรับได้ใน Admin */ +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, + gauntletJumpHeightMult: GAUNTLET_DEFAULT_JUMP_HEIGHT_MULT, + gauntletTimeLimitSec: GAUNTLET_DEFAULT_TIME_LIMIT_SEC, + troublesomeOfferSec: 15, + ...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', + /** จำนวนลูกโป่งต่อคน (0 = ใช้ค่าจากแมป หรือ default 3) */ + balloonBossBalloonsPerPlayer: 0, + /** ดาเมจต่อการยิงโดนบอส (ลบเลือดบอส/นัด) — ตั้งใน Admin Minigame-7 · แยกจากคะแนน */ + balloonBossDamagePerHit: 10, + /** โหมดเทสต์: เกมบังคับต่อการ์ด 3 ใบ — ['','',''] = สุ่มหมด, แต่ละช่องใส่ key เพื่อเจาะจงใบนั้น */ + forcedMinigameKeys: ['', '', ''], + /** โหมดเทสต์: บังคับการ์ดพิเศษต่อเกม { mapId: cardId } */ + testSpecialCardByMap: {}, + /** โหมดเทสต์: บังคับเสนอบทตัวป่วน (ปกติเสนอเฉพาะเมื่อมีคนจริง ≥ 4) */ + troublesomeForceOffer: false, + troublesomeRollMaxSec: 8, + /** เวลาที่ไอคอนคำถามพิเศษอยู่บนฉากก่อนหายไป (วินาที) — 0 = ไม่หาย */ + specialQuizIconExpireSec: 10, + /** เวลาโหวตเลือกผู้เล่น (การ์ด Silence/Ban) วินาที */ + pickVoteSec: 25, + /** เวลาโหวตชี้ตัวคนร้าย (พิจารณาคดี) วินาที */ + trialVoteSec: 30, + /** เวลาโชว์ผลโหวตคนร้าย (ถูก/ผิด) ก่อนตัดไปหน้าผลเต็ม — มิลลิวินาที */ + trialResultRevealMs: 7000, + /** ปลายทางหลังจบคดี (โชว์ผล+หน้าความรู้แล้ว): 'mainlobby' = กลับ Main-Lobby (เดิม) · + 'lobbya' = รีเซ็ตห้องเดิมกลับ LobbyA ให้เล่นคดีใหม่ต่อได้เลย */ + postCaseDestination: 'mainlobby', + }; +} + +/** อ่านเวลา expiry ไอคอนคำถามพิเศษ (วินาที) จาก game-timing — clamp 0..120 */ +function readSpecialQuizIconExpireSec() { + const v = Math.floor(Number(runtimeGameTiming && runtimeGameTiming.specialQuizIconExpireSec)); + if (!Number.isFinite(v) || v < 0) return 10; + return Math.min(120, v); +} + +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 < 8; i++) { /* 8 slot ตาม lobby theme (#4) — เดิม 6 */ + 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(1, Math.min(40, Math.floor(v))); +} +function clampGauntletJumpHeightMult(n) { + const v = Number(n); + if (Number.isNaN(v)) return GAUNTLET_DEFAULT_JUMP_HEIGHT_MULT; + return Math.max(0.2, Math.min(1.5, v)); +} +/** เวลารับข้อเสนอ "ตัวป่วน" (วินาที) — admin ปรับได้ใน game-timing; 5–120, default 15 */ +function clampTroublesomeOfferSec(n) { + const v = Number(n); + if (!Number.isFinite(v)) return 15; + return Math.max(5, Math.min(120, Math.floor(v))); +} +/* เวลาสูงสุด (วินาที) ที่ระบบรอให้ทุกคนรายงานพร้อม ก่อน "บังคับสุ่ม" ผู้รับบทตัวป่วน (เดิม hardcode 8000ms) */ +function clampTroublesomeRollMaxSec(n) { + const v = Number(n); + if (!Number.isFinite(v)) return 8; + return Math.max(1, Math.min(30, 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))); +} + +/** แปลง "ภาพ/สี" ของ gauntlet จาก request — field ที่ **ไม่ได้ส่งมา** ต้องคงค่าเดิม (prev) ไว้เสมอ + ห้ามรีเซ็ตเป็น default. เดิมอ่านจาก d อย่างเดียว → PUT บางส่วน (เช่น {postCaseDestination}) + ทำให้ gauntletLaneImageUrls/laser ถูกล้างทั้งชุด = อุปสรรค MG2 "texture หาย" (fallback สี่เหลี่ยมทึบ). + ส่ง [] มาตรง ๆ ยังล้างได้ตามปกติ (hasOwnProperty = ตั้งใจล้าง). */ +function normalizeGauntletVisualsFromRequest(d, prev) { + const def = defaultGauntletVisuals(); + const p = (prev && typeof prev === 'object') ? prev : {}; + const keep = (k) => (p[k] !== undefined ? p[k] : def[k]); + const base = { + gauntletLaneImageUrls: Array.isArray(p.gauntletLaneImageUrls) ? p.gauntletLaneImageUrls.slice() : def.gauntletLaneImageUrls, + gauntletLaserTopUrl: keep('gauntletLaserTopUrl'), + gauntletLaserBottomUrl: keep('gauntletLaserBottomUrl'), + gauntletLaserLineUrl: keep('gauntletLaserLineUrl'), + gauntletLaserFillColor: keep('gauntletLaserFillColor'), + gauntletLaserStrokeColor: keep('gauntletLaserStrokeColor'), + gauntletLaserLineWidthPx: keep('gauntletLaserLineWidthPx'), + }; + if (!d || typeof d !== 'object') return base; + const has = (k) => Object.prototype.hasOwnProperty.call(d, k); + const out = { ...base }; + if (has('gauntletLaneImageUrls')) { + let laneArr = d.gauntletLaneImageUrls; + if (typeof laneArr === 'string') laneArr = laneArr.split(/[\n\r]+/); + if (!Array.isArray(laneArr)) laneArr = []; + const urls = []; + for (let i = 0; i < laneArr.length && urls.length < 24; i++) { + const u = sanitizeGauntletAssetUrl(laneArr[i]); + if (u) urls.push(u); + } + out.gauntletLaneImageUrls = urls; + } + if (has('gauntletLaserTopUrl')) out.gauntletLaserTopUrl = sanitizeGauntletAssetUrl(d.gauntletLaserTopUrl); + if (has('gauntletLaserBottomUrl')) out.gauntletLaserBottomUrl = sanitizeGauntletAssetUrl(d.gauntletLaserBottomUrl); + if (has('gauntletLaserLineUrl')) out.gauntletLaserLineUrl = sanitizeGauntletAssetUrl(d.gauntletLaserLineUrl); + if (has('gauntletLaserFillColor')) out.gauntletLaserFillColor = clampGauntletLaserColor(d.gauntletLaserFillColor, def.gauntletLaserFillColor); + if (has('gauntletLaserStrokeColor')) out.gauntletLaserStrokeColor = clampGauntletLaserColor(d.gauntletLaserStrokeColor, def.gauntletLaserStrokeColor); + if (has('gauntletLaserLineWidthPx')) out.gauntletLaserLineWidthPx = clampGauntletLaserLineWidthPx(d.gauntletLaserLineWidthPx); + return out; +} + +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), + gauntletJumpHeightMult: clampGauntletJumpHeightMult(j.gauntletJumpHeightMult), + gauntletTimeLimitSec: clampGauntletTimeLimitSec(j.gauntletTimeLimitSec), + troublesomeOfferSec: Object.prototype.hasOwnProperty.call(j, 'troublesomeOfferSec') ? clampTroublesomeOfferSec(j.troublesomeOfferSec) : 15, + ...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) + : '', + balloonBossBalloonsPerPlayer: Object.prototype.hasOwnProperty.call(j, 'balloonBossBalloonsPerPlayer') + ? Math.max(0, Math.min(12, Math.floor(Number(j.balloonBossBalloonsPerPlayer)) || 0)) + : 0, + balloonBossDamagePerHit: Object.prototype.hasOwnProperty.call(j, 'balloonBossDamagePerHit') + ? Math.max(1, Math.min(100, Math.floor(Number(j.balloonBossDamagePerHit)) || 10)) + : 10, + forcedMinigameKeys: normalizeForcedMinigameKeys( + j.forcedMinigameKeys != null ? j.forcedMinigameKeys : j.forcedMinigameKey, + ), + testSpecialCardByMap: normalizeTestSpecialCardByMap(j.testSpecialCardByMap), + troublesomeForceOffer: !!j.troublesomeForceOffer, + troublesomeRollMaxSec: clampTroublesomeRollMaxSec(j.troublesomeRollMaxSec), + specialQuizIconExpireSec: Object.prototype.hasOwnProperty.call(j, 'specialQuizIconExpireSec') + ? Math.max(0, Math.min(120, Math.floor(Number(j.specialQuizIconExpireSec)) || 0)) + : 10, + pickVoteSec: Object.prototype.hasOwnProperty.call(j, 'pickVoteSec') + ? Math.max(5, Math.min(120, Math.floor(Number(j.pickVoteSec)) || 25)) + : 25, + trialVoteSec: Object.prototype.hasOwnProperty.call(j, 'trialVoteSec') + ? Math.max(5, Math.min(180, Math.floor(Number(j.trialVoteSec)) || 30)) + : 30, + trialResultRevealMs: Object.prototype.hasOwnProperty.call(j, 'trialResultRevealMs') + ? Math.max(1500, Math.min(20000, Math.floor(Number(j.trialResultRevealMs)) || 7000)) + : 7000, + postCaseDestination: (j.postCaseDestination === 'lobbya') ? 'lobbya' : 'mainlobby', + }; + } + } catch (e) { console.error('loadGameTiming', e.message); } + return defaultGameTiming(); +} + +let runtimeGameTiming = loadGameTiming(); + +/** MG5 jump_survive: pool ของ pattern แพลตฟอร์ม (verify reachability offline แล้ว) — สุ่ม 1 อัน/เกม + เพื่อให้ layout หลากหลาย/เดายาก · pattern[0] = layout เดิม (baseline) · โหลดครั้งเดียวตอน boot */ +let jumpSurvivePatterns = []; +function loadJumpSurvivePatterns() { + try { + const p = path.join(__dirname, 'data', 'jump-survive-patterns.json'); + if (fs.existsSync(p)) { + const j = JSON.parse(fs.readFileSync(p, 'utf8')); + if (j && Array.isArray(j.patterns)) { + jumpSurvivePatterns = j.patterns.filter((x) => x && Array.isArray(x.platformArea) && Array.isArray(x.variantArea)); + } + } + } catch (e) { console.error('loadJumpSurvivePatterns', e.message); jumpSurvivePatterns = []; } +} +loadJumpSurvivePatterns(); +/** เลือก pattern สุ่ม → เก็บบน space (ใช้ทั้ง server bot physics + ส่งให้ client) · คงเดิมถ้าไม่มี pool */ +function pickJumpSurviveLayoutForSpace(space) { + if (!space || !jumpSurvivePatterns.length) return null; + const idx = Math.floor(Math.random() * jumpSurvivePatterns.length); + const pat = jumpSurvivePatterns[idx] || jumpSurvivePatterns[0]; + space.jsLayout = { idx, platformArea: pat.platformArea, variantArea: pat.variantArea }; + space._jsEffMd = null; /* invalidate effMd cache */ + return space.jsLayout; +} +/** md ที่ override platformArea/variantArea ด้วย pattern ที่เลือก (สร้าง 1 ครั้ง/เกม, cache บน space) + ไม่ mutate แมป share (หลายห้องเล่นพร้อมกันได้) */ +function jsMap(space) { + const base = (space && space.mapId && maps.get(space.mapId)) || (space && space.mapData) || null; + if (!base || !space || !space.jsLayout) return base; + if (space._jsEffMd && space._jsEffMd.__base === base) return space._jsEffMd; + space._jsEffMd = Object.assign(Object.create(base), { + __base: base, + jumpSurvivePlatformArea: space.jsLayout.platformArea, + jumpSurvivePlatformVariantArea: space.jsLayout.variantArea, + }); + return space._jsEffMd; +} + +/** จำนวนลูกโป่งต่อคน Mega Virus — แมปกำหนดก่อน, ไม่งั้นใช้ค่ากลางจาก admin, สุดท้าย default 3 (clamp 1-12) */ +function balloonBossBalloonsForMap(md) { + const fromMap = Math.floor(Number(md && md.balloonBossBalloonsPerPlayer)) || 0; + const fromCfg = Math.floor(Number(runtimeGameTiming && runtimeGameTiming.balloonBossBalloonsPerPlayer)) || 0; + return Math.max(1, Math.min(12, fromMap || fromCfg || 3)); +} + +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, prev); /* prev = คงค่าเดิมของ field ที่ไม่ได้ส่งมา */ + 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; + /* จำนวนลูกโป่งต่อคน (0 = ใช้ค่าจากแมป หรือ default 3) */ + const prevBbCount = Object.prototype.hasOwnProperty.call(prev, 'balloonBossBalloonsPerPlayer') + ? Math.max(0, Math.min(12, Math.floor(Number(prev.balloonBossBalloonsPerPlayer)) || 0)) + : 0; + const balloonBossBalloonsPerPlayer = Object.prototype.hasOwnProperty.call(d, 'balloonBossBalloonsPerPlayer') + ? Math.max(0, Math.min(12, Math.floor(Number(d.balloonBossBalloonsPerPlayer)) || 0)) + : prevBbCount; + /* ดาเมจต่อการยิงโดนบอส (แยกจากคะแนน) — Admin Minigame-7 */ + const prevBbDmg = Object.prototype.hasOwnProperty.call(prev, 'balloonBossDamagePerHit') + ? Math.max(1, Math.min(100, Math.floor(Number(prev.balloonBossDamagePerHit)) || 10)) + : 10; + const balloonBossDamagePerHit = Object.prototype.hasOwnProperty.call(d, 'balloonBossDamagePerHit') + ? Math.max(1, Math.min(100, Math.floor(Number(d.balloonBossDamagePerHit)) || 10)) + : prevBbDmg; + 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 = { + /* เช่นเดียวกับ vis: ถ้า request ไม่ส่ง field มา ให้คงค่าเดิม (prev) — ไม่ใช่รีเซ็ตเป็น default */ + gauntletTickMs: clampGauntletTickMs(Object.prototype.hasOwnProperty.call(d, 'gauntletTickMs') ? d.gauntletTickMs : prev.gauntletTickMs), + gauntletJumpTicks: clampGauntletJumpTicks(Object.prototype.hasOwnProperty.call(d, 'gauntletJumpTicks') ? d.gauntletJumpTicks : prev.gauntletJumpTicks), + gauntletJumpHeightMult: clampGauntletJumpHeightMult(Object.prototype.hasOwnProperty.call(d, 'gauntletJumpHeightMult') ? d.gauntletJumpHeightMult : prev.gauntletJumpHeightMult), + gauntletTimeLimitSec: clampGauntletTimeLimitSec(Object.prototype.hasOwnProperty.call(d, 'gauntletTimeLimitSec') ? d.gauntletTimeLimitSec : prev.gauntletTimeLimitSec), + troublesomeOfferSec: Object.prototype.hasOwnProperty.call(d, 'troublesomeOfferSec') ? clampTroublesomeOfferSec(d.troublesomeOfferSec) : ((runtimeGameTiming && runtimeGameTiming.troublesomeOfferSec) || 15), + ...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, + balloonBossBalloonsPerPlayer, + balloonBossDamagePerHit, + forcedMinigameKeys: Object.prototype.hasOwnProperty.call(d, 'forcedMinigameKeys') + ? normalizeForcedMinigameKeys(d.forcedMinigameKeys) + : (Object.prototype.hasOwnProperty.call(d, 'forcedMinigameKey') + ? normalizeForcedMinigameKeys(d.forcedMinigameKey) + : normalizeForcedMinigameKeys(prev.forcedMinigameKeys)), + testSpecialCardByMap: Object.prototype.hasOwnProperty.call(d, 'testSpecialCardByMap') + ? normalizeTestSpecialCardByMap(d.testSpecialCardByMap) + : normalizeTestSpecialCardByMap(prev.testSpecialCardByMap), + troublesomeForceOffer: Object.prototype.hasOwnProperty.call(d, 'troublesomeForceOffer') + ? !!d.troublesomeForceOffer + : !!prev.troublesomeForceOffer, + troublesomeRollMaxSec: Object.prototype.hasOwnProperty.call(d, 'troublesomeRollMaxSec') + ? clampTroublesomeRollMaxSec(d.troublesomeRollMaxSec) + : clampTroublesomeRollMaxSec(prev.troublesomeRollMaxSec), + specialQuizIconExpireSec: Object.prototype.hasOwnProperty.call(d, 'specialQuizIconExpireSec') + ? Math.max(0, Math.min(120, Math.floor(Number(d.specialQuizIconExpireSec)) || 0)) + : (prev.specialQuizIconExpireSec != null ? Math.max(0, Math.min(120, Math.floor(Number(prev.specialQuizIconExpireSec)) || 0)) : 10), + pickVoteSec: Object.prototype.hasOwnProperty.call(d, 'pickVoteSec') + ? Math.max(5, Math.min(120, Math.floor(Number(d.pickVoteSec)) || 25)) + : (prev.pickVoteSec != null ? Math.max(5, Math.min(120, Math.floor(Number(prev.pickVoteSec)) || 25)) : 25), + trialVoteSec: Object.prototype.hasOwnProperty.call(d, 'trialVoteSec') + ? Math.max(5, Math.min(180, Math.floor(Number(d.trialVoteSec)) || 30)) + : (prev.trialVoteSec != null ? Math.max(5, Math.min(180, Math.floor(Number(prev.trialVoteSec)) || 30)) : 30), + trialResultRevealMs: Object.prototype.hasOwnProperty.call(d, 'trialResultRevealMs') + ? Math.max(1500, Math.min(20000, Math.floor(Number(d.trialResultRevealMs)) || 7000)) + : (prev.trialResultRevealMs != null ? Math.max(1500, Math.min(20000, Math.floor(Number(prev.trialResultRevealMs)) || 7000)) : 7000), + postCaseDestination: Object.prototype.hasOwnProperty.call(d, 'postCaseDestination') + ? (d.postCaseDestination === 'lobbya' ? 'lobbya' : 'mainlobby') + : (prev.postCaseDestination === 'lobbya' ? 'lobbya' : 'mainlobby'), + /* DEV: เปิด HUD โหลด LobbyA (Iron Man) — admin toggle. preserve ค่าเดิมถ้า request ไม่ส่งมา */ + lobbyDebugHud: Object.prototype.hasOwnProperty.call(d, 'lobbyDebugHud') + ? !!d.lobbyDebugHud + : !!prev.lobbyDebugHud, + lobbyDebugHudNames: Object.prototype.hasOwnProperty.call(d, 'lobbyDebugHudNames') + ? (Array.isArray(d.lobbyDebugHudNames) ? d.lobbyDebugHudNames.slice(0, 30).map((s) => String(s).slice(0, 40)) : []) + : (Array.isArray(prev.lobbyDebugHudNames) ? prev.lobbyDebugHudNames : []), + }; + 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, + spawnJoinOrder: typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : null, + lobbyColorThemeIndex: p.lobbyColorThemeIndex || null, + lobbySkinToneIndex: p.lobbySkinToneIndex || null, + bannedSpectator: !!p.bannedSpectator, + nr: p.netReady !== false, /* net-ready: false = เพิ่ง join กำลังโหลด (client โชว์ badge) */ + }; + if (extraFields && extraFields.gauntlet) { + row.gauntletJumpTicks = p.gauntletJumpTicks || 0; + row.gauntletScore = p.gauntletScore || 0; + row.gauntletEliminated = !!p.gauntletEliminated; + } + return row; + }); +} + +/* payload ของ quiz-carry-lobby-sync — แนบ peers snapshot ไปด้วยเสมอ + → client (โดยเฉพาะ host) upsert others ที่ขาดหายให้ครบ กัน host ไม่เห็นตัวละคร client + เมื่อ join/reconnect ไม่ทันรอบ (race ตอนเปลี่ยนหน้า lobbyB→play.html) */ +/* true ถ้า peer นี้คือผู้ถูกแบนรอบนี้ (Card 3 Ban) — เข้ามาดูเฉยๆ ไม่ต้อง Ready และไม่นับใน gate เริ่มเกม + (เช็คทั้ง id เดิมตอนโหวต และ flag bannedSpectator ของ peer ที่ reconnect เข้า play.html) */ +/** Card 3 Ban — แปลง target ตอนโหวต (socket.id) เป็น playerKey = "ตัวตนถาวร" + socket.id เปลี่ยนทุกครั้งที่เปลี่ยนหน้า (LobbyB ⇄ play.html) → ผูกกับ id อย่างเดียวจะหาคนโดนแบน + ไม่เจอตอนเริ่มเกมจริง = ตัวละครโผล่ในเกม + ถูกนับใน gate ready จน host กด START ไม่ได้. + บอท (__lobby_bot_N) ไม่มี playerKey → คืน '' แล้วใช้ id เดิมต่อ (บอท id คงที่อยู่แล้ว) */ +function resolveBanTargetPlayerKey(space, targetId) { + if (!space || !targetId) return ''; + if (String(targetId).indexOf('__lobby_bot_') === 0) return ''; + const p = space.peers && space.peers.get(targetId); + const k = p && p.playerKey; + return (k && /^[a-zA-Z0-9_-]{8,128}$/.test(String(k))) ? String(k) : ''; +} + +function isBannedPeerForRun(space, id) { + if (!space) return false; + if (space.bannedThisRunPlayerId && id === space.bannedThisRunPlayerId) return true; + const pp = space.peers && space.peers.get(id); + if (pp && pp.bannedSpectator) return true; + /* เทียบด้วย playerKey — ครอบเคส socket.id เปลี่ยนหลัง reconnect/เปลี่ยนหน้า (id เดิมไม่มีทางแมตช์) */ + if (space.bannedThisRunPlayerKey && pp && pp.playerKey + && String(pp.playerKey) === String(space.bannedThisRunPlayerKey)) return true; + return false; +} + +/* สำเนา ready map โดยตัด id ผู้ถูกแบนออก — กัน client นับ banned เป็นผู้เล่นที่ต้องรอ Ready (host ค้าง N/N+1) */ +function readyMapWithoutBanned(space, srcMap) { + const out = {}; + if (srcMap && typeof srcMap === 'object') { + Object.keys(srcMap).forEach((id) => { + if (!isBannedPeerForRun(space, id)) out[id] = srcMap[id]; + }); + } + return out; +} + +function quizCarryLobbySyncPayload(space) { + return { + readyMap: readyMapWithoutBanned(space, space && space.quizCarryLobbyReady), + peers: buildPeersSnapForSpace(space), + expectedActive: (space && space.minigameExpectedActiveCount) || 0, + }; +} + +/* payload ของ gauntlet-crown-lobby-sync — ตัด banned ออกจาก readyMap เช่นกัน + แนบ peers snapshot ด้วย (เหมือน quiz_carry) → client/host เติม others ที่ขาด เห็นจำนวนผู้เล่นเท่ากัน */ +function crownLobbySyncPayload(space) { + const payload = { + readyMap: readyMapWithoutBanned(space, space && space.gauntletCrownLobbyReady), + expectedActive: (space && space.minigameExpectedActiveCount) || 0, + peers: buildPeersSnapForSpace(space), + }; + /* MG2: ส่งตำแหน่ง spawn ของบอทมาด้วยตอน pregame → client วางบอทที่จุดเกิด (เดิมไม่ส่ง = บอทค้าง (1,1) มุมบน) */ + const gr = space && space.gauntletRun; + if (gr && gr.bots && gr.bots.size) { + const bots = []; + gr.bots.forEach((b, id) => { + if (!b) return; + bots.push({ id, x: b.x, y: b.y, direction: b.dir || 'right', gauntletScore: b.gauntletScore || 0, gauntletEliminated: !!b.gauntletEliminated }); + }); + if (bots.length) payload.bots = bots; + } + return payload; +} + +/* เช็คว่าพร้อมเริ่มมินิเกม crown-lobby ไหม: ยังไม่เริ่ม + เข้าครบ + ทุกคน (ไม่นับ banned) กด Ready */ +function crownLobbyAllReadyToStart(space) { + const gr = space && space.gauntletRun; + if (!gr || !gr.crownRunHeld || gr.lobbyStarted) return false; + const rm = space.gauntletCrownLobbyReady || {}; + const humanIds = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)); + if (humanIds.length === 0) return false; + const expectedActive = space.minigameExpectedActiveCount || 0; + const enough = !(expectedActive > 0 && humanIds.length < expectedActive); + const allReady = humanIds.every((id) => rm[id]); + /* [ban-gate-dbg] วินิจฉัย "แบนแล้วเริ่มเกมไม่ได้" — log สถานะ gate เมื่อมีแบนในรอบ + ยังไม่ผ่าน (throttle 1.5s). + เผยว่า: banned ไม่ถูก flag? playerKey ซ้ำ (duplicate-MONE)? expectedActive ผิด? หรือมีคนยังไม่ ready */ + if (!(enough && allReady) && space.bannedThisRunPlayerId && (Date.now() - (space._banGateLogAt || 0) > 1500)) { + space._banGateLogAt = Date.now(); + const rows = [...space.peers.entries()].map(([id, p]) => String(id).slice(0, 6) + (isBannedPeerForRun(space, id) ? '=BAN' : '') + (rm[id] ? '=R' : '=-') + (p.bannedSpectator ? 'sp' : '') + '#' + String(p.playerKey || '').slice(0, 6)); + console.log('[ban-gate-dbg] enough=' + enough + ' allReady=' + allReady + ' expected=' + expectedActive + ' humans=' + humanIds.length + ' bannedRun=' + String(space.bannedThisRunPlayerId).slice(0, 10) + ' peers=[' + rows.join(' ') + ']'); + } + return enough && allReady; +} + +/* เริ่มมินิเกม crown-lobby (server เป็นคนสั่ง) — idempotent ด้วย gr.lobbyStarted (รีเซ็ตเองทุกมินิเกมเพราะ gr สร้างใหม่) + ใช้ทั้ง host กด START และ auto-start เมื่อทุกคน ready (Track B: MG1 ไม่ต้องพึ่ง host) */ +function performCrownLobbyStart(sid, space) { + const gr = space && space.gauntletRun; + if (!gr || !gr.crownRunHeld || gr.lobbyStarted) return false; + gr.lobbyStarted = true; + const liveBeginsAt = Date.now() + LIVE_COUNTDOWN_MS; + space.missionLiveBeginsAt = liveBeginsAt; + if (isQuizQuestionMissionShellSpace(space)) { + /* quiz: เลื่อน server timer ไปเริ่มที่ liveBeginsAt (ตรงกับตอน client จบ 3-2-1) */ + setTimeout(() => { + const sp = spaces.get(sid); + if (!sp) return; + const md2 = maps.get(sp.mapId) || sp.mapData; + if (md2 && md2.gameType === 'quiz') { + clearSpaceQuizTimers(sp); + startQuizGame(sid, sp, md2); + } + }, LIVE_COUNTDOWN_MS); + } + if (isStackTowerMissionShellSpace(space)) { + /* MG3 stack: เริ่ม server engine ตอน client จบ countdown (ตรง liveBeginsAt) */ + setTimeout(() => { + const sp = spaces.get(sid); + if (!sp) return; + const md2 = maps.get(sp.mapId) || sp.mapData; + if (md2 && md2.gameType === 'stack') startStackTicker(sid, sp); + }, LIVE_COUNTDOWN_MS); + } + if (isJumpSurviveMissionShellSpace(space)) { + /* MG5 jump_survive: เริ่ม server engine (บอท+รอบ) ตอน client จบ countdown (ตรง liveBeginsAt) */ + setTimeout(() => { + const sp = spaces.get(sid); + if (!sp) return; + const md2 = maps.get(sp.mapId) || sp.mapData; + if (md2 && md2.gameType === 'jump_survive') startJumpSurviveGame(sid, sp); + }, LIVE_COUNTDOWN_MS); + } + if (isSpaceShooterMissionShellSpace(space)) { + /* MG6 space_shooter: เริ่ม server engine (บอท+รอบ) ตอน client จบ countdown (ตรง liveBeginsAt) */ + setTimeout(() => { + const sp = spaces.get(sid); + if (!sp) return; + const md2 = maps.get(sp.mapId) || sp.mapData; + if (md2 && md2.gameType === 'space_shooter') startSpaceShooterGame(sid, sp); + }, LIVE_COUNTDOWN_MS); + } + if (isBalloonBossMissionShellSpace(space)) { + /* MG7 balloon_boss: เริ่ม server engine (บอท+รอบ+HP บอส) ตอน client จบ countdown */ + setTimeout(() => { + const sp = spaces.get(sid); + if (!sp) return; + const md2 = maps.get(sp.mapId) || sp.mapData; + if (md2 && md2.gameType === 'balloon_boss') startBalloonBossGame(sid, sp); + }, LIVE_COUNTDOWN_MS); + } + io.to(sid).emit('gauntlet-crown-lobby-started', { liveBeginsAt }); + return true; +} + +/** สุ่ม 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; +} + +/* key ผู้เล่นในคดี — ใช้ playerKey (คงที่ต่อ browser ข้าม reconnect) กันเคส "2 คนตั้งชื่อซ้ำ = ถูกนับเป็นคนเดียว" + fallback เป็นชื่อ (n:) ถ้า peer ไม่มี playerKey ที่ valid */ +function detectiveParticipantKey(p) { + if (!p) return ''; + if (typeof p.playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(p.playerKey)) return 'k:' + p.playerKey; + const n = normalizeLobbyNickname(p.nickname || ''); + return n ? 'n:' + n : ''; +} + +/* นับผู้เล่นในคดี (caseParticipantKeys = playerKey) ที่กลับเข้า LobbyB + โหลดเสร็จ เทียบกับทั้งหมด + ใช้ตรวจว่าครบหรือยังก่อนให้ host เริ่มมินิเกมรอบถัดไป */ +function detectiveLobbyBMissingParticipants(space) { + const roster = space && space.caseParticipantKeys; + if (!roster || roster.size === 0) return { missing: 0, present: 0, total: 0 }; + /* present = participantKey ที่ "มี peer" และ "โหลด LobbyB เสร็จจริง" (lobbyb-loaded) + — กันเคส peer มีอยู่ (socket re-join ช่วงเปลี่ยนหน้า) แต่ client ยังค้างฉากเกมเก่า = นับครบผิด + fallback: ก่อนมีใครส่ง lobbyb-loaded (loadedSet ว่าง) ใช้ peer-count (กัน deadlock ช่วงแรกสุด) */ + const loadedSet = space.lobbyBLoadedKeys; + const useLoaded = !!(loadedSet && loadedSet.size > 0); + const present = new Set(); + space.peers.forEach((p) => { + const k = detectiveParticipantKey(p); + if (!k || !roster.has(k)) return; + if (useLoaded) { if (loadedSet.has(k)) present.add(k); } + else present.add(k); + }); + const total = roster.size; + return { missing: Math.max(0, total - present.size), present: present.size, total }; +} + +/* แจ้งทุกคนใน LobbyB ว่ามีผู้เล่นกลับเข้ามาแล้วกี่คน/ทั้งหมดกี่คน → host ใช้เปิด/ปิดปุ่มเริ่ม + แสดงสถานะรอ */ +function emitDetectiveLobbyBPresence(sid, space) { + if (!sid || !space) return; + if (!serverMapIsPostCaseLobbyB(space)) return; + const info = detectiveLobbyBMissingParticipants(space); + if (info.total <= 0) return; + io.to(sid).emit('detective-lobbyb-presence', { + present: info.present, + total: info.total, + allHere: info.missing === 0, + }); + /* มีการ์ด Ban ค้าง → เปิดโหวตเมื่อ "ทุกคนกลับเข้า LobbyB ครบ" เท่านั้น (เช็คทุกครั้งที่ presence เปลี่ยน) */ + try { maybeOpenPendingBanVote(sid, space); } catch (e) { /* ignore */ } +} + +/* [2026-07-19] ตัดผู้เล่นออกจาก roster คดี "ทันที" + re-emit presence — ใช้ตอน host kick / คนหลุดระหว่างเฟส + เลือกผู้ต้องสงสัย. ต้นตอบั๊ก: TC144 gate (suspect-pick-start) ใช้ total = caseParticipantKeys.size ที่ล็อก + ตอนเริ่มคดี และลดได้ทางเดียว = grace-prune 22วิ ที่ต้องมี playerKey → host kick (ไม่ prune เลย) และ + disconnect ที่ playerKey ว่าง (grace ข้าม) ทำให้ gate ค้าง "รอผู้เล่น" ถาวร host กด Start ไม่ได้. + ตัดทั้ง 'k:'+playerKey และ 'n:'+nick (เหมือน grace-prune ที่ server.js:13186-13195) เพื่อครอบ roster + ทั้งสองแบบ. emitDetectiveLobbyBPresence จะ no-op เองถ้าไม่ได้อยู่ LobbyB (กันเรียกผิดจังหวะ) */ +function pruneDetectiveParticipantNow(sid, space, playerKey, nickname, permanent) { + if (!space) return; + const nick = normalizeLobbyNickname(nickname || ''); + const keyOk = !!(playerKey && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)); + try { + if (space.caseParticipantKeys) { + if (keyOk) space.caseParticipantKeys.delete('k:' + playerKey); + if (nick) space.caseParticipantKeys.delete('n:' + nick); + } + if (space.caseParticipantNicknames && nick) space.caseParticipantNicknames.delete(nick); + /* [2026-07-23] permanent = ตั้งใจถอดออกถาวร (host kick) → ตัด roster ถาวรด้วย ห้ามให้ rejoin + ส่วน disconnect ธรรมดา (เปลี่ยนหน้าเข้า/ออกมินิเกม) ตัดแค่ตัวนับ — ต้องกลับเข้าห้องได้ */ + if (permanent) { + if (space.caseRosterEverKeys) { + if (keyOk) space.caseRosterEverKeys.delete('k:' + playerKey); + if (nick) space.caseRosterEverKeys.delete('n:' + nick); + } + if (space.caseRosterEverNicknames && nick) space.caseRosterEverNicknames.delete(nick); + } + } catch (e) { /* ignore */ } + emitDetectiveLobbyBPresence(sid, space); +} + +/** โหวต Ban เปิดเมื่อผู้เล่นในคดี "กลับเข้า LobbyB ครบทุกคน" เท่านั้น — กัน candidate ไม่ครบ + timer เริ่ม + * ก่อนคนเข้าครบ ("เล่น 4 คนแต่โหวตได้ 3"). safety: รอเกิน BAN_VOTE_MAX_WAIT_MS (บางคนไม่กลับ) → เปิดเลย กัน deadlock */ +const BAN_VOTE_MAX_WAIT_MS = 30000; +function maybeOpenPendingBanVote(sid, space) { + if (!space || !space.pendingBanVoteCard || space.pickVote) return; + if (!serverMapIsPostCaseLobbyB(space)) return; + const info = detectiveLobbyBMissingParticipants(space); + const allHere = info.total > 0 && info.missing === 0; + /* socket ต้อง "นิ่ง" — ไม่มี rejoin ภายใน 2s ล่าสุด. กันเปิดโหวตตอน LobbyB transition ยัง reconnect อยู่: + ถ้าเปิดตอน socket ยังเปลี่ยน → candidate/vote ผูก socket id เก่า + จอโหวตเด้งซ้ำ (re-broadcast) → "คนค้างขยับไม่ได้" */ + const socketsStable = (Date.now() - (space._lastLobbyBRejoinAt || 0)) > 2000; + if (!space._banVoteWaitSince) space._banVoteWaitSince = Date.now(); + const waited = Date.now() - space._banVoteWaitSince; + if ((!allHere || !socketsStable) && waited < BAN_VOTE_MAX_WAIT_MS) { + /* ยังไม่ครบ/ยังไม่นิ่ง + ยังไม่หมดเวลารอ → รอ แล้ว re-check อีก 1s */ + if (!space._banVoteWaitTimer) { + space._banVoteWaitTimer = setTimeout(() => { + const sp = spaces.get(sid); + if (sp) { sp._banVoteWaitTimer = null; maybeOpenPendingBanVote(sid, sp); } + }, 1000); + } + return; + } + if (space._banVoteWaitTimer) { clearTimeout(space._banVoteWaitTimer); space._banVoteWaitTimer = null; } + space._banVoteWaitSince = 0; + const banCard = space.pendingBanVoteCard; + space.pendingBanVoteCard = null; + console.log('[ban-debug] OPEN ban vote (peers=', space.peers.size, 'present=' + info.present + '/' + info.total + (allHere ? '' : ' TIMEOUT') + ')'); + io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(banCard), context: 'ban' }); + startPlayerPickVote(sid, space, 'ban', (targetId) => { + const sp2 = spaces.get(sid); + console.log('[ban-debug] ban vote RESOLVED target=', targetId); + if (sp2) { + sp2.bannedPlayerId = targetId || null; + sp2.bannedPlayerKey = resolveBanTargetPlayerKey(sp2, targetId); /* ตัวตนถาวร — id จะเปลี่ยนก่อนเกมเริ่ม */ + console.log('[ban-debug] ban target key=', sp2.bannedPlayerKey || '(none/bot)'); + } + }); +} + +/* TC166 safety: กัน lobbyb-loaded gate deadlock — ถ้า 25s แล้วยังไม่ครบ ให้ force นับ present จาก peer ที่อยู่จริงใน LobbyB */ +function armLobbyBLoadedSafety(sid, space) { + if (!sid || !space) return; + if (space.lobbyBLoadedTimer) { clearTimeout(space.lobbyBLoadedTimer); space.lobbyBLoadedTimer = null; } + space.lobbyBLoadedTimer = setTimeout(() => { + const sp = spaces.get(sid); + if (!sp || !serverMapIsPostCaseLobbyB(sp)) return; + const roster = sp.caseParticipantKeys; + if (!roster || roster.size === 0) return; + if (!sp.lobbyBLoadedKeys) sp.lobbyBLoadedKeys = new Set(); + let added = 0; + sp.peers.forEach((p) => { + const k = detectiveParticipantKey(p); + if (k && roster.has(k) && !sp.lobbyBLoadedKeys.has(k)) { sp.lobbyBLoadedKeys.add(k); added++; } + }); + if (added) { + try { glog(sid, sp, 'lobbyb-gate', '[TC166] safety 25s → force นับ present จาก peer ที่อยู่จริง (+' + added + ')'); } catch (e) { /* ignore */ } + try { emitDetectiveLobbyBPresence(sid, sp); } catch (e) { /* ignore */ } + } + }, 25000); +} + +/* TC156 story-gate: นับผู้เล่นในคดี "ที่อ่าน story (cutscene) จบแล้ว" เทียบกับคนที่อยู่ในห้องตอนนี้ + allRead = ทุก roster-nick ที่ยัง present ได้ส่ง story-read-done แล้ว (คนที่ left ไม่นับ — presence gate คุมแยก) */ +function detectiveStoryReadStatus(space) { + const roster = space && space.caseParticipantKeys; + if (!roster || roster.size === 0) return { present: 0, total: 0, allRead: true }; + const read = space.storyReadKeys || new Set(); + const presentRosterKeys = new Set(); + space.peers.forEach((p) => { + const k = detectiveParticipantKey(p); + if (k && roster.has(k)) presentRosterKeys.add(k); + }); + let readCount = 0; + presentRosterKeys.forEach((k) => { if (read.has(k)) readCount++; }); + const allRead = !!space.storyReadComplete || (presentRosterKeys.size > 0 && readCount >= presentRosterKeys.size); + return { present: readCount, total: presentRosterKeys.size, allRead }; +} +function emitDetectiveStoryReadPresence(sid, space) { + if (!sid || !space) return; + if (!serverMapIsPostCaseLobbyB(space)) return; + const st = detectiveStoryReadStatus(space); + io.to(sid).emit('detective-story-read-presence', { present: st.present, total: st.total, allRead: st.allRead }); +} + +function assignDetectiveSuspectCardMinigames(space) { + const available = DETECTIVE_MINIGAME_POOL.filter((e) => maps.has(e.mapId)); + /** โหมดเทสต์ (admin game-timing): เลือกเกมต่อการ์ด 3 ใบ — ช่องที่ตั้ง key = เจาะจง, ช่องว่าง = สุ่ม */ + const forcedKeys = normalizeForcedMinigameKeys(runtimeGameTiming && runtimeGameTiming.forcedMinigameKeys); + const randomPool = shuffleQuizQuestions(available.length ? available : [{ key: 'quiz', mapId: SUSPECT_INVESTIGATION_QUIZ_MAP_ID, labelTh: 'QuestionGame' }]); + let ri = 0; + const pick3 = []; + for (let i = 0; i < 3; i++) { + const fk = forcedKeys[i]; + let entry = fk ? available.find((e) => e.key === fk) : null; + if (!entry) { entry = randomPool[ri % randomPool.length]; ri += 1; } + pick3.push(entry); + } + space.suspectCardMinigames = pick3.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) || '', + }; + }); + // ชุดการ์ดใหม่ = ล้างหลักฐานที่ผู้เล่นเก็บได้ (รวม evidenceByNick fallback — กันหลักฐานคดีเก่า leak ข้ามคดี) + space.playerEvidence = {}; + space.evidenceByNick = {}; + space.caseMinigameSuspects = {}; /* [achv b2] มินิเกมที่รันในคดีนี้ (suspectIndex) — เช็ค "เล่นครบทุกรอบ" */ + space.caseCardTargetedKeys = {}; /* [achv d2] playerKey ที่โดนโหวตการ์ด Silence/Ban ในคดีนี้ */ + /* การ์ดพิเศษ — PRE-SCHEDULE ต่อคดี: สุ่มไว้เลยว่าผู้ต้องสงสัยแต่ละคน (0-2) การ์ดจะออก play ครั้งที่เท่าไหร่ + (1-3; ผู้ต้องสงสัย 1 คนเล่นได้ 3 ครั้ง) → ได้การ์ดคนละ 1 ใบแน่นอน แทนสุ่ม 1/3 ทุกเกม (กัน variance) */ + space.specialQuizPlayBySuspect = { 0: 1 + Math.floor(Math.random() * 3), 1: 1 + Math.floor(Math.random() * 3), 2: 1 + Math.floor(Math.random() * 3) }; + space.suspectPlayCount = { 0: 0, 1: 0, 2: 0 }; + /* [2026-07-17] สำรับการ์ดต่อคดี — สุ่ม 3 ใบ "ไม่ซ้ำกัน" จาก 7 ใบ แจกผู้ต้องสงสัยคนละใบ (user request: + "ได้การ์ดเดียวกันสามเกม จริงๆต้องไม่ตรงกัน"). เดิมทุก suspect ได้ set.specialCard ที่ Admin ตั้งไว้ + "ใบเดียวต่อคดี" → 3 เกมได้ใบเดิมเสมอ. แจกตรงนี้เพราะเป็นจุดเริ่มคดี = ล้างพร้อม suspectPlayCount + ไม่ค้างข้ามคดี. ค่า specialCard ใน Admin จะไม่ถูกใช้เป็นตัวเลือกอีก (ยังใช้เป็น fallback ถ้าสำรับหาย) */ + space.specialCardDeck = specialQuizShuffle([1, 2, 3, 4, 5, 6, 7]).slice(0, 3); + console.log('[special-quiz] schedule set (per-case):', JSON.stringify(space.specialQuizPlayBySuspect), 'deck=' + JSON.stringify(space.specialCardDeck)); + /* #6: ความคืบหน้าการสืบต่อผู้ต้องสงสัย (shared — เท่ากันทุกคน) = จำนวนมินิเกมที่เล่นให้คนนั้น (0-3) + ใช้โชว์ช่องหลักฐานใต้การ์ด suspect แทนหลักฐานส่วนตัว (เดิมแต่ละคนเห็นไม่เท่ากัน) */ + space.suspectTeamProgress = [0, 0, 0]; + // สุ่มคนร้ายตัวจริง 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 */ +/* คีย์เก็บหลักฐานต่อผู้เล่น — ใช้ playerKey (unique ต่อ browser) เป็นหลัก กัน "ชื่อซ้ำ" + (เช่น 2-3 คนชื่อ MONE) ใช้ evidence ก้อนเดียวกัน = ถูกแจกซ้ำ/เกิน. fallback ชื่อเฉพาะกรณีไม่มี key */ +function peerNickKey(space, playerId) { + if (!space || !playerId) return ''; + const p = space.peers && space.peers.get ? space.peers.get(playerId) : null; + if (!p) return ''; + if (typeof p.playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(p.playerKey)) return 'k:' + p.playerKey; + if (!p.nickname) return ''; + return 'n:' + 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]); + /* กันเกิน 3 ใบ/ผู้ต้องสงสัย (#1) — defensive ถ้ามีรอบที่ 4 หลุดมา */ + if (list.length >= 3) { + space.playerEvidence[playerId][suspectIndex] = list; + syncEvidenceByNick(space, playerId); + return null; + } + 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; +} + +/* [achv Phase A 2026-07-05] นับหลักฐานที่ผู้เล่นเก็บได้ → achievements + b4_data_miner(รวมทุกใบ ×100) · b1_evidence_collector(common ×20) · a2_sharp_eye(rare/Silver ×10) · b3_hidden_fragment(legendary ครั้งแรก) + เฉพาะผู้เล่นจริง (มี playerKey) — ข้ามบอท + เรียกจาก "จุดแจกจริง" 2 ที่เท่านั้น (ไม่รวม debug hotkey) */ +function trackEvidenceCollectAchv(space, playerId, card) { + if (!card || !card.rarity) return; + const p = space.peers && space.peers.get ? space.peers.get(playerId) : null; + const pkey = p && typeof p.playerKey === 'string' ? p.playerKey : null; + if (!pkey) return; + const achv = [{ playerKey: pkey, id: 'b4_data_miner', inc: 1 }]; + if (card.rarity === 'common') achv.push({ playerKey: pkey, id: 'b1_evidence_collector', inc: 1 }); + else if (card.rarity === 'rare') achv.push({ playerKey: pkey, id: 'a2_sharp_eye', inc: 1 }); + else if (card.rarity === 'legendary') achv.push({ playerKey: pkey, id: 'b3_hidden_fragment', inc: 1 }); + submitAchievementProgress(achv); +} + +function awardDetectiveEvidenceForMinigameEnd(space, suspectIndex, playerIds, grade) { + const out = {}; + /* [achv b2] บันทึกว่า suspect นี้มีมินิเกมรันในคดีนี้ (ใช้เช็ค "เล่นมินิเกมครบทุกรอบ") */ + if (suspectIndex >= 0 && suspectIndex <= 2) { + space.caseMinigameSuspects = space.caseMinigameSuspects || {}; + space.caseMinigameSuspects[suspectIndex] = true; + } + const grantedKeys = new Set(); /* แจก 1 การ์ด/playerKey ต่อเกม — กัน "หลายแท็บ/ชื่อซ้ำ" key เดียวได้เกิน */ + (playerIds || []).forEach((pid) => { + if (!space.peers.has(pid)) return; + const k = peerNickKey(space, pid); + if (k && grantedKeys.has(k)) return; /* key นี้ได้การ์ดรอบนี้แล้ว (evidence เก็บก้อนเดียวกันตาม key) */ + const card = awardRandomEvidenceCardToPlayer(space, pid, suspectIndex, grade); + if (card != null) { out[pid] = card; if (k) grantedKeys.add(k); trackEvidenceCollectAchv(space, 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 = findPendingSpecialCard(space, 7); + if (pend) { + const def = specialCardDef(7); + freeEvidenceCount = (def && def.evidence) || 1; + for (let n = 0; n < freeEvidenceCount; n++) { + const freeKeys = new Set(); /* 1 ใบ/playerKey ต่อรอบ (กันชื่อซ้ำ/หลายแท็บได้เกิน) */ + ids.forEach((pid) => { + if (!space.peers.has(pid)) return; + const k = peerNickKey(space, pid); + if (k && freeKeys.has(k)) return; + const extra = awardRandomEvidenceCardToPlayer(space, pid, suspectIdx, grade); + if (extra != null) { awardedList[pid].push(extra); if (awarded[pid] == null) awarded[pid] = extra; if (k) freeKeys.add(k); trackEvidenceCollectAchv(space, pid, extra); } + }); + } + pend.consumed = true; + } + space.detectiveAwardDoneForRun = true; + space.lastDetectiveInvestigatedSuspect = suspectIdx; + space.lastEvidenceGrant = { suspectIdx, awarded, awardedList, grade, freeEvidenceCount }; + /* [TC182] verify: จำนวนใบต่อ playerKey ที่แจกรอบนี้ — ควรได้ 1 ใบ/key/เกม (กันชื่อซ้ำแจกเกิน) */ + try { + const keyCounts = {}; + Object.keys(awardedList).forEach((pid) => { + const k = peerNickKey(space, pid) || String(pid); + keyCounts[k] = (keyCounts[k] || 0) + (awardedList[pid] ? awardedList[pid].length : 0); + }); + const vals = Object.values(keyCounts); + const over = vals.some((n) => n > 1); + glog(sid, space, 'evidence-grant', '[TC182] แจกหลักฐาน suspect#' + (suspectIdx + 1) + ' → ' + vals.length + ' key · ใบ/key=' + JSON.stringify(vals) + (over ? ' ⚠️เกิน' : ' (1/key · pass)')); + } catch (e) { /* ignore */ } + 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.lobbyBLoadedKeys = new Set(); /* ออกไปเล่นมินิเกม → ล้าง loaded; ตอนกลับ LobbyB ทุกคนต้องส่ง lobbyb-loaded ใหม่ */ + armLobbyBLoadedSafety(sid, space); /* backstop กัน deadlock ถ้า signal ไม่ครบ */ + space.detectiveAwardDoneForRun = false; + space.coinAwardDoneForRun = false; + space.lastCrownMissionGrade = null; + /* รีเซ็ตคะแนนที่ client รายงาน (stack/quiz_carry) ของรอบก่อน */ + space.peers.forEach((p) => { p.reportedMiniScore = 0; p.reportedMiniSurvived = true; }); + /* เริ่มรอบใหม่ → ล้าง over event เก่า (กัน resume ส่ง over ของรอบก่อนให้คน reconnect) */ + space.lastMinigameOver = null; + /* Card 3 Ban — ผู้ที่ถูกแบนไม่ได้เล่นมินิเกมรอบนี้ (1 รอบ) */ + space.bannedThisRunPlayerId = space.bannedPlayerId || null; + space.bannedThisRunPlayerKey = space.bannedPlayerKey || ''; /* ตัวหลักที่ใช้แมตช์จริง (id เปลี่ยนได้) */ + console.log('[ban-debug] startMinigame: bannedThisRunPlayerId=', space.bannedThisRunPlayerId, 'key=', space.bannedThisRunPlayerKey || '(none/bot)'); + space.bannedPlayerId = null; + space.bannedPlayerKey = ''; + /* จำนวนผู้เล่นที่ต้องเข้าเกม + กด Ready ให้ครบก่อน host เริ่มได้ (ไม่นับผู้ถูกแบน) + สแนปจาก peers ใน LobbyB ตอนนี้ — กัน host เริ่มก่อน client โหลด/กด Ready ครบ (เกมไม่ sync/จบไม่พร้อมกัน) */ + space.minigameExpectedActiveCount = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)).length; + 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; + /* MG5: สุ่ม pattern แพลตฟอร์ม 1 อัน/เกม (layout หลากหลาย/เดายาก) — เก็บบน space, ส่งให้ client ผ่าน game-start */ + space.jsLayout = null; space._jsEffMd = null; + if (md && md.gameType === 'jump_survive') pickJumpSurviveLayoutForSpace(space); + glog(sid, space, 'game-start', 'เริ่มมินิเกม: ' + ((md && md.name) || mapId) + ' (การ์ด ' + ((selectedIndex != null ? selectedIndex + 1 : '?')) + ')'); + /* สุ่มไอคอนคำถามพิเศษสำหรับรอบนี้ (1 ใน 3) — ส่งจริงผ่าน joinCb เมื่อ client เข้า play.html */ + maybeSpawnSpecialQuizForRun(space); + /* ไอคอนยังไม่โผล่ตอนนี้ — รอ client ส่ง 'special-quiz-arm' เมื่อเกม "live จริง" (พ้น howto/นับถอยหลัง) แล้วค่อยหน่วง 2 วิโผล่ */ + + space.peers.forEach((p) => { + const sp = pickSpawnForJoin(md, typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : 0); + p.x = sp.x; + p.y = sp.y; + p.direction = 'down'; + p.gauntletJumpTicks = 0; + p.gauntletScore = 0; + p.gauntletJumpPending = false; + p.gauntletEliminated = false; + p.detectiveFinished = false; /* MG2: รอให้ทุกคน(ที่ยังไม่ตาย)เข้าเส้นชัยก่อนจบเกมพร้อมกัน */ + }); + + 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, + bannedPlayerKey: space.bannedThisRunPlayerKey || '', /* client เทียบ playerKey (id เปลี่ยนหลัง reconnect) */ + }); + 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(); + } + /* quiz_carry ใช้ ready map ของตัวเอง (ไม่ใช่ crown) — รีเซ็ตสถานะ Ready ใหม่ทุกคนเริ่มรอบ แล้วแจ้งทุก client + กัน host เห็นจำนวนผู้เล่น/สถานะ ready ไม่ตรง และให้ flow READY→START ทำงานเหมือนเกมอื่น */ + if (md.gameType === 'quiz_carry') { + space.quizCarryLobbyReady = {}; + space.peers.forEach((_p, id) => { space.quizCarryLobbyReady[id] = false; }); + io.to(sid).emit('quiz-carry-lobby-sync', quizCarryLobbySyncPayload(space)); + } + + const gameStartPayload = { + mapId, + peersSnap, + detectiveMinigame: true, + detectiveReturnLobbyB: true, + minigameLabel: cardEntry.labelTh, + bannedPlayerId: space.bannedThisRunPlayerId || null, + bannedPlayerKey: space.bannedThisRunPlayerKey || '', /* client เทียบ playerKey (id เปลี่ยนหลัง reconnect) */ + }; + if (space.jsLayout) gameStartPayload.jsLayout = space.jsLayout; + 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(); + /* แจกเหรียญตามอันดับ + คะแนนสะสม ก่อนล้างสถานะมินิเกม (คะแนน/ผู้รอดยังครบ) */ + awardMinigameRankCoins(sid, space); + clearSpaceQuizTimers(space); + clearSpecialQuizTimers(space); + space.specialQuiz = null; + if (space.quizSession) space.quizSession.active = false; + space.quizSession = null; + space.detectiveMinigameActive = false; + space.gauntletReturnMapId = null; + /* Last Light (MG2): เก็บเกรด mission (รวมบอท server-authoritative) ก่อนล้าง gauntletRun + → ใช้เป็น "แหล่งเดียว" ของเกรดหลักฐาน ให้ตรงกับสรุปภารกิจที่ผู้เล่นเห็น */ + if (space.gauntletRun && isGauntletCrownHeistMapBySpace(space)) { + try { space.lastCrownMissionGrade = buildGauntletCrownMissionPayload(space).grade; } + catch (_e) { space.lastCrownMissionGrade = null; } + } + stopGauntletTicker(space); + space.gauntletRun = null; + /* MG3 stack (server-auth): หยุด engine + ล้าง session (เกรดถูกเก็บไว้ใน lastStackMissionGrade ตอนจบแล้ว) */ + stopStackTicker(space); + if (space.stackSession) space.stackSession.active = false; + space.stackSession = null; + /* MG4 quiz_carry (server-auth): ล้าง session (คะแนนถูกอ่านไปแจกเหรียญใน awardMinigameRankCoins ด้านบนแล้ว) */ + if (space.quizCarrySession) space.quizCarrySession.active = false; + space.quizCarrySession = null; + if (space.quizCarryWatchdog) { clearInterval(space.quizCarryWatchdog); space.quizCarryWatchdog = null; } + /* MG5 jump_survive (server-auth): ล้าง session */ + if (space.jumpSurviveSession) space.jumpSurviveSession.active = false; + space.jumpSurviveSession = null; + /* MG6 space_shooter (server-auth): ล้าง session */ + if (space.spaceShooterSession) space.spaceShooterSession.active = false; + space.spaceShooterSession = null; + /* MG7 balloon_boss (server-auth): ล้าง session */ + if (space.balloonBossSession) space.balloonBossSession.active = false; + space.balloonBossSession = null; + const suspectIdx = resolveDetectiveSuspectAwardIndex(space); + if (suspectIdx >= 0) { + space.lastDetectiveInvestigatedSuspect = suspectIdx; + /* #6: นับความคืบหน้าการสืบ (shared) +1 ต่อการเล่นมินิเกม 1 รอบของผู้ต้องสงสัยนั้น (สูงสุด 3) */ + if (!Array.isArray(space.suspectTeamProgress)) space.suspectTeamProgress = [0, 0, 0]; + if (suspectIdx >= 0 && suspectIdx <= 2) { + space.suspectTeamProgress[suspectIdx] = Math.min(3, (space.suspectTeamProgress[suspectIdx] || 0) + 1); + try { glog(sid, space, 'suspect-progress', '[TC179] สืบผู้ต้องสงสัย #' + (suspectIdx + 1) + ' → team progress ' + space.suspectTeamProgress.join('/') + ' (shared เท่ากันทุกจอ · pass)'); } catch (e) { /* ignore */ } + } + 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 — ผู้ที่ถูกแบนรอบนี้เข้ามาดูเฉยๆ ไม่ได้เล่น จึงไม่ได้รับหลักฐาน + (เช็คทั้ง id เดิมตอนโหวต และ flag bannedSpectator ของ peer ใน play.html ที่ reconnect ใหม่) */ + awardIds = awardIds.filter((id) => { + if (space.bannedThisRunPlayerId && id === space.bannedThisRunPlayerId) return false; + const pp = space.peers.get(id); + if (pp && pp.bannedSpectator) return false; + return true; + }); + /* ไม่มอบการ์ดเมื่อกลับ 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; + /* [TC166-fix] arm backstop ตรงนี้ = "ตอนกลับถึง LobbyB จริง" — เดิม arm ไว้ตั้งแต่ beginDetectiveSuspectMinigame + (25วิ) แต่มินิเกมยาวกว่านั้น → timer ยิงตอนยังอยู่ในมินิเกม → เจอ guard !serverMapIsPostCaseLobbyB แล้ว + return ทิ้ง → พอกลับ LobbyB ไม่เหลือ backstop เลย. ถ้า client ไหนค้างไม่ส่ง lobbyb-loaded (เช่นโดนดึงออก + กลางมินิเกม) gate จะค้าง present { + const sp = pickSpawnForJoin(lb, typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : 0); + 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 || [], + suspectTeamProgress: Array.isArray(space.suspectTeamProgress) ? space.suspectTeamProgress.slice() : [0, 0, 0], /* #6 shared */ + detectiveReturn: true, + }; + emitDetectiveLobbyReturnToPeers(sid, space, message, basePayload); + /* Card 3 Ban (when: lobby_next): ชนะการ์ดแบนรอบนี้ → "โหวตทันทีที่กลับ LobbyB หลังเกมจบ" + ตั้ง bannedPlayerId ให้มินิเกมรอบถัดไปกันไม่ให้คนที่ถูกโหวตเล่น */ + const banPend = findPendingSpecialCard(space, 3); + console.log('[card-dbg] lobby return: ban(3)=' + !!banPend + ' pickVote=' + !!space.pickVote + ' list=' + dbgCards(space)); + if (banPend && !space.pickVote) { + banPend.consumed = true; + /* ตั้ง flag — เริ่มโหวตตอนผู้เล่น "rejoin เข้า room-lobby" (ตอน return ผู้เล่นกำลัง navigate play.html→room-lobby.html อยู่ event จะหาย) */ + space.pendingBanVoteCard = banPend.card; + } +} + +/* ===== ขั้นพิจารณาคดี — โหวตชี้ตัวคนร้าย ===== */ +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 ครั้ง */ + { + /* โหมดเทสต์ (Ctrl+Alt+W): บังคับให้รอบนี้ "นับเป็นโหวตผิด" ครั้งเดียว เพื่อทริกการ์ด 5 */ + const forceWrong = !!space.testForceWrongVote; + space.testForceWrongVote = false; + 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 = forceWrong ? anyVotes : (anyVotes && mostVoted !== culprit); + const pend = findPendingSpecialCard(space, 5); + if (wrong && pend && !space.trialBailUsed) { + pend.consumed = true; + space.trialBailUsed = true; + space.trialRevoteExcluded = mostVoted; /* ผู้ต้องสงสัยที่โหวตไปแล้ว (ผิด) — ห้ามโหวตซ้ำในรอบใหม่ */ + io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'bail' }); + openTrialVotingPhase(sid, space, { revote: true, excludedSuspect: mostVoted }); + 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); + }); + /* ตัวป่วน (ถ้ามีคนรับบท) — หา socket.id ปัจจุบันจาก playerKey/nickname (id เดิมตอนเสนออาจเปลี่ยนแล้ว) */ + let disruptorId = null; + if (space.troublesomeAccept && (space.disruptorPlayerKey || space.disruptorNickname)) { + for (const [pid, p] of space.peers) { + const keyMatch = space.disruptorPlayerKey && p.playerKey === space.disruptorPlayerKey; + const nickMatch = space.disruptorNickname && normalizeLobbyNickname(p.nickname) === space.disruptorNickname; + if (keyMatch || nickMatch) { disruptorId = pid; break; } + } + } + io.to(sid).emit('trial-result', { + culpritIndex: culprit, + counts, + winners, + winnerNames, + cardMinigames: space.suspectCardMinigames || [], + disruptorId, + hasDisruptor: !!disruptorId, + }); + + /* ===== Achievement auto-track ===== */ + try { + const achv = []; + /* Phase B: majority vote (เช็ค lone-wolf) + มีคนโหวตไหม */ + let mvAchv = 0; for (let k = 1; k <= 2; k++) { if (counts[k] > counts[mvAchv]) mvAchv = k; } + const anyVotesAchv = (counts[0] + counts[1] + counts[2]) > 0; + /* a1 First Deduction (target 1) + a5 Truth Hunter (target 20) — ผู้โหวตถูก (ผู้เล่นจริงเท่านั้น) */ + winners.forEach((id) => { + const p = space.peers.get(id); + if (p && p.playerKey) { + achv.push({ playerKey: p.playerKey, id: 'a1_first_deduction', inc: 1 }); + achv.push({ playerKey: p.playerKey, id: 'a5_truth_hunter', inc: 1 }); + /* ===== Phase B (2026-07-05) — เงื่อนไขต่อผู้โหวตถูก ===== */ + /* c5 Lone Wolf: โหวตถูกแต่เสียงส่วนใหญ่ผิด (สวนทางทีม) */ + if (anyVotesAchv && mvAchv !== culprit) achv.push({ playerKey: p.playerKey, id: 'c5_lone_wolf', inc: 1 }); + /* หลักฐานในมือคดีนี้ (ต่อผู้ต้องสงสัย 3 กอง) */ + const evRows = clonePlayerEvidence(space, id); + const evTotal = evRows.reduce((s, row) => s + (Array.isArray(row) ? row.length : 0), 0); + /* c2 High Stakes: โหวตถูกโดยมีหลักฐาน ≤3 ใบ */ + if (evTotal <= 3) achv.push({ playerKey: p.playerKey, id: 'c2_high_stakes', inc: 1 }); + /* c1 Early Accusation: โหวตถูกโดยมีหลักฐาน ≤1 ใบ (ชี้ตัวก่อนเปิดหลักฐานครบ) */ + if (evTotal <= 1) achv.push({ playerKey: p.playerKey, id: 'c1_early_accusation', inc: 1 }); + /* a4 Logic Over Luck: โหวตถูกโดยไม่มีหลักฐาน legendary (ชี้ชัด) เลย */ + const hasLegendary = evRows.some((row) => Array.isArray(row) && row.some((c) => c && c.rarity === 'legendary')); + if (!hasLegendary) achv.push({ playerKey: p.playerKey, id: 'a4_logic_over_luck', inc: 1 }); + /* c4 Clutch Mind: โหวตถูกใน 10 วิสุดท้ายก่อนหมดเวลา */ + const voteAt = space.trialVoteAt && space.trialVoteAt[id]; + if (voteAt && space.trialVoteEndsAt) { + const remain = space.trialVoteEndsAt - voteAt; + if (remain >= 0 && remain <= 10000) achv.push({ playerKey: p.playerKey, id: 'c4_clutch_mind', inc: 1 }); + } + } + }); + /* ตัวป่วน — นับเฉพาะผู้เล่นจริง (มี playerKey) */ + if (disruptorId && space.disruptorPlayerKey) { + const dKey = space.disruptorPlayerKey; + achv.push({ playerKey: dKey, id: 'e2_the_impostor', inc: 1 }); /* target 1 */ + achv.push({ playerKey: dKey, id: 'e4_agent_of_chaos', inc: 1 }); /* target 10 */ + /* ตัวป่วนชนะ = ทีมโหวตผิดคน (ไม่จับคนร้ายตัวจริง) */ + let mv = 0; for (let k = 1; k <= 2; k++) { if (counts[k] > counts[mv]) mv = k; } + const anyV = (counts[0] + counts[1] + counts[2]) > 0; + if (anyV && mv !== culprit) { + achv.push({ playerKey: dKey, id: 'e3_master_of_doubt', inc: 1 }); /* target 1 */ + achv.push({ playerKey: dKey, id: 'e5_slippery_eel', inc: 1 }); /* target 5 */ + } + } + /* ===== Phase C/D (2026-07-06) — ต่อผู้เล่นที่จบคดี (ไม่ใช่แค่ผู้โหวตถูก) ===== */ + const streakItems = []; + /* c3 Quick Draw: ผู้โหวตถูกที่ "โหวตเร็วที่สุด" ในทีม (ใช้ trialVoteAt) */ + { + let fastId = null, fastAt = Infinity; + winners.forEach((id) => { + const at = space.trialVoteAt && space.trialVoteAt[id]; + const wp = space.peers.get(id); + if (at && wp && wp.playerKey && at < fastAt) { fastAt = at; fastId = id; } + }); + if (fastId) { const fp = space.peers.get(fastId); if (fp && fp.playerKey) achv.push({ playerKey: fp.playerKey, id: 'c3_quick_draw', inc: 1 }); } + } + /* วนผู้เล่นจริงทุกคนที่จบคดี (ข้ามบอท/ผู้ถูกแบน spectator) */ + const caseMgSuspects = Object.keys(space.caseMinigameSuspects || {}).map((n) => parseInt(n, 10)); + const targetedKeys = space.caseCardTargetedKeys || {}; + space.peers.forEach((p, pid) => { + if (!p || !p.playerKey || p.bannedSpectator) return; + const evRows = clonePlayerEvidence(space, pid); + const rarSet = {}; + evRows.forEach((row) => { if (Array.isArray(row)) row.forEach((c) => { if (c && c.rarity) rarSet[c.rarity] = true; }); }); + /* a3 Mind Architect: มีหลักฐานครบ 3 ระดับในคดีนี้ (common+rare+legendary) */ + if (rarSet.common && rarSet.rare && rarSet.legendary) achv.push({ playerKey: p.playerKey, id: 'a3_mind_architect', inc: 1 }); + /* b2 Relentless Investigator: เก็บหลักฐานครบทุก suspect ที่มีมินิเกมรันในคดีนี้ (= เล่นครบทุกรอบ) */ + if (caseMgSuspects.length > 0 && caseMgSuspects.every((si) => Array.isArray(evRows[si]) && evRows[si].length > 0)) { + achv.push({ playerKey: p.playerKey, id: 'b2_relentless_investigator', inc: 1 }); + } + /* d2 Silent Guardian: ไม่โดนโหวตการ์ด Silence/Ban ในคดีนี้ */ + if (!targetedKeys[p.playerKey]) achv.push({ playerKey: p.playerKey, id: 'd2_silent_guardian', inc: 1 }); + /* e1 The Observer (streak): จบคดีนี้โดยไม่แตะ legendary = hit, แตะ = รีเซ็ต */ + streakItems.push({ playerKey: p.playerKey, id: 'e1_the_observer', event: rarSet.legendary ? 'miss' : 'hit' }); + }); + /* a6 Unbreakable Logic (โหวตถูกติดกัน) + d4 Flawless Diver (ไม่โหวตผิดติดกัน) — เฉพาะคนที่โหวตจริง */ + Object.keys(votes).forEach((id) => { + const p = space.peers.get(id); + if (!p || !p.playerKey) return; + const ok = votes[id] === culprit; + streakItems.push({ playerKey: p.playerKey, id: 'a6_unbreakable_logic', event: ok ? 'hit' : 'miss' }); + streakItems.push({ playerKey: p.playerKey, id: 'd4_flawless_diver', event: ok ? 'hit' : 'miss' }); + }); + submitAchievementProgress(achv); + submitAchievementStreak(streakItems); + } catch (e) { console.error('[achv] trial-result', e && e.message); } + + space.silencedPlayerId = null; + + /* item1: admin ตั้งปลายทางหลังจบคดี = 'lobbya' → รีเซ็ตห้องเดิมกลับ LobbyA (ครั้งเดียว/คดี, terminal + เท่านั้น — revote คืนก่อนถึงจุดนี้แล้ว). ทำเงียบ (ไม่ broadcast) → client โชว์ผล+หน้าความรู้ต่อได้ + แล้ว reload (room-lobby.html?space=) มาเจอ LobbyA สด */ + try { + if (runtimeGameTiming && runtimeGameTiming.postCaseDestination === 'lobbya' && !space.postCaseLobbyAReset) { + space.postCaseLobbyAReset = true; + resetDetectiveSpaceToLobbyA(sid, space); + } + } catch (e) { console.error('[item1] post-case reset', e && e.message); } +} + +/* item1: รีเซ็ตห้อง detective กลับสภาพ LobbyA สดๆ — สร้าง space object ใหม่ (คง identity/peers/host/config) + เพื่อกัน state คดีค้าง (evidence/ban/trial/suspect/timers). client reload → join-space เจอ LobbyA. */ +function resetDetectiveSpaceToLobbyA(sid, space) { + if (!space) return; + const lobbyMapId = space.lobbyAMapId || space.mapId; + const lobbyMap = (lobbyMapId && maps.get(lobbyMapId)) || space.lobbyAMapData || space.mapData; + /* หยุด timer ทั้งหมดของห้องเดิม (กัน callback ค้างมายุ่งกับ object ใหม่) */ + try { stopGauntletTicker(space); } catch (e) { /* ignore */ } + try { clearSpaceQuizTimers(space); } catch (e) { /* ignore */ } + try { clearTrialVoteTimer(space); } catch (e) { /* ignore */ } + ['storyReadTimer', 'troublesomeDebTimer', 'troublesomeMaxTimer', 'lobbyBLoadedSafetyTimer', 'pickVoteTimer', 'banVoteTimer'].forEach((t) => { + if (space[t]) { try { clearTimeout(space[t]); } catch (e) { /* ignore */ } space[t] = null; } + }); + /* คืน spawn LobbyA + ยกเลิก ready ให้ทุก peer (client reload จะสร้าง peer ใหม่อยู่แล้ว — นี่กันช่วงคาบเกี่ยว) */ + space.peers.forEach((p) => { + try { + const sp = pickSpawnForJoin(lobbyMap, typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : 0); + p.x = sp.x; p.y = sp.y; p.direction = 'down'; + } catch (e) { /* ignore */ } + p.ready = false; + p.bannedSpectator = false; + }); + /* สร้าง space ใหม่แบบ LobbyA สด (เท่ากับห้องที่เพิ่งสร้าง) — คง identity/peers/host/config, ทิ้ง state คดี */ + const fresh = { + mapData: lobbyMap, mapId: lobbyMapId, peers: space.peers, spaceName: space.spaceName, + hostId: space.hostId, hostPlayerKey: space.hostPlayerKey, isPrivate: space.isPrivate, + maxPlayers: space.maxPlayers, createdAt: space.createdAt, roomPassword: space.roomPassword, + previewEditorTest: space.previewEditorTest, botSlotCount: space.botSlotCount, + lobbyAMapId: lobbyMapId, lobbyAMapData: lobbyMap, + suspectCardMinigames: null, suspectPhaseActive: false, suspectPickIndex: 0, detectiveMinigameActive: false, + graceReconnect: space.graceReconnect || {}, graceTimers: space.graceTimers || {}, + /* กันห้องถูกลบตอน client reload กลับเข้า LobbyA (peer=0 ชั่วขณะ) — keep-alive 5 นาที ให้ทุกคน rejoin ทัน */ + postCaseKeepAliveUntil: Date.now() + 300000, + }; + spaces.set(sid, fresh); + try { glog(sid, fresh, 'post-case', '[item1] reset ห้องกลับ LobbyA (postCaseDestination=lobbya) — รอ client reload'); } catch (e) { /* ignore */ } +} + +/* ===== ห้องสรุปหลักฐาน — การไต่สวน (ปากคำ 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 เพิ่มให้ */ +/* เวลาโหวตชี้ตัวคนร้าย (ms) — ตั้งได้ในหน้า admin (game-timing.trialVoteSec) */ +function trialVoteMs() { return Math.max(5, Math.min(180, Math.floor(Number(runtimeGameTiming.trialVoteSec)) || 30)) * 1000; } +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 = {}; + /* ผู้ต้องสงสัยที่ถูกตัดออก (โหวตผิดไปแล้วในรอบจับผิดตัว) — null ถ้าเป็นการโหวตปกติ */ + const excludedSuspect = opts.revote + ? (Number.isInteger(opts.excludedSuspect) ? opts.excludedSuspect + : (Number.isInteger(space.trialRevoteExcluded) ? space.trialRevoteExcluded : null)) + : null; + if (!opts.revote) space.trialRevoteExcluded = null; + autoVoteBotsTrial(space); + clearTrialVoteTimer(space); + /* Card 2 Extension — เพิ่มเวลาโหวต +10 วิ (ใช้ครั้งเดียว) */ + let extraMs = 0; + let extensionCard = false; + const pend = findPendingSpecialCard(space, 2); + if (pend) { + extraMs = TRIAL_VOTE_EXTENSION_MS; + extensionCard = true; + pend.consumed = true; + } + const durMs = trialVoteMs() + extraMs; + space.trialVoteEndsAt = Date.now() + durMs; + space.trialVoteAt = {}; /* [achv c4] รีเซ็ตเวลาโหวตทุกครั้งที่เปิดโหวต (รวม revote) */ + 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, + excludedSuspect: excludedSuspect, + silenced: !!(space.silencedPlayerId && pid === space.silencedPlayerId), + }); + }); + 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 ห้ามเล่น) ===== */ +/* เวลาโหวตเลือกผู้เล่น Silence/Ban (ms) — ตั้งได้ในหน้า admin (game-timing.pickVoteSec) */ +function pickVoteMs() { return Math.max(5, Math.min(120, Math.floor(Number(runtimeGameTiming.pickVoteSec)) || 25)) * 1000; } + +function clearPickVoteTimer(space) { + if (space.pickVoteTimer) { clearTimeout(space.pickVoteTimer); space.pickVoteTimer = null; } +} + +function pickVoteCandidates(space) { + const arr = []; + /* แนบ characterId + color index มาด้วย → client เรนเดอร์ avatar ได้แม้ peer ยังไม่เข้า cache + ท้องถิ่น (race หลังกลับ LobbyB) กัน avatar เขียว-blob บนจอโหวต Silence/Ban */ + space.peers.forEach((p, id) => arr.push({ + id, + nickname: (p && p.nickname) ? p.nickname : String(id).slice(0, 6), + isBot: false, + characterId: (p && p.characterId != null) ? p.characterId : null, + lobbyColorThemeIndex: (p && p.lobbyColorThemeIndex) || null, + lobbySkinToneIndex: (p && p.lobbySkinToneIndex) || null, + })); + testimonyBotIds(space).forEach((bid, i) => arr.push({ id: bid, nickname: 'บอท ' + (i + 1), isBot: true, characterId: null, lobbyColorThemeIndex: null, lobbySkinToneIndex: null })); + return arr; +} + +/* map candidate → payload (รวม char info สำหรับเรนเดอร์ avatar ฝั่ง client) */ +function pickVoteCandidatePayload(c) { + return { + id: c.id, + nickname: c.nickname, + isBot: c.isBot, + characterId: c.characterId != null ? c.characterId : null, + lobbyColorThemeIndex: c.lobbyColorThemeIndex || null, + lobbySkinToneIndex: c.lobbySkinToneIndex || null, + }; +} + +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 PV_MS = pickVoteMs(); + const candidates = pickVoteCandidates(space); + space.pickVote = { purpose, votes: {}, candidates, endsAt: Date.now() + PV_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(pickVoteCandidatePayload), + endsAt: space.pickVote.endsAt, + durationMs: PV_MS, + }); + }); + emitPickVoteUpdate(sid, space); + space.pickVoteTimer = setTimeout(() => { + const sp = spaces.get(sid); + if (!sp || !sp.pickVote || sp.pickVote.resolved) return; + resolvePlayerPickVote(sid, sp); + }, PV_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 = 0; + Object.keys(counts).forEach((tid) => { if (counts[tid] > max) max = counts[tid]; }); + const maxIds = Object.keys(counts).filter((tid) => counts[tid] === max); + /* เสมอ (มากกว่า 1 คนได้สูงสุดเท่ากัน) หรือไม่มีใครโหวต → ไม่มีใครโดน (เป้าหมายปลอดภัย) */ + const target = (max > 0 && maxIds.length === 1) ? maxIds[0] : 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 }); + /* [achv d2] บันทึก playerKey ที่โดนโหวตการ์ด Silence/Ban ในคดีนี้ (→ ไม่ได้ d2_silent_guardian) */ + if (target && (pv.purpose === 'silence' || pv.purpose === 'ban')) { + const tp = space.peers.get(target); + if (tp && tp.playerKey) { + space.caseCardTargetedKeys = space.caseCardTargetedKeys || {}; + space.caseCardTargetedKeys[tp.playerKey] = true; + } + } + 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; +} + +/* [2026-07-27 · S6] "โหวตครบหรือยัง" — เดิมเช็คอยู่ที่เดียวในหน้าที่ trial-vote เท่านั้น + ทำให้เคสนี้ค้าง: ทุกคนโหวตครบแล้ว *คนที่ยังไม่โหวตค่อยหลุด* → จำนวนผู้มีสิทธิ์โหวตลดลง + แต่ไม่มีอะไร re-check → ห้องต้องรอ trialVoteSec (30 วิ) หมดเวลาเอง (วัดได้ 27.5-29.2s) + แยกออกมาเป็นฟังก์ชันเพื่อให้เส้นทาง disconnect เรียกซ้ำได้ */ +function maybeRevealTrialIfAllVoted(sid, space, reason) { + if (!space || space.trialPhase !== 'voting') return false; + const total = trialEligibleVoterTotal(space); + if (!(total > 0)) return false; + /* นับเฉพาะโหวตของคนที่ "ยังอยู่จริง" — คนที่หลุดไปแล้วไม่ควรถูกนับเป็นผู้มีสิทธิ์ */ + const voted = Object.keys(space.trialVotes || {}).length; + if (voted < total) return false; + try { glog(sid, space, 'trial-vote', '[S6] โหวตครบ ' + voted + '/' + total + ' (' + (reason || 'recheck') + ') → reveal ทันที ไม่ต้องรอหมดเวลา'); } catch (e) { /* ignore */ } + computeAndEmitTrialResult(sid, space); + return true; +} + +function autoVoteBotsTrial(space) { + if (!space.trialVotes) space.trialVotes = {}; + const culprit = (typeof space.culpritIndex === 'number') ? space.culpritIndex : Math.floor(Math.random() * 3); + const excluded = Number.isInteger(space.trialRevoteExcluded) ? space.trialRevoteExcluded : -1; + 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); + } + /* รอบจับผิดตัว: ห้ามบอทโหวตผู้ต้องสงสัยที่ถูกตัดออก → เปลี่ยนไปโหวตคนร้ายจริง */ + if (pick === excluded) pick = culprit; + 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 = findPendingSpecialCard(space, 4); + console.log('[card-dbg] trial pre-vote: silence(4)=' + !!pend + ' list=' + dbgCards(space)); + if (pend) { + 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); + /* คะแนนสุดท้าย authoritative (ผู้เล่นจริง) — ส่งไปกับ quiz-ended ให้ทุก client ตัดสินผล complete/gameover ตรงกัน + (เดิมไม่ส่ง → client อ่าน playLiveQuizScores local ที่ quiz-result ข้อสุดท้ายอาจมาไม่ทัน → ผล 2 จอไม่ตรง) */ + const quizFinalScores = {}; + const sessF = space.quizSession; + if (sessF && sessF.players) { + Object.keys(sessF.players).forEach((pid) => { + const st = sessF.players[pid]; + quizFinalScores[pid] = (st && typeof st.score === 'number') ? Math.max(0, st.score) : 0; + }); + } + if (space.quizSession) space.quizSession.active = false; + if (space.detectiveMinigameActive) { + /* MG1: ไม่ auto-return/auto-grant — ส่ง quiz-ended ให้ client โชว์ผล (รอกดปุ่มรับการ์ดเอง เหมือนเกมอื่น) + เดิม returnDetectiveSpaceToLobbyB ที่นี่ = server แจกการ์ด+เด้งกลับเองทันที = "auto รับผล/การ์ด" + การแจกการ์ด/กลับ LobbyB จะเกิดเมื่อ client กดปุ่ม → detective-minigame-finished (เส้นทางเดียวกับ MG2-7) */ + io.to(sid).emit('quiz-ended', { message: message || 'จบเกม', returnToLobbyB: true, scores: quizFinalScores }); + /* คง quizSession (active=false แล้ว) ไว้ → ตอน client กด finish → returnDetectiveSpaceToLobbyB อ่านคะแนน quiz มาคิดเหรียญได้ + (ถ้า null ที่นี่ minigamePlayerScoreSurvived('quiz') = 0 หมด → เหรียญเพี้ยน) ; จะถูกแทนเมื่อเริ่มมินิเกมรอบใหม่ */ + return; + } + io.to(sid).emit('quiz-ended', { message: message || 'จบเกม', scores: quizFinalScores }); + 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, + }); + pausableSpecialQuizTimeout(sid, space, () => scheduleQuizAnswerPhase(sid, space, md), readMs); /* หยุดนับตอนคำถามพิเศษ */ +} + +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); + /* บอท: server ตัดสินคำตอบข้อนี้ + ตั้งเป้าเดินไปโซน (server tick เดินให้ในช่วง answer) */ + const qCur = sess.questions[sess.qIndex]; + if (qCur) quizBotsDecideAnswers(space, sess.quizMapMd || md, !!qCur.answerTrue); + pausableSpecialQuizTimeout(sid, space, () => resolveQuizRound(sid, space, md), answerMs); /* หยุดนับตอนคำถามพิเศษ */ +} + +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++; + /* จับที่ "แถวเท้า" (bottom row ของ footprint) เท่านั้น — หัว/ตัวโผล่เข้าโซนแต่เท้ายังไม่ถึง = ไม่นับ + (ต้องตรงกับ client quizResolveChoiceFromStanding) */ + const { cw: qCw, ch: qCh } = getCharacterFootprintWHForMove(mdLive); + const qBaseTx = Math.floor(Number(p.x)); + const qFeetTy = Math.floor(Number(p.y)) + qCh - 1; + let inT = false, inF = false; + for (let qdx = 0; qdx < qCw; qdx++) { + const qftx = qBaseTx + qdx; + if (!inT && quizCellOn(tGrid, qftx, qFeetTy)) inT = true; + if (!inF && quizCellOn(fGrid, qftx, qFeetTy)) inF = true; + } + 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; + }); + + /* บอท: server เป็นเจ้าของคะแนน (จากคำตอบที่ตัดสินไว้) + คัดออกถ้าผิด (กฎเดียวกับคน "ตอบผิดถูกคัดออก") */ + if (sess.bots) { + sess.bots.forEach((b, botId) => { + if (!b.eliminated) { + if (b.answerRight) { + b.score += QUIZ_TF_POINTS_PER_CORRECT; + } else { + b.eliminated = true; + /* คัดออก → วาร์ปเข้าโซนผีทันที (เหมือนคนจริง) */ + const gc = quizRandomCellInZone(mdLive.quizGhostArea, mdLive); + if (gc) { b.x = gc.x + 0.5; b.y = gc.y + 0.5; } + b.wanderTX = null; b.retargetAt = 0; + } + } + scores[botId] = b.score; + results.push({ + id: botId, nickname: 'บอท', right: !!b.answerRight, choice: b.choice, + eliminated: !!b.eliminated, score: b.score, + }); + b.tx = null; b.ty = null; /* รีเซ็ตเป้า → เดินมั่วจนข้อถัดไป */ + }); + } + + 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, + }; + space.quizSession.bots = createQuizBots(space, md); + scheduleQuizReadPhase(sid, space, md); +} + +/* ===== MG1 quiz bots — server-authoritative (roster + เดิน + ตอบ + คะแนน) ===== + เดิม host เดิน+คิดคะแนนบอทเอง → host หลุด = บอทหยุด + คะแนนเพี้ยน. ย้ายมา server → บอทเดินเอง time-based + คะแนน server เป็นเจ้าของ + id `__pv_bot_` 0..N-1 (N = effectiveBotSlotCount) ตรงกับ client */ +const QUIZ_BOT_PREFIX = '__pv_bot_'; +const QUIZ_BOT_SPEED_PER_SEC = 0.15 * 1.28 * 60; /* ~11.5 u/s ใกล้ความเร็วบอทเดิม */ + +function quizBotTierForSlot(i) { + const r = ((i * 73 + 17) % 100) / 100; /* deterministic per slot */ + if (r < 0.30) return 'sharp'; + if (r < 0.75) return 'avg'; + return 'weak'; +} +function quizBotCorrectProb(tier) { + return tier === 'sharp' ? 0.84 : tier === 'weak' ? 0.22 : 0.52; +} +function quizTileWalkableServer(md, x, y) { + const w = md.width || 20, h = md.height || 15; + const tx = Math.floor(x), ty = Math.floor(y); + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const ob = md.objects && md.objects[ty] ? md.objects[ty][tx] : 0; + return ob !== 1; +} +function quizCellInGrid(grid, x, y) { + if (!Array.isArray(grid)) return false; + const tx = Math.floor(x), ty = Math.floor(y); + return !!(grid[ty] && Number(grid[ty][tx]) === 1); +} +function quizRandomCellInZone(grid, md) { + if (!Array.isArray(grid)) return null; + const w = md.width || 20, 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 && quizTileWalkableServer(md, x + 0.5, y + 0.5)) pool.push({ x, y }); + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; +} +function createQuizBots(space, md) { + const botCount = effectiveBotSlotCount(space); + const humans = space.peers ? space.peers.size : 0; + const bots = new Map(); + for (let i = 0; i < botCount; i++) { + const id = QUIZ_BOT_PREFIX + i; + const sp = quizWrongAnswerRespawnPosition(md, humans + i); + bots.set(id, { + tier: quizBotTierForSlot(i), + x: Number(sp.x), y: Number(sp.y), dir: 'down', isWalking: false, + score: 0, eliminated: false, choice: null, answerRight: false, + tx: null, ty: null, wanderTX: null, wanderTY: null, retargetAt: 0, + }); + } + return bots; +} +/* ตัดสินคำตอบบอทแต่ละข้อ (สุ่มตาม tier เดิม) + ตั้งเป้าเดินไปโซนนั้น — ข้ามบอทที่ถูกคัดออกแล้ว */ +function quizBotsDecideAnswers(space, md, correctTrue) { + const sess = space.quizSession; + if (!sess || !sess.bots) return; + const tGrid = md.quizTrueArea || [], fGrid = md.quizFalseArea || []; + sess.bots.forEach((b) => { + b.choice = null; b.answerRight = false; b.tx = null; b.ty = null; + if (b.eliminated) return; + if (Math.random() < 0.14) return; /* เดินมั่ว = ไม่ตอบ = ผิด */ + const wantsCorrect = Math.random() < quizBotCorrectProb(b.tier); + const targetTrue = wantsCorrect ? correctTrue : !correctTrue; + b.choice = targetTrue; + b.answerRight = (targetTrue === correctTrue); + const cell = quizRandomCellInZone(targetTrue ? tGrid : fGrid, md); + if (cell) { b.tx = cell.x + 0.5; b.ty = cell.y + 0.5; } + }); +} +function stepQuizBots(sid, space, dt, now) { + const sess = space.quizSession; + if (!sess || !sess.active || !sess.bots || sess.bots.size === 0) return; + const md = sess.quizMapMd; + if (!md) return; + const w = md.width || 20, h = md.height || 15; + const step = QUIZ_BOT_SPEED_PER_SEC * dt; + const answering = sess.phase === 'answer'; + const ghostGrid = md.quizGhostArea; + sess.bots.forEach((b) => { + /* บอทถูกคัดออก (ตอบผิด) = "ผี" → ขังในโซนผี เดินออกไม่ได้ (เหมือนคนจริง) */ + const confineGhost = b.eliminated && Array.isArray(ghostGrid); + const canWalk = (x, y) => quizTileWalkableServer(md, x, y) && (!confineGhost || quizCellInGrid(ghostGrid, x, y)); + /* hard-snap: ถ้าผีอยู่นอกโซนผี (warp พลาด/ติดขอบ) → ดึงกลับเข้าโซนทันทีทุก tick (กันผีหลุดออกสนาม) */ + if (confineGhost && !quizCellInGrid(ghostGrid, b.x, b.y)) { + const gc = quizRandomCellInZone(ghostGrid, md); + if (gc) { b.x = gc.x + 0.5; b.y = gc.y + 0.5; b.wanderTX = null; b.retargetAt = 0; } + } + let tgtX = b.tx, tgtY = b.ty; + if (b.eliminated || !answering || tgtX == null) { + /* ถูกคัดออก / ไม่ใช่ช่วงตอบ / บอทไม่ตอบ → เดินมั่ว (ผี = มั่วในโซนผี) */ + const arrived = (typeof b.wanderTX === 'number') && Math.hypot(b.wanderTX - b.x, b.wanderTY - b.y) < 0.5; + if (typeof b.wanderTX !== 'number' || arrived || now >= b.retargetAt) { + if (confineGhost) { + const c = quizRandomCellInZone(ghostGrid, md); + b.wanderTX = c ? c.x + 0.5 : b.x; + b.wanderTY = c ? c.y + 0.5 : b.y; + } else { + b.wanderTX = 1 + Math.random() * Math.max(1, w - 2); + b.wanderTY = 1 + Math.random() * Math.max(1, h - 2); + } + b.retargetAt = now + 2000 + Math.floor(Math.random() * 3000); + } + tgtX = b.wanderTX; tgtY = b.wanderTY; + } + let dx = tgtX - b.x, dy = tgtY - b.y; + const dist = Math.hypot(dx, dy); + if (dist < 0.05) { b.isWalking = false; return; } + dx /= dist; dy /= dist; + if (Math.abs(dy) > Math.abs(dx)) b.dir = dy > 0 ? 'down' : 'up'; + else b.dir = dx > 0 ? 'right' : 'left'; + const sx = Math.min(step, dist); + const ox = b.x, oy = b.y; + const nx = b.x + dx * sx, ny = b.y + dy * sx; + if (canWalk(nx, ny)) { b.x = nx; b.y = ny; } + else if (canWalk(nx, b.y)) { b.x = nx; } + else if (canWalk(b.x, ny)) { b.y = ny; } + b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5; + }); + const arr = []; + sess.bots.forEach((b, id) => arr.push({ + id, x: Math.round(b.x * 1000) / 1000, y: Math.round(b.y * 1000) / 1000, + direction: b.dir || 'down', isWalking: !!b.isWalking, elim: !!b.eliminated, + })); + if (arr.length) io.to(sid).emit('quiz-bot-move', { bots: arr }); +} + +/* ============================================================ + MG4 quiz_carry — server-authoritative engine + เดิม host-sim (เดิน/หยิบ/ส่ง/คะแนน/บอท บน client host) → ย้ายมา server: + server เป็นเจ้าของ คำถาม/เฟส/หยิบ-ส่ง/คะแนน/จบรอบ(ตอบถูกคนแรก)/บอท(BFS) + disconnect takeover + เปิดเฉพาะเกมจริง (space.detectiveMinigameActive) — editor preview คง client-sim เดิม + ============================================================ */ +const QUIZ_CARRY_POINTS_PER_CORRECT = 10; +const QUIZ_CARRY_BOT_PREFIX = '__pv_bot_'; +const QUIZ_CARRY_BOT_SPEED_PER_SEC = 0.15 * 1.28 * 60; /* ~11.5 u/s ใกล้บอทเดิม */ +const QUIZ_CARRY_BETWEEN_MS = 2500; + +function quizCarryNormalizeQuestionServer(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 == null ? '' : c)).slice(0, QUIZ_CARRY_MAX_OPTION_SLOTS); + let ci = Number(q.correctIndex); + if (!Number.isFinite(ci) || ci < 0 || ci >= choices.length) ci = 0; + if (!text) text = 'เลือกคำตอบที่ถูกต้อง'; + const out = { text, choices, correctIndex: Math.floor(ci) }; + if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) out.choiceImageUrls = q.choiceImageUrls.slice(0, choices.length); + return out; + } + if (!text) return null; + const t = !!q.answerTrue; + return { text, choices: ['จริง', 'เท็จ'], correctIndex: t ? 0 : 1 }; +} + +/** ชุดคำถาม carry — ลำดับ source ตรงกับ client: global carryQuestions → global questions → map.quizQuestions */ +function getQuizCarryQuestionPool(md) { + const g = loadQuizSettings(); + const src = (Array.isArray(g.carryQuestions) && g.carryQuestions.length) ? g.carryQuestions + : (Array.isArray(g.questions) && g.questions.length) ? g.questions + : (Array.isArray(md.quizQuestions) ? md.quizQuestions : []); + const out = []; + for (const q of src) { const n = quizCarryNormalizeQuestionServer(q); if (n) out.push(n); } + return out; +} + +function quizCarryShuffleServer(a) { + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const t = a[i]; a[i] = a[j]; a[j] = t; + } + return a; +} + +/** entity (รวม footprint) ยืนที่ tile (tx,ty) ได้ไหม — เดินได้ทุกช่อง + ไม่ทับ hub (hub = กำแพง) */ +function quizCarryEntityCanStandServer(md, tx, ty) { + const keys = quizCarryFootprintTileKeys(md, tx + 0.5, ty + 0.5); + for (const k of keys) { + const [cx, cy] = k.split(',').map(Number); + if (!quizTileWalkableServer(md, cx + 0.5, cy + 0.5)) return false; + if (quizCellOn(md.quizCarryHubArea || [], cx, cy)) return false; + /* โต๊ะ submit กลาง (interactive/เขียว) = สิ่งกีดขวาง ห้ามยืนทับ/เดินทะลุ (ส่งคำตอบจากช่องข้างๆ ได้ — quizCarryCanSubmitAtServer รับ adjacent) */ + if (quizCellOn(md.interactive || [], cx, cy)) return false; + } + return true; +} + +/** option index (0-based) ที่ footprint ที่ (px,py) ยืนทับ (grid เก็บ 1..N) — null=ไม่ทับ */ +function quizCarryOptionIndexAtServer(md, px, py, choiceCount) { + const oa = md.quizCarryOptionArea; + if (!Array.isArray(oa)) return null; + for (const k of quizCarryFootprintTileKeys(md, px, py)) { + const [tx, ty] = k.split(',').map(Number); + const v = oa[ty] && Number(oa[ty][tx]); + if (Number.isFinite(v) && v >= 1 && v <= QUIZ_CARRY_MAX_OPTION_SLOTS) { + const idx = v - 1; + if (!(choiceCount > 0) || idx < choiceCount) return idx; + } + } + return null; +} + +function quizCarryFootprintAdjacentHubServer(md, px, py) { + if (!md || !Array.isArray(md.quizCarryHubArea)) return false; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (const k of quizCarryFootprintTileKeys(md, px, py)) { + const [tx, ty] = k.split(',').map(Number); + if (quizCellOn(md.quizCarryHubArea, tx, ty)) continue; + for (const d of dirs) if (quizCellOn(md.quizCarryHubArea, tx + d[0], ty + d[1])) return true; + } + return false; +} +function quizCarryMapHasInteractiveServer(md) { + if (!md || !Array.isArray(md.interactive)) return false; + for (let y = 0; y < md.interactive.length; y++) { + const row = md.interactive[y]; + if (!row) continue; + for (let x = 0; x < row.length; x++) if (row[x] === 1) return true; + } + return false; +} +function quizCarryFootprintOverlapsInteractiveServer(md, px, py) { + if (!md || !Array.isArray(md.interactive)) return false; + for (const k of quizCarryFootprintTileKeys(md, px, py)) { + const [tx, ty] = k.split(',').map(Number); + if (quizCellOn(md.interactive, tx, ty)) return true; + } + return false; +} +function quizCarryFootprintAdjacentInteractiveServer(md, px, py) { + if (!md || !Array.isArray(md.interactive)) return false; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (const k of quizCarryFootprintTileKeys(md, px, py)) { + const [tx, ty] = k.split(',').map(Number); + if (quizCellOn(md.interactive, tx, ty)) continue; + for (const d of dirs) if (quizCellOn(md.interactive, tx + d[0], ty + d[1])) return true; + } + return false; +} +/** ส่งคำตอบได้ไหมที่ (px,py) — มีโซน interactive(เขียว) ใช้โซนนั้น ไม่งั้นชิด/ทับ hub(ม่วง) */ +function quizCarryCanSubmitAtServer(md, px, py) { + if (quizCarryMapHasInteractiveServer(md)) { + return quizCarryFootprintOverlapsInteractiveServer(md, px, py) || quizCarryFootprintAdjacentInteractiveServer(md, px, py); + } + return quizCarryFootprintOverlapsHubServer(md, px, py) || quizCarryFootprintAdjacentHubServer(md, px, py); +} + +/** option idx นี้มีคน/บอทถืออยู่แล้วไหม (exclusive lock) — ยกเว้น actor เอง */ +function quizCarryOptionHeldByAnyoneServer(s, optionIdx, exceptId) { + if (optionIdx == null || optionIdx < 0) return false; + for (const id in s.players) { + if (String(id) === String(exceptId)) continue; + if (s.players[id] && s.players[id].held === optionIdx) return true; + } + let found = false; + s.bots.forEach((b, bid) => { + if (found || String(bid) === String(exceptId)) return; + if (b && b.held === optionIdx) found = true; + }); + return found; +} + +/* ช่องที่บอท "เดินถึงได้จริง" (footprint-aware BFS) จากโซน option/spawn — กันบอทสุ่มเกิด/ติดใน + "คอนโซลกลาง" ที่ยืนได้แต่ออกไม่ได้ (footprint 3×4 ลอดช่องแคบ/ทางที่ติดกำแพง+hub+interactive ไม่ได้ + = อาการ "บอทยืนบนโต๊ะ"). cache ต่อ md (โครงสร้างแมปคงที่ต่อรอบเกม) */ +function quizCarryReachableTilesServer(md) { + if (md.__qcReach) return md.__qcReach; + const w = md.width || 20, h = md.height || 15; + const oa = md.quizCarryOptionArea, sa = md.spawnArea; + const seeds = []; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const isSeed = (Array.isArray(oa) && oa[y] && Number(oa[y][x]) >= 1) || (Array.isArray(sa) && quizCellOn(sa, x, y)); + if (isSeed && quizCarryEntityCanStandServer(md, x, y)) seeds.push([x, y]); + } + } + const reach = new Set(); + if (!seeds.length) { + /* ไม่มี seed (แมปไม่มี option/spawn) → ไม่กรอง: ทุก standable นับว่าถึงได้ */ + for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (quizCarryEntityCanStandServer(md, x, y)) reach.add(x + ',' + y); + md.__qcReach = reach; + return reach; + } + const q = []; + for (const s of seeds) { const k = s[0] + ',' + s[1]; if (!reach.has(k)) { reach.add(k); q.push(s); } } + while (q.length) { + const cur = q.shift(); + const nb = [[cur[0] + 1, cur[1]], [cur[0] - 1, cur[1]], [cur[0], cur[1] + 1], [cur[0], cur[1] - 1]]; + for (const n of nb) { + const k = n[0] + ',' + n[1]; + if (!reach.has(k) && quizCarryEntityCanStandServer(md, n[0], n[1])) { reach.add(k); q.push(n); } + } + } + md.__qcReach = reach; + return reach; +} +function quizCarryTileReachableServer(md, x, y) { + return quizCarryReachableTilesServer(md).has(Math.floor(x) + ',' + Math.floor(y)); +} + +/** รวมรายชื่อช่องว่าง (เดินยืนได้ + ถึงได้จริง + ไม่ใช่ option/interactive) สำหรับเกิดบอท */ +function quizCarrySpawnTilesServer(md) { + const w = md.width || 20, h = md.height || 15; + const free = [], any = []; + const sa = md.spawnArea; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (!quizCarryEntityCanStandServer(md, x, y)) continue; + if (!quizCarryTileReachableServer(md, x, y)) continue; /* ข้ามช่องที่ยืนได้แต่ออกไม่ได้ (คอนโซลกลาง) → บอทไม่เกิดติด */ + any.push({ x, y }); + const isZone = (Array.isArray(md.quizCarryOptionArea) && md.quizCarryOptionArea[y] && Number(md.quizCarryOptionArea[y][x]) >= 1) + || quizCellOn(md.interactive || [], x, y); + if (isZone) continue; + if (Array.isArray(sa) && sa.length) { if (quizCellOn(sa, x, y)) free.push({ x, y }); } + else free.push({ x, y }); + } + } + return free.length ? free : any; +} + +function createQuizCarryBots(space, md) { + const botCount = effectiveBotSlotCount(space); + const bots = new Map(); + const tiles = quizCarrySpawnTilesServer(md); + for (let i = 0; i < botCount; i++) { + const id = QUIZ_CARRY_BOT_PREFIX + i; + const t = tiles.length ? tiles[(i * 3 + 1) % tiles.length] : { x: 1, y: 1 }; + bots.set(id, { + tier: quizBotTierForSlot(i), + x: t.x + 0.5, y: t.y + 0.5, dir: 'down', isWalking: false, + held: null, score: 0, + botPath: [], pathfindAfter: 0, wantIdx: null, + wanderTX: null, wanderTY: null, retargetAt: 0, + }); + } + return bots; +} + +/** BFS หาเส้นทาง tile→tile (entity stand check + หลบ hub) → คืน array tile-center {x,y} (ไม่รวมจุดเริ่ม) */ +function quizCarryPathfindServer(md, sx, sy, gx, gy) { + const w = md.width || 20, h = md.height || 15; + const startX = Math.floor(sx), startY = Math.floor(sy); + const goalX = Math.floor(gx), goalY = Math.floor(gy); + if (startX === goalX && startY === goalY) return []; + const key = (x, y) => y * w + x; + const prev = new Map(); + const seen = new Uint8Array(w * h); + const q = [[startX, startY]]; + seen[key(startX, startY)] = 1; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + let found = false; + while (q.length) { + const [cx, cy] = q.shift(); + if (cx === goalX && cy === goalY) { found = true; break; } + for (const d of dirs) { + const nx = cx + d[0], ny = cy + d[1]; + if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue; + if (seen[key(nx, ny)]) continue; + /* ปลายทางอาจเป็น option/hub-edge ที่ entity ยืนได้; เช็ค stand เฉพาะช่องที่ไม่ใช่ goal */ + if (!(nx === goalX && ny === goalY) && !quizCarryEntityCanStandServer(md, nx, ny)) continue; + if (nx === goalX && ny === goalY && !quizCarryEntityCanStandServer(md, nx, ny)) { + /* goal ยืนไม่ได้ (เช่นชิด hub) — ยอมรับถ้าเดินได้พื้นฐาน */ + if (!quizTileWalkableServer(md, nx + 0.5, ny + 0.5)) continue; + } + seen[key(nx, ny)] = 1; + prev.set(key(nx, ny), key(cx, cy)); + q.push([nx, ny]); + } + } + if (!found) return []; + const path = []; + let cur = key(goalX, goalY); + const startKey = key(startX, startY); + while (cur !== startKey) { + const x = cur % w, y = Math.floor(cur / w); + path.push({ x: x + 0.5, y: y + 0.5 }); + const p = prev.get(cur); + if (p == null) break; + cur = p; + } + path.reverse(); + return path; +} + +/** หยิบ/ส่ง (server เป็นเจ้าของ) — คืน true ถ้ามีการเปลี่ยนสถานะ · clientPos = ตำแหน่งที่ client รายงานตอนกด grab (ใช้ถ้าใกล้ server ≤1.25 tile) */ +function quizCarryServerTryInteract(sid, space, actorId, isBot, clientPos) { + const s = space.quizCarrySession; + if (!s || !s.active || s.ended) return false; + if (s.phase !== 'answer') return false; + const md = s.mapMd; + if (!md || !s.current) return false; + let px, py; + if (isBot) { const b = s.bots.get(actorId); if (!b) return false; px = b.x; py = b.y; } + else { + const p = space.peers.get(actorId); + if (!p) return false; + px = Number(p.x); py = Number(p.y); + /* client รายงานตำแหน่งตอนกด F — ถ้าใกล้ server พอ ใช้ client เพื่อลด desync หยิบไม่ติด/เด้ง */ + if (clientPos && typeof clientPos === 'object') { + const cx = Number(clientPos.x), cy = Number(clientPos.y); + if (Number.isFinite(cx) && Number.isFinite(cy) && Number.isFinite(px) && Number.isFinite(py)) { + if (Math.hypot(cx - px, cy - py) <= 1.25) { px = cx; py = cy; } + } + } + } + if (!Number.isFinite(px) || !Number.isFinite(py)) return false; + const holder = isBot ? s.bots.get(actorId) : s.players[actorId]; + if (!holder) return false; + const choiceCount = (s.current.choices || []).length; + const pickupIdx = quizCarryOptionIndexAtServer(md, px, py, choiceCount); + const canSubmit = quizCarryCanSubmitAtServer(md, px, py); + const held = holder.held; + + /* ถือป้าย + ยืนโซนส่ง → ส่งคำตอบ */ + if (held != null && canSubmit) { + holder.held = null; + const ok = held === s.current.correctIndex; + io.to(sid).emit('quiz-carry-held', { id: actorId, held: null }); + /* แจ้งผลถูก/ผิด "บรอดแคสต์ให้ทุกคน" → โชว์ไอคอน ✓/✗ เหนือหัวทั้งคนจริงและบอท (ทุกจอเห็นพร้อมกัน) */ + io.to(sid).emit('quiz-carry-answer-feedback', { id: actorId, correct: ok }); + if (ok) { + holder.score = (Number(holder.score) || 0) + QUIZ_CARRY_POINTS_PER_CORRECT; + resolveQuizCarryRound(sid, space, actorId); + } + return true; + } + /* ว่างมือ + ยืนโซนตัวเลือก → หยิบ (ถ้าไม่มีคนถือ) */ + if (held == null && pickupIdx != null) { + if (quizCarryOptionHeldByAnyoneServer(s, pickupIdx, actorId)) return false; + holder.held = pickupIdx; + io.to(sid).emit('quiz-carry-held', { id: actorId, held: pickupIdx }); + return true; + } + return false; +} + +function quizCarryScoresSnapshot(space) { + const s = space.quizCarrySession; + const scores = {}; + if (!s) return scores; + for (const id in s.players) scores[id] = Number(s.players[id].score) || 0; + s.bots.forEach((b, bid) => { scores[bid] = Number(b.score) || 0; }); + return scores; +} + +/** จบรอบ (มีคนตอบถูก หรือหมดเวลา) → เคลียร์ held ทุกคน → ข้อถัดไป / จบเซสชัน */ +function resolveQuizCarryRound(sid, space, winnerId) { + const s = space.quizCarrySession; + if (!s || !s.active || s.ended) return; + if (s.roundResolved) return; + s.roundResolved = true; + s.phase = 'between'; /* บอทหยุด interact ระหว่างช่วงพักรอข้อถัดไป */ + clearSpaceQuizTimers(space); + /* เคลียร์ held ทุกคน (รอบจบ) */ + for (const id in s.players) s.players[id].held = null; + s.bots.forEach((b) => { b.held = null; b.botPath = []; b.wantIdx = null; }); + s.roundsCompleted++; + const willEnd = s.sessionLength > 0 && s.roundsCompleted >= s.sessionLength; + /* resolve ชื่อผู้ชนะ (ให้ client โชว์ชื่อจริง ไม่ใช่ 'ผู้เล่น' ตอน churn) + diag: ใครตอบถูก (คน/บอท) */ + let winnerNick = '', winnerIsBot = false; + if (winnerId) { + if (s.bots && s.bots.has(winnerId)) { winnerIsBot = true; } + else { const wp = space.peers.get(winnerId); if (wp && wp.nickname) winnerNick = wp.nickname; } + } + try { glog(sid, space, 'mg4-round', 'resolve ' + s.roundsCompleted + '/' + s.sessionLength + ' q=' + s.qIndex + ' winner=' + (winnerId ? ('[TC169] ' + (winnerNick || (winnerIsBot ? 'BOT ' + winnerId : winnerId))) : '-(หมดเวลา ไม่มีคนถูก)') + ' correctIdx=' + (s.current ? s.current.correctIndex : '?') + ' willEnd=' + willEnd); } catch (e) { /* ignore */ } + io.to(sid).emit('quiz-carry-result', { + questionIndex: s.qIndex + 1, + correctIndex: s.current ? s.current.correctIndex : null, + winnerId: winnerId || null, + winnerNick, + winnerIsBot, + scores: quizCarryScoresSnapshot(space), + willEnd, + }); + if (willEnd) { + const t = setTimeout(() => endQuizCarryGame(sid, space), 900); + space.quizTimers.push(t); + return; + } + const t = setTimeout(() => scheduleQuizCarryReadPhase(sid, space), QUIZ_CARRY_BETWEEN_MS); + space.quizTimers.push(t); +} + +/** หมดเวลาตอบ (ไม่มีใครถูก) → จบรอบเหมือนกัน (ไม่ให้คะแนน) */ +function resolveQuizCarryTimeup(sid, space) { + const s = space.quizCarrySession; + if (!s || !s.active || s.ended || s.roundResolved) return; + resolveQuizCarryRound(sid, space, null); +} + +function scheduleQuizCarryReadPhase(sid, space) { + const s = space.quizCarrySession; + if (!s || !s.active || s.ended) return; + if (!s.pool.length) { endQuizCarryGame(sid, space); return; } + /* server สุ่มคำถามใหม่ (วนชุดถ้าเล่นยาวกว่าจำนวนข้อ) */ + if (s.deck == null || !s.deck.length) s.deck = quizCarryShuffleServer(s.pool.slice()); + s.current = s.deck.shift(); + s.qIndex++; + s.lastAdvanceAt = Date.now(); + try { glog(sid, space, 'mg4-read', 'q=' + s.qIndex + ' deck=' + (s.deck ? s.deck.length : 0) + ' rounds=' + s.roundsCompleted + '/' + s.sessionLength); } catch (e) { /* ignore */ } + s.roundResolved = false; + s.phase = 'read'; + const readMs = Math.max(0, s.carryReadMs | 0); + s.optionRevealAt = Date.now() + readMs; + s.phaseEndsAt = s.optionRevealAt; + io.to(sid).emit('quiz-carry-phase', { + srv: true, + phase: 'read', + questionIndex: s.qIndex, + questionTotal: s.sessionLength || s.pool.length, + text: s.current.text, + choices: s.current.choices, + correctIndex: s.current.correctIndex, + choiceImageUrls: s.current.choiceImageUrls || null, + optionRevealAt: s.optionRevealAt, + endsAt: s.phaseEndsAt, + }); + if (readMs <= 0) { scheduleQuizCarryAnswerPhase(sid, space); return; } + pausableSpecialQuizTimeout(sid, space, () => scheduleQuizCarryAnswerPhase(sid, space), readMs); /* หยุดนับตอนคำถามพิเศษ */ +} + +function scheduleQuizCarryAnswerPhase(sid, space) { + const s = space.quizCarrySession; + if (!s || !s.active || s.ended) return; + s.phase = 'answer'; + s.lastAdvanceAt = Date.now(); + const ansMs = Math.max(1000, s.carryAnswerMs | 0); + s.optionRevealAt = Date.now(); + s.phaseEndsAt = Date.now() + ansMs; + io.to(sid).emit('quiz-carry-phase', { + srv: true, + phase: 'answer', + questionIndex: s.qIndex, + questionTotal: s.sessionLength || s.pool.length, + text: s.current.text, + choices: s.current.choices, + correctIndex: s.current.correctIndex, + choiceImageUrls: s.current.choiceImageUrls || null, + optionRevealAt: s.optionRevealAt, + endsAt: s.phaseEndsAt, + }); + pausableSpecialQuizTimeout(sid, space, () => resolveQuizCarryTimeup(sid, space), ansMs); /* หยุดนับตอนคำถามพิเศษ */ +} + +function startQuizCarryGame(sid, space) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'quiz_carry') return; + if (!space.detectiveMinigameActive) return; /* เกมจริงเท่านั้น — editor preview คง client-sim */ + if (space.quizCarrySession && space.quizCarrySession.active) return; + clearSpaceQuizTimers(space); + const settings = loadQuizSettings(); + const pool = getQuizCarryQuestionPool(md); + const players = {}; + space.peers.forEach((_, peerId) => { players[peerId] = { held: null, score: 0 }; }); + let sessionLength = clampCarrySessionLength(settings.carrySessionLength, 0); + /* 0 = ไม่ได้ตั้งค่า → จบเมื่อ "ถามครบทุกข้อในชุด" รอบเดียว (ไม่วนซ้ำ) ตรงกับที่ผู้เล่นคาดหวังว่า + "พอจบทุกคำถาม เกมต้องจบ"; ถ้า pool ว่างค่อย fallback 10 (กันหารด้วยศูนย์/ค้าง) */ + if (!(sessionLength > 0)) sessionLength = Math.max(1, pool.length || 10); + space.quizCarrySession = { + active: true, ended: false, + pool, deck: null, current: null, qIndex: 0, + phase: 'idle', phaseEndsAt: 0, optionRevealAt: 0, + carryReadMs: Math.max(0, Number(settings.carryReadMs) || 3000), + carryAnswerMs: Math.max(1000, Number(settings.carryAnswerMs) || 5000), + sessionLength, roundsCompleted: 0, roundResolved: false, + players, bots: createQuizCarryBots(space, md), mapMd: md, + }; + /* คนหลุดช่วง how-to → seed เป็นบอท takeover (BFS เล่นต่อ) — แทนคนที่ออก ใช้ socket.id เดิม ไม่ชน id บอท AI */ + { + const sC = space.quizCarrySession; + drainPendingMissionTakeovers(space, 'quiz_carry', (rec) => { + sC.bots.set(rec.id, { + tier: 'avg', + x: Number.isFinite(rec.x) ? rec.x : 1, y: Number.isFinite(rec.y) ? rec.y : 1, + dir: rec.direction || 'down', isWalking: false, + held: null, score: 0, + botPath: [], pathfindAfter: 0, wantIdx: null, + wanderTX: null, wanderTY: null, retargetAt: 0, + takeover: true, takeoverPlayerKey: rec.playerKey || '', + }); + }); + } + if (!pool.length) { endQuizCarryGame(sid, space); return; } + /* watchdog กันเกมค้างไม่จบ: เก็บนอก space.quizTimers (เพราะ resolveQuizCarryRound เคลียร์ array นั้นทุกข้อ) + ถ้าไม่มีความคืบหน้า (lastAdvanceAt) นานเกิน → force resolve/จบ — ป้องกัน "เล่นจนครบแต่เกมไม่ยอมจบ" */ + { + const sC = space.quizCarrySession; + sC.lastAdvanceAt = Date.now(); + const perRound = Math.max(0, sC.carryReadMs) + Math.max(1000, sC.carryAnswerMs) + QUIZ_CARRY_BETWEEN_MS + 1500; + const stallMs = perRound + 20000; /* ช่วงเวลาที่ "ยอมรับได้" ต่อข้อ ก่อนถือว่าค้าง */ + if (space.quizCarryWatchdog) clearInterval(space.quizCarryWatchdog); + space.quizCarryWatchdog = setInterval(() => { + const sp = spaces.get(sid); + const ss = sp && sp.quizCarrySession; + if (!sp || !ss || !ss.active || ss.ended) { clearInterval(space.quizCarryWatchdog); space.quizCarryWatchdog = null; return; } + const idle = Date.now() - (ss.lastAdvanceAt || 0); + if (idle > stallMs) { + try { glog(sid, sp, 'mg4-watchdog', 'STALL idle=' + idle + 'ms phase=' + ss.phase + ' round=' + ss.roundsCompleted + '/' + ss.sessionLength); } catch (e) { /* ignore */ } + ss.lastAdvanceAt = Date.now(); + /* ดันให้ไปต่อ: ถ้ายังตอบ/อ่านค้าง → จบรอบนี้ (ไม่ให้คะแนน) แล้วระบบจะเลื่อนข้อหรือจบเองตาม willEnd */ + if (!ss.roundResolved) { try { resolveQuizCarryRound(sid, sp, null); } catch (e) { /* ignore */ } } + else { try { scheduleQuizCarryReadPhase(sid, sp); } catch (e) { /* ignore */ } } + } + }, 5000); + } + scheduleQuizCarryReadPhase(sid, space); +} + +function endQuizCarryGame(sid, space) { + const s = space.quizCarrySession; + if (s) { s.active = false; s.ended = true; } + if (space.quizCarryWatchdog) { clearInterval(space.quizCarryWatchdog); space.quizCarryWatchdog = null; } + try { glog(sid, space, 'mg4-end', 'END rounds=' + (s ? s.roundsCompleted : '?') + '/' + (s ? s.sessionLength : '?')); } catch (e) { /* ignore */ } + clearSpaceQuizTimers(space); + io.to(sid).emit('quiz-carry-ended', { scores: quizCarryScoresSnapshot(space) }); + if (space.detectiveMinigameActive) { + returnDetectiveSpaceToLobbyB(sid, space, 'จบมินิเกม', { allPlayers: true }); + } +} + +/** เดินบอท carry (BFS ไป option → หยิบ → BFS ไปโซนส่ง → ส่ง) + emit ตำแหน่ง/held */ +function stepQuizCarryBots(sid, space, dt, now) { + const s = space.quizCarrySession; + if (!s || !s.active || s.ended || !s.bots || s.bots.size === 0) return; + const md = s.mapMd; + if (!md) return; + const w = md.width || 20, h = md.height || 15; + const step = QUIZ_CARRY_BOT_SPEED_PER_SEC * dt; + const answering = s.phase === 'answer'; + const choiceCount = s.current ? (s.current.choices || []).length : 0; + + /* ก้าวไป (x,y) ได้ไหม: ต้อง "ยืนได้ทั้ง footprint" (canStand = ทุกช่องของตัวละครไม่ใช่กำแพง/ไม่ทับ hub/ไม่ทับโต๊ะ interactive) + — เดิมเช็คแค่ quizTileWalkableServer ที่ "ช่อง anchor จุดเดียว" ทำให้ footprint ขอบตัวเลื่อนคร่อมกำแพงได้ → + ตกไปช่องที่ reachability-BFS (footprint-aware) ถือว่า "ออกไม่ได้" → โดน push-out teleport ทุก tick = อาการ "บอทว๊าป". + ใช้ canStand ให้ตรงกับ pathfind (ที่วางเส้นบน canStand อยู่แล้ว) → บอทไม่เดินเข้าช่องที่จะถูกดันออก */ + const canStepTo = (x, y) => quizCarryEntityCanStandServer(md, Math.floor(x), Math.floor(y)); + + const followPath = (b) => { + if (!b.botPath || !b.botPath.length) return false; + const tgt = b.botPath[0]; + let dx = tgt.x - b.x, dy = tgt.y - b.y; + const dist = Math.hypot(dx, dy); + if (dist < 0.08) { b.botPath.shift(); b.isWalking = b.botPath.length > 0; return true; } + dx /= dist; dy /= dist; + if (Math.abs(dy) > Math.abs(dx)) b.dir = dy > 0 ? 'down' : 'up'; + else b.dir = dx > 0 ? 'right' : 'left'; + const sx = Math.min(step, dist); + const nx = b.x + dx * sx, ny = b.y + dy * sx; + const ox = b.x, oy = b.y; + if (canStepTo(nx, ny)) { b.x = nx; b.y = ny; } + else if (canStepTo(nx, b.y)) { b.x = nx; } + else if (canStepTo(b.x, ny)) { b.y = ny; } + else { b.botPath = []; } + b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5; + return true; + }; + + const wander = (b) => { + const arrived = (typeof b.wanderTX === 'number') && Math.hypot(b.wanderTX - b.x, b.wanderTY - b.y) < 0.5; + if (typeof b.wanderTX !== 'number' || arrived || now >= b.retargetAt) { + b.wanderTX = 1 + Math.random() * Math.max(1, w - 2); + b.wanderTY = 1 + Math.random() * Math.max(1, h - 2); + b.retargetAt = now + 2000 + Math.floor(Math.random() * 3000); + } + let dx = b.wanderTX - b.x, dy = b.wanderTY - b.y; + const dist = Math.hypot(dx, dy); + if (dist < 0.05) { b.isWalking = false; return; } + dx /= dist; dy /= dist; + if (Math.abs(dy) > Math.abs(dx)) b.dir = dy > 0 ? 'down' : 'up'; + else b.dir = dx > 0 ? 'right' : 'left'; + const sx = Math.min(step, dist); + const nx = b.x + dx * sx, ny = b.y + dy * sx; + const ox = b.x, oy = b.y; + if (canStepTo(nx, ny)) { b.x = nx; b.y = ny; } + else if (canStepTo(nx, b.y)) { b.x = nx; } + else if (canStepTo(b.x, ny)) { b.y = ny; } + else { b.wanderTX = null; } + b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5; + }; + + s.bots.forEach((b, bid) => { + /* ช่วงอ่านคำถาม (read) → บอท freeze อยู่ตรงไหนตรงนั้น (ตรงกับผู้เล่นที่ freeze ฝั่ง client) */ + if (s.phase === 'read') { b.isWalking = false; return; } + /* ทับ hub / โต๊ะ submit กลาง (interactive) / หรือ "ติดในคอนโซลกลาง" (ยืนได้แต่เดินออกไม่ได้) → ดันออกไปช่องว่างที่ถึงได้ + หมายเหตุ: หลังแก้ canStepTo → canStand แล้ว บอทไม่ควรเดินเข้าช่องแบบนี้เอง teleport นี้จึงเป็น safety-net (ควรยิงน้อยมาก) + — diag: glog ทุกครั้งที่ teleport จริง เพื่อยืนยันว่า "ว๊าป" ลดลง (ถอด log ออกเมื่อยืนยันแล้ว) */ + if (quizCarryFootprintOverlapsHubServer(md, b.x, b.y) || quizCarryFootprintOverlapsInteractiveServer(md, b.x, b.y) || !quizCarryTileReachableServer(md, b.x, b.y)) { + const tiles = quizCarrySpawnTilesServer(md); + if (tiles.length) { + const fromX = b.x, fromY = b.y; + const t = tiles[(b.x | 0) % tiles.length]; b.x = t.x + 0.5; b.y = t.y + 0.5; b.botPath = []; + try { glog(sid, space, 'mg4-warp', '[TC158] push-out teleport ' + bid + ' (' + fromX.toFixed(2) + ',' + fromY.toFixed(2) + ')->(' + b.x.toFixed(2) + ',' + b.y.toFixed(2) + ') — event นี้ = ยังว๊าป (fail); ไม่มี event ตลอดเกม = pass'); } catch (e) { /* ignore */ } + } + } + /* ยังไม่มีเส้นทาง → ลองหยิบ/ส่งที่จุดยืนก่อน */ + if (!b.botPath || !b.botPath.length) quizCarryServerTryInteract(sid, space, bid, true); + if (!answering || !s.current) { wander(b); return; } + if (b.botPath && b.botPath.length) { followPath(b); return; } + if (now < (b.pathfindAfter || 0)) { wander(b); return; } + const tier = b.tier || 'avg'; + const pCorrect = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.35 : 0.65; + if (b.held == null) { + /* เลือกข้อจะหยิบ (เลี่ยงข้อที่มีคนถือ) */ + let want = Math.random() < pCorrect ? s.current.correctIndex : Math.floor(Math.random() * Math.max(2, choiceCount)); + if (quizCarryOptionHeldByAnyoneServer(s, want, bid)) { + for (let i = 0; i < choiceCount; i++) { if (!quizCarryOptionHeldByAnyoneServer(s, i, bid)) { want = i; break; } } + } + const dest = quizCarryFindOptionTileServer(md, want, b); + if (dest) { b.botPath = quizCarryPathfindServer(md, b.x, b.y, dest.x + 0.5, dest.y + 0.5); b.wantIdx = want; } + b.pathfindAfter = now + 160 + Math.floor(Math.random() * 200); + if (!b.botPath || !b.botPath.length) wander(b); + } else { + const sub = quizCarryFindSubmitTileServer(md, b); + if (sub) b.botPath = quizCarryPathfindServer(md, b.x, b.y, sub.x + 0.5, sub.y + 0.5); + b.pathfindAfter = now + 160 + Math.floor(Math.random() * 200); + if (!b.botPath || !b.botPath.length) wander(b); + } + }); + + const arr = []; + s.bots.forEach((b, id) => arr.push({ + id, x: Math.round(b.x * 1000) / 1000, y: Math.round(b.y * 1000) / 1000, + direction: b.dir || 'down', isWalking: !!b.isWalking, held: b.held == null ? null : b.held, + })); + if (arr.length) io.to(sid).emit('quiz-carry-bot-move', { bots: arr }); +} + +/** หา tile โซน option (เลขช่อง want+1) ที่เดินถึงได้ — สุ่ม */ +function quizCarryFindOptionTileServer(md, optionIndex, b) { + 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 && quizTileWalkableServer(md, x + 0.5, y + 0.5)) pool.push({ x, y }); + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; +} +/** หา tile โซนส่งคำตอบ (interactive ถ้ามี ไม่งั้นช่องชิด hub) */ +function quizCarryFindSubmitTileServer(md, b) { + const w = md.width || 20, h = md.height || 15; + const pool = []; + if (quizCarryMapHasInteractiveServer(md)) { + /* ช่องที่ "ยืนได้จริง + ชิดโต๊ะ interactive" (ไม่ใช่ยืนทับโต๊ะ — โต๊ะเป็นสิ่งกีดขวางแล้ว) → ส่งจากข้างๆ */ + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (!quizCarryEntityCanStandServer(md, x, y)) continue; + if (quizCarryFootprintAdjacentInteractiveServer(md, x + 0.5, y + 0.5)) pool.push({ x, y }); + } + } + if (pool.length) return pool[Math.floor(Math.random() * pool.length)]; + } + /* ช่องเดินได้ที่ชิด hub (ยืนส่งได้) */ + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (!quizCarryEntityCanStandServer(md, x, y)) continue; + if (quizCarryFootprintAdjacentHubServer(md, x + 0.5, y + 0.5)) pool.push({ x, y }); + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; +} + +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; +} + +/** โดมติดกัน = 1 ข้อ (comp id 1,2,3…) — ตรงกับ play.js */ +function normalizeQuizBattleDomeCompOnMap(m) { + if (!m || m.gameType !== 'quiz_battle') return; + normalizeQuizBattleDomeAreaOnMap(m); + const w = m.width || 20, h = m.height || 15; + const grid = m.quizBattleDomeArea; + 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] || 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] || grid[ny][nx] !== 1 || comp[ny][nx]) continue; + comp[ny][nx] = nextId; + stack.push([nx, ny]); + } + } + } + } + m.quizBattleDomeComp = comp; +} + +/** + * ประตูบนเลน — ช่องเลนที่อยู่ "หลัง" โดม C ต้องตอบถูกโดม 1..C ก่อน + * ยืนบนโดมได้โดยไม่ต้องตอบ (dist เท่ากัน) · ผ่านจุดหลังโดมต้องตอบก่อน + */ +function hasQuizBattleManualPathGates(m) { + const mg = m && m.quizBattlePathGateArea; + if (!mg || !Array.isArray(mg)) return false; + for (let ty = 0; ty < mg.length; ty++) { + const row = mg[ty]; + if (!row) continue; + for (let tx = 0; tx < row.length; tx++) { + if ((row[tx] | 0) > 0) return true; + } + } + return false; +} + +function buildQuizBattlePathGateFromManualOnMap(m) { + const w = m.width || 20, h = m.height || 15; + const path = m.quizBattlePathArea; + const dome = m.quizBattleDomeArea; + const manual = m.quizBattlePathGateArea; + const gate = Array(h).fill(0).map(() => Array(w).fill(0)); + const bestReq = Array(h).fill(0).map(() => Array(w).fill(-1)); + const queue = []; + + function enqueue(tx, ty, req) { + if (tx < 0 || ty < 0 || tx >= w || ty >= h) return; + const onPath = path[ty] && path[ty][tx] === 1; + const onDome = dome && dome[ty] && dome[ty][tx] === 1; + if (!onPath && !onDome) return; + /* เก็บ req ต่ำสุดที่เข้าช่องได้ — กันเส้นทางวนกลับไปยก req ทับ spawn */ + if (bestReq[ty][tx] >= 0 && req >= bestReq[ty][tx]) return; + bestReq[ty][tx] = req; + queue.push({ tx, ty, req }); + } + + const sp = m.spawn || { x: 1, y: 1 }; + const stx = Math.max(0, Math.min(w - 1, Math.floor(Number(sp.x)) || 0)); + const sty = Math.max(0, Math.min(h - 1, Math.floor(Number(sp.y)) || 0)); + enqueue(stx, sty, 0); + if (bestReq[sty][stx] < 0) { + for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) enqueue(stx + dx, sty + dy, 0); + } + const sa = m.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) enqueue(tx, ty, 0); + } + } + } + + while (queue.length) { + const cur = queue.shift(); + const { tx, ty, req } = cur; + if (bestReq[ty][tx] !== req) continue; + const onDome = dome && dome[ty] && dome[ty][tx] === 1; + let enterReq = req; + const mg = manual && manual[ty] && manual[ty][tx] ? (manual[ty][tx] | 0) : 0; + if (onDome) enterReq = 0; + else if (mg > 0) enterReq = Math.max(enterReq, mg); + if (path[ty] && path[ty][tx] === 1) gate[ty][tx] = enterReq; + /* [2026-07-31] ค่าที่ส่งต่อไปช่องถัดไปต้องเป็น `req` (ค่าที่ติดตัวมาก่อนเข้าช่องนี้) + ไม่ใช่ `enterReq` ที่เพิ่งถูกรีเซ็ตเป็น 0 เพราะยืนอยู่บนโดม + เดิมใช้ enterReq → **เดินผ่านช่องโดมทีไร ค่าที่ต้องตอบถูกล้างเป็น 0 หมด** ทุกช่องหลังจากนั้น + จึงไม่มีประตูกั้นอีกเลย · สมัยโดมเป็นช่องเดียวยังพอรอด เพราะ BFS อ้อมข้างโดมได้ + พอโดมกินเต็มความกว้างเลน (แก้บั๊กมุดใต้โดม 07-30) ทุกเส้นทางต้องผ่านโดม = ประตูตายทั้งด่าน + พิสูจน์: ผู้เล่นที่ไม่ตอบเลยเดินถึงข้อ 20 ได้ (playwright/qb-multi-check.js) + "ยืนบนโดมได้ฟรี" ยังคงเดิม เพราะ enterReq ยังเป็น 0 สำหรับช่องโดมเอง */ + let outReq = req; + if (mg > 0) outReq = Math.max(outReq, mg); + for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { + enqueue(tx + dx, ty + dy, outReq); + } + } + m.quizBattlePathGateRequired = gate; +} + +function buildQuizBattlePathGateRequiredOnMap(m) { + if (!m || m.gameType !== 'quiz_battle' || !quizBattlePathModeActiveServer(m)) { + if (m) m.quizBattlePathGateRequired = null; + return; + } + if (hasQuizBattleManualPathGates(m)) { + buildQuizBattlePathGateFromManualOnMap(m); + return; + } + normalizeQuizBattleDomeCompOnMap(m); + const w = m.width || 20, h = m.height || 15; + const path = m.quizBattlePathArea; + const dome = m.quizBattleDomeArea; + const domeComp = m.quizBattleDomeComp; + const gate = Array(h).fill(0).map(() => Array(w).fill(0)); + const dist = Array(h).fill(0).map(() => Array(w).fill(-1)); + const queue = []; + const sp = m.spawn || { x: 1, y: 1 }; + const stx = Math.max(0, Math.min(w - 1, Math.floor(Number(sp.x)) || 0)); + const sty = Math.max(0, Math.min(h - 1, Math.floor(Number(sp.y)) || 0)); + + function tryEnqueue(tx, ty, d) { + if (tx < 0 || ty < 0 || tx >= w || ty >= h) return; + if (dist[ty][tx] >= 0) return; + const onPath = path[ty] && path[ty][tx] === 1; + const onDome = dome && dome[ty] && dome[ty][tx] === 1; + if (!onPath && !onDome) return; + dist[ty][tx] = d; + queue.push({ tx, ty, d }); + } + + tryEnqueue(stx, sty, 0); + if (dist[sty][stx] < 0) { + for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) tryEnqueue(stx + dx, sty + dy, 0); + } + const sa = m.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) tryEnqueue(tx, ty, 0); + } + } + } + + const domeMinDist = {}; + let maxComp = 0; + while (queue.length) { + const cur = queue.shift(); + const { tx, ty, d } = cur; + const dc = domeComp[ty] && domeComp[ty][tx] ? domeComp[ty][tx] : 0; + if (dc > 0) { + maxComp = Math.max(maxComp, dc); + if (domeMinDist[dc] == null || d < domeMinDist[dc]) domeMinDist[dc] = d; + } + for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { + const nx = tx + dx, ny = ty + dy; + if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue; + if (dist[ny][nx] >= 0) continue; + const onPath = path[ny] && path[ny][nx] === 1; + const onDome = dome && dome[ny] && dome[ny][nx] === 1; + if (onPath || onDome) tryEnqueue(nx, ny, d + 1); + } + } + + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!path[ty] || path[ty][tx] !== 1) continue; + const d = dist[ty][tx]; + if (d < 0) { + gate[ty][tx] = maxComp; + continue; + } + let req = 0; + for (let c = 1; c <= maxComp; c++) { + if (domeMinDist[c] != null && domeMinDist[c] < d) req = Math.max(req, c); + } + gate[ty][tx] = req; + } + } + m.quizBattlePathGateRequired = gate; +} + +function finalizeQuizBattleMapLayersOnMap(m) { + if (!m || m.gameType !== 'quiz_battle') return; + normalizeQuizBattleDomeAreaOnMap(m); + normalizeQuizBattlePathAreaOnMap(m); + normalizeQuizBattleDomeCompOnMap(m); + normalizeQuizBattleFencesOnMap(m); + buildQuizBattlePathGateRequiredOnMap(m); +} + +/** รั้วกั้นมาจาก Editor ตรง ๆ → กรองให้เหลือเฉพาะสี่เหลี่ยมที่ใช้ได้จริง ก่อนเก็บลงไฟล์ + (ค่าเพี้ยน 1 ตัวไม่ควรทำให้ทั้งด่านเดินไม่ได้ · จำกัดจำนวนกันเผลอสร้างรัว ๆ จนไฟล์บวม) */ +function normalizeQuizBattleFencesOnMap(m) { + if (!m || m.gameType !== 'quiz_battle') return; + const src = m.quizBattleFences; + if (!Array.isArray(src)) { + if (src != null) delete m.quizBattleFences; + return; + } + const w = m.width || 20, h = m.height || 15; + const out = []; + for (let i = 0; i < src.length && out.length < 200; i++) { + const f = src[i]; + if (!f) continue; + const x = Number(f.x), y = Number(f.y), fw = Number(f.w), fh = Number(f.h); + if (![x, y, fw, fh].every(Number.isFinite)) continue; + if (!(fw > 0) || !(fh > 0)) continue; + if (x + fw <= 0 || y + fh <= 0 || x >= w || y >= h) continue; /* อยู่นอกแมปทั้งอัน */ + const r2 = (v) => Math.round(v * 100) / 100; + out.push({ x: r2(x), y: r2(y), w: r2(Math.min(fw, w)), h: r2(Math.min(fh, h)) }); + } + m.quizBattleFences = out; +} + +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; +} + +/* ============================================================ + รั้วกั้นวางอิสระ (quizBattleFences) — [2026-07-30] + กริด 20x40 หยาบเกินกว่าจะกั้นตรงรั้วที่อาร์ตวาดไว้กลางแถบได้ (1 ช่อง = 22px แต่รั้วอยู่กลางช่อง) + รั้วชุดนี้จึงเก็บเป็น "สี่เหลี่ยมพิกัดทศนิยม" หน่วยเป็นช่อง ไม่ยึดกริด วางตรงไหนก็ได้ + รูปแบบ: [{ x, y, w, h }] โดย x,y = มุมบนซ้าย · ทุกค่าหน่วยเป็นช่อง (ทศนิยมได้) + ตัดสินด้วยกล่องชนของสไปรต์ (1.15w x 1.35h วัดขึ้นจากเท้า) ซ้อนทับกล่องรั้วหรือไม่ + ต้องใช้กติกาเดียวกันเป๊ะกับ play.js ไม่งั้น client เดินผ่านแล้วโดน server ดึงกลับ = กระตุก + ============================================================ */ +function quizBattleSpriteAabbServer(m, px, py) { + if (!m || typeof px !== 'number' || typeof py !== 'number') return null; + if (!Number.isFinite(px) || !Number.isFinite(py)) return null; + const { cw, ch } = getCharacterFootprintWHForMove(m); + const cx = px + cw * 0.5; + const feetY = py + ch; + return { + left: cx - (cw * QUIZ_BATTLE_SPRITE_COL_SCALE_W) / 2, + right: cx + (cw * QUIZ_BATTLE_SPRITE_COL_SCALE_W) / 2, + top: feetY - ch * QUIZ_BATTLE_SPRITE_COL_SCALE_H, + bottom: feetY, + }; +} +function quizBattleFenceBlocksServer(m, px, py) { + const list = m && m.quizBattleFences; + if (!Array.isArray(list) || !list.length) return false; + const b = quizBattleSpriteAabbServer(m, px, py); + if (!b) return false; + const EPS = 1e-6; /* แตะขอบพอดีไม่นับว่าชน — ไม่งั้นยืนชิดรั้วแล้วขยับไม่ได้เลย */ + for (let i = 0; i < list.length; i++) { + const f = list[i]; + if (!f) continue; + const fx = Number(f.x), fy = Number(f.y), fw = Number(f.w), fh = Number(f.h); + if (!Number.isFinite(fx) || !Number.isFinite(fy) || !(fw > 0) || !(fh > 0)) continue; + if (b.right - EPS <= fx || b.left + EPS >= fx + fw) continue; + if (b.bottom - EPS <= fy || b.top + EPS >= fy + fh) continue; + return true; + } + return false; +} + +/** ยืน/เดินได้บนเลนม่วง หรือทุกช่องสไปรต์อยู่ใน spawnArea (ฟ้า) */ +function quizBattlePositionAllowedServer(m, px, py) { + if (quizBattleFenceBlocksServer(m, px, py)) return false; /* รั้วกั้น — เช็คก่อนทุกกรณี */ + 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 quizBattlePlayerSolvedCompsServer(space, socketId) { + if (!space || !space.qbwSolved) return new Set(); + const set = space.qbwSolved.get(socketId); + return set || new Set(); +} + +function quizBattlePathGateRequiredAtServer(m, px, py) { + const gate = m && m.quizBattlePathGateRequired; + if (!gate || !quizBattlePathModeActiveServer(m)) return 0; + const keys = quizBattleSpriteBoundsTileKeys(m, px, py); + const path = m.quizBattlePathArea; + let maxReq = 0; + for (const k of keys) { + const [tx, ty] = k.split(',').map(Number); + if (!path[ty] || path[ty][tx] !== 1) continue; + maxReq = Math.max(maxReq, gate[ty][tx] || 0); + } + return maxReq; +} + +function quizBattleFootprintOnDomeServer(m, px, py) { + const dome = m && m.quizBattleDomeArea; + if (!dome) return false; + const keys = quizBattleSpriteBoundsTileKeys(m, px, py); + for (const k of keys) { + const [tx, ty] = k.split(',').map(Number); + if (dome[ty] && dome[ty][tx] === 1) return true; + } + return false; +} + +function quizBattlePathGateAllowsServer(m, space, socketId, px, py) { + if (!m || !quizBattlePathModeActiveServer(m)) return true; + if (quizBattleFootprintOnDomeServer(m, px, py)) return true; + const maxReq = quizBattlePathGateRequiredAtServer(m, px, py); + if (maxReq <= 0) return true; + const solved = quizBattlePlayerSolvedCompsServer(space, socketId); + for (let c = 1; c <= maxReq; c++) { + if (!solved.has(c)) 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); + finalizeQuizBattleMapLayersOnMap(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); + finalizeQuizBattleMapLayersOnMap(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.specialQuizSpawnArea) m.specialQuizSpawnArea = []; + 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); + finalizeQuizBattleMapLayersOnMap(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.specialQuizSpawnArea) m.specialQuizSpawnArea = []; + 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); + finalizeQuizBattleMapLayersOnMap(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/system-stats') { + 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(getSystemStats())); + } + res.writeHead(405); + return res.end(JSON.stringify({ ok: false, error: 'Method not allowed' })); + } + 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' })); + } + /* Quiz Battle: เปิด/ปิด 10 โหมด + วันหมดเขต (เวลาไทย) — อ่านอย่างเดียว ไม่ต้องล็อกอิน + ("แก้" อยู่ที่ Admin/api/quiz-battle-modes.php ซึ่งมีระบบสิทธิ์ของมันเอง) */ + const qbModesUrlPath = url.split('?')[0].replace(/\/+$/, '') || '/'; + if (qbModesUrlPath === BASE_PATH + '/api/quiz-battle-modes') { + 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', + }); + const qbCfg = loadQuizBattleModes(); + const qbOut = {}; + for (let n = 1; n <= QBM_ROOM_COUNT; n++) { + const st = quizBattleRoomState(n); + const m = qbCfg.modes[String(n)] || {}; + qbOut[String(n)] = { + title: m.title || '', enabled: st.enabled, openUntil: m.openUntil || '', + open: st.open, expired: st.expired, + maxPlayers: quizBattleRoomMaxPlayers(n), /* [2026-07-23] ให้หน้าเลือกห้องโชว์ X/max + เช็คเต็มตามค่าจริง */ + }; + } + return res.end(JSON.stringify({ ok: true, tz: QBM_TZ, nowThai: thaiNowString(), modes: qbOut })); + } + res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8' }); + 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('?')[0].split('#')[0].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; + /* นับจาก "ว่างตั้งแต่เมื่อไหร่" (emptySince) ไม่ใช่อายุห้อง — ห้องที่อยู่มานานแล้วเพิ่งว่าง (เช่นกด refresh) จะไม่โดนลบทันที */ + const since = s.emptySince || s.createdAt; + const stale = since == null || now - since > SPACE_EMPTY_TTL_MS; + if (stale) spaces.delete(id); + } + const list = [...spaces] + .filter(([, s]) => !s.isPrivate) + .filter(([, s]) => (s.peers && s.peers.size > 0)) + .filter(([, s]) => !s.joinLocked) /* ห้องที่เริ่มคดีแล้ว (ออกจาก LobbyA) → ซ่อนจาก Join Room list ไม่ให้ใครเข้า */ + .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; + let roomPassword = ''; + if (isPrivate) { + /* ห้องส่วนตัว: ใช้ "ชื่อห้อง" เป็น spaceId (canonical = trim+lowercase) → join ด้วยชื่อ+รหัสได้ + (เดิมเป็นรหัสสุ่ม 6 ตัว ทำให้ join ด้วยชื่อไม่เจอ) */ + const canon = String(d.name || '').trim().toLowerCase(); + if (!canon) return res.writeHead(400), res.end(JSON.stringify({ ok: false, error: 'กรุณาใส่ชื่อห้อง' })); + if (spaces.has(canon)) return res.writeHead(409), res.end(JSON.stringify({ ok: false, error: 'ชื่อห้องนี้ถูกใช้แล้ว ลองชื่ออื่น' })); + sid = canon; + roomPassword = String(d.password == null ? '' : d.password).replace(/\D/g, '').slice(0, 4); + } 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(), + roomPassword, + 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), + ...aiKeyInfo(s.openai_api_key), /* provider + keyMasked — ให้หน้า Admin ยืนยันได้ว่าคีย์ถูกเก็บจริง */ + 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); + const saved = saveAiSettings(s); + if (!saved.ok) { res.writeHead(500); return res.end(JSON.stringify(saved)); } + res.writeHead(200); + res.end(JSON.stringify({ ok: true, ...aiKeyInfo(s.openai_api_key) })); + } catch (e) { + res.writeHead(400), res.end(JSON.stringify({ ok: false, error: e.message })); + } + }); + return; + } + /* เสียง: volume master/music/sfx — GET เปิดสาธารณะ (sound.js ทุก client อ่าน) · PUT เฉพาะแอดมิน */ + if (url === BASE_PATH + '/api/sound-settings' && req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); + res.end(JSON.stringify(loadSoundSettings())); + return; + } + if (url === BASE_PATH + '/api/sound-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 = loadSoundSettings(); + const clamp = (v, cur) => { const n = Number(v); return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : cur; }; + if (d.masterVol !== undefined) s.masterVol = clamp(d.masterVol, s.masterVol); + if (d.musicVol !== undefined) s.musicVol = clamp(d.musicVol, s.musicVol); + if (d.sfxVol !== undefined) s.sfxVol = clamp(d.sfxVol, s.sfxVol); + saveSoundSettings(s); + res.writeHead(200); + res.end(JSON.stringify({ ok: true, settings: s })); + } 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 }); + /* [2026-07-23] เดิม hardcode ปลายทางเป็น api.openai.com อย่างเดียว → คีย์ OpenRouter + (ขึ้นต้น sk-or-) โดนตีกลับ "Incorrect API key provided ... platform.openai.com" + แยกปลายทางตาม prefix ของคีย์ ไม่ต้องเพิ่มช่องในหน้า Admin ใช้ได้ทั้งสองเจ้า */ + const aiKey = String(s.openai_api_key || ''); + const useOpenRouter = /^sk-or-/i.test(aiKey); + const apiUrl = useOpenRouter + ? 'https://openrouter.ai/api/v1/chat/completions' + : 'https://api.openai.com/v1/chat/completions'; + /* OpenRouter ต้องระบุผู้ให้บริการนำหน้าชื่อโมเดล (openai/gpt-4o-mini) — รายการใน AI_MODELS + เป็นชื่อฝั่ง OpenAI ล้วน จึงเติม prefix ให้เองถ้าแอดมินยังไม่ได้ใส่ */ + const sendModel = (useOpenRouter && model.indexOf('/') < 0) ? ('openai/' + model) : model; + const payload = JSON.stringify({ + model: sendModel, + messages: messages, + max_tokens: 500, + }); + const urlObj = new URL(apiUrl); + const headersOut = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + aiKey, + 'Content-Length': Buffer.byteLength(payload, 'utf8'), + }; + if (useOpenRouter) { + /* OpenRouter แนะนำให้ส่ง 2 อันนี้เพื่อระบุแอป (ไม่บังคับ แต่ช่วยตอนดู usage/rate limit) */ + headersOut['HTTP-Referer'] = 'https://srv1361159.hstgr.cloud/'; + headersOut['X-Title'] = 'Justice Divers'; + } + const opts = { + hostname: urlObj.hostname, + path: urlObj.pathname, + method: 'POST', + headers: headersOut, + }; + 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 : ((useOpenRouter ? 'OpenRouter' : 'OpenAI') + ' error') })); + } + } catch (e) { + res.writeHead(502), res.end(JSON.stringify({ error: 'Invalid response from ' + (useOpenRouter ? 'OpenRouter' : '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]); + // production: nginx เสิร์ฟ /Game/img/* เป็น static ทั้งหมด — local node จึงต้องเสิร์ฟไฟล์ที่มีอยู่จริงในโฟลเดอร์ด้วย + // (ไม่จำกัดเฉพาะไฟล์อัปโหลด gauntlet-) ไม่งั้น asset ดีไซน์ (popup-Howto, Avartar, btn-ready…) จะ 400 เฉพาะตอนรันผ่าน node ตรง ๆ + const fname = safeGauntletStoredFilename(sub) || path.basename(sub); // basename กัน path traversal + const fp = path.join(GAUNTLET_ASSETS_DIR, fname); + if (!fname || !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'; + // ตัด query string / hash ออกก่อน resolve เป็น path ไฟล์ (เช่น /play.html?space=...&map=... → /play.html) + // ไม่ตัด จะหาไฟล์ชื่อ "play.html?space=..." ไม่เจอ → 404 (เข้า node ตรง ๆ พร้อม query) + url = url.split('?')[0].split('#')[0] || '/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; +/** เข้าเส้นชัยแล้ว — หยุด obstacle ทันที แล้วรอ grace นี้ให้ client เดินทุกคนเข้าเส้นครบก่อนค่อยจบ (ms) + ต้อง >= client GAUNTLET_FINISH_WALK_MAX_MS (2600) + เผื่อยืนที่เส้นเล็กน้อย */ +const GAUNTLET_FINISH_WALK_MS = 3000; +/** กัน client ยิง gauntlet-finish-phase เร็วผิดปกติ (ปลอม/desync) — run จริงใช้เวลานานกว่านี้มากเสมอ + server เป็นผู้ตัดสินว่ายังถึงเส้นชัยไม่ได้ ก่อนเวลานี้นับจาก liveStartMs */ +const GAUNTLET_FINISH_MIN_RUN_MS = 4000; +/** G2: หน่วงเริ่มวิ่ง — หลัง 3-2-1 จบ ค้างที่จุด start ให้ผู้เล่นเห็นเส้นเริ่มก่อน (ทั้ง bg+bots/obstacles sync จาก liveStartMs เดียวกัน) */ +const GAUNTLET_PRERUN_HOLD_MS = 1000; +/** ระยะอยู่กลางอากาศ (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); +} + +/** มินิเกม "live แล้ว" (พ้น how-to/นับถอยหลัง — session engine เริ่มแล้ว หรือ gauntlet ปล่อยวิ่งแล้ว)? + ใช้ตอน rejoin: ถ้า live แล้ว client ต้องข้าม how-to แล้วเข้าเล่นต่อทันที (เดิมโชว์ how-to เสมอ → คน reconnect ค้าง) + signal = session.active (สร้างตอน 3-2-1 จบ, ล้างตอนกลับ LobbyB) — เชื่อถือได้กว่า missionLiveBeginsAt (ไม่เคย reset ต่อเกม) */ +function isMinigameLiveForRejoinResume(space) { + if (!space) return false; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md) return false; + switch (md.gameType) { + case 'gauntlet': return !!(space.gauntletRun && !space.gauntletRun.crownRunHeld); + case 'stack': return !!(space.stackSession && space.stackSession.active); + case 'jump_survive': return !!(space.jumpSurviveSession && space.jumpSurviveSession.active); + case 'space_shooter': return !!(space.spaceShooterSession && space.spaceShooterSession.active); + case 'balloon_boss': return !!(space.balloonBossSession && space.balloonBossSession.active); + case 'quiz': return !!(space.quizSession && space.quizSession.active); + case 'quiz_carry': return !!(space.quizCarrySession && space.quizCarrySession.active); + default: return false; + } +} + +/* ── How-to / pre-live disconnect takeover (MG3–MG7) ─────────────────────────── + มินิเกมเหล่านี้สร้าง session (bots/turnOrder) ตอน 3-2-1 จบเท่านั้น → ถ้าคนหลุดช่วง how-to / + นับถอยหลัง ยังไม่มี session ให้แปลงเป็นบอท เดิม avatar เลยหายไปเลย (ตกไป user-left). + แก้: เก็บ "pending takeover" ไว้บน space + แจ้ง client เก็บ avatar (*-player-to-bot) แล้ว + seed เป็นบอท server ตอน startXxxGame → บอทรับช่วงเล่นต่อทันทีเมื่อเกมเริ่ม เหมือน MG1/MG2 */ +function missionShellGameTypeForSpace(space) { + if (!space) return null; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + const gt = md && md.gameType; + if (!gt) return null; + if (gt === 'space_shooter' && isSpaceShooterMissionShellSpace(space)) return 'space_shooter'; + if (gt === 'jump_survive' && isJumpSurviveMissionShellSpace(space)) return 'jump_survive'; + if (gt === 'balloon_boss' && isBalloonBossMissionShellSpace(space)) return 'balloon_boss'; + if (gt === 'stack' && isStackTowerMissionShellSpace(space)) return 'stack'; + if (gt === 'quiz_carry') return 'quiz_carry'; + return null; +} +function recordPendingMissionTakeover(space, gameType, socketId, info, playerKey, nick, wasElim) { + if (!space || !gameType || !socketId || !info) return; + if (!Array.isArray(space.pendingMissionTakeovers)) space.pendingMissionTakeovers = []; + space.pendingMissionTakeovers = space.pendingMissionTakeovers.filter((r) => r.id !== socketId); + const themeIdx = parseInt(info.lobbyColorThemeIndex, 10); + const skinIdx = parseInt(info.lobbySkinToneIndex, 10); + space.pendingMissionTakeovers.push({ + gameType, id: socketId, playerKey: playerKey || '', nick: (nick || 'ผู้เล่น'), wasEliminated: !!wasElim, + x: Number(info.x), y: Number(info.y), direction: info.direction || 'down', + characterId: info.characterId != null ? info.characterId : null, + themeIdx: Number.isFinite(themeIdx) ? themeIdx : null, + skinIdx: Number.isFinite(skinIdx) ? skinIdx : null, + spaceShooterScore: Math.max(0, Number(info.spaceShooterScore) || 0), + spaceShooterEliminated: !!info.spaceShooterEliminated, + balloonBossScore: Math.max(0, Number(info.balloonBossScore) || 0), + balloonBossBossDmg: Math.max(0, Number(info.balloonBossBossDmg) || 0), + balloonBossBalloons: Math.max(0, Number(info.balloonBossBalloons) || 0), + balloonBossEliminated: !!info.balloonBossEliminated, + jumpSurviveEliminated: !!info.jumpSurviveEliminated, + }); +} +/** seed บอท takeover จาก pending (เฉพาะ gameType นี้) — makeBot(rec) สร้างบอทใน session แล้วคืน true ถ้าสำเร็จ */ +function drainPendingMissionTakeovers(space, gameType, makeBot) { + if (!space || !Array.isArray(space.pendingMissionTakeovers) || typeof makeBot !== 'function') return 0; + const keep = []; + let n = 0; + for (const rec of space.pendingMissionTakeovers) { + if (rec.gameType !== gameType) { keep.push(rec); continue; } + try { if (makeBot(rec) !== false) n++; } catch (e) { /* ignore */ } + } + space.pendingMissionTakeovers = keep; + return n; +} + +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, + isBot: false, + })); + /* รวมบอท server-authoritative ในอันดับ/เกรด (client เติมชื่อ/อวตารจาก id) — แหล่งเดียว ไม่ให้ client คำนวณซ้ำ */ + const gr = space && space.gauntletRun; + if (gr && gr.bots && gr.bots.size) { + gr.bots.forEach((b, id) => { + rows.push({ + id, + nickname: 'บอท', + characterId: null, + baseScore: b.gauntletEliminated ? 0 : Math.max(0, b.gauntletScore | 0), + eliminated: !!b.gauntletEliminated, + isBot: true, + }); + }); + } + 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, + isBot: !!rows[i].isBot, + 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 survivorCount = rows.filter((r) => !r.eliminated).length; + /* เกรด F = ไม่มีผู้รอด (ตรงกับ client เดิม) → ไม่มีการ์ดรางวัล */ + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (survivorCount <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : rollGauntletCrownRewardCard(grade); + return { + ranked, + totalSum, + averageScore, + grade, + rewardCard, + totalParts: ranked.map((r) => r.finalScore), + participantCount: ranked.length, + survivorCount, + }; +} + +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; +} + +/* ===== MG2 gauntlet bots — server-authoritative (roster + กระโดด + ชน + คะแนน) ===== + เดิม host จำลองบอท __pv_bot_N เอง (AI+ฟิสิกส์+ตายตกขอบ) → host หลุด = บอทหยุด/คะแนนเพี้ยน. + ย้ายมา server: บอทกระโดด/ชน/คะแนนบน server แล้วส่งใน gauntlet-sync.players (isBot) + id `__pv_bot_` 0..N-1 (N = effectiveBotSlotCount) ตรงกับ client (PREVIEW_BOT_PREFIX) */ +const GAUNTLET_BOT_PREFIX = '__pv_bot_'; + +function gauntletBotTierForSlot(i) { + const r = ((i * 73 + 17) % 100) / 100; /* deterministic per slot */ + if (r < 0.28) return 'sharp'; + if (r < 0.62) return 'avg'; + return 'weak'; +} + +/** ความน่าจะกระโดด (ตรง maybePreviewBotGauntletJump ฝั่ง client) — ชนเซลล์เดียวกัน vs คอลัมน์ถัดไป */ +function gauntletBotJumpProbs(tier) { + return { + pSame: tier === 'sharp' ? 0.96 : tier === 'weak' ? 0.62 : 0.86, + pAhead: tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.48 : 0.72, + }; +} + +/** เลเซอร์ครอบแถว py ไหม (ตรงกับ collision ใน runGauntletTick) */ +function gauntletLaserOverlapsRowServer(o, py, h) { + 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); + return py >= lo && py <= hi; +} + +/** ตัดสินใจกระโดดของบอท 1 ตัว (server) — mirror maybePreviewBotGauntletJump */ +function gauntletBotDecideJump(b, obstacles, md, w, h) { + if ((b.gauntletJumpTicks || 0) > 0) return; + let px = Math.floor(Number(b.x)) || 0; + let py = Math.floor(Number(b.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + const { ch: gauntletCh } = getCharacterFootprintWHForMove(md); + let sameCell = false; + let incomingCol = false; + for (let i = 0; i < obstacles.length; i++) { + const o = obstacles[i]; + if (!o) continue; + if (o.kind === 'lane' && typeof o.y === 'number') { + if (o.x === px && o.y >= py && o.y < py + gauntletCh) sameCell = true; + if (o.x === px + 1 && o.y >= py && o.y < py + gauntletCh) incomingCol = true; + } + if (o.kind === 'laser' && typeof o.x === 'number') { + if (o.x === px && gauntletLaserOverlapsRowServer(o, py, h)) sameCell = true; + if (o.x === px + 1 && gauntletLaserOverlapsRowServer(o, py, h)) incomingCol = true; + } + } + const { pSame, pAhead } = gauntletBotJumpProbs(b.tier || 'avg'); + const roll = Math.random(); + if (sameCell && roll < pSame) { + b.gauntletJumpTicks = getGauntletJumpTicks(); + b.gauntletJumpSeq = (b.gauntletJumpSeq || 0) + 1; + return; + } + if (incomingCol && roll < pAhead) { b.gauntletJumpTicks = getGauntletJumpTicks(); b.gauntletJumpSeq = (b.gauntletJumpSeq || 0) + 1; } +} + +/** ฟิสิกส์บอท 1 ตัว 1 tick (server) — mirror applyGauntletPhysicsToPreviewBot + runGauntletTick peer loop */ +function gauntletBotStepPhysics(b, obstacles, md, w, h, crownHeist) { + if (crownHeist && b.gauntletEliminated) return; + let px = Math.floor(Number(b.x)) || 0; + let py = Math.floor(Number(b.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + const air = (b.gauntletJumpTicks || 0) > 0; + const { ch: gauntletCh } = getCharacterFootprintWHForMove(md); + let advanceX = false; + let hitBack = false; + for (const o of 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 && gauntletLaserOverlapsRowServer(o, py, h)) { + if (air) advanceX = true; + else hitBack = true; + } + } + if (advanceX) { + px = Math.min(w - 2, px + 1); + b.gauntletJumpTicks = 0; + if (!crownHeist) b.gauntletScore = (b.gauntletScore || 0) + 1; + } else if (hitBack) { + if (crownHeist) { + if (px <= 0) { + b.gauntletEliminated = true; + b.gauntletScore = 0; + } else { + b.gauntletScore = Math.max(0, (b.gauntletScore || 0) - 10); + px = Math.max(0, px - 1); + } + } else { + px = Math.max(0, px - 1); + } + } + if ((b.gauntletJumpTicks || 0) > 0) b.gauntletJumpTicks--; + b.x = px; + b.y = py; +} + +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, + finishPhase: false, + finishEndAt: null, + gameoverAt: null, /* != null = อยู่ในช่วง grace "ตายหมด" (โชว์ game over ก่อนกลับ LobbyB) */ + }; +} + +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', + nickname: (p.nickname || '').trim() || 'ผู้เล่น', + characterId: p.characterId ?? null, + /* สีจาก lobbyB — กัน client ที่ re-create peer ผ่าน gauntlet-sync ได้สี hash สุ่มไม่ตรง */ + lobbyColorThemeIndex: p.lobbyColorThemeIndex || null, + lobbySkinToneIndex: p.lobbySkinToneIndex || null, + gauntletJumpTicks: p.gauntletJumpTicks || 0, + gauntletJumpSeq: p.gauntletJumpSeq || 0, + gauntletScore: p.gauntletScore || 0, + gauntletEliminated: !!p.gauntletEliminated, + })); + /* บอท server-authoritative — ส่งตำแหน่ง/คะแนน/สถานะให้ทุก client (identity/สี client gen จาก id) */ + if (gr.bots && gr.bots.size) { + gr.bots.forEach((b, id) => { + const row = { + id, + x: b.x, + y: b.y, + direction: b.dir || 'right', + isBot: true, + gauntletJumpTicks: b.gauntletJumpTicks || 0, + gauntletJumpSeq: b.gauntletJumpSeq || 0, + gauntletScore: b.gauntletScore || 0, + gauntletEliminated: !!b.gauntletEliminated, + }; + /* บอท takeover (คนหลุด → id เป็น socket.id จริง ไม่ใช่ __pv_bot_) — ส่งชื่อ+"(บอท)"+สี เดิม + ให้ client คงชื่อ/สีเดิมไว้ แม้ avatar ถูก re-create จาก sync (กันชื่อกลับเป็น "ผู้เล่น" + สีเพี้ยน) */ + if (b.takeover) { + row.isTakeover = true; + const baseNick = String(b.nickname || '').trim(); + if (baseNick) row.nickname = baseNick.indexOf('(บอท)') >= 0 ? baseNick : (baseNick + ' (บอท)'); + if (b.characterId != null) row.characterId = b.characterId; + if (b.lobbyColorThemeIndex != null) row.lobbyColorThemeIndex = b.lobbyColorThemeIndex; + if (b.lobbySkinToneIndex != null) row.lobbySkinToneIndex = b.lobbySkinToneIndex; + } + playersOut.push(row); + }); + } + 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, + gauntletLiveStartMs: gr.liveStartMs || null, /* G1: เวลาเริ่ม run (server) → client anchor scroll เดียวกันทุกเครื่อง */ + ...getGauntletVisualsForClient(), + }); +} + +/* ============================================================ + MG3 Stack Tower — server-authoritative engine (mnn93hpi) + server เป็นเจ้าของ: ลำดับตา (turn order), เฟสแกว่ง (swing), ดรอปบอท, คะแนน/ความคืบหน้า, จบเกม + แล้ว emit event เดิม (stack-state/swing/drop/over) ไปทุกเครื่อง (io.to) → client เป็น renderer + (client เลิก host-sim เมื่อได้ stack-state ที่มี srv:true) — บอท avatar ยังถูก spawn ฝั่ง client ตามเดิม + ============================================================ */ +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; +/* [2026-07-30] 0.5 → 0.62 ให้ตรงกับ STACK_STABLE_SUPPORT_RATIO_MIN ฝั่ง client (play.js:198) + เดิมสองค่านี้ไม่เท่ากัน → ดรอปที่ ratio อยู่ใน [0.50, 0.62) client "ไถลบล็อกร่วงทิ้ง" (แอนิเมชัน + 2.0-2.4 วิ) แต่ server ไม่นับ miss = **บล็อกหายไปต่อหน้าแต่หัวใจไม่ลด** (~10% ของดรอปจริง) + ทั้งยัง push ชั้นฝั่ง server ไปแล้ว → ตึกที่เห็นเตี้ยกว่าที่ server คิด 1 ชั้น และสีเลิก sync ตลอดรอบ + repro: playwright\mg3-slowfall-check.js — ต้องเป็นค่าเดียวกับ client เสมอ ห้ามแก้ทีละฝั่ง */ +const STACK_OVERLAP_MISS_FRAC_ON_LAYER = 0.62; /* miss เมื่อ overlap < 62% บล็อก — ต้องตรงกับ client */ +const STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND = 0.38; +const STACK_MAX_CENTER_DRIFT_NO_LAND_MULT = 1.0; +const STACK_TOWER_BLOCK_WORLD_SCALE = 1.5; +const STACK_TICK_MS = 60; +const STACK_DROP_BUSY_MS = 1200; /* รอแอนิเมชันบล็อกร่วง/นิ่งบน client ก่อนถึงตาถัดไป (ลดจาก 1500 + swing เดินต่อระหว่าง busy → ไม่ค้าง) */ +const STACK_BOT_THINK_MIN_MS = 560; +const STACK_BOT_THINK_MAX_MS = 1180; +const STACK_SEATS = 6; +/* เฟส client (ตอนกดวาง) ต้องใกล้เฟส server ไม่เกินนี้ (rad) ถึงจะใช้. + เดิม 1.2rad (~0.35s@0.55Hz) → latency/desync เกินนั้น server ทิ้งเฟส client ใช้ s.phase (เลื่อนไปแล้ว) + = "เล็งกลางแต่ลงเยื้อง/ร่วง" (intermittent ตาม latency). ขยายเป็น ~1รอบ (2π ≈ 1.8s@0.55Hz) + = ครอบ latency เล่นได้จริงทั้งหมด → ใช้เฟสที่ผู้เล่นเห็นเสมอ (ลงตรงจุดที่เห็น) เหลือไว้แค่กันค่า garbage. + เกม co-op สร้างตึกร่วมกัน ไม่ต้องกันโกงเฟส */ +const STACK_DROP_PHASE_TOL = Math.PI * 2; + +function stackBoundsServer(area, w, h) { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity, 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, minY, maxX, maxY, cx: (minX + maxX + 1) / 2, cy: (minY + maxY + 1) / 2 }; +} +function stackProgressBlocksServer() { + const n = Number(runtimeGameTiming.stackTowerProgressBlocks); + return Number.isFinite(n) && n >= 1 ? Math.max(1, Math.min(500, Math.floor(n))) : 50; +} +function stackTimeLimitSecServer() { + const t = Number(runtimeGameTiming.stackTowerMissionTimeSec); + return Number.isFinite(t) && t > 0 ? Math.max(10, Math.min(7200, Math.floor(t))) : 90; +} +function stackTeamMissesServer() { + const m = Math.floor(Number(runtimeGameTiming.stackTeamMissesMax) || 3); + return Math.max(1, Math.min(20, m)); +} +function newStackSessionState(space, md) { + const w = md.width || 20, h = md.height || 15; + const land = stackBoundsServer(md.stackLandArea, w, h); + let towerCX = land ? land.cx : w * 0.5; + let swingAmp = 1.85; + if (land) { + const span = Math.max(1, land.maxX - land.minX + 1); + swingAmp = Math.min(span * 0.44, Math.max(0.6, span * 0.36)); + } + /* rel-boost: ความกว้างสวิงตาม stackReleaseArea (ต้องตรงกับ client play.js ~12177-12180 เป๊ะ) + เดิม server ใช้แค่ land span → client (มี rel-boost) แกว่ง "กว้างกว่า" server: บล็อกห้อยกว้างแต่ตกแคบ + = ไถลเฉียงตอนปล่อย + logical ตกใกล้กลางเสมอจน "ทับ support ตลอด ไม่นับ miss → หัวใจไม่ลด". + ทำให้ 2 ฝั่งใช้สวิงเดียวกัน → ตกตรง (straight) + miss/หัวใจ ทำงานถูก */ + const rel = stackBoundsServer(md.stackReleaseArea, w, h); + if (rel) { + const spanR = Math.max(1, rel.maxX - rel.minX + 1); + swingAmp = Math.max(swingAmp, Math.min(spanR * 0.32, 3.0)); /* ต้องตรงกับ client play.js — ลดจาก 0.42/4.2 (สวิงกว้างเกิน = หน่วงนิ้วนิดเดียวเยื้องเยอะ "ต่อตรงแต่ลงเยื้อง") แต่ยังกว้างพอให้ miss ได้เมื่อเยื้องจริง */ + } + const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2)); + const cfgW = Number(runtimeGameTiming.stackBlockWidthTiles); + const initW = Number.isFinite(cfgW) && cfgW > 0 ? Math.max(0.85, Math.min(3.2, cfgW)) : autoW; + const hz = Number(runtimeGameTiming.stackSwingHz); + return { + active: true, + topCenterX: towerCX, + widthTiles: initW, + initialWidthTiles: initW, + swingAmp, + phase: Math.random() * Math.PI * 2, + phaseSpeed: Number.isFinite(hz) && hz > 0 ? hz : 0.6, + layers: [], + score: 0, + combo: 0, + progressPct: 0, + teamMissesLeft: stackTeamMissesServer(), + turnOrder: [], + turnIndex: 0, + bots: new Map(), /* botId -> { tier, score, takeover?, takeoverPlayerKey? } */ + humanPts: {}, /* peerId -> pts */ + startMs: Date.now(), + busyUntil: 0, + botThinkUntil: 0, + botAimPhase: null, /* เฟสเป้าหมายฝั่งหน้า (ปล่อยตอนแกว่งวิ่งมาถึงเองธรรมชาติ — ไม่สแนป) */ + botAimDeadline: 0, + lastSwingEmit: 0, + lastStateEmit: 0, + ended: false, + timerId: null, + }; +} +/* port computeStackDropResult (gameplay-only) + phaseOverride: ตา "คนจริง" ส่งเฟสที่เห็นตอนกดมาด้วย (client phase) → คำนวณจุดลงตามที่ผู้เล่นเห็น + ไม่ใช่เฟส server ณ ตอน req มาถึง (ช้ากว่า half-RTT = บล็อกลงเยื้อง). บอท/fallback ใช้ s.phase */ +function stackComputeDropServer(s, md, phaseOverride) { + const dropPhase = (typeof phaseOverride === 'number' && Number.isFinite(phaseOverride)) ? phaseOverride : s.phase; + const raw = s.swingAmp * Math.sin(dropPhase); + const w = md.width || 20, h = md.height || 15; + const land = stackBoundsServer(md.stackLandArea, w, h); + const fallW = Math.max(0.85, Math.min(3.2, s.widthTiles != null ? s.widthTiles : s.initialWidthTiles)); + const wMul = STACK_TOWER_BLOCK_WORLD_SCALE; + const fallWEff = fallW * wMul; + let supportCx, supportW; + if (s.layers.length === 0) { + supportCx = land ? land.cx : w * 0.5; + supportW = land ? (land.maxX - land.minX + 1) : Math.max(8, fallW * 2.2); + } else { + const sup = s.layers[s.layers.length - 1]; + supportCx = sup.cx; + supportW = Math.max(0.85, Math.min(3.2, sup.w != null ? sup.w : s.initialWidthTiles)); + } + const c1 = s.topCenterX + raw; + const half1 = fallWEff * 0.5; + const half0 = (s.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 = s.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; + /* MISS = แค่ overlapMiss (บล็อกไม่ทับบล็อกล่างพอ) เท่านั้น. + เดิมมี driftMiss เทียบ placedCx กับ "กลางพื้นดินตายตัว" (anchorCx=land.cx) → ต่อเอียงเป็นบันได + (แต่ละชั้นทับชั้นล่างพอดี) พอ center สะสมห่างจากกลางพื้น >38% ฐาน บล็อกที่วางดีๆ ก็ถูกนับ miss + = อาการ "ตึกโยกตกทั้งที่ไม่ได้อยู่ริมๆ". ตัดออก — overlapMiss คือเงื่อนไขตกที่ถูกต้องอยู่แล้ว. */ + const miss = overlapMiss; + 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 = s.combo || 0; + const mult = !miss && perfect && comboBefore >= 1 ? 2 : 1; + const progPerLayer = 100 / stackProgressBlocksServer(); + const pts = miss ? 0 : (10 * mult); + const progressDelta = miss ? 0 : (progPerLayer * mult); + const supportRatio = fallWEff > 0 ? overlap / fallWEff : 0; + return { miss, perfect, pts, progressDelta, landOffsetTiles: raw, placedW: fallW, placedCx, overlap, supportRatio, delta, missThreshold }; +} +/* port applyStackHitFromDrop (state-only, no FX) */ +function stackApplyHitServer(s, hit, actor) { + if (hit.miss) { + s.combo = 0; + s.teamMissesLeft = Math.max(0, (s.teamMissesLeft != null ? s.teamMissesLeft : stackTeamMissesServer()) - 1); + return; + } + s.layers.push({ w: hit.placedW, cx: hit.placedCx }); + s.score += hit.pts; + if (hit.progressDelta > 0) s.progressPct = Math.min(100, s.progressPct + hit.progressDelta); + s.combo = hit.perfect ? s.combo + 1 : 0; + s.topCenterX = hit.placedCx; + if (actor) { + if (actor.botId) { const b = s.bots.get(actor.botId); if (b) b.score = (b.score || 0) + hit.pts; } + else if (actor.id != null) { s.humanPts[actor.id] = (s.humanPts[actor.id] || 0) + hit.pts; } + } +} +/* port previewBotAlignStackSwingPhasePlay — เลื่อนเฟสให้บอทเล็งก่อนปล่อย (ตาม tier) */ +function stackBotAlignServer(s, md, tier) { + const w = md.width || 20, h = md.height || 15; + const land = stackBoundsServer(md.stackLandArea, w, h); + const fallW = Math.max(0.85, Math.min(3.2, s.widthTiles != null ? s.widthTiles : s.initialWidthTiles)); + let supportCx, supportW; + if (s.layers.length === 0) { supportCx = land ? land.cx : w * 0.5; supportW = land ? (land.maxX - land.minX + 1) : Math.max(8, fallW * 2.2); } + else { const sup = s.layers[s.layers.length - 1]; supportCx = sup.cx; supportW = Math.max(0.85, Math.min(3.2, sup.w != null ? sup.w : s.initialWidthTiles)); } + const errMax = tier === 'sharp' ? 0.016 : tier === 'avg' ? 0.038 : 0.17; + const layerTight = 1 / Math.sqrt(1 + (s.layers.length || 0) * 1.05); + const tri = Math.random() + Math.random() - 1; + let errTiles = tri * errMax * layerTight; + if (s.layers.length >= 2) { const L = s.layers.length; errTiles -= (s.layers[L - 1].cx - s.layers[L - 2].cx) * 0.28; } + let targetCx = supportCx + errTiles; + if (land && s.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 - s.topCenterX; + const amp = Math.max(1e-5, s.swingAmp); + const sNorm = Math.max(-1, Math.min(1, rawWant / amp)); + const phi0 = Math.asin(sNorm); + const phi1 = Math.PI - phi0; + const twoPi = Math.PI * 2; + const nearest = (anchor, p) => { let best = p, bd = Infinity; for (let k = -5; k <= 5; k++) { const c = p + k * twoPi; const dd = Math.abs(c - anchor); if (dd < bd) { bd = dd; best = c; } } return best; }; + const n0 = nearest(s.phase, phi0), n1 = nearest(s.phase, phi1); + s.phase = Math.abs(n0 - s.phase) <= Math.abs(n1 - s.phase) ? n0 : n1; +} +/* เหมือน stackBotAlignServer แต่ "ไม่สแนป" — คืนเฟสเป้าหมายที่อยู่ "ข้างหน้า" (forward) ของเฟสปัจจุบัน + ให้บอทรอแกว่งวิ่งมาถึงเองตามธรรมชาติแล้วค่อยปล่อย → client เห็นแกว่งลื่น ไม่กระตุก (กันบล็อกวาปก่อนวาง) */ +function stackBotComputeAimPhaseServer(s, md, tier) { + const w = md.width || 20, h = md.height || 15; + const land = stackBoundsServer(md.stackLandArea, w, h); + let supportCx; + if (s.layers.length === 0) { supportCx = land ? land.cx : w * 0.5; } + else { supportCx = s.layers[s.layers.length - 1].cx; } + const errMax = tier === 'sharp' ? 0.016 : tier === 'avg' ? 0.038 : 0.17; + const layerTight = 1 / Math.sqrt(1 + (s.layers.length || 0) * 1.05); + const tri = Math.random() + Math.random() - 1; + let errTiles = tri * errMax * layerTight; + if (s.layers.length >= 2) { const L = s.layers.length; errTiles -= (s.layers[L - 1].cx - s.layers[L - 2].cx) * 0.28; } + let targetCx = supportCx + errTiles; + if (land && s.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 - s.topCenterX; + const amp = Math.max(1e-5, s.swingAmp); + const sNorm = Math.max(-1, Math.min(1, rawWant / amp)); + const phi0 = Math.asin(sNorm); + const phi1 = Math.PI - phi0; + const twoPi = Math.PI * 2; + /* หาเฟสรูปแบบ {phi0,phi1}+2πk ที่ "มากกว่า" เฟสปัจจุบันเล็กน้อย (รอบถัดไปตามทิศแกว่ง) */ + const margin = 0.06; + let best = Infinity; + for (const base of [phi0, phi1]) { + let k = Math.ceil((s.phase + margin - base) / twoPi); + let cand = base + k * twoPi; + if (cand < s.phase + margin) cand += twoPi; + if (cand < best) best = cand; + } + return Number.isFinite(best) ? best : s.phase + twoPi * 0.5; +} +/* สี block ของ "คนจริง" = lobbyColorThemeIndex (1-8) → ตรงกับสี avatar ทุกจอ + (เดิมโค้ด stack ใช้ p.lobbyThemeIndex ที่ไม่เคยถูกเซ็ต → null เสมอ; และ (x%8)+1 บนค่า 1-based = เลื่อนสีผิด 1 ช่อง) */ +function normStackTheme8(n) { + const v = Math.floor(Number(n)); + return Number.isFinite(v) ? ((v - 1) % 8 + 8) % 8 + 1 : null; +} +function stackHumanThemeServer(p) { + return p ? normStackTheme8(p.lobbyColorThemeIndex) : null; +} +/* สี block ของบอท AI = สี avatar บอท slot นั้น (lobbyBotThemeBySlot[slot].themeIndex) ไม่ใช่ (slot%8)+1 */ +function stackBotThemeServer(space, slot) { + const m = space && space.lobbyBotThemeBySlot; + const t = m && m[slot] ? normStackTheme8(m[slot].themeIndex) : null; + return t || (((Math.floor(Number(slot)) % 8) + 8) % 8 + 1); +} +/* สร้างลำดับตา: humans (เรียง spawnJoinOrder) + bots เติมที่ว่าง ≤ 6 ที่นั่ง */ +function buildStackTurnOrderServer(space, s) { + try { syncLobbyBotThemeSlots(space); } catch (_e) { /* ให้ lobbyBotThemeBySlot ตรงกับสีที่ client ได้รับ → สี block บอทตรง avatar */ } + const humans = [...space.peers.entries()].filter(([id]) => !isBannedPeerForRun(space, id)); + humans.sort((a, b) => (((a[1].spawnJoinOrder || 0) - (b[1].spawnJoinOrder || 0)) || (String(a[0]) < String(b[0]) ? -1 : 1))); + const order = []; + let seat = 1; + for (const [id, p] of humans) { + if (seat > STACK_SEATS) break; + order.push({ kind: 'human', seat, id, botId: null, theme: stackHumanThemeServer(p) }); + seat++; + } + /* บอท takeover (คนหลุดช่วง how-to) ได้ seat ต่อจากคน ก่อนบอท AI — คง avatar/ตำแหน่งลำดับเดิมไว้ + ส่ง characterId/สี/ชื่อ ของคนเดิมไปด้วย: client ลบ peer ทิ้งไปแล้วตอน 'user-left' (play.js:21769) + และ botId = socket.id เดิมของเขา → others.get(botId) = null → เดิมวาดเป็นอวตารเขียว + ไม่รู้ว่าใคร */ + for (const [botId, b] of s.bots) { + if (!b || !b.takeover) continue; + if (seat > STACK_SEATS) break; + order.push({ + kind: 'bot', seat, id: null, botId, + theme: (b.theme != null ? b.theme : ((seat % 8) + 1)), + characterId: b.characterId != null ? b.characterId : null, + lobbyColorThemeIndex: b.colorThemeIdx != null ? b.colorThemeIdx : null, + lobbySkinToneIndex: b.skinToneIdx != null ? b.skinToneIdx : null, + nickname: b.nick || '', + takeover: true, /* client: บอทที่มาจากคนหลุด (ไม่ใช่บอท AI) → โชว์ชื่อเดิม + "(บอท)" */ + }); + seat++; + } + const botCount = Math.max(0, Math.min(STACK_SEATS - order.length, effectiveBotSlotCount(space))); + let bannedBotSlot = -1; + const bannedId = space.bannedThisRunPlayerId; + if (typeof bannedId === 'string' && bannedId.indexOf('__lobby_bot_') === 0) { + const n = parseInt(bannedId.slice('__lobby_bot_'.length), 10); + if (Number.isFinite(n) && n >= 0) bannedBotSlot = n; + } + const want = bannedBotSlot >= 0 ? Math.max(0, botCount - 1) : botCount; + let created = 0, slot = 0; + while (created < want && slot < STACK_SEATS + botCount && seat <= STACK_SEATS) { + if (slot === bannedBotSlot) { slot++; continue; } + const botId = GAUNTLET_BOT_PREFIX + slot; + if (!s.bots.has(botId)) s.bots.set(botId, { tier: gauntletBotTierForSlot(slot), score: 0 }); + order.push({ kind: 'bot', seat, id: null, botId, theme: stackBotThemeServer(space, slot) }); + created++; slot++; seat++; + } + s.turnOrder = order; + if (s.turnIndex >= order.length) s.turnIndex = 0; +} +function stackCurrentTurnEntry(s) { + if (!s.turnOrder || !s.turnOrder.length) return null; + const n = s.turnOrder.length; + return s.turnOrder[((s.turnIndex % n) + n) % n] || null; +} +function emitStackStateServer(sid, space) { + const s = space.stackSession; + if (!s) return; + const lthemes = s.layers.map((l) => (l.theme && l.theme >= 1 && l.theme <= 8 ? l.theme : 0)); + io.to(sid).emit('stack-state', { + srv: true, + /* ต้องส่ง characterId/สี/ชื่อ ของบอท takeover ต่อด้วย — ถ้า map ทิ้งตรงนี้ ที่ใส่ไว้ใน + buildStackTurnOrderServer ก็สูญเปล่า (client ได้แต่ theme → หน้าเขียว) */ + order: s.turnOrder.map((e) => ({ + kind: e.kind, seat: e.seat, id: e.id, botId: e.botId, theme: e.theme, + characterId: e.characterId != null ? e.characterId : null, + lobbyColorThemeIndex: e.lobbyColorThemeIndex != null ? e.lobbyColorThemeIndex : null, + lobbySkinToneIndex: e.lobbySkinToneIndex != null ? e.lobbySkinToneIndex : null, + nickname: e.nickname || '', + takeover: !!e.takeover, + })), + idx: s.turnIndex, + hostId: null, + lthemes, + /* full-sync หัวใจ + decrypt% → client ที่พลาด packet stack-drop (ช้า/churn) self-correct + (เดิมส่งเฉพาะตอน drop → พลาดแล้ว "หัวใจไม่ลด/% ไม่ตรง" ค้าง) */ + teamMissesLeft: s.teamMissesLeft, + progressPct: s.progressPct, + }); +} +function stackAdvanceTurnServer(s) { + if (!s.turnOrder || !s.turnOrder.length) return; + s.turnIndex = (s.turnIndex + 1) % s.turnOrder.length; + s.botThinkUntil = 0; + s.botAimPhase = null; +} +function stackSessionMissionGrade(space) { + const s = space.stackSession; + if (!s) return 'C'; + const scores = []; + s.turnOrder.forEach((e) => { + if (e.kind === 'bot' && e.botId) { const b = s.bots.get(e.botId); scores.push(Math.max(0, (b && b.score) | 0)); } + else if (e.id != null) scores.push(Math.max(0, s.humanPts[e.id] | 0)); + }); + const n = scores.length || 1; + const avg = Math.floor(scores.reduce((a, b) => a + b, 0) / n); + let grade = avg >= 80 ? 'A' : avg >= 60 ? 'B' : 'C'; + return grade; +} +function stackSessionEnd(sid, space, outcome) { + const s = space.stackSession; + if (!s || s.ended) return; + s.ended = true; + stopStackTicker(space); + /* คะแนนผู้เล่นจริง → reportedMiniScore (ใช้ทางเดิมของ coin/evidence) */ + s.turnOrder.forEach((e) => { + if (e.kind === 'human' && e.id != null) { + const p = space.peers.get(e.id); + if (p) { p.reportedMiniScore = Math.max(0, s.humanPts[e.id] | 0); p.reportedMiniSurvived = true; } + } + }); + /* [TC183] verify คะแนน MG3: team score + คะแนนต่อคน (ดูว่าสูงผิดปกติ/ซ้ำจากชื่อซ้ำไหม) */ + try { + const per = s.turnOrder.map((e) => { + if (e.kind === 'bot' && e.botId) { const b = s.bots.get(e.botId); return 'bot=' + Math.max(0, (b && b.score) | 0); } + if (e.id != null) { const p = space.peers.get(e.id); return ((p && p.nickname) || e.id).toString().slice(0, 6) + '=' + Math.max(0, s.humanPts[e.id] | 0); } + return '?'; + }); + glog(sid, space, 'stack-score', '[TC183] จบ MG3 outcome=' + outcome + ' team=' + (s.score | 0) + ' layers=' + s.layers.length + ' progress=' + Math.floor(s.progressPct || 0) + '% · ต่อคน: ' + per.join(' ')); + } catch (e) { /* ignore */ } + try { space.lastStackMissionGrade = stackSessionMissionGrade(space); } catch (_e) { space.lastStackMissionGrade = null; } + const overPayload = { outcome: typeof outcome === 'string' ? outcome.slice(0, 32) : 'time_up' }; + io.to(sid).emit('stack-over', overPayload); + space.lastMinigameOver = { event: 'stack-over', payload: overPayload, atMs: Date.now() }; +} +/* [mg3] เดินเฟสแกว่งตาม "เวลาจริง" ที่ผ่านไป (ไม่ใช่ก้าว STACK_TICK_MS คงที่ ที่สมมติ tick สม่ำเสมอ) → + s.phase ตรงกับ t:now ที่ emit เสมอ → client extrapolate (serverNow-t)*speed แล้วตรงพอดี = + ไม่มี re-anchor กระโดดทุก packet = ตัวห้อยแกว่งลื่น ไม่กระตุก. clamp กัน tick หายนาน (วาป) */ +function advanceStackSwingPhase(s, now) { + const last = s.lastPhaseAdvanceAt; + const dtSec = (last && now > last) ? Math.min(0.2, (now - last) / 1000) : (STACK_TICK_MS / 1000); + s.lastPhaseAdvanceAt = now; + s.phase += s.phaseSpeed * dtSec * Math.PI * 2; +} + +function runStackTick(sid, space) { + const s = space.stackSession; + if (!s || !s.active || s.ended) return; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'stack') { stopStackTicker(space); return; } + const now = Date.now(); + /* คำถามพิเศษ → หยุดเกม: เลื่อน startMs ตาม tick (คงเวลาที่เหลือ) + ไม่แกว่ง/ไม่เช็คจบ (เหมือน MG2/6/7) */ + /* [2026-07-17] เวลาจริง ไม่ใช่ก้าว STACK_TICK_MS คงที่ — ก้าวคงที่ชดเชยไม่ครบเมื่อ tick จริงเดินช้ากว่า + ที่ตั้งไว้ (บทเรียน MG5 07-16: กู้คืนได้แค่ 68%) */ + if (isSpecialQuizPausedServer(space)) { + const nowS = Date.now(); + s.startMs += Math.max(0, Math.min(1000, nowS - (s.lastPauseShiftAt || nowS))); + s.lastPauseShiftAt = nowS; + return; + } + s.lastPauseShiftAt = Date.now(); + /* เงื่อนไขจบเกม (server ตัดสินแหล่งเดียว) */ + if (s.progressPct >= 100) { stackSessionEnd(sid, space, 'decrypt_complete'); return; } + if (s.teamMissesLeft != null && s.teamMissesLeft <= 0) { stackSessionEnd(sid, space, 'misses_exhausted'); return; } + if ((now - s.startMs) >= stackTimeLimitSecServer() * 1000) { stackSessionEnd(sid, space, 'time_up'); return; } + if (!s.turnOrder.length) return; + /* บล็อกก่อนหน้ากำลังร่วง (busy) — ยังดรอปไม่ได้ (drop handler เช็ค busyUntil) แต่ให้ "บล็อกถัดไปแกว่งต่อ" + (advance phase + emit swing) → ตาถัดไปไม่เห็นบล็อกห้อยนิ่ง = ไม่ค้าง · client มี guard (!stackFall) ไม่ให้กระตุกตอนยังร่วง */ + if (now < s.busyUntil) { + advanceStackSwingPhase(s, now); + if (now - s.lastSwingEmit > 50) { + s.lastSwingEmit = now; + io.to(sid).emit('stack-swing', { phase: s.phase, speed: s.phaseSpeed, swingAmp: s.swingAmp, topCenterX: s.topCenterX, t: now }); + } + return; + } + const entry = stackCurrentTurnEntry(s); + if (!entry) return; + if (entry.kind === 'bot' && entry.botId) { + const bot = s.bots.get(entry.botId); + const tier = bot ? bot.tier : 'avg'; + if (s.botThinkUntil === 0) { + s.botThinkUntil = now + STACK_BOT_THINK_MIN_MS + Math.floor(Math.random() * (STACK_BOT_THINK_MAX_MS - STACK_BOT_THINK_MIN_MS)); + } else { + /* เลื่อนเฟสตามเวลา (ให้ client เห็นแกว่ง) */ + advanceStackSwingPhase(s, now); + /* ส่ง swingAmp+topCenterX ด้วย → client วาดบล็อกที่ห้อยตรงกับโมเดล server เป๊ะ (drop landOffset = swingAmp*sin(phase)) ไม่วาปตอนปล่อย */ + if (now - s.lastSwingEmit > 50) { s.lastSwingEmit = now; io.to(sid).emit('stack-swing', { phase: s.phase, speed: s.phaseSpeed, swingAmp: s.swingAmp, topCenterX: s.topCenterX, t: now }); } + if (now >= s.botThinkUntil) { + /* คิดเสร็จ → ล็อกเฟสเป้าหมาย "ข้างหน้า" ครั้งเดียว แล้วรอแกว่งวิ่งมาถึงเองธรรมชาติ + (ไม่สแนปเฟส = ไม่กระตุก) ปล่อยเมื่อถึงเป้า หรือเลย deadline กันค้าง */ + if (s.botAimPhase == null) { + s.botAimPhase = stackBotComputeAimPhaseServer(s, md, tier); + /* deadline ต้องยาวพอให้สวิง "ถึงเป้า" จริง = เวลาแกว่งจาก phase ปัจจุบันไปถึง aimPhase + buffer. + เดิม 2600ms ตายตัว → ตอน hz ต่ำ (สวิงช้า) กว่าจะถึงเป้าใช้ >2.6s → deadline เตะก่อน = บอทปล่อยผิด phase (เฉียง/miss) */ + const _gap = Math.max(0, s.botAimPhase - s.phase); + const _radPerSec = Math.max(1e-4, s.phaseSpeed * Math.PI * 2); + s.botAimDeadline = now + Math.min(15000, Math.ceil((_gap / _radPerSec) * 1000) + 1500); + } + if (!(s.phase >= s.botAimPhase || now >= s.botAimDeadline)) return; + s.botAimPhase = null; + const hit = stackComputeDropServer(s, md); + stackApplyHitServer(s, hit, { botId: entry.botId }); + io.to(sid).emit('stack-drop', { + placedCx: hit.placedCx, placedW: hit.placedW, miss: hit.miss, perfect: hit.perfect, + pts: hit.pts, progressDelta: hit.progressDelta, landOffsetTiles: hit.landOffsetTiles, + overlap: hit.overlap, supportRatio: hit.supportRatio, delta: hit.delta, + seat: entry.seat, heavy: false, theme: entry.theme || null, + actorId: entry.botId, isBot: true, + phase: s.phase, + teamMissesLeft: s.teamMissesLeft, + }); + s.busyUntil = now + STACK_DROP_BUSY_MS; + if (s.teamMissesLeft != null && s.teamMissesLeft <= 0) { + stackSessionEnd(sid, space, 'misses_exhausted'); + return; + } + stackAdvanceTurnServer(s); + emitStackStateServer(sid, space); + } + } + } else { + /* ตาคนจริง — server แกว่งให้ + emit ให้ทุกเครื่อง (รวมเจ้าของตา) เห็นเฟสเดียวกัน รอ stack-drop-req */ + advanceStackSwingPhase(s, now); + if (now - s.lastSwingEmit > 50) { s.lastSwingEmit = now; io.to(sid).emit('stack-swing', { phase: s.phase, speed: s.phaseSpeed, swingAmp: s.swingAmp, topCenterX: s.topCenterX, t: now }); } + } + /* broadcast stack-state เป็นระยะ (กัน late-join / desync) */ + if (now - s.lastStateEmit > 700) { s.lastStateEmit = now; emitStackStateServer(sid, space); } +} +function startStackTicker(sid, space) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'stack') return; + if (!space.detectiveMinigameActive) return; /* เกมจริงเท่านั้น — editor preview (เล่นเดี่ยว) คง client-sim เดิม */ + if (space.stackSession && space.stackSession.timerId) return; + space.stackSession = newStackSessionState(space, md); + /* คนหลุดช่วง how-to → seed เป็นบอท takeover ก่อนจัดลำดับตา (buildStackTurnOrderServer จะใส่เป็น seat ให้) */ + { + const s3 = space.stackSession; + drainPendingMissionTakeovers(space, 'stack', (rec) => { + s3.bots.set(rec.id, { + tier: 'avg', score: 0, takeover: true, takeoverPlayerKey: rec.playerKey || '', + theme: normStackTheme8(rec.themeIdx), + /* หน้า/สี/ชื่อ ของคนที่หลุด — recordPendingMissionTakeover เก็บไว้ให้ครบแล้ว (7982-7984) แต่เดิม + ตรงนี้หยิบมาแค่ theme → order row ไม่มี characterId → client วาดหน้าไม่ได้ = "อวตารตัวเขียว" + (setCyberHudScoreAvatarImg: ไม่มี cid → defaultAvatarImg). คอมเมนต์ที่ buildStackTurnOrderServer + เขียนไว้เองว่า "คง avatar เดิมไว้" — ตอนนี้ทำจริงตามนั้น */ + characterId: rec.characterId != null ? rec.characterId : null, + colorThemeIdx: rec.themeIdx != null ? rec.themeIdx : null, + skinToneIdx: rec.skinIdx != null ? rec.skinIdx : null, + nick: rec.nick || '', + }); + }); + } + buildStackTurnOrderServer(space, space.stackSession); + emitStackStateServer(sid, space); + space.stackSession.timerId = setInterval(() => { runStackTick(sid, space); }, STACK_TICK_MS); +} +function stopStackTicker(space) { + if (space && space.stackSession && space.stackSession.timerId) { + clearInterval(space.stackSession.timerId); + space.stackSession.timerId = null; + } +} + +/* ============================================================ + MG5 Jump Survive — server-authoritative bots + รอบ/เกรด + disconnect takeover (mnptfts2) + ผู้เล่นจริง sim ฟิสิกส์เองในเครื่อง (ลื่น/ไม่หน่วง) แล้วรายงานตำแหน่ง+สถานะตาย ผ่าน 'move'; + server เป็นเจ้าของ: บอท (port ฟิสิกส์ pixel-space ด้วย ts=md.tileSize คงที่), จับเวลา/จบรอบ, ตัดสินรอด/เกรด, + และแปลงคนหลุดเป็นบอท server. emit jump-survive-bot-move / jump-survive-player-to-bot / jump-survive-ended + (client เป็น renderer — เลิก host-sim บอทเมื่อ server-auth) — บอทใช้ id __pv_bot_N (ตรงกับ avatar ฝั่ง client) + ============================================================ */ +const JS_BOT_PREFIX = '__pv_bot_'; +const JS_TICK_MS = 33; /* ~30fps server sim — client lerp tx/ty นุ่มอยู่แล้ว */ +const JS_BOT_EMIT_MS = 60; +function jsTileSize(md) { const t = Math.floor(Number(md && md.tileSize)); return Number.isFinite(t) && t > 0 ? t : 32; } +function jsFootprint(md) { + if (!md) return { cw: 1, ch: 1 }; + let cw = Math.floor(Number(md.characterCellsW)); + let ch = Math.floor(Number(md.characterCellsH)); + if (Number.isFinite(cw) && cw >= 1 && Number.isFinite(ch) && ch >= 1) { + 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 jsPlatformPeriodPx(md, ts) { return (md.height || 15) * ts; } +function jsOverlapsObjectSolids(md, ts, left, top, right, bottom) { + const w = md.width || 20, h = md.height || 15; + const x0 = Math.floor(left / ts), x1 = Math.floor((right - 1e-6) / ts); + const y0 = Math.floor(top / ts), 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; + if (ty >= h) continue; /* แพลตฟอร์มวนแนวตั้ง — ไม่ถือ ty>=h เป็นซีลพื้น */ + const ob = md.objects && md.objects[ty] && md.objects[ty][tx]; + if (ob === 1) return true; + } + } + return false; +} +function jsOverlapsObjectWall(md, ts, left, top, right, bottom) { + const w = md.width || 20, h = md.height || 15; + const x0 = Math.floor(left / ts), x1 = Math.floor((right - 1e-6) / ts); + const y0 = Math.floor(top / ts), 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 jsOverlapsPlatforms(md, ts, left, top, right, bottom, scrollPx) { + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + if (!pa) return false; + const period = jsPlatformPeriodPx(md, ts); + 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); + 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, tileRight = (tx + 1) * ts; + if (platBot > top && platTop < bottom && right > tileLeft && left < tileRight) return true; + } + } + } + return false; +} +function jsOverlapsHazardArea(md, ts, left, top, right, bottom) { + const ha = md && md.jumpSurviveHazardArea; + if (!ha) return false; + const pa = md && md.jumpSurvivePlatformArea; + 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 jsOverlapsSolid(md, ts, left, top, right, bottom, scrollPx) { + if (jsOverlapsObjectSolids(md, ts, left, top, right, bottom)) return true; + return jsOverlapsPlatforms(md, ts, left, top, right, bottom, scrollPx); +} +function jsOverlapsSolidForHorzMove(md, ts, left, top, right, bottom, scrollPx) { + if (jsOverlapsObjectSolids(md, ts, left, top, right, bottom)) return true; + const footSlop = ts * 0.16; + const platformBottom = bottom - footSlop; + if (platformBottom <= top + ts * 0.05) return false; + return jsOverlapsPlatforms(md, ts, left, top, right, platformBottom, scrollPx); +} +/* port jumpSurvivePhysicsIntegrate (mission map เสมอ → moveX*1.35, jump mult 1.5) */ +function jsPhysicsIntegrate(md, ts, scrollPx, opts) { + const { dt, ch, pxc0, feetY0, vy0, wasOnGround, jumpQueued, moveLeft, moveRight } = opts; + const w = md.width || 20, h = md.height || 15; + const bw = ts * 0.34; + const bh = ts * 0.9; + let pxc = pxc0, feetY = feetY0, vy = vy0, 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 = 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; + moveX *= 1.35; /* mission map */ + if (jumpQueued && onGround) { + vy = jumpV0; onGround = false; + } else if (wasOnGround) { + const platformRise = (Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42) * dt; + feetY += platformRise; + } + 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; + if (!jsOverlapsSolidForHorzMove(md, ts, nx - bw, headY0, nx + bw, feetY, scrollPx)) pxc = nx; + feetY += vy * dt; + let headTop = feetY - bh; + if (vy > 0 && jsOverlapsSolid(md, ts, pxc - bw, headTop, pxc + bw, feetY, scrollPx)) { + 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)); + let cleared = false; + for (let s = 0; s < maxSteps; s++) { + feetY -= step; headTop = feetY - bh; + if (!jsOverlapsSolid(md, ts, pxc - bw, headTop, pxc + bw, feetY, scrollPx)) { cleared = true; break; } + } + if (cleared) { + let lo = feetY, hi = feetY + step; + for (let i = 0; i < 20; i++) { + const mid = (lo + hi) * 0.5; + if (jsOverlapsSolid(md, ts, pxc - bw, mid - bh, pxc + bw, mid, scrollPx)) hi = mid; else lo = mid; + } + feetY = lo; headTop = feetY - bh; + } + if (feetY - bh < ts) { + feetY = ts + bh + ts; vy = Math.max(vy, ts * 6); onGround = false; headTop = feetY - bh; + } else { vy = 0; onGround = true; } + } else if (vy < 0 && jsOverlapsObjectSolids(md, ts, pxc - bw, headTop, pxc + bw, feetY)) { + 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 (!jsOverlapsObjectSolids(md, ts, pxc - bw, headTop, pxc + bw, feetY)) { vy = 0; break; } + } + } else { onGround = false; } + headTop = feetY - bh; + const headBandBottom = headTop + bh * 0.42; + if (vy < 0 && jsOverlapsSolid(md, ts, pxc - bw, headTop, pxc + bw, feetY, scrollPx) + && jsOverlapsObjectWall(md, ts, pxc - bw, headTop, pxc + bw, headBandBottom)) vy = 0; + headTop = feetY - bh; + if (headTop < ts) { feetY = ts + bh; if (vy < 0) vy = ts * 6; headTop = feetY - bh; } + headTop = feetY - bh; + if (jsOverlapsHazardArea(md, ts, pxc - bw, headTop, pxc + bw, feetY)) return { pxc, feetY, vy, onGround, died: true }; + if (feetY > h * ts + ts * 8) return { pxc, feetY, vy, onGround, died: true }; + return { pxc, feetY, vy, onGround, died: false }; +} +function jsBotHasFloorAhead(md, ts, pxc, feetY, wishDir, scrollPx) { + if (!wishDir) return true; + const bw = ts * 0.34; + const probeX = pxc + wishDir * (bw + ts * 0.82); + const pad = ts * 0.24; + return jsOverlapsPlatforms(md, ts, probeX - pad, feetY - ts * 0.45, probeX + pad, feetY + ts * 8, scrollPx); +} +function jsCollectPlatformCells(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 jsGridPosStandingOnPlatform(md, ts, tx, ty, scrollPx) { + const h = md.height || 15; + const period = jsPlatformPeriodPx(md, ts); + const { cw, ch } = jsFootprint(md); + const rawTop = ty * ts + scrollPx; + const hintY = h * ts * 0.72; /* server ไม่มีกล้อง → ใช้ hint กลางค่อนล่างเหมือน client ตอนยังไม่มี camCenter */ + const k = Math.round((hintY - rawTop - ts * 0.35) / period); + const platTop = rawTop + k * period; + return { x: (tx + 0.5) - cw * 0.5, y: platTop / ts - ch }; +} +/* [2026-07-17] MG5 ช่วงท้ายเลื่อนเร็วขึ้น ×2 (user request) — 15 วิสุดท้ายของรอบ. + ⚠️ สูตรนี้ต้อง "เหมือนกันเป๊ะ" กับฝั่ง client (play.js:jsScrollPxAtPlay / jsRiseVelNowPlay) + ไม่งั้นแท่นจะอยู่คนละที่ระหว่าง server กับที่ผู้เล่นเห็น = ชน/ไม่ชนไม่ตรงกัน + (บทเรียนทั้งวันนี้: สูตรที่คำนวณสองฝั่งต้องแก้พร้อมกันเสมอ) + ตำแหน่งต้องต่อเนื่อง: ใช้ผลรวมของสองช่วง (t₀·v + (t−t₀)·2v) ไม่ใช่คูณทั้งก้อน ไม่งั้นฉากจะกระโดด */ +const JS_ENDGAME_BOOST_SEC = 15; +const JS_ENDGAME_BOOST_MUL = 2; +function jsScrollPxAt(elapsedSec, risePerSec, totalSec) { + const t0 = Math.max(0, (Number(totalSec) || 0) - JS_ENDGAME_BOOST_SEC); + if (!(Number(totalSec) > 0) || elapsedSec <= t0) return elapsedSec * risePerSec; + return (t0 * risePerSec) + ((elapsedSec - t0) * risePerSec * JS_ENDGAME_BOOST_MUL); +} +function jsScrollPx(s, md) { + const risePerSec = Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42; + const elapsed = Math.max(0, (Date.now() - (s.liveBeginsAt || s.startMs)) / 1000); + return jsScrollPxAt(elapsed, risePerSec, jsTimeLimitSec(md)); +} +function jsTimeLimitSec(md) { + /* ตรงกับ client jumpSurviveTimeLimitSecForMission: md.jumpSurviveTimeSec → game-timing.jumpSurviveMissionTimeSec → 60 */ + const t = Number(md.jumpSurviveTimeSec); + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + const rt = Number(runtimeGameTiming && runtimeGameTiming.jumpSurviveMissionTimeSec); + return Number.isFinite(rt) && rt > 0 && rt < 7200 ? Math.floor(rt) : 60; +} +function jsBannedBotSlot(space) { + const bannedId = space.bannedThisRunPlayerId; + if (typeof bannedId === 'string' && bannedId.indexOf('__lobby_bot_') === 0) { + const n = parseInt(bannedId.slice('__lobby_bot_'.length), 10); + if (Number.isFinite(n) && n >= 0) return n; + } + return -1; +} +function createJumpSurviveBots(space, md) { + const s = space.jumpSurviveSession; + const ts = jsTileSize(md); + const cells = jsCollectPlatformCells(md); + cells.sort((a, b) => (b.y - a.y) || (a.x - b.x)); /* ล่างก่อน (ใกล้พื้นเริ่ม) */ + const sc = 0; + const pool = cells.length + ? cells.map((c) => jsGridPosStandingOnPlatform(md, ts, c.x, c.y, sc)) + : [jsGridPosStandingOnPlatform(md, ts, Number(md.spawn && md.spawn.x) || 1, Number(md.spawn && md.spawn.y) || 1, sc)]; + const humanCount = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)).length; + /* บอท takeover (จาก how-to) ที่ seed ไว้แล้ว = นับเป็น "คน" กันรวมเกิน 6 + เลื่อนช่อง spawn บอท AI */ + const existing = s.bots.size; + const botCount = Math.max(0, Math.min(6 - Math.min(humanCount + existing, 6), effectiveBotSlotCount(space))); + const bannedSlot = jsBannedBotSlot(space); + let poolIdx = 1 + humanCount + existing; + let created = 0, slot = 0; + while (created < botCount && slot < 64) { + if (slot === bannedSlot) { slot++; continue; } + const id = JS_BOT_PREFIX + slot; + const p = pool[poolIdx % pool.length] || pool[0]; + poolIdx++; + s.bots.set(id, { + tier: 'avg', + x: p.x + (Math.random() - 0.5) * 0.08, + y: p.y + (Math.random() - 0.5) * 0.08, + vy: 0, onGround: true, dir: 'down', walking: false, eliminated: false, + wishX: 0, wishNext: Date.now() + 200, nextJumpAi: Date.now() + 300 + Math.floor(Math.random() * 500), + takeover: false, takeoverPlayerKey: '', + }); + created++; slot++; + } +} +function startJumpSurviveGame(sid, space) { + const md = jsMap(space); /* ใช้ pattern ที่สุ่มไว้ (effMd) — bot/spawn ตรงกับ client */ + if (!md || md.gameType !== 'jump_survive') return; + if (!space.detectiveMinigameActive) return; /* เกมจริงเท่านั้น — editor preview คง client-sim */ + if (space.jumpSurviveSession && space.jumpSurviveSession.active) return; + const now = Date.now(); + const liveBeginsAt = space.missionLiveBeginsAt && space.missionLiveBeginsAt > now ? space.missionLiveBeginsAt : now; + space.jumpSurviveSession = { + active: true, ended: false, + startMs: liveBeginsAt, liveBeginsAt, + /* +grace: client (anchored serverNow) จบ time_up ก่อนเสมอ; server เป็น backstop เผื่อ host หลุด/ไม่มีใครจบ */ + endsAt: liveBeginsAt + jsTimeLimitSec(md) * 1000 + 2000, + bots: new Map(), + players: {}, /* peerId -> { eliminated } */ + lastEmit: 0, + }; + /* คนหลุดช่วง how-to → seed บอท takeover ก่อนสร้างบอท AI */ + seedJumpSurviveTakeoverBots(space, md); + createJumpSurviveBots(space, md); +} +function seedJumpSurviveTakeoverBots(space, md) { + const s = space.jumpSurviveSession; + if (!s) return; + const now = Date.now(); + drainPendingMissionTakeovers(space, 'jump_survive', (rec) => { + s.bots.set(rec.id, { + tier: 'avg', + x: Number.isFinite(rec.x) ? rec.x : 1, y: Number.isFinite(rec.y) ? rec.y : 1, + vy: 0, onGround: false, dir: rec.direction || 'down', walking: false, + eliminated: !!rec.jumpSurviveEliminated, + wishX: 0, wishNext: now + 200, nextJumpAi: now + 200, + takeover: true, takeoverPlayerKey: rec.playerKey || '', + }); + }); +} +function endJumpSurviveGame(sid, space, reason) { + const s = space.jumpSurviveSession; + if (!s || s.ended) return; + s.ended = true; s.active = false; + /* นับผู้รอด (คนจริงจาก players + บอท server ที่ยังไม่ตาย) → เกรดจากจำนวนรอด */ + let survivors = 0; + space.peers.forEach((p, id) => { + const st = s.players[id]; + const dead = st && st.eliminated; + if (!dead) survivors++; + if (p) { p.reportedMiniSurvived = !dead; p.reportedMiniScore = dead ? 0 : 100; } + }); + s.bots.forEach((b) => { if (b && !b.eliminated) survivors++; }); + const grade = survivors >= 6 ? 'A' : survivors === 5 ? 'B' : survivors === 4 ? 'C' : survivors === 3 ? 'D' : survivors === 2 ? 'E' : 'F'; + space.lastJumpSurviveGrade = grade; + io.to(sid).emit('jump-survive-ended', { reason: reason || 'time_up', grade, survivors }); +} +function stepJumpSurviveBots(sid, space, dt, now) { + const s = space.jumpSurviveSession; + if (!s || !s.active || s.ended) return; + const md = jsMap(space); /* ใช้ pattern ที่สุ่มไว้ (effMd) */ + if (!md || md.gameType !== 'jump_survive') return; + /* ขยับบอทเฉพาะหลังนับถอยหลัง 3-2-1 จบ (liveBeginsAt) — ก่อนหน้านี้บอทเริ่มทันทีที่ session active + ทำให้บอทวิ่ง/กระโดดก่อน "1" จบ → กันด้วยการ return จนถึงเวลาจริง (เหมือน scroll ที่อิง liveBeginsAt อยู่แล้ว) */ + if (now < (s.liveBeginsAt || s.startMs || 0)) return; + /* คำถามพิเศษเปิดอยู่ → หยุดบอท + เลื่อน endsAt ไม่ให้เวลาเดิน (เหมือน MG2/MG3/MG6/MG7) + pt8 (07-11) audit ข้าม MG5 ไปเกมเดียว: วันนั้นแก้แต่ฝั่ง client (jumpSurviveTickFrame early-return + ตอน specialQuizFreeze) โดยคิดว่า "scroll/timer อิง anchor อยู่แล้ว" — แต่ s.endsAt ฝั่ง server เป็น + เวลาสัมบูรณ์ ไม่มีใครเลื่อน → รอบจบ time_up ได้ทั้งที่ผู้เล่นยังตอบคำถามค้างอยู่ (วัดได้: pause 35 วิ + แต่เกมจบที่ 63 วิ = เท่า deadline ที่ไม่ pause). + ⚠️ เลื่อนด้วย "เวลาจริงที่ผ่านไป" ไม่ใช่ก้าว JS_TICK_MS คงที่แบบ MG6/MG7: sweep นี้เป็น + setInterval(33ms) ตัวเดียวที่วนทุกห้อง — ของจริงเดิน ~48ms ตอนมีโหลด → บวกทีละ 33 = ชดเชยไม่ครบ + (วัดได้: บวกแบบคงที่กู้คืนได้แค่ 24.1 จาก 35.3 วิ = 68% → เกมยังจบก่อนเวลา 11 วิ). + เป็นบทเรียนเดียวกับ advanceStackSwingPhase (07-10): อย่าสมมติว่า tick มาตรงเวลา */ + if (isSpecialQuizPausedServer(space)) { + if (s.endsAt > 0) s.endsAt += Math.max(0, Math.min(1000, now - (s.lastPauseShiftAt || now))); + s.lastPauseShiftAt = now; + return; + } + s.lastPauseShiftAt = now; /* ไม่ pause → คง anchor ไว้ให้สด (กันกระโดดตอนเริ่ม pause ครั้งถัดไป) */ + /* จบรอบ: หมดเวลา หรือทุก entity ตาย */ + if (now >= s.endsAt) { endJumpSurviveGame(sid, space, 'time_up'); return; } + const ts = jsTileSize(md); + const { cw, ch } = jsFootprint(md); + const h = md.height || 15; + const wpx = (md.width || 20) * ts; + const centerPx = wpx * 0.5; + const sc = jsScrollPx(s, md); + const lowDangerY = h * ts * 0.60; + const outBots = []; + s.bots.forEach((b, id) => { + /* บอทตายแล้ว: ยัง emit elim:true ต่อทุก tick (เดิม return ข้าม = ส่ง elim ครั้งเดียว → client พลาด = ค้างนิ่ง) */ + if (b.eliminated) { outBots.push({ id, x: b.x, y: b.y, dir: b.dir || 'down', walking: false, elim: true }); return; } + let pxc = (b.x + cw * 0.5) * ts; + let feetY = (b.y + ch) * ts; + const lowDanger = feetY > lowDangerY; + if (now >= b.wishNext) { + const wMin = lowDanger ? 180 : 380; + const wSpan = lowDanger ? 480 : 700; + b.wishNext = now + wMin + Math.floor(Math.random() * wSpan); + if (lowDanger && Math.random() < 0.55) b.wishX = 0; + else if (pxc < centerPx - ts * 1.8) b.wishX = 1; + else if (pxc > centerPx + ts * 1.8) b.wishX = -1; + else b.wishX = Math.random() < 0.3 ? (Math.random() < 0.5 ? -1 : 1) : 0; + } + let moveLeft = b.wishX < 0, moveRight = b.wishX > 0; + let hazardJump = false; + if (b.onGround) { + if (moveRight && !jsBotHasFloorAhead(md, ts, pxc, feetY, 1, sc)) { b.wishX = Math.random() < 0.65 ? -1 : 0; hazardJump = true; } + else if (moveLeft && !jsBotHasFloorAhead(md, ts, pxc, feetY, -1, sc)) { b.wishX = Math.random() < 0.65 ? 1 : 0; hazardJump = true; } + moveLeft = b.wishX < 0; moveRight = b.wishX > 0; + } + let jumpQ = false; + if (hazardJump) b.nextJumpAi = Math.min(b.nextJumpAi, now); + if (b.onGround && now >= b.nextJumpAi) { + b.nextJumpAi = now + 300 + Math.floor(Math.random() * (hazardJump ? 520 : 1100)); + let jp = 0.36 + 0.12; /* mission */ + if (lowDanger) jp += 0.22; + jp = Math.min(0.9, jp); + if (hazardJump || Math.random() < jp) jumpQ = true; + } + if (jumpQ) b.justJumped = true; /* [mg5] บอทกระโดดรอบนี้ → ส่ง flag ให้ client เล่นเสียง (เคลียร์ตอน emit) */ + const r = jsPhysicsIntegrate(md, ts, sc, { + dt, ch, pxc0: pxc, feetY0: feetY, vy0: b.vy, wasOnGround: b.onGround, jumpQueued: jumpQ, moveLeft, moveRight, + }); + b.x = r.pxc / ts - cw * 0.5; + b.y = r.feetY / ts - ch; + if (r.died) { + b.eliminated = true; b.vy = 0; b.walking = false; + outBots.push({ id, x: b.x, y: b.y, dir: b.dir, walking: false, elim: true }); + return; + } + b.vy = r.vy; b.onGround = r.onGround; + if (moveRight) b.dir = 'right'; else if (moveLeft) b.dir = 'left'; + b.walking = (!r.onGround && Math.abs(r.vy) > 40) || moveLeft || moveRight; + outBots.push({ id, x: b.x, y: b.y, dir: b.dir, walking: b.walking, elim: false, jmp: !!b.justJumped }); + }); + /* ผู้เล่นจริงที่สลับแท็บ (hidden) → จำลองการตกแทน timer ฝั่ง client ที่ถูก throttle → ส่งตำแหน่ง full-rate (คนอื่นเห็นลื่น). + ไม่มี input (ตก/ไหลตามแท่นเฉย ๆ) เหมือนคนปล่อยจอ. ตาย → mark eliminated + ตั้ง p.jumpSurviveEliminated (เข้าเช็ค all_dead/เกรด) */ + if (s.players) { + for (const pid in s.players) { + const st = s.players[pid]; + if (!st || !st.hidden || st.eliminated) continue; + if (!space.peers.has(pid)) { st.hidden = false; continue; } /* หลุดห้องแล้ว → ปล่อยให้ disconnect logic จัดการ */ + const pxc = ((Number.isFinite(st.x) ? st.x : 1) + cw * 0.5) * ts; + const feetY = ((Number.isFinite(st.y) ? st.y : 1) + ch) * ts; + const r = jsPhysicsIntegrate(md, ts, sc, { + dt, ch, pxc0: pxc, feetY0: feetY, vy0: st.vy || 0, wasOnGround: !!st.onGround, + jumpQueued: false, moveLeft: false, moveRight: false, + }); + st.x = r.pxc / ts - cw * 0.5; + st.y = r.feetY / ts - ch; + st.vy = r.vy; st.onGround = r.onGround; + if (r.died) { + st.eliminated = true; st.hidden = false; st.vy = 0; + const p = space.peers.get(pid); if (p) p.jumpSurviveEliminated = true; + outBots.push({ id: pid, x: st.x, y: st.y, dir: st.dir || 'down', walking: false, elim: true }); + } else { + outBots.push({ id: pid, x: st.x, y: st.y, dir: st.dir || 'down', walking: Math.abs(r.vy) > 40, elim: false }); + } + } + } + if (now - s.lastEmit >= JS_BOT_EMIT_MS) { + s.lastEmit = now; + if (outBots.length) io.to(sid).emit('jump-survive-bot-move', { bots: outBots }); + s.bots.forEach((b) => { b.justJumped = false; }); /* [mg5] เคลียร์ flag หลัง emit แล้ว (กันเล่นเสียงซ้ำ/พลาดถ้าอยู่ระหว่าง emit) */ + } + /* ทุก entity ตายหมด → จบรอบ (all_dead) */ + let anyAlive = false; + space.peers.forEach((p, id) => { const st = s.players[id]; if (!(st && st.eliminated)) anyAlive = true; }); + if (!anyAlive) s.bots.forEach((b) => { if (!b.eliminated) anyAlive = true; }); + if (!anyAlive) endJumpSurviveGame(sid, space, 'all_dead'); +} + +/* ============================================================ + MG6 space_shooter — server-authoritative บอท + รอบ + disconnect takeover (mnpz6rkp) + โลก (อุกาบาต deterministic ต่อ client, กระสุนคน relay) คงเดิม → host หลุด asteroid/timer ไม่พัง; + server ขับ "บอท" (เดิน+ยิง+คะแนน), จับเวลา/จบรอบ (backstop), แปลงคนหลุดเป็นบอท. + reuse 'space-shooter-shot' (กระสุนบอท) + emit 'space-shooter-bot-move' + จบรอบด้วย 'space-shooter-over' + ผู้เล่นจริง sim ยาน/อุกาบาต/ชน เอง (รายงานคะแนน/ตาย ผ่าน 'move') + ============================================================ */ +const SS_TICK_MS = 50; +const SS_BOT_EMIT_MS = 60; +const SS_MAX_ASTEROID_HITS = 3; /* ต้องตรงกับ client SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS */ +function ssTimeLimitSec(md) { + const t = Number(md.spaceShooterTimeSec); + if (Number.isFinite(t) && t === 0) return 0; + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + const rt = Number(runtimeGameTiming && runtimeGameTiming.spaceShooterMissionTimeSec); + return Number.isFinite(rt) && rt > 0 && rt < 7200 ? Math.floor(rt) : 60; +} +function ssClampCy(cy, mh) { const lo = mh * 0.5; const hi = Math.max(lo + 4, mh - 20); return Math.max(lo, Math.min(hi, cy)); } +function ssShooterSpawnCenter(md, slot, ts) { + const w = md.width || 20, h = md.height || 15; + const g = md.shooterSpawnSlots; + if (!g) return null; + for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { if (g[y] && Number(g[y][x]) === slot) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; } + return null; +} +function createSpaceShooterBots(space, md) { + const s = space.spaceShooterSession; + const ts = jsTileSize(md); + const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; + const humanCount = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)).length; + /* บอท takeover (จาก how-to) ที่ seed ไว้แล้ว = นับเป็น "คน" ในการจำกัด 6 ช่อง กันรวมเกิน 6 */ + const existing = s.bots.size; + const botCount = Math.max(0, Math.min(6 - Math.min(humanCount + existing, 6), effectiveBotSlotCount(space))); + const bannedSlot = jsBannedBotSlot(space); + let created = 0, slot = 0; + while (created < botCount && slot < 64) { + if (slot === bannedSlot) { slot++; continue; } + const id = JS_BOT_PREFIX + slot; + const slotNum = ((humanCount + existing + created) % 6) + 1; + const cen = ssShooterSpawnCenter(md, slotNum, ts); + let cx = cen ? cen.cx : ((created + 1) / (botCount + 1)) * mw; + let cy = cen ? cen.cy : (mh - ts * 1.5); + cx = Math.max(ts * 0.5, Math.min(mw - ts * 0.5, cx)); + cy = ssClampCy(cy, mh); + s.bots.set(id, { + tier: 'avg', cx, cy, dir: 'up', score: 0, eliminated: false, ssHits: 0, + fireCd: 0.3 + Math.random() * 0.4, + wanderVx: Math.random() < 0.5 ? -1 : 1, wanderVy: Math.random() < 0.5 ? -1 : 1, wanderNext: Date.now() + 600, + takeover: false, takeoverPlayerKey: '', + }); + created++; slot++; + } +} +function startSpaceShooterGame(sid, space) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'space_shooter') return; + if (!space.detectiveMinigameActive) return; + if (space.spaceShooterSession && space.spaceShooterSession.active) return; + const now = Date.now(); + const liveBeginsAt = space.missionLiveBeginsAt && space.missionLiveBeginsAt > now ? space.missionLiveBeginsAt : now; + const lim = ssTimeLimitSec(md); + space.spaceShooterSession = { + active: true, ended: false, startMs: liveBeginsAt, liveBeginsAt, + endsAt: lim > 0 ? liveBeginsAt + lim * 1000 + 2000 : 0, /* +grace: client จบ time_up ก่อน; server backstop */ + bots: new Map(), players: {}, lastEmit: 0, + }; + /* คนหลุดช่วง how-to → seed เป็นบอท takeover ก่อนสร้างบอท AI (createSpaceShooterBots นับ s.bots.size เป็น human-like) */ + seedSpaceShooterTakeoverBots(space, md); + createSpaceShooterBots(space, md); +} +function seedSpaceShooterTakeoverBots(space, md) { + const s = space.spaceShooterSession; + if (!s) return; + const ts = jsTileSize(md); + const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; + drainPendingMissionTakeovers(space, 'space_shooter', (rec) => { + const cx = Number.isFinite(rec.x) ? Math.max(ts * 0.5, Math.min(mw - ts * 0.5, (rec.x + 0.5) * ts)) : mw * 0.5; + const cy = ssClampCy(Number.isFinite(rec.y) ? (rec.y + 0.5) * ts : mh - ts * 1.5, mh); + s.bots.set(rec.id, { + tier: 'avg', cx, cy, dir: 'up', + score: rec.spaceShooterScore || 0, eliminated: !!rec.spaceShooterEliminated, ssHits: 0, + fireCd: 0.3 + Math.random() * 0.4, + wanderVx: Math.random() < 0.5 ? -1 : 1, wanderVy: Math.random() < 0.5 ? -1 : 1, wanderNext: Date.now() + 500, + takeover: true, takeoverPlayerKey: rec.playerKey || '', + }); + }); +} +function endSpaceShooterGame(sid, space, reason) { + const s = space.spaceShooterSession; + if (!s || s.ended) return; + s.ended = true; s.active = false; + const scores = []; + space.peers.forEach((p, id) => { + const st = s.players[id]; + const dead = st && st.eliminated; + scores.push({ id, score: Math.max(0, p.spaceShooterScore | 0), hits: 0, eliminated: !!dead }); + }); + s.bots.forEach((b, id) => scores.push({ id, score: Math.max(0, b.score | 0), hits: 0, eliminated: !!b.eliminated })); + io.to(sid).emit('space-shooter-over', { kind: reason === 'all_dead' ? 'all_dead' : 'time_up', scores, srv: true }); +} +function stepSpaceShooterBots(sid, space, dt, now) { + const s = space.spaceShooterSession; + if (!s || !s.active || s.ended) return; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'space_shooter') return; + /* คำถามพิเศษเปิดอยู่ → หยุดบอท (เดิน+ยิง) + เลื่อน endsAt ไม่ให้เวลาเดิน (เหมือน gauntlet) */ + /* [2026-07-17] เวลาจริง ไม่ใช่ก้าว SS_TICK_MS คงที่ (flaw เดียวกับ MG5 ที่ชดเชยได้แค่ 68%) */ + if (isSpecialQuizPausedServer(space)) { + const nowS = Date.now(); + const shift = Math.max(0, Math.min(1000, nowS - (s.lastPauseShiftAt || nowS))); + if (s.endsAt > 0) s.endsAt += shift; + s.lastPauseShiftAt = nowS; + return; + } + s.lastPauseShiftAt = Date.now(); + if (s.endsAt > 0 && now >= s.endsAt) { endSpaceShooterGame(sid, space, 'time_up'); return; } + const ts = jsTileSize(md); + const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; + const padX = ts * 0.6; + const outBots = []; + s.bots.forEach((b, id) => { + /* บอทตายแล้ว: ไม่เดิน/ไม่ยิง แต่ "ยัง emit elim:true ต่อ" — เดิม return ข้ามเลย (ส่ง elim ครั้งเดียวตอนตาย) + ถ้า client พลาด packet นั้น = ไม่รู้ว่าตาย → ยานค้างนิ่งบนจอ. ส่งซ้ำทุก tick → client ซ่อนได้แน่นอน */ + if (!b.eliminated) { + if (now >= b.wanderNext) { + b.wanderNext = now + 400 + Math.floor(Math.random() * 700); + b.wanderVx = Math.random() < 0.5 ? -1 : (Math.random() < 0.5 ? 1 : 0); + b.wanderVy = Math.random() < 0.5 ? -1 : 1; + } + const spd = b.tier === 'sharp' ? 195 : b.tier === 'weak' ? 115 : 155; + b.cx = Math.max(padX, Math.min(mw - padX, b.cx + b.wanderVx * spd * dt)); + b.cy = ssClampCy(b.cy + b.wanderVy * spd * 0.55 * dt, mh); + b.fireCd -= dt; + if (b.fireCd <= 0) { + b.fireCd = b.tier === 'sharp' ? 0.22 : b.tier === 'weak' ? 0.48 : 0.34; + const bX = b.cx, bY = b.cy - 16; + io.to(sid).emit('space-shooter-shot', { x: bX, y: bY, vy: -500, ownerId: id, t0: now }); + /* คะแนนบอท: โอกาสยิงโดน (ตาม tier) → +5 (ทำให้คะแนนสมเหตุผลโดยไม่ต้องจำลอง asteroid ฝั่ง server) */ + const hitProb = b.tier === 'sharp' ? 0.6 : b.tier === 'weak' ? 0.28 : 0.42; + if (Math.random() < hitProb) b.score += 5; + } + } + outBots.push({ id, cx: b.cx, cy: b.cy, dir: 'up', score: b.score, elim: !!b.eliminated, hits: b.ssHits || 0 }); + }); + if (now - s.lastEmit >= SS_BOT_EMIT_MS) { + s.lastEmit = now; + if (outBots.length) io.to(sid).emit('space-shooter-bot-move', { bots: outBots }); + } + /* [2026-07-17] ตายหมด → จบรอบ — บั๊กเดียวกับ MG7 (เดิมมีแค่หมดเวลา). endSpaceShooterGame รองรับ + reason 'all_dead' อยู่แล้ว (emit kind:'all_dead') แค่ไม่เคยมีใครเรียก. เช็คทั้งสองที่เพราะตอนคนตาย + server เซ็ตคู่กัน (:13516 p.spaceShooterEliminated + s.players[id].eliminated) */ + let ssAnyAlive = false; + space.peers.forEach((p, id) => { + const st = s.players[id]; + if (!((st && st.eliminated) || p.spaceShooterEliminated)) ssAnyAlive = true; + }); + if (!ssAnyAlive) s.bots.forEach((b) => { if (!b.eliminated) ssAnyAlive = true; }); + if (!ssAnyAlive) endSpaceShooterGame(sid, space, 'all_dead'); +} + +/* ============================================================ + MG7 balloon_boss — server-authoritative บอท + รอบ + disconnect takeover (mnq1eml7) + โลก (บอส/กระสุนบอส deterministic ต่อ client, กระสุนคน relay) คงเดิม → host หลุดไม่พัง; + server ขับ "บอท" (ลอย+ยิง+คะแนน+ดาเมจบอส), รวม HP บอส (host-independent), จบรอบ, แปลงคนหลุดเป็นบอท. + reuse 'balloon-boss-shot' (กระสุนบอท) + emit 'balloon-boss-bot-move' + จบรอบด้วย 'balloon-boss-over' + ผู้เล่นจริง sim ยาน/ชน/บอส เอง (รายงานคะแนน/ดาเมจ/ลูกโป่ง/ตาย ผ่าน 'move') + ============================================================ */ +const BB_TICK_MS = 50; +const BB_BOT_EMIT_MS = 70; +function bbTimeLimitSec(md) { + const t = Number(md.balloonBossTimeSec); + if (Number.isFinite(t) && t === 0) return 0; + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + const rt = Number(runtimeGameTiming && runtimeGameTiming.balloonBossMissionTimeSec); + return Number.isFinite(rt) && rt > 0 && rt < 7200 ? Math.floor(rt) : 120; +} +function bbMaxHp(md) { const n = Number(md.balloonBossMaxHp); return Math.max(20, Math.min(9999, Number.isFinite(n) ? Math.floor(n) : 100)); } +/* ดาเมจต่อการยิงโดนบอส (ลบเลือดบอส/นัด) จาก game-timing (Admin Minigame-7) — แยกจากคะแนน · default 10 */ +function bbDamagePerHit() { const n = Number(runtimeGameTiming && runtimeGameTiming.balloonBossDamagePerHit); return Math.max(1, Math.min(100, Number.isFinite(n) && n > 0 ? Math.floor(n) : 10)); } +function bbBossCenter(md, ts) { + const bx = 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 bbPlayerSpawnCenter(md, slot, ts) { + const g = md.balloonBossPlayerSlots; + if (!g) return null; + const w = md.width || 20, h = md.height || 15; + for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { if (g[y] && Number(g[y][x]) === slot) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; } + return null; +} +function bbClampWorld(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 bbTeamDmg(space) { + let s = 0; + space.peers.forEach((p) => { s += Math.max(0, p.balloonBossBossDmg | 0); }); + const sess = space.balloonBossSession; + if (sess && sess.bots) sess.bots.forEach((b) => { s += Math.max(0, b.dmg | 0); }); + return s; +} +function createBalloonBossBots(space, md) { + const s = space.balloonBossSession; + const ts = jsTileSize(md); + const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; + const startBalloons = balloonBossBalloonsForMap(md); + const humanCount = [...space.peers.keys()].filter((id) => !isBannedPeerForRun(space, id)).length; + /* บอท takeover (จาก how-to) ที่ seed ไว้แล้ว = นับเป็น "คน" กันรวมเกิน 6 + เลื่อนช่อง spawn บอท AI */ + const existing = s.bots.size; + const botCount = Math.max(0, Math.min(6 - Math.min(humanCount + existing, 6), effectiveBotSlotCount(space))); + const bannedSlot = jsBannedBotSlot(space); + let created = 0, slot = 0; + while (created < botCount && slot < 64) { + if (slot === bannedSlot) { slot++; continue; } + const id = JS_BOT_PREFIX + slot; + const slotNum = ((humanCount + existing + created) % 6) + 1; + const cen = bbPlayerSpawnCenter(md, slotNum, ts); + let cx = cen ? cen.cx : ((created + 1) / (botCount + 1)) * mw; + let cy = cen ? cen.cy : (mh - ts * 1.4); + const cl = bbClampWorld(cx, cy, mw, mh); + s.bots.set(id, { + tier: 'avg', homeCx: cl.cx, homeCy: cl.cy, cx: cl.cx, cy: cl.cy, + score: 0, dmg: 0, balloons: startBalloons, eliminated: false, + fireCd: 0.8 + Math.random() * 1.0, phase: Math.random() * Math.PI * 2, + takeover: false, takeoverPlayerKey: '', + }); + created++; slot++; + } +} +function startBalloonBossGame(sid, space) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'balloon_boss') return; + if (!space.detectiveMinigameActive) return; + if (space.balloonBossSession && space.balloonBossSession.active) return; + const now = Date.now(); + const liveBeginsAt = space.missionLiveBeginsAt && space.missionLiveBeginsAt > now ? space.missionLiveBeginsAt : now; + const lim = bbTimeLimitSec(md); + space.balloonBossSession = { + active: true, ended: false, startMs: liveBeginsAt, liveBeginsAt, + endsAt: lim > 0 ? liveBeginsAt + lim * 1000 + 2000 : 0, + maxHp: bbMaxHp(md), bots: new Map(), lastEmit: 0, + /* [P5 v4] ฐานเวลาวงลูกศร — server เป็นเจ้าของ, เลื่อนตอนคำถามพิเศษ, ส่งลงไปกับ bot-move ทุก 50ms */ + ringAnchorMs: liveBeginsAt, lastPauseShiftAt: 0, + }; + /* คนหลุดช่วง how-to → seed บอท takeover ก่อนสร้างบอท AI */ + seedBalloonBossTakeoverBots(space, md); + createBalloonBossBots(space, md); +} +function seedBalloonBossTakeoverBots(space, md) { + const s = space.balloonBossSession; + if (!s) return; + const ts = jsTileSize(md); + const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; + const startBalloons = balloonBossBalloonsForMap(md); + drainPendingMissionTakeovers(space, 'balloon_boss', (rec) => { + const cx = Number.isFinite(rec.x) ? (rec.x + 0.5) * ts : mw * 0.5; + const cy = Number.isFinite(rec.y) ? (rec.y + 0.5) * ts : mh - ts * 1.4; + const cl = bbClampWorld(cx, cy, mw, mh); + s.bots.set(rec.id, { + tier: 'avg', homeCx: cl.cx, homeCy: cl.cy, cx: cl.cx, cy: cl.cy, + score: rec.balloonBossScore || 0, dmg: rec.balloonBossBossDmg || 0, + balloons: rec.balloonBossBalloons || startBalloons, eliminated: !!rec.balloonBossEliminated, + fireCd: 0.6 + Math.random() * 0.8, phase: Math.random() * Math.PI * 2, + takeover: true, takeoverPlayerKey: rec.playerKey || '', + }); + }); +} +function endBalloonBossGame(sid, space, reason) { + const s = space.balloonBossSession; + if (!s || s.ended) return; + s.ended = true; s.active = false; + const timeUp = reason === 'time'; + const scores = []; + space.peers.forEach((p, id) => { + scores.push({ id, score: Math.max(0, p.balloonBossScore | 0), eliminated: timeUp ? true : !!p.balloonBossEliminated }); + }); + s.bots.forEach((b, id) => scores.push({ id, score: Math.max(0, b.score | 0), eliminated: timeUp ? true : !!b.eliminated })); + io.to(sid).emit('balloon-boss-over', { reason: reason === 'victory' ? 'victory' : (timeUp ? 'time' : 'all_dead'), scores, srv: true }); +} +/* ตำแหน่งบอส "เคลื่อนที่จริง" — port จาก client getBalloonBossBossLiveCenterPlay (anchor=liveBeginsAt เดียวกัน → ตรงกันทุกเครื่อง) + server เดิมคิดบอสเป็นจุดนิ่ง → บอทไม่รู้ว่าบอสขยับมาทับ. มีตัวนี้ บอทหลบบอสที่เคลื่อนที่ได้ + เล็งยิงตรงบอสจริง */ +function bbBossLiveCenter(md, ts, s, now) { + const base = bbBossCenter(md, ts); + const w = md.width || 20, h = md.height || 15; + const mw = w * ts, mh = h * ts; + const anchor = (s && s.liveBeginsAt) ? s.liveBeginsAt : now; + const tSec = Math.max(0, (now - anchor) / 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 bbClampWorld(cx, cy, mw, mh); +} +function stepBalloonBossBots(sid, space, dt, now) { + const s = space.balloonBossSession; + if (!s || !s.active || s.ended) return; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'balloon_boss') return; + /* คำถามพิเศษเปิดอยู่ → หยุดบอท (ลอย+ยิง) + เลื่อน endsAt ไม่ให้เวลาเดิน (เหมือน gauntlet) + กันกระสุนบอทถูก emit สะสมตอน pause แล้วไหลรัวหลังตอบเสร็จ */ + if (isSpecialQuizPausedServer(space)) { + /* [P5 v4 2026-07-17] เลื่อนด้วย "เวลาจริง" ไม่ใช่ก้าว BB_TICK_MS คงที่ — บทเรียน MG5 (07-16): + ตอนมีโหลด tick จริงเดินช้ากว่าค่าที่ตั้งไว้ → ก้าวคงที่ชดเชยไม่ครบ (MG5 ได้แค่ 68% ของเวลาที่ควรคืน) */ + const shift = Math.max(0, Math.min(1000, now - (s.lastPauseShiftAt || now))); + if (s.endsAt > 0) s.endsAt += shift; + s.ringAnchorMs += shift; /* วงลูกศรหยุดหมุนพร้อมกันทุกเครื่อง */ + s.lastPauseShiftAt = now; + /* ยัง emit ระหว่าง pause → ทุกเครื่อง re-align anchor ต่อเนื่อง. ถ้าเงียบ client จะ extrapolate + perf.now() ของตัวเองต่อ (วงหมุนต่อคนละมุม) แล้วกระตุกตอน emit กลับมา. bots:[] = no-op ฝั่ง client + (handler แค่ forEach ไม่มี logic ลบบอท) — ตำแหน่งบอทไม่ขยับตอน pause อยู่แล้ว */ + if (now - s.lastEmit >= BB_BOT_EMIT_MS) { + s.lastEmit = now; + io.to(sid).emit('balloon-boss-bot-move', { bots: [], t: Date.now(), anchor: s.ringAnchorMs }); + } + return; + } + s.lastPauseShiftAt = now; + /* จบรอบ: ชนะบอส (HP→0) ก่อน, แล้วหมดเวลา */ + if (bbTeamDmg(space) >= s.maxHp) { endBalloonBossGame(sid, space, 'victory'); return; } + if (s.endsAt > 0 && now >= s.endsAt) { endBalloonBossGame(sid, space, 'time'); return; } + const ts = jsTileSize(md); + const mw = (md.width || 20) * ts, mh = (md.height || 15) * ts; + const boss = bbBossLiveCenter(md, ts, s, now); /* บอสเคลื่อนที่จริง (หลบ+เล็งตามตัวนี้) */ + const fleeR = Math.max(ts * 3.4, 96); /* หลบบอสเมื่อ home เข้าใกล้ ~3 ช่อง */ + const fleeSpd = 165; /* px/s ดริฟต์ home หนีบอส (เร็วกว่าบอส → ไม่ติด) */ + const sepMin = ts * 2.4; /* ระยะ home ขั้นต่ำระหว่างบอท (หลบกันเอง) */ + const outBots = []; + const alive = []; + s.bots.forEach((b, id) => { + if (b.eliminated) { outBots.push({ id, cx: b.cx, cy: b.cy, score: b.score, dmg: b.dmg, balloons: b.balloons, elim: true }); return; } /* ตายแล้ว: emit elim:true ต่อ (กัน client พลาด packet = ค้างนิ่ง) */ + alive.push(b); + /* หลบบอส: ดริฟต์ "home" ออกจากบอสที่เคลื่อนเข้าใกล้ (แทน clamp ตำแหน่ง = ไม่ติดนิ่งขอบบอส) */ + const hx = b.homeCx - boss.cx, hy = b.homeCy - boss.cy; + const hd = Math.hypot(hx, hy) || 1e-6; + if (hd < fleeR) { b.homeCx += (hx / hd) * fleeSpd * dt; b.homeCy += (hy / hd) * fleeSpd * dt; } + b.phase += dt; + const wob = Math.sin(b.phase * 1.25) * 26; + const wob2 = Math.cos(b.phase * 0.9) * 18; + const cl = bbClampWorld(b.homeCx + wob, b.homeCy + wob2, mw, mh); + b.cx = cl.cx; b.cy = cl.cy; + b.fireCd -= dt; + if (b.fireCd <= 0) { + b.fireCd = 0.9 + Math.random() * 1.1; + const aim = Math.atan2(boss.cy - b.cy, boss.cx - b.cx) + (Math.random() - 0.5) * 0.5; + const bspd = 500 + Math.random() * 90; + const vx = Math.cos(aim) * bspd, vy = Math.sin(aim) * bspd; + const sx = b.cx + Math.cos(aim) * 26, sy = b.cy - 10.5 + Math.sin(aim) * 26; + io.to(sid).emit('balloon-boss-shot', { sx, sy, vx, vy, ownerId: id, t0: now }); + b.score = Math.max(0, (b.score | 0) - 5); /* สเปก: ยิง 1 ครั้ง −5 */ + const hitProb = b.tier === 'sharp' ? 0.55 : b.tier === 'weak' ? 0.26 : 0.4; + if (Math.random() < hitProb) { b.score += 10; b.dmg += bbDamagePerHit(); } /* โดนบอส: +10 คะแนน, −HP บอสตามค่า Admin (default 10) */ + } + outBots.push({ id, cx: b.cx, cy: b.cy, score: b.score, dmg: b.dmg, balloons: b.balloons, elim: false }); + }); + /* หลบกันเอง: ดัน home ที่ใกล้กันเกินให้แยกออก (รอบถัดไปบอทจะ wobble รอบ home ใหม่ → ไม่ซ้อนกัน) */ + for (let i = 0; i < alive.length; i++) { + for (let j = i + 1; j < alive.length; j++) { + const a = alive[i], c = alive[j]; + const dx = a.homeCx - c.homeCx, dy = a.homeCy - c.homeCy; + const d = Math.hypot(dx, dy) || 1e-6; + if (d < sepMin) { const p = (sepMin - d) * 0.5, nx = dx / d, ny = dy / d; a.homeCx += nx * p; a.homeCy += ny * p; c.homeCx -= nx * p; c.homeCy -= ny * p; } + } + } + alive.forEach((b) => { const cl = bbClampWorld(b.homeCx, b.homeCy, mw, mh); b.homeCx = cl.cx; b.homeCy = cl.cy; }); /* กัน home ดริฟต์หลุดขอบ */ + if (now - s.lastEmit >= BB_BOT_EMIT_MS) { + s.lastEmit = now; + /* [P5 v3] แนบนาฬิกา server (Date.now) → ทุก client (รวม host) ใช้เป็นฐานเวลา "วงลูกศรหมุน" แทน + serverNow() ของตัวเอง (playClockOffset ต่างต่อเครื่อง = วงไม่ sync). ในเกมจริง MG7 เป็น server-auth + (host ไม่ส่ง balloon-boss-sync) → ฐานเวลาต้องมาจาก server. bot-move ส่งทุก 50ms = re-align ถี่. + ส่ง "เสมอ" (แม้ไม่มีบอท = ห้อง 6 คนจริง) เพื่อให้ฐานเวลาไหลเสมอ — empty array 20Hz cost น้อยมาก + และอยู่ใน balloon tick อยู่แล้ว (ไม่กระทบมินิเกมอื่น) */ + /* [P5 v4] anchor = ฐานเวลาวงลูกศรของ server (pause-adjusted). v2/v3 ส่งแค่ `t` (นาฬิกา) ซึ่งตรงแล้ว + แต่ client ยังลบด้วย anchor "ของตัวเอง" ที่ shiftSpecialQuizFrozenTimers เลื่อนเองทุกเฟรม = สะสม + ต่อเครื่อง และไม่มี path ไหน adopt กลับในโหมด server-auth (balloon-boss-sync ถูกปิด) → ไม่มีวัน converge */ + io.to(sid).emit('balloon-boss-bot-move', { bots: outBots, t: Date.now(), anchor: s.ringAnchorMs }); + } + /* [2026-07-17] ทุก entity ตายหมด → จบรอบ (user: "mg7 ตายหมดเกมไม่จบ"). เดิม MG7 มีเงื่อนไขจบแค่ + victory (HP บอสหมด) กับหมดเวลา → ตายครบก็นั่งรอจนหมดเวลาเปล่าๆ. ใช้ pattern เดียวกับ MG5 (:9345) + คน: p.balloonBossEliminated (server เซ็ตจาก move payload :13564, sticky) · บอท: b.eliminated */ + let bbAnyAlive = false; + space.peers.forEach((p) => { if (!p.balloonBossEliminated) bbAnyAlive = true; }); + if (!bbAnyAlive) s.bots.forEach((b) => { if (!b.eliminated) bbAnyAlive = true; }); + if (!bbAnyAlive) endBalloonBossGame(sid, space, 'all_dead'); +} + +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 botCount = Math.max(0, Math.min(GAUNTLET_MAX_PLAYERS - list.length, effectiveBotSlotCount(space))); + const pos = gauntletSpawnPositions(md, list.length + botCount); + const crown = isGauntletCrownHeistMapBySpace(space); + /* จัด lane ให้ "ไม่ทับกันแน่นอน": เรียงตาม spawnJoinOrder (id เป็น tiebreak ให้เสถียร) + แล้วแจกช่อง pos แบบ unique — ถ้า ord ชนกัน/เกินจำนวนช่อง (pos[ord] ซ้ำ) ดันไปช่องว่างถัดไป + = แก้ MG2 user1/user2 spawn ทับกัน (เดิม pos[ord] ซ้ำเมื่อ ord ชนหรือ fallback ช่องสุดท้าย) */ + const ordered = list + .map((p, i) => ({ p, ord: (typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : i) })) + .sort((a, b) => (a.ord - b.ord) || (String(a.p.id) < String(b.p.id) ? -1 : 1)); + const usedPosIdx = new Set(); + ordered.forEach((entry, seq) => { + const p = entry.p; + let idx = Number.isFinite(entry.ord) ? Math.max(0, Math.min(entry.ord, pos.length - 1)) : seq; + let guard = 0; + while (usedPosIdx.has(idx) && guard < pos.length) { idx = (idx + 1) % pos.length; guard++; } + usedPosIdx.add(idx); + const pt = pos[idx] || 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); + /* บอท server-authoritative — แจกช่อง spawn ที่เหลือ (ไม่ทับผู้เล่น) + Card Ban: ถ้าแบน "บอท" (__lobby_bot_N) → ข้าม slot N + ลดจำนวน 1 (ตรงกับ client rebalancePreviewBots) */ + if (botCount > 0) { + let bannedBotSlot = -1; + const bannedId = space.bannedThisRunPlayerId; + if (typeof bannedId === 'string' && bannedId.indexOf('__lobby_bot_') === 0) { + const n = parseInt(bannedId.slice('__lobby_bot_'.length), 10); + if (Number.isFinite(n) && n >= 0) bannedBotSlot = n; + } + const want = bannedBotSlot >= 0 ? Math.max(0, botCount - 1) : botCount; + const bots = new Map(); + let created = 0; + let slot = 0; + while (created < want && slot < GAUNTLET_MAX_PLAYERS + botCount) { + if (slot === bannedBotSlot) { slot++; continue; } + let idx = 0; + while (usedPosIdx.has(idx) && idx < pos.length) idx++; + usedPosIdx.add(idx); + const pt = pos[idx] || pos[pos.length - 1] || { x: 1, y: 1 }; + bots.set(GAUNTLET_BOT_PREFIX + slot, { + tier: gauntletBotTierForSlot(slot), + x: pt.x, + y: pt.y, + dir: 'right', + gauntletJumpTicks: 0, + gauntletScore: crown ? 100 : 0, + gauntletEliminated: false, + }); + created++; + slot++; + } + if (bots.size) space.gauntletRun.bots = bots; + } + 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; +} + +/* ===== Lobby authoritative (server เป็นเจ้าของ LobbyA/B) ===================== + เดิม: บอท lobby เดินบน "host client" (frame-based) แล้ว broadcast → บอทวิ่งเร็วช้าต่างกันตาม FPS/host + + ผู้เล่นออกแล้ว client อื่นไม่ได้ user-left = ผีค้าง + ใหม่: server เดินบอทเอง (time-based, ความเร็วเดียวทุกเครื่อง) + ส่ง roster snapshot ให้ client reconcile (ฆ่าผี) + bot id = '__lobby_bot_' (ตรงกับ client LOBBY_BOT_PREFIX) → client map สี/ตัวละครได้ถูกตัว */ +const LOBBY_BOT_PREFIX = '__lobby_bot_'; +const LOBBY_MOVE_SPEED_PER_SEC = 0.15 * 60; /* ตรงกับ client MOVE_SPEED_PER_SEC (0.15/เฟรม × 60fps) */ + +function lobbyMapOfSpace(space) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + return (md && md.gameType === 'lobby') ? md : null; +} + +/* จุดเกิดผู้เล่น 6 ช่อง (mirror client parseLobbyPlayerSpawnsFromMapLobby) — บอทเกิดที่ช่องเดียวกับคน */ +function parseLobbyPlayerSpawnsServer(md) { + const w = md.width || 20, 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; +} + +/* เดินได้ไหม — mirror client canWalkLobbyBot เป๊ะ: กำแพง objects===1 เดินไม่ได้ (เดินได้ทุก tile ที่ไม่ใช่กำแพง เหมือนคน); + tile blockPlayer===1 = ห้ามทับ peer/บอทตัวอื่น (tile ปกติทับได้) + หมายเหตุ: ห้ามจำกัดด้วย spawnArea — นั่นคือ "จุดเกิด" ไม่ใช่ "พื้นที่เดิน" (เคยทำให้บอทติดอยู่กับที่) */ +function lobbyTileWalkable(md, space, x, y, selfBotId) { + const w = md.width || 20, h = md.height || 15; + const tx = Math.floor(x), ty = Math.floor(y); + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const ob = md.objects && md.objects[ty] ? md.objects[ty][tx] : 0; + if (ob === 1) return false; + const bp = md.blockPlayer; + if (bp && bp[ty] && bp[ty][tx] === 1) { + for (const p of space.peers.values()) { + if (p && Math.floor(p.x) === tx && Math.floor(p.y) === ty) return false; + } + if (space.lobbyBots) { + for (const [bid, b] of space.lobbyBots) { + if (bid === selfBotId) continue; + if (Math.floor(b.x) === tx && Math.floor(b.y) === ty) return false; + } + } + } + return true; +} + +/* เกิดบอทที่ "ช่องเกิดผู้เล่น" เดียวกับคน (slotOrd = humans + botIndex → ช่องไม่ทับคน, ไม่ทับกันเอง) + mirror client pickLobbySpawnForJoin: slots6 → lobbyPlayerSpawns[j], fixed → md.spawn */ +function pickLobbyBotSpawnServer(md, space, slotOrd) { + const w = md.width || 20, h = md.height || 15; + const mode = md.lobbySpawnMode; + const ord = slotOrd | 0; + if (mode === 'slots6') { + const j = Math.min(Math.max(0, ord), 5); + const slots = parseLobbyPlayerSpawnsServer(md); + const pick = slots[j]; + if (pick) return { x: pick.x, y: pick.y }; + } else if (mode === 'fixed' && md.spawn) { + const fx = Math.floor(Number(md.spawn.x)); + const fy = Math.floor(Number(md.spawn.y)); + if (Number.isFinite(fx) && Number.isFinite(fy)) return { x: Math.max(0, Math.min(w - 1, fx)), y: Math.max(0, Math.min(h - 1, fy)) }; + } + /* fallback: หาช่อง spawn ที่ define ไว้ตัวแรก ๆ แล้วค่อยสุ่ม tile ที่ไม่ใช่กำแพง */ + const slots = parseLobbyPlayerSpawnsServer(md); + const defined = slots.filter(Boolean); + if (defined.length) { const s = defined[Math.min(ord, defined.length - 1)]; return { x: s.x, y: s.y }; } + for (let i = 0; i < 24; i++) { + const x = 1 + Math.floor(Math.random() * Math.max(1, w - 2)); + const y = 1 + Math.floor(Math.random() * Math.max(1, h - 2)); + if (lobbyTileWalkable(md, space, x, y, null)) return { x, y }; + } + const sp = md.spawn; + if (sp && Number.isFinite(Number(sp.x)) && Number.isFinite(Number(sp.y))) return { x: Math.floor(Number(sp.x)), y: Math.floor(Number(sp.y)) }; + return { x: 1, y: 1 }; +} + +/* คอมโพเนนต์เดินได้ "หลัก" (BFS จาก spawn slot) — กันบอทไปติดเกาะแยก/strip ข้างฉาก (isolated walkable) ที่กำแพงล้อม + cache ต่อ space (invalidate เมื่อ mapId เปลี่ยน) */ +function lobbyMainCompServer(md, space) { + const mid = space && space.mapId; + if (space && space._lobbyMainComp && space._lobbyMainCompMap === mid) return space._lobbyMainComp; + const w = md.width || 20, h = md.height || 15; + const isW = (x, y) => x >= 0 && x < w && y >= 0 && y < h && !(md.objects && md.objects[y] && md.objects[y][x] === 1); + const slots = parseLobbyPlayerSpawnsServer(md).filter(Boolean); + let seed = null; + for (const s of slots) { const sx = Math.floor(s.x), sy = Math.floor(s.y); if (isW(sx, sy)) { seed = [sx, sy]; break; } } + if (!seed && md.spawn && isW(Math.floor(md.spawn.x), Math.floor(md.spawn.y))) seed = [Math.floor(md.spawn.x), Math.floor(md.spawn.y)]; + if (!seed) { for (let y = 0; y < h && !seed; y++) for (let x = 0; x < w; x++) { if (isW(x, y)) { seed = [x, y]; break; } } } + const comp = new Set(); + if (seed) { + comp.add(seed[0] + ',' + seed[1]); + const q = [seed]; + while (q.length) { const [a, b] = q.shift(); for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const nx = a + dx, ny = b + dy, k = nx + ',' + ny; if (!comp.has(k) && isW(nx, ny)) { comp.add(k); q.push([nx, ny]); } } } + } + if (space) { space._lobbyMainComp = comp; space._lobbyMainCompMap = mid; } + return comp; +} +/* เดินบอท lobby ของ space เดียว (เรียกจาก sweeper ทุก ~60ms) — time-based, ความเร็วคงที่ */ +function stepLobbyBotsForSpace(sid, space, dt, nowMs) { + const md = lobbyMapOfSpace(space); + const botCount = md ? effectiveBotSlotCount(space) : 0; + const humans = space.peers ? space.peers.size : 0; + if (!md || botCount <= 0 || humans <= 0) { + if (space.lobbyBots) space.lobbyBots = null; + return; + } + if (!space.lobbyBots) space.lobbyBots = new Map(); + const bots = space.lobbyBots; + /* ปรับจำนวนบอทให้ตรง botCount (id ตาม slot) — เกิดที่ช่องผู้เล่น humans+i (ไม่ทับคน/ไม่ทับกัน) */ + for (let i = 0; i < botCount; i++) { + const id = LOBBY_BOT_PREFIX + i; + if (!bots.has(id)) { + const sp = pickLobbyBotSpawnServer(md, space, humans + i); + bots.set(id, { x: sp.x, y: sp.y, direction: 'down', isWalking: false, wTX: null, wTY: null, retargetAt: 0, pauseUntil: 0 }); + } + } + for (const id of [...bots.keys()]) { + const slot = parseInt(id.slice(LOBBY_BOT_PREFIX.length), 10); + if (!(slot >= 0 && slot < botCount)) bots.delete(id); + } + const w = md.width || 20, h = md.height || 15; + const step = LOBBY_MOVE_SPEED_PER_SEC * dt; + const mainComp = lobbyMainCompServer(md, space); + const inMain = (x, y) => !mainComp.size || mainComp.has(Math.floor(x) + ',' + Math.floor(y)); + bots.forEach((b, id) => { + /* กันบอทไปติดเกาะแยก/strip ข้างฉาก (isolated) → ดึงกลับ spawn หลัก (main component) */ + if (!inMain(b.x, b.y)) { + const slot = parseInt(id.slice(LOBBY_BOT_PREFIX.length), 10); + const sp = pickLobbyBotSpawnServer(md, space, slot); + if (inMain(sp.x, sp.y)) { b.x = sp.x + 0.5; b.y = sp.y + 0.5; } + else { const first = mainComp.values().next().value; if (first) { const [mx, my] = first.split(',').map(Number); b.x = mx + 0.5; b.y = my + 0.5; } } + b.wTX = null; b.wTY = null; b.retargetAt = 0; b.pauseUntil = 0; + } + const arrived = (typeof b.wTX === 'number') && Math.hypot(b.wTX - b.x, b.wTY - b.y) < 0.5; + if (typeof b.wTX !== 'number' || arrived || nowMs >= b.retargetAt) { + /* เป้า wander = สุ่มทั้งแมป (เหมือน client เดิม) — การเดินถูกจำกัดด้วยกำแพงอยู่แล้ว */ + b.wTX = 1 + Math.random() * Math.max(1, w - 2); + b.wTY = 1 + Math.random() * Math.max(1, h - 2); + b.retargetAt = nowMs + 3000 + Math.floor(Math.random() * 4500); + b.pauseUntil = (arrived && Math.random() < 0.45) ? nowMs + 500 + Math.floor(Math.random() * 1600) : 0; + } + if (b.pauseUntil && nowMs < b.pauseUntil) { b.isWalking = false; return; } + let dx = b.wTX - b.x, dy = b.wTY - b.y; + const dist = Math.hypot(dx, dy) || 1; + dx /= dist; dy /= dist; + if (Math.abs(dy) > Math.abs(dx)) b.direction = dy > 0 ? 'down' : 'up'; + else b.direction = dx > 0 ? 'right' : 'left'; + const ox = b.x, oy = b.y; + const nx = b.x + dx * step, ny = b.y + dy * step; + if (lobbyTileWalkable(md, space, nx, ny, id)) { b.x = nx; b.y = ny; } + else if (lobbyTileWalkable(md, space, nx, b.y, id)) { b.x = nx; } + else if (lobbyTileWalkable(md, space, b.x, ny, id)) { b.y = ny; } + else { b.retargetAt = 0; } /* ติด → หาเป้าใหม่เฟรมหน้า */ + b.x = Math.max(0, Math.min(w - 0.01, b.x)); + b.y = Math.max(0, Math.min(h - 0.01, b.y)); + b.isWalking = Math.abs(b.x - ox) > 1e-5 || Math.abs(b.y - oy) > 1e-5; + }); + const arr = []; + bots.forEach((b, id) => arr.push({ + id, + x: Math.round(b.x * 1000) / 1000, + y: Math.round(b.y * 1000) / 1000, + direction: b.direction || 'down', + isWalking: !!b.isWalking, + })); + if (arr.length) io.to(sid).emit('lobby-bot-move', { bots: arr }); + /* roster snapshot (L3 ฆ่าผี) — ทุก ~1.5s ส่ง id ผู้เล่นจริงทั้งหมด ให้ client ตัดตัวที่ไม่อยู่ทิ้ง + (throttle ย้ายเข้าไปใน emitLobbyRosterSync แล้ว — กันยิงซ้ำเมื่อทั้ง sweep นี้และ interval P3 เรียก) */ + emitLobbyRosterSync(sid, space); +} + +/* throttle ~1.5s ต่อห้อง — ปลอดภัยไม่ว่าถูกเรียกจาก bot sweep (ห้องมีบอท) หรือ interval P3 (ทุกห้อง) */ +function emitLobbyRosterSync(sid, space) { + if (!space || !space.peers) return; + const nowMs = Date.now(); + if (space.lobbyRosterSyncTs && nowMs - space.lobbyRosterSyncTs <= 1500) return; + space.lobbyRosterSyncTs = nowMs; + const ids = [...space.peers.keys()]; + io.to(sid).emit('lobby-roster', { ids }); +} + +let lobbyBotSweeperTs = 0; +function lobbyBotSweepTick() { + const now = Date.now(); + let dt = lobbyBotSweeperTs ? (now - lobbyBotSweeperTs) / 1000 : 0.06; + if (!(dt > 0)) dt = 0.06; + if (dt > 0.2) dt = 0.2; + lobbyBotSweeperTs = now; + for (const [sid, space] of spaces) { + try { stepLobbyBotsForSpace(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องพังทั้ง sweeper */ } + } +} +setInterval(lobbyBotSweepTick, 60); + +/* roster heartbeat ระหว่างเล่นมินิเกม (gameType != lobby) → client (play.js) ตัด "ผี" (peer ที่ไม่อยู่ใน roster จริง) + แก้บั๊ก: คนเดียวมี avatar 2 อัน (อันเก่าค้างตอน reconnect/ออก เพราะ others เป็น add-only ไม่เคย prune) */ +setInterval(() => { + for (const [sid, space] of spaces) { + if (!space || !space.peers || space.peers.size === 0) continue; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType === 'lobby') continue; /* lobby ใช้ lobby-roster แยก (heartbeat ด้านล่าง) */ + try { io.to(sid).emit('play-roster', { ids: [...space.peers.keys()] }); } catch (e) { /* กัน 1 ห้องล้ม */ } + } +}, 1500); + +/* [P3 2026-07-16] roster heartbeat ของ "lobby" — ต้องยิงทุกห้อง lobby ที่มีคน "ไม่ว่ามีบอทหรือไม่". + บั๊ก: เดิม emitLobbyRosterSync ถูกเรียก "ที่เดียว" คือใน stepLobbyBotsForSpace (bot sweep) ซึ่ง + return ก่อนถ้า effectiveBotSlotCount() <= 0 — และค่านั้น = 0 เสมอเมื่อ maxPlayers >= 4 (สร้างห้อง + เลือก ≥4 คน). ผลคือห้อง 4+ คน "ไม่เคยได้ lobby-roster" → ตัวฆ่าผี L3 ไม่ทำงาน → ถ้า user-left + หลุด/มาไม่ถึงเครื่องไหน peer ผีค้างถาวร → peers.size ต่างกัน → เลข PLAYERS ของ host ไม่ตรงคนอื่น + ("ในบาง case" = เฉพาะห้องที่เลือก ≥4 คน). ยืนยันจาก gamelog: ห้อง 'บอท 0' คือห้องที่มีอาการ. + แยกออกมาเป็น interval ของตัวเอง → roster ไม่ผูกกับการมีบอทอีกต่อไป */ +setInterval(() => { + for (const [sid, space] of spaces) { + if (!space || !space.peers || space.peers.size === 0) continue; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'lobby') continue; /* เฉพาะโถง lobby (มินิเกมใช้ play-roster ด้านบน) */ + try { emitLobbyRosterSync(sid, space); } catch (e) { /* กัน 1 ห้องล้ม */ } + } +}, 1500); + +/* เดินบอท MG1 quiz บน server (time-based) → บอทเดินเองไม่พึ่ง host (host หลุดก็ไม่หยุด) */ +let quizBotSweepTs = 0; +setInterval(() => { + const now = Date.now(); + let dt = quizBotSweepTs ? (now - quizBotSweepTs) / 1000 : 0.06; + if (!(dt > 0)) dt = 0.06; + if (dt > 0.2) dt = 0.2; + quizBotSweepTs = now; + for (const [sid, space] of spaces) { + const sess = space && space.quizSession; + if (!sess || !sess.active || !sess.bots || sess.bots.size === 0) continue; + try { stepQuizBots(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องล้ม */ } + } +}, 60); + +/* เดินบอท MG4 quiz_carry บน server (time-based, BFS) → host หลุดก็ไม่หยุด */ +let quizCarryBotSweepTs = 0; +setInterval(() => { + const now = Date.now(); + let dt = quizCarryBotSweepTs ? (now - quizCarryBotSweepTs) / 1000 : 0.06; + if (!(dt > 0)) dt = 0.06; + if (dt > 0.2) dt = 0.2; + quizCarryBotSweepTs = now; + for (const [sid, space] of spaces) { + const s = space && space.quizCarrySession; + if (!s || !s.active || s.ended || !s.bots || s.bots.size === 0) continue; + try { stepQuizCarryBots(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องล้ม */ } + } +}, 60); + +/* MG5 jump_survive — sweep บอท server + จับเวลา/จบรอบ (server-auth) */ +let jumpSurviveBotSweepTs = 0; +setInterval(() => { + const now = Date.now(); + let dt = jumpSurviveBotSweepTs ? (now - jumpSurviveBotSweepTs) / 1000 : 0.033; + if (!(dt > 0)) dt = 0.033; + if (dt > 0.05) dt = 0.05; /* clamp เหมือน client (กันกระโดดไกลตอน lag) */ + jumpSurviveBotSweepTs = now; + for (const [sid, space] of spaces) { + const s = space && space.jumpSurviveSession; + if (!s || !s.active || s.ended) continue; + try { stepJumpSurviveBots(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องล้ม */ } + } +}, JS_TICK_MS); + +/* MG6 space_shooter — sweep บอท server + จับเวลา/จบรอบ (server-auth) */ +let spaceShooterBotSweepTs = 0; +setInterval(() => { + const now = Date.now(); + let dt = spaceShooterBotSweepTs ? (now - spaceShooterBotSweepTs) / 1000 : 0.05; + if (!(dt > 0)) dt = 0.05; + if (dt > 0.08) dt = 0.08; + spaceShooterBotSweepTs = now; + for (const [sid, space] of spaces) { + const s = space && space.spaceShooterSession; + if (!s || !s.active || s.ended) continue; + try { stepSpaceShooterBots(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องล้ม */ } + } +}, SS_TICK_MS); + +/* MG7 balloon_boss — sweep บอท server + รวม HP บอส/จบรอบ (server-auth) */ +let balloonBossBotSweepTs = 0; +setInterval(() => { + const now = Date.now(); + let dt = balloonBossBotSweepTs ? (now - balloonBossBotSweepTs) / 1000 : 0.05; + if (!(dt > 0)) dt = 0.05; + if (dt > 0.08) dt = 0.08; + balloonBossBotSweepTs = now; + for (const [sid, space] of spaces) { + const s = space && space.balloonBossSession; + if (!s || !s.active || s.ended) continue; + try { stepBalloonBossBots(sid, space, dt, now); } catch (e) { /* กัน 1 ห้องล้ม */ } + } +}, BB_TICK_MS); + +/** หมดเวลา Gauntlet — หยุด tick, คืนแผนที่ lobby, แจ้งไคลเอนต์ */ +function endGauntletGame(sid, space, reason) { + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'gauntlet') return; + try { + const sq = space.specialQuiz; const gr = space.gauntletRun; + console.log('[mg2-end-dbg] reason=' + reason + ' sq={trig:' + (sq && sq.triggered) + ',done:' + (sq && sq.done) + ',sess:' + !!(sq && sq.session) + '} awaitCont=' + !!space.specialQuizAwaitContinue + ' endsAtIn=' + (gr && gr.endsAt != null ? (gr.endsAt - Date.now()) : 'n/a') + 'ms gameoverAt=' + (gr && gr.gameoverAt != null ? (gr.gameoverAt - Date.now()) + 'ms' : 'null')); + } catch (eDbg) { /* ignore */ } + if (space.detectiveMinigameActive) { + const msg = reason === 'time' ? 'หมดเวลา — จบมินิเกม' + : (reason === 'all_dead' ? 'ตกขอบหมดแล้ว — จบมินิเกม' : 'จบมินิเกม'); + 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; + /* หยุด run ระหว่างตอบคำถามพิเศษ (special-quiz) — เลื่อน endsAt ไปด้วยให้เวลาไม่เดิน, ไม่ไหล obstacle/ชน */ + if (isSpecialQuizPausedServer(space)) { + /* [2026-07-17] เดิม `gr.endsAt += getGauntletTickMs()` = ก้าวคงที่ → ชดเชยไม่ครบ (user: "เหมือนยัง + ไม่หยุดวิ่ง" — obstacle หยุดจริงเพราะ return ออกไป แต่ deadline เดินต่อ รอบเลยจบเร็วกว่าที่ควร). + บทเรียนเดียวกับ MG5 (07-16): interval ที่ตั้ง 33ms ของจริงเดิน ~48ms ตอนมีโหลด → บวกทีละ 33 + กู้คืนได้แค่ ~68%. ใช้ "เวลาจริงที่ผ่านไป" clamp 1s กันกระโดดตอน tick หาย */ + const nowG = Date.now(); + const shift = Math.max(0, Math.min(1000, nowG - (gr.lastPauseShiftAt || nowG))); + if (gr.endsAt != null) gr.endsAt += shift; + /* [2026-07-30] BUG1 "รับการ์ดพิเศษแล้วไปโผล่ใกล้เส้นชัย" — ต้องเลื่อน liveStartMs ด้วย + client คำนวณ scroll ของลู่วิ่งเป็น **ฟังก์ชันของเวลานาฬิกา**: scroll = base + speed × (serverNow − liveStartMs) + (play.js:2168 — G1 เปลี่ยนจากเดิมที่บวกทีละเฟรม เพื่อให้ทุกเครื่องเลื่อนตรงกัน) ส่วน freeze ฝั่ง client + ทำได้แค่ "ข้ามการอัปเดต" (play.js:2092) ซึ่ง**หยุดสูตรที่อิงนาฬิกาไม่ได้** — พอ freeze จบ ค่าที่คำนวณได้ + กระโดดไปข้างหน้าเท่ากับ speed × ระยะเวลาที่ค้างตอบ ในเฟรมเดียว (มี guard monotonic ด้วย = เด้งหน้าอย่างเดียว) + แมปนี้ speed=100px/s · ลู่ยาว 5402px (~54 วิ) · ค้างตอบขั้นต่ำ 24.6 วิ (2s ไอคอน + 20s ตอบ + 2.6s เฉลย + ยังไม่รวมจอการ์ดที่รอกด "เล่นเกมต่อ") = **กระโดด 2460px ≈ 46% ของทางทั้งหมด** + shiftSpecialQuizFrozenTimers ฝั่ง client เลื่อน anchor ให้ทุกเกม (MG3/5/6/7) แต่ **ตกไปเกมเดียวคือ MG2** + เพราะโน้ต 07-17 สรุปว่า "MG2 ไม่ต้องเลื่อนเอง server ส่ง gauntletEndsAt มาให้ทุก tick อยู่แล้ว" — + จริงสำหรับ "นาฬิกา" แต่ MG2 มี anchor ตัวที่สอง (ลู่วิ่ง) ที่ server ไม่เคยเลื่อน + ต้องแก้ที่ server เท่านั้น: pause branch นี้ emit sync ทุก tick และ client re-anchor ตาม + gauntletLiveStartMs ทุกครั้ง (play.js:13342) → ถ้าไปเลื่อนฝั่ง client จะโดน sync ทับทันที + ปลอดภัยกับ pre-run hold (10561): ตอน pause จบ now − liveStartMs ใหม่ = เวลาที่วิ่งไปก่อน pause ≥ 0 เสมอ + จึงไม่มีทางกลายเป็นอนาคต · แถมยังเลื่อน GAUNTLET_FINISH_MIN_RUN_MS (13597) ให้ถูกต้องตามไปด้วย */ + if (gr.liveStartMs != null) gr.liveStartMs += shift; + gr.lastPauseShiftAt = nowG; + emitGauntletSync(sid, space); /* ส่ง gauntletEndsAt ใหม่ทุก tick → ทุก client ยึดเลขเดียวกับ server */ + return; + } + gr.lastPauseShiftAt = Date.now(); /* ไม่ pause → คง anchor ให้สด กันกระโดดตอนเริ่ม pause รอบหน้า */ + ensureGauntletEndsAtIfNeeded(space); + if (gr.endsAt != null && Date.now() >= gr.endsAt) { + endGauntletGame(sid, space, 'time'); + return; + } + + /* ตายหมดแล้ว (all_dead) — อยู่ในช่วง grace: หยุดเกม + ให้เวลา client วาดจอ result-gameover → GCM + ก่อน server ดึงกลับ LobbyB. เดิม all_dead จบ+return ทันที (0 grace) = client ยังไม่ทันวาด game over + ก็โดนดึงกลับ = "ตายหมดแต่ไม่ขึ้น game over". ใช้ grace เท่ากับทางชนะ (GAUNTLET_FINISH_WALK_MS) */ + if (gr.gameoverAt != null) { + if (gr.obstacles && gr.obstacles.length) gr.obstacles = []; + if (Date.now() >= gr.gameoverAt) { + endGauntletGame(sid, space, 'all_dead'); + return; + } + /* ไม่ emit sync ระหว่าง grace — ปล่อย client คุม overlay result เอง (เหมือน finish grace) */ + return; + } + + /* เข้าโซนเส้นชัยแล้ว (client แจ้ง) — หยุด obstacle/collision ทันที แล้วรอ grace ให้ client + เดินทุกคน "ค่อยๆ" เข้าเส้นชัยจนครบ ค่อยสรุปผล (ไม่ snap กระพริบ / ไม่จบทันที) */ + if (gr.finishPhase) { + if (gr.obstacles.length) { gr.obstacles = []; } + /* ตายหมดตั้งแต่ก่อน/ระหว่าง finish-walk grace → เข้า grace game over (แทน result-complete จากทางเส้นชัย) + (race: client ตัวหนึ่ง scroll ถึงเส้นชัยก่อน → set finishPhase → early-return ข้ามเช็ค all_dead ท้าย tick) */ + if (gauntletAllHumansEliminated(space)) { + gr.gameoverAt = Date.now() + GAUNTLET_FINISH_WALK_MS; + gr.obstacles = []; + try { glog(sid, space, 'gauntlet-end', '[TC172] ตายหมด (ระหว่าง finish-phase) → grace game over → กลับ LobbyB (pass)'); } catch (e) { /* ignore */ } + return; + } + if (gr.finishEndAt == null) gr.finishEndAt = Date.now() + GAUNTLET_FINISH_WALK_MS; + if (Date.now() >= gr.finishEndAt) { + endGauntletGame(sid, space, 'finish'); + } + /* ไม่ emitGauntletSync ระหว่าง grace — ปล่อย client คุมการเดินเข้าเส้นเอง (กัน sync ดึงตำแหน่งกลับ) */ + return; + } + + if (gr.crownRunHeld) { + space.peers.forEach((p) => { + p.gauntletJumpPending = false; + }); + emitGauntletSync(sid, space); + return; + } + + /* G2: pre-run hold — หลัง 3-2-1 จบ ค้างที่จุด start ให้เห็นเส้นเริ่มก่อน (ยังไม่เลื่อน/เกิด obstacle/ขยับบอท) + client ค้าง bg เองด้วย max(0, serverNow − liveStartMs) → bg+bots/obstacles sync ที่ start พร้อมกัน */ + if (gr.liveStartMs != null && Date.now() < gr.liveStartMs) { + 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(); + /* นับครั้งเริ่มกระโดด (ไม่รีเซ็ตตอนข้ามสำเร็จ) → ให้ peer เห็นอาร์คกระโดดเต็ม + แม้ gauntletJumpTicks ถูกตัด 0 ใน tick เดียวกันตอน advanceX (peer ไม่เคยเห็นค่า >0) */ + p.gauntletJumpSeq = (p.gauntletJumpSeq || 0) + 1; + } + 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); + }); + /* บอท server-authoritative — แถวเลนของบอทที่ยังเล่นอยู่ (ไม่ต้องพึ่ง gauntlet-preview-rows จาก host) */ + if (gr.bots && gr.bots.size) { + gr.bots.forEach((b) => { + if (b.gauntletEliminated) return; + const lr = gauntletLaneGridRowFromPlayerTop(md, b.y, h); + if (lr != null) laneSpawnRows.add(lr); + }); + } + /* แถวบนที่ client แจ้ง (บอท preview — editor หรือ client เก่า) — แปลงเป็นแถว 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: เลเซอร์ — ทุกแถว */ + let _gauntletLaserThisTick = false; + 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 }); + _gauntletLaserThisTick = true; + } + /* ประเภท 1: แยกเลน — แถวเท้า · กันซ้อนคอลัมน์เดียวกับเลเซอร์ (laser+lane ไม่โผล่คอลัมน์เดียวกัน) */ + if (!_gauntletLaserThisTick) { + 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; + }); + + /* บอท server-authoritative — ตัดสินใจกระโดด + ฟิสิกส์ รอบ tick เดียวกับผู้เล่น (obstacle เลื่อนแล้วข้างบน) */ + if (gr.bots && gr.bots.size) { + gr.bots.forEach((b) => { + if (crownHeist && b.gauntletEliminated) return; + gauntletBotDecideJump(b, gr.obstacles, md, w, h); + gauntletBotStepPhysics(b, gr.obstacles, md, w, h, crownHeist); + }); + } + + /* ผู้เล่น (คนจริง) ตกขอบหมด → เข้า grace game over (ไม่รอหมดเวลา) แล้วค่อย return LobbyB + ส่ง sync ครั้งสุดท้าย (ทุกคน eliminated) ให้ client ทริก overlay result-gameover ก่อนหมด grace */ + if (gauntletAllHumansEliminated(space)) { + gr.gameoverAt = Date.now() + GAUNTLET_FINISH_WALK_MS; + gr.obstacles = []; + try { glog(sid, space, 'gauntlet-end', '[TC172] ผู้เล่นตกขอบหมด → grace game over แล้วกลับ LobbyB (pass)'); } catch (e) { /* ignore */ } + emitGauntletSync(sid, space); + return; + } + + emitGauntletSync(sid, space); +} + +/** MG2 crown/gauntlet: คนจริงที่ "ยังเล่นอยู่" (ไม่นับผู้ถูกแบน/ผู้ชม) ตกขอบหมด = จบเกม all_dead + บอทอยู่ใน gr.bots ไม่ใช่ space.peers → ไม่ถูกนับอยู่แล้ว (server ตัดสินจากคนจริงเท่านั้น) + - นับเฉพาะ crown map · ต้องมีคนเล่นจริง ≥1 คน (กันจบทันทีตอน 0 คน) */ +function gauntletAllHumansEliminated(space) { + if (!isGauntletCrownHeistMapBySpace(space) || !space.peers || space.peers.size === 0) return false; + let active = 0; + let alive = 0; + space.peers.forEach((p) => { + if (!p || p.bannedSpectator) return; /* ผู้ถูกแบน = เข้ามาดู ไม่ได้เล่น → ไม่บล็อกการจบเกม */ + active++; + if (!p.gauntletEliminated) alive++; + }); + return active > 0 && alive === 0; +} + +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 หลังเริ่มคดี (กันช่องว่าง/ยูนิโค้ดต่างรูปแบบ) */ +/** ชื่อผู้เล่นสูงสุด — ต้องตรงกับ MAX_LEN ใน Game/public/js/display-name.js (2026-07-16: 24 → 10). + นับเป็น code unit เหมือน (ไทย: สระ/วรรณยุกต์นับแยก ไม่ใช่ 10 ตัวที่ตาเห็น). */ +const NICKNAME_MAX_LEN = 10; +/** ตัดชื่อให้อยู่ในลิมิต — server เป็นตัวบังคับจริง: client แก้ maxlength อย่างเดียวไม่พอ + (join-space เดิมรับ nickname ดิบ ไม่ cap เลย → ชื่อยาวเท่าไหร่ก็เข้าได้) */ +function clampNickname(n) { + let s = String(n == null ? '' : n).trim(); + try { s = s.normalize('NFKC'); } catch (e) { /* ignore */ } + s = s.replace(/[\x00-\x1f\x7f]/g, ''); + if (s.length > NICKNAME_MAX_LEN) s = s.slice(0, NICKNAME_MAX_LEN); + return s; +} + +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); +} + +/* spawnJoinOrder เสถียรข้ามการเปลี่ยนหน้า (lobbyA→lobbyB→minigame): จำลำดับด้วย playerKey (คงที่ต่อ browser) + ไม่ใช่ peers.size ที่เปลี่ยนตามลำดับ reconnect → ตำแหน่งเกิด / สีผิว / slot ของแต่ละคนสลับกันมั่ว */ +/** เลือก host คนถัดไปเมื่อ host ปัจจุบันหลุด — คนที่อยู่ในห้องนานสุด (spawnJoinOrder ต่ำสุด) + เสถียรข้ามการ reconnect (spawnJoinOrder ผูกกับ playerKey) — แทน Map-first ที่สลับได้ */ +function pickNextHostPeer(space) { + if (!space || !space.peers || space.peers.size === 0) return null; + let best = null; + let bestOrd = Infinity; + space.peers.forEach((p, id) => { + if (!p) return; + const ord = (typeof p.spawnJoinOrder === 'number' && p.spawnJoinOrder >= 0) ? p.spawnJoinOrder : 9999; + if (ord < bestOrd || (ord === bestOrd && (best == null || String(id) < String(best.id)))) { + bestOrd = ord; + best = p; + } + }); + return best; +} + +function resolveSpawnJoinOrder(space, playerKey) { + if (!space.spawnOrderByKey || typeof space.spawnOrderByKey !== 'object') space.spawnOrderByKey = {}; + const key = (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? playerKey : ''; + const used = new Set(); + space.peers.forEach((p) => { + if (typeof p.spawnJoinOrder === 'number' && p.spawnJoinOrder >= 0) used.add(p.spawnJoinOrder); + }); + /* host/ผู้เล่นเดิมกลับเข้ามา → คืนลำดับเดิมถ้ายังว่าง (ไม่มี peer อื่นถืออยู่) */ + if (key && Object.prototype.hasOwnProperty.call(space.spawnOrderByKey, key)) { + const prev = space.spawnOrderByKey[key]; + if (typeof prev === 'number' && prev >= 0 && !used.has(prev)) return prev; + } + let ord = 0; + while (used.has(ord)) ord++; + if (key) space.spawnOrderByKey[key] = ord; + return ord; +} + +/** สุ่มจุดเกิดในช่อง 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; +} + +/** เปิดเฟส "เลือกผู้ต้องสงสัย" ให้ทั้งห้อง — เรียกหลังตอบข้อเสนอตัวป่วน หรือเมื่อข้ามการเสนอ (คน < 4) */ +function openSuspectPhaseForRoom(sid, space, disruptorAccepted) { + if (!space) return; + 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: !!disruptorAccepted, + 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), + }); + }); +} + +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; + /* [TC190] ทางปกติเดินแล้ว → ปลด backstop (guard troublesomeOfferSent กันซ้ำอยู่แล้ว แต่เก็บให้สะอาด + เหมือน deb/max timer ข้างล่าง ไม่ให้ timer ค้างถือ ref ห้องไว้อีก 45 วิ) */ + if (space.troublesomeDeadlockTimer) { clearTimeout(space.troublesomeDeadlockTimer); space.troublesomeDeadlockTimer = null; } + /* เสนอบทตัวป่วนเฉพาะเมื่อมี "คนจริง ≥ 4 คน" — ยกเว้น admin บังคับ (troublesomeForceOffer) */ + const forceOffer = !!(runtimeGameTiming && runtimeGameTiming.troublesomeForceOffer); + if (peerIds.length < 4 && !forceOffer) { + /* ไม่เสนอบทตัวป่วน (คน < 4) → ข้ามไปเปิดหน้าเลือกผู้ต้องสงสัยเลย (ไม่มีตัวป่วน) */ + space.troublesomeOfferSent = true; + space.troublesomeResponseReceived = true; + space.troublesomeAccept = false; + space.disruptorPlayerKey = ''; + space.disruptorNickname = ''; + if (space.troublesomeDebTimer) clearTimeout(space.troublesomeDebTimer); + if (space.troublesomeMaxTimer) clearTimeout(space.troublesomeMaxTimer); + space.troublesomeDebTimer = null; + space.troublesomeMaxTimer = null; + openSuspectPhaseForRoom(sid, space, false); + 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: clampTroublesomeOfferSec(runtimeGameTiming && runtimeGameTiming.troublesomeOfferSec) }); +} + +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) { + const rollMaxMs = Math.max(1000, Math.round(clampTroublesomeRollMaxSec(runtimeGameTiming && runtimeGameTiming.troublesomeRollMaxSec) * 1000)); + space.troublesomeMaxTimer = setTimeout(() => attemptTroublesomeRoll(sid, true), rollMaxMs); + } +} + +/** + * ห้องทดสอบเอดิเตอร์ / 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')); +} + +/* ============================================================ + * ⚠️⚠️ DEAD CODE — ไม่มี client ตัวไหนเรียกใช้เลย (ตรวจ 2026-07-22) ⚠️⚠️ + * + * Quiz Battle (Kahoot/ZEP-style) — ห้องตอบคำถามพร้อมกัน รับได้สูงสุด 50 คน + * 10 ห้อง · auto-start (กด Start แล้วนับถอยหลังเริ่มเอง) · คำถามจาก battleQuizMcq + * แยกจากระบบ spaces (โลกเดิน) ใช้ io เดียวกัน · event ขึ้นต้น qb-* + * + * ── ทำไมถึงบอกว่าตายแล้ว ────────────────────────────────── + * Quiz Battle ที่ "ใช้จริง" คือแบบ **เดินตอบในแผนที่** (mapId `qbroom1..10`, gameType + * `quiz_battle`) ซึ่งอยู่ในระบบ spaces ปกติ + โค้ดฝั่ง client อยู่ใน play.js + * ส่วนบล็อกนี้เป็นดีไซน์เก่าคนละแบบ (Kahoot: ทุกคนตอบพร้อมกันเป็นรอบๆ) + * + * ยืนยันด้วย: grep ทั้ง repo หา client ที่ยิง event เหล่านี้ → **ไม่เจอเลยสักตัว** + * grep -rn "emit('qb-join'\|emit('qb-answer'\|emit('qb-start'" --include=*.js --include=*.html + * (ที่ Quiz-Battle/index.html แมตช์คำว่า qb- เป็นแค่ชื่อ CSS class เช่น qb-bg / qb-grid) + * + * ── ถ้าจะลบ ────────────────────────────────────────────── + * ลบตั้งแต่บรรทัดนี้ถึงก่อนคอมเมนต์บล็อกถัดไป (~233 บรรทัด) + ตัวคงที่ QB_* ด้านล่าง + * ⚠️ ระวัง: ตัวคงที่ชื่อ QB_ROOM_COUNT / QB_MAX_PLAYERS ประกาศในบล็อกนี้ + * ของใหม่ (เปิด/ปิดโหมด) จงใจใช้ prefix **QBM_** เพื่อไม่ให้ชนกัน — ลบบล็อกนี้แล้ว QBM_* ไม่กระทบ + * ยังไม่ลบเพราะ user ยังไม่ตัดสินใจ · เก็บไว้ก่อนไม่ได้เสียหาย (ไม่มีใครเรียก = ไม่กินทรัพยากร) + * ============================================================ */ +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 ที่ "ผ่านแล้ว" — ถูกหรือผิดก็ผ่าน · ใช้เปิดประตู) + * qbwScore = Map(socketId -> Set(compId ที่ "ตอบถูก" — ใช้เป็นคะแนน/อันดับ)) + * event: quizbattle-hello (ขอคะแนนปัจจุบัน) · quizbattle-solve {compId} · server ส่งกลับ quizbattle-scores + * ============================================================ */ +function qbwScoresSnapshot(space) { + const scores = {}; + /* [2026-07-31] คะแนน = เฉพาะข้อที่ "ตอบถูก" (qbwScore) — ส่วน qbwSolved คือ "ผ่านโดมแล้ว" + ซึ่งรวมข้อที่ตอบผิดด้วย (ใช้เปิดประตูให้ตรงกับฝั่ง client ที่ถือว่าตอบครั้งเดียวจบ) + client เก่าที่ไม่ส่ง correct มาจะไม่มี qbwScore → ตกกลับไปใช้ qbwSolved เหมือนเดิม */ + const src = (space && space.qbwScore) || (space && space.qbwSolved); + if (src) { + for (const [pid, set] of src) 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 global leaderboard (คะแนนสูงสุดต่อ room/หมวด ข้ามเซสชัน) ===== */ + socket.on('qb-leaderboard-submit', (d) => { + if (!d || typeof d !== 'object') return; + qbLeaderboardSubmit(d.roomId, d.name, d.score, d.characterId); + }); + socket.on('qb-leaderboard-get', (d, cb) => { + if (typeof cb !== 'function') return; + try { cb(qbLeaderboardGet(d && d.roomId, d && d.name)); } catch (e) { cb({ top: [], me: null, total: 0 }); } + }); + /* ===== 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); /* ผ่านโดมแล้ว (ถูกหรือผิดก็ผ่าน) → ใช้เปิดประตู */ + /* [2026-07-31] คะแนนนับเฉพาะข้อที่ตอบถูก · ไม่ส่ง correct มา = client เก่า ถือว่าถูก (พฤติกรรมเดิม) */ + const correct = !(data && data.correct === false); + if (!space.qbwScore) space.qbwScore = new Map(); + let cset = space.qbwScore.get(socket.id); + if (!cset) { cset = new Set(); space.qbwScore.set(socket.id, cset); } + if (correct) cset.add(compId); + qbwBroadcastScores(sid, space); + reply({ ok: true, solved: set.size, score: cset.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, playerKey, desiredLobbyColorThemeIndex, desiredLobbySkinToneIndex, bannedSpectator, roomPassword }, cb) => { + const space = spaces.get(spaceId); + if (!space || !space.mapData) return cb && cb({ ok: false, error: 'ไม่พบห้อง' }); + /* ห้องส่วนตัวที่ตั้งรหัส → ต้องกรอกรหัสให้ตรง ครั้งแรก แล้วจำด้วย playerKey + (กันด่านรหัสพังตอนเปลี่ยนหน้า LobbyA→play→LobbyB ที่ socket.id เปลี่ยน — ไม่ต้องแนบ pass ทุกลิงก์) */ + if (space.isPrivate && space.roomPassword) { + space.privateVerifiedKeys = space.privateVerifiedKeys || new Set(); + const pk = String(playerKey == null ? '' : playerKey); + const provided = String(roomPassword == null ? '' : roomPassword).replace(/\D/g, '').slice(0, 4); + const passOk = provided === space.roomPassword; + const alreadyOk = (pk && space.privateVerifiedKeys.has(pk)) || space.peers.has(socket.id); + if (!passOk && !alreadyOk) { + return cb && cb({ ok: false, error: 'รหัสผ่านห้องไม่ถูกต้อง' }); + } + if (passOk && pk) space.privateVerifiedKeys.add(pk); + } + let maxPlayers = space.maxPlayers || 10; + /* [2026-07-23] ห้อง Quiz Battle: ยึด "จำนวนคนต่อห้อง" จาก config เป็นหลัก (แอดมินปรับได้ ≤50) + ไม่ยึดค่าที่ตั้งตอนสร้างห้อง — แอดมินลดเพดานระหว่างที่ห้องเปิดอยู่ ก็มีผลทันที (ห้องเดิมด้วย) + set กลับเข้า space.maxPlayers ให้ด่านเช็คที่นั่งด้านล่าง + HUD อ่านค่าตรงกัน */ + { + const qbNoCap = quizBattleRoomNumberOf(space); + if (qbNoCap > 0) { maxPlayers = quizBattleRoomMaxPlayers(qbNoCap); space.maxPlayers = maxPlayers; } + } + 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) { + /* rejoin whitelist: เช็ค playerKey เป็นหลัก (กันเคส 2 คนชื่อซ้ำ — ลบ nick คนนึงตอนออก ทำให้อีกคน rejoin ไม่ได้) + fallback เป็นชื่อ (คนที่ไม่มี playerKey / ความเข้ากันได้เดิม) */ + const jk = (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? ('k:' + playerKey) : ''; + /* [2026-07-23] ใช้ roster "ถาวร" (ไม่โดน prune) เป็นหลัก — คนที่อยู่ตอนเริ่มคดีต้องกลับเข้าได้เสมอ + fallback เป็น roster ตัวนับ สำหรับห้องที่เริ่มคดีไว้ก่อน restart (ยังไม่มี field ใหม่) */ + const wlKeys = (space.caseRosterEverKeys && space.caseRosterEverKeys.size > 0) + ? space.caseRosterEverKeys : space.caseParticipantKeys; + const wlNicks = (space.caseRosterEverNicknames && space.caseRosterEverNicknames.size > 0) + ? space.caseRosterEverNicknames : space.caseParticipantNicknames; + const okByKey = !!(jk && wlKeys && wlKeys.size > 0 && wlKeys.has(jk)); + const okByNick = !!(joinNickNorm && wlNicks && wlNicks.size > 0 && wlNicks.has(joinNickNorm)); + if (!okByKey && !okByNick) { + return cb && cb({ ok: false, error: 'ห้องนี้เริ่มคดีแล้ว ไม่รับผู้เล่นเพิ่ม' }); + } + } + /* [2026-07-22] ประตูโหมด Quiz Battle — แอดมินปิดโหมด/หมดเวลาแล้ว ต้องเข้าไม่ได้ + บังคับที่นี่ด้วย ไม่ใช่แค่ล็อกปุ่มฝั่งหน้าเว็บ (ยิง socket ตรงก็ต้องโดนปฏิเสธ) */ + { + const qbNo = quizBattleRoomNumberOf(space); + if (qbNo > 0) { + const st = quizBattleRoomState(qbNo); + if (!st.open) { + try { glog(spaceId, space, 'qb-mode-gate', 'BLOCK เข้าห้อง QB ' + qbNo + ' — ' + st.reason); } catch (e) { /* ignore */ } + return cb && cb({ ok: false, error: st.reason || 'โหมดนี้ปิดอยู่', qbClosed: true, room: qbNo }); + } + } + } + socket.join(spaceId); + socket.data.spaceId = spaceId; + /* Host เสถียรข้ามการเปลี่ยนหน้า (lobby→play→lobbyB): จำ host ด้วย playerKey (คงที่ต่อ browser) + ไม่ใช่ socket.id (เปลี่ยนทุกครั้งที่โหลดหน้าใหม่ → host สลับไปมาแบบสุ่ม) */ + const joinPlayerKey = (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? playerKey : ''; + const prevHostId = space.hostId; + /* [2026-07-22] Quiz Battle = **server เป็นเจ้าของห้อง ไม่ตั้งผู้เล่นเป็น host** + ห้อง QB ไม่มี action ไหน gate ด้วย host อยู่แล้ว (ต่างจาก detective ที่ host คุมเริ่มคดี/ชี้ตัว) + ปล่อยให้คนแรกเป็น host = ห้อง 50 คนขึ้นกับคนคนเดียว + host ย้ายไปมาทุกครั้งที่คนออก โดยไม่จำเป็น */ + const joinIsQuizBattle = quizBattleRoomNumberOf(space) > 0; + if (joinIsQuizBattle) { + space.hostId = null; + space.hostPlayerKey = ''; + space.serverOwnedRoom = true; + } else if (joinPlayerKey && space.hostPlayerKey && joinPlayerKey === space.hostPlayerKey) { + space.hostId = socket.id; /* host ตัวจริงกลับเข้ามา → คืนสถานะ host เสมอ */ + } else if (!space.hostId) { + space.hostId = socket.id; /* ยังไม่มี host → คนนี้เป็น host (ชั่วคราวถ้ามี hostPlayerKey อยู่แล้ว) */ + if (!space.hostPlayerKey && joinPlayerKey) space.hostPlayerKey = joinPlayerKey; + } + /* reconnect/เปลี่ยนหน้า: ผู้เล่นคนเดิม (playerKey เดิม) ได้ socket.id ใหม่ แต่ peer เก่ายังค้างใน space.peers + จนกว่า disconnect เก่าจะมา (อาจช้า) → ทุกคนเห็น "ตัวละครซ้ำ 2 ตัว" (ghost) + แก้: ลบ ghost peer ที่ playerKey ตรงแต่ socket.id ต่าง ก่อนเพิ่มตัวใหม่ + แจ้ง user-left ให้ client ลบทิ้ง */ + if (joinPlayerKey) { + const staleIds = []; + space.peers.forEach((p, pid) => { + if (pid !== socket.id && p && p.playerKey && p.playerKey === joinPlayerKey) staleIds.push(pid); + }); + staleIds.forEach((pid) => { + space.peers.delete(pid); + if (space.quizCarryLobbyReady) delete space.quizCarryLobbyReady[pid]; + if (space.gauntletCrownLobbyReady) delete space.gauntletCrownLobbyReady[pid]; + io.to(spaceId).emit('user-left', { id: pid }); + }); + /* กลับมาทัน grace 10 วิ → ยกเลิก bot takeover + ปิด banner countdown */ + if (space.graceReconnect && space.graceReconnect[joinPlayerKey]) { + delete space.graceReconnect[joinPlayerKey]; + if (space.graceTimers && space.graceTimers[joinPlayerKey]) { clearTimeout(space.graceTimers[joinPlayerKey]); delete space.graceTimers[joinPlayerKey]; } + io.to(spaceId).emit('peer-grace-cancel', { nickname: nickname || 'ผู้เล่น' }); + } + /* MG1 takeover: ผู้เล่นกลับมากลาง quiz (playerKey เดิม) → ถอนบอทที่แปลงจากเขา (sess.bots) คืนสิทธิ์เล่น + กัน "ตัวซ้ำ" (บอท takeover + ตัวที่ rejoin) */ + const sessJ = space.quizSession; + if (sessJ && sessJ.bots) { + const dropBotIds = []; + sessJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropBotIds.push(bid); }); + dropBotIds.forEach((bid) => { + sessJ.bots.delete(bid); + io.to(spaceId).emit('user-left', { id: bid }); /* ลบ avatar บอท takeover เก่า — ตัวจริงจะมาใหม่ผ่าน user-joined */ + }); + } + /* MG2 takeover เช่นกัน: ผู้เล่นกลับมากลาง gauntlet (playerKey เดิม) → ถอนบอทที่แปลงจากเขา (gauntletRun.bots) */ + const grJ = space.gauntletRun; + if (grJ && grJ.bots) { + const dropG = []; + grJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropG.push(bid); }); + dropG.forEach((bid) => { + grJ.bots.delete(bid); + io.to(spaceId).emit('user-left', { id: bid }); + }); + } + /* MG3 takeover เช่นกัน: กลับมากลาง stack (playerKey เดิม) → ถอนบอท + คืน seat ให้คนจริง (inherit คะแนนกลับ) */ + const s3J = space.stackSession; + if (s3J && s3J.bots) { + let restored = false; + const dropS = []; + s3J.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropS.push(bid); }); + dropS.forEach((bid) => { + const inherited = Math.max(0, ((s3J.bots.get(bid) || {}).score) | 0); + s3J.bots.delete(bid); + const entry = (s3J.turnOrder || []).find((e) => e.kind === 'bot' && String(e.botId) === String(bid)); + if (entry) { entry.kind = 'human'; entry.id = socket.id; entry.botId = null; s3J.humanPts[socket.id] = inherited; restored = true; } + io.to(spaceId).emit('user-left', { id: bid }); + }); + if (restored) emitStackStateServer(spaceId, space); + } + /* MG4 takeover เช่นกัน: กลับมากลาง quiz_carry (playerKey เดิม) → ถอนบอท + คืน slot ให้คนจริง (inherit ป้าย/คะแนนกลับ) */ + const sCJ = space.quizCarrySession; + if (sCJ && sCJ.bots) { + const dropC = []; + sCJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropC.push(bid); }); + dropC.forEach((bid) => { + const bb = sCJ.bots.get(bid) || {}; + sCJ.bots.delete(bid); + sCJ.players[socket.id] = { held: (bb.held == null ? null : bb.held), score: Math.max(0, Number(bb.score) || 0) }; + io.to(spaceId).emit('user-left', { id: bid }); + }); + } + /* MG5 takeover เช่นกัน: กลับมากลาง jump_survive (playerKey เดิม) → ถอนบอท + คืน slot คนจริง (inherit สถานะตาย) */ + const sJJ = space.jumpSurviveSession; + if (sJJ && sJJ.bots) { + const dropJ = []; + sJJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropJ.push(bid); }); + dropJ.forEach((bid) => { + const bb = sJJ.bots.get(bid) || {}; + sJJ.bots.delete(bid); + sJJ.players[socket.id] = { eliminated: !!bb.eliminated }; + io.to(spaceId).emit('user-left', { id: bid }); + }); + } + /* MG6 takeover: กลับมากลาง space_shooter (playerKey เดิม) → ถอนบอท + คืน slot คนจริง (inherit คะแนน/ตาย) */ + const sSJ = space.spaceShooterSession; + if (sSJ && sSJ.bots) { + const dropSS = []; + sSJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropSS.push(bid); }); + dropSS.forEach((bid) => { + const bb = sSJ.bots.get(bid) || {}; + sSJ.bots.delete(bid); + sSJ.players[socket.id] = { eliminated: !!bb.eliminated }; + io.to(spaceId).emit('user-left', { id: bid }); + }); + } + /* MG7 takeover: กลับมากลาง balloon_boss (playerKey เดิม) → ถอนบอท + คืน slot คนจริง */ + const sBJ = space.balloonBossSession; + if (sBJ && sBJ.bots) { + const dropBB = []; + sBJ.bots.forEach((b, bid) => { if (b && b.takeover && b.takeoverPlayerKey && b.takeoverPlayerKey === joinPlayerKey) dropBB.push(bid); }); + dropBB.forEach((bid) => { sBJ.bots.delete(bid); io.to(spaceId).emit('user-left', { id: bid }); }); + } + /* MG3–MG7 pending takeover (คนหลุดช่วง how-to/นับถอยหลัง — ยังไม่มี session live): กลับมาทันก่อนเกมเริ่ม + → ล้าง pending ที่ playerKey ตรง กัน drainPendingMissionTakeovers seed เป็นบอทซ้ำตอน startXxxGame + ROOT CAUSE "บอทเข้าไปเล่นด้วย MG3/5/7": rejoin เดิมถอนบอทเฉพาะ session ที่ live แล้ว (บล็อกข้างบน) + แต่ shell (MG3-7) สร้าง session ตอน 3-2-1 จบ → ตอน rejoin ยังไม่มี session = ไม่มีอะไรถอน + + pending ค้าง → seed เป็น "บอทผี" คู่กับคนจริงที่ rejoin. emit user-left(id เก่า) ลบ avatar บอท placeholder */ + if (Array.isArray(space.pendingMissionTakeovers) && space.pendingMissionTakeovers.length) { + const cancelled = space.pendingMissionTakeovers.filter((r) => r && r.playerKey && r.playerKey === joinPlayerKey); + if (cancelled.length) { + space.pendingMissionTakeovers = space.pendingMissionTakeovers.filter((r) => !(r && r.playerKey && r.playerKey === joinPlayerKey)); + cancelled.forEach((r) => { if (r.id) io.to(spaceId).emit('user-left', { id: r.id }); }); + glog(spaceId, space, 'join', '[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover ' + cancelled.length + ' (กันบอทผี MG3/5/7 · pass)', nickname); + } + } + } + /* reconnect dedup ชั้น 2: ghost ที่ playerKey block จับไม่ได้ (peer เก่าไม่มี playerKey) + ลบด้วย "ชื่อ custom เดียวกัน" — แต่ **เฉพาะ peer เก่าที่ไม่มี valid playerKey** เท่านั้น + ⚠️ ห้ามลบ peer ที่มี playerKey คนละค่า: 2 คนตั้งชื่อเหมือนกัน (เช่น "MONE") = คนละคน ต้องเก็บทั้งคู่ + (เดิมลบทุกตัวที่ชื่อตรง → client ที่ชื่อซ้ำ host ถูกลบ = "ไม่ได้เล่นเลย") */ + const validPkStr = (p) => (p && typeof p.playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(p.playerKey)) ? p.playerKey : ''; + const newNickNorm = normalizeLobbyNickname(nickname); + const newNickIsCustom = newNickNorm && newNickNorm !== 'ผู้เล่น' + && newNickNorm.indexOf('ผู้เล่น') !== 0 && newNickNorm.indexOf('บอท') !== 0; + if (newNickIsCustom) { + const ghostIds = []; + space.peers.forEach((p, pid) => { + if (pid === socket.id || !p) return; + const ppk = validPkStr(p); + if (ppk && ppk !== joinPlayerKey) return; /* คนละ playerKey = คนละคน (ชื่อซ้ำได้) → ไม่ลบ */ + if (normalizeLobbyNickname(p.nickname) === newNickNorm) ghostIds.push(pid); + }); + ghostIds.forEach((pid) => { + space.peers.delete(pid); + if (space.quizCarryLobbyReady) delete space.quizCarryLobbyReady[pid]; + if (space.gauntletCrownLobbyReady) delete space.gauntletCrownLobbyReady[pid]; + io.to(spaceId).emit('user-left', { id: pid }); + }); + } + if (serverMapIsPostCaseLobbyB(space)) initTroublesomeState(space); + ensurePeerLobbyThemes(space); + const mdJoin = (space.mapId && maps.get(space.mapId)) || space.mapData; + const spawnJoinOrder = resolveSpawnJoinOrder(space, playerKey); + const spawnPt = pickSpawnForJoin(mdJoin, spawnJoinOrder); + const bbStartBalloons = balloonBossBalloonsForMap(mdJoin); + /* ใช้สีที่ 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 = { + /* [2026-07-16] cap ชื่อตรงนี้ด้วย — เดิมรับค่าดิบจาก client: แก้ maxlength ฝั่ง client อย่างเดียว + ไม่ได้บังคับอะไรเลย (join-space เป็นทางเข้าหลักของชื่อ ไม่ใช่หน้าแก้ชื่อ) */ + id: socket.id, x: +spawnPt.x, y: +spawnPt.y, direction: 'down', nickname: clampNickname(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, + playerKey: (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? playerKey : '', + reportedMiniScore: 0, reportedMiniSurvived: true, + bannedSpectator: !!bannedSpectator, + netReady: false, /* เพิ่ง join → กำลังโหลด จนกว่า client ส่ง client-ready (badge ให้คนอื่นเห็นว่า "กำลังโหลด" ไม่ใช่ค้าง) */ + }; + 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); + /* เก็บกวาด peer ซ้ำทั้งก้อน (ghost) — เผื่อ dedup ต่อ-join ด้านบนพลาด/race จนคนเดิมค้าง 2 ตัว */ + pruneDuplicateSpacePeers(spaceId, space); + /* [2026-07-23] คู่กับ roster ถาวร (caseRosterEver*): คนในคดีที่กลับเข้ามา ต้องใส่คืน "roster ตัวนับ" ด้วย + ไม่งั้น gate TC144 นับ present ไม่เจอ → ค้าง "รอผู้เล่นกลับเข้าห้องให้ครบก่อน (0/1)" host กดเริ่มไม่ได้ + (เดิมไม่เจอเพราะคนที่ถูก prune จะโดนเด้งออกจากห้องไปเลย ตัวนับเลยไม่ค้าง — พอปล่อยให้ rejoin ได้ + แต่ไม่คืนชื่อเข้าตัวนับ ก็ deadlock อีกแบบ) + ลบรูปเก่าของคนเดียวกันก่อน ('k:' และ 'n:') กันนับเป็น 2 คน */ + if (space.joinLocked && space.caseRosterEverKeys && space.caseRosterEverKeys.size > 0) { + const everKey = joinPlayerKey ? ('k:' + joinPlayerKey) : ''; + const everNick = joinNickNorm ? ('n:' + joinNickNorm) : ''; + const isCaseMember = !!((everKey && space.caseRosterEverKeys.has(everKey)) + || (joinNickNorm && space.caseRosterEverNicknames && space.caseRosterEverNicknames.has(joinNickNorm))); + if (isCaseMember) { + if (!space.caseParticipantKeys) space.caseParticipantKeys = new Set(); + if (!space.caseParticipantNicknames) space.caseParticipantNicknames = new Set(); + if (everKey) space.caseParticipantKeys.delete(everKey); + if (everNick) space.caseParticipantKeys.delete(everNick); + const curKey = detectiveParticipantKey(peer); + if (curKey) space.caseParticipantKeys.add(curKey); + if (joinNickNorm) space.caseParticipantNicknames.add(joinNickNorm); + try { emitDetectiveLobbyBPresence(spaceId, space); } catch (ePr) { /* ignore */ } + } + } + glog(spaceId, space, 'join', 'สี#' + (peer.lobbyColorThemeIndex || '?') + ' · คน ' + space.peers.size + ' + บอท ' + effectiveBotSlotCount(space) + (joinPlayerKey ? '' : ' · NO-KEY'), nickname || 'ผู้เล่น'); + /* invariant: คน + บอท ≤ 6 — กัน "เงาสะท้อนของ user": คนหลุดแล้ว grace แปลงเป็นบอท (botSlotCount+1, สีตามคนออก) + พอคนนั้นกลับเข้ามา (reconnect หลัง grace) จะมีทั้งคนจริง + บอทสีเดียวกัน = ตัวซ้ำ. ตัดบอทส่วนเกินออกให้รวมไม่เกิน 6 + (มินิเกม cap ที่ 6-humans อยู่แล้ว ~rebalancePreviewBots แต่ LobbyB ไม่ได้ cap → ตัดที่ server ต้นทาง) */ + { + const humansNow = space.peers.size; + const botsNow = effectiveBotSlotCount(space); + if (humansNow + botsNow > LOBBY_SLOT_TOTAL) { + space.botSlotCount = Math.max(0, LOBBY_SLOT_TOTAL - humansNow); + } + /* re-sync สีบอททุกครั้งที่มีคน (re)join + แจ้ง "ทุกจอ" (รวมคนที่อยู่ในมินิเกม/LobbyB อยู่แล้ว): + เคส "เงาสะท้อนของ user" = คนหลุด→บอทใช้สีคนนั้น (pin)→คนนั้นกลับมา. join handler เคลียร์ pin ที่ชนสีคนแล้ว + (7704) + syncLobbyBotThemeSlots เลือกสีใหม่ที่ไม่ชนคน แต่เดิม emit เฉพาะตอน total>6 → จออื่นไม่อัปเดต = บอทยังสีเดิม. + emit ทุก join → บอทเปลี่ยนสีพ้นจากสี user ทันทีทุกจอ */ + emitLobbyTintSync(spaceId, space); + } + /* กลับ LobbyB เฉพาะเมื่อไม่ได้เข้า play ฉากมินิเกมที่กำลังเล่น (เช่น refresh room-lobby) + — แต่ "เฉพาะตอนไม่มีคนอื่นเล่นค้างอยู่": เดิมดึง "ทั้งห้อง" ออกจากมินิเกมเพราะคนเดียวที่ rejoin มา + ด้วย map ไม่ตรง (คนหลุดกลางเกมแล้ว reconnect เข้า room-lobby) → คนที่เหลือโดนตัดกลางเกม, client + ค้างไม่ส่ง lobbyb-loaded → gate ค้าง = ทั้งห้องเล่นต่อไม่ได้ (เคส 2026-07-15 18:48). + ถ้ายังมีคนอื่นเล่นอยู่ → ปล่อยเกมเดินต่อ, คนที่ rejoin รอจนจบรอบแล้ว return ปกติจะดึงเข้า LobbyB เอง */ + if (space.detectiveMinigameActive) { + const clientMap = playMapId != null ? String(playMapId).trim() : ''; + const activeMap = space.mapId != null ? String(space.mapId).trim() : ''; + if (!clientMap || !activeMap || clientMap !== activeMap) { + const othersInRun = Math.max(0, space.peers.size - 1); /* คนอื่นที่ยังอยู่ในรอบมินิเกม (ไม่นับคนที่เพิ่ง join) */ + if (othersInRun === 0) { + returnDetectiveSpaceToLobbyB(spaceId, space, 'กลับ LobbyB'); + } else { + try { glog(spaceId, space, 'lobbyb-gate', '[TC144] rejoin map ไม่ตรง (' + (clientMap || 'none') + '≠' + activeMap + ') แต่ยังมีคนเล่นอยู่ ' + othersInRun + ' คน → ไม่ดึงทั้งห้องออก (pass)', nickname || 'ผู้เล่น'); } catch (e) { /* ignore */ } + } + } + } + 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', quizCarryLobbySyncPayload(space)); + } + if (mdJoin.gameType === 'gauntlet') { + const ord = typeof peer.spawnJoinOrder === 'number' ? peer.spawnJoinOrder : [...space.peers.keys()].indexOf(socket.id); + /* นับช่อง spawn ให้พอกับ "คน + บอท" — เดิมส่งแค่ peers.size: ห้อง 1 คน = 1 ช่อง = pos[0] (เลนบนสุด) เสมอ + แล้วไปทับบอทที่ initGauntletPeersAll วางไว้ที่ pos[0] → คน(host ใหม่)+บอท วิ่งเลนเดียวกันเลนบนสุด (bug ที่เจอซ้ำ) */ + const grJoin = space.gauntletRun; + const botCntJoin = (grJoin && grJoin.bots) ? grJoin.bots.size : 0; + const pos = gauntletSpawnPositions(mdJoin, space.peers.size + botCntJoin); + /* เลี่ยงทับทั้ง "peer อื่น" และ "บอท" ที่อยู่ในเลนแล้ว — ดันไปช่องว่างถัดไป (เดิมเช็คแต่ peer ไม่เช็คบอท) */ + const occupied = new Set(); + space.peers.forEach((q) => { if (q && q.id !== socket.id) occupied.add(Math.floor(q.x) + ',' + Math.floor(q.y)); }); + if (grJoin && grJoin.bots) grJoin.bots.forEach((b) => { if (b) occupied.add(Math.floor(Number(b.x)) + ',' + Math.floor(Number(b.y))); }); + let pidx = Number.isFinite(ord) ? Math.max(0, Math.min(ord, pos.length - 1)) : pos.length - 1; + let pguard = 0; + while (pos[pidx] && occupied.has(Math.floor(pos[pidx].x) + ',' + Math.floor(pos[pidx].y)) && pguard < pos.length) { pidx = (pidx + 1) % pos.length; pguard++; } + const pt = pos[pidx] != null ? pos[pidx] : 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', crownLobbySyncPayload(space)); + } + 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', crownLobbySyncPayload(space)); + } + 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), + suspectTeamProgress: Array.isArray(space.suspectTeamProgress) ? space.suspectTeamProgress.slice() : [0, 0, 0], /* #6 shared */ + botSlotCount: effectiveBotSlotCount(space), + lobbyBotThemes: syncLobbyBotThemeSlots(space), + joinLocked: !!space.joinLocked, + /* [TC154] host กำลังเปิด wizard เลือกระดับ/คดี = set-ready ถูก "ปฏิเสธฝั่ง server" (12391). + เดิมไม่ได้ส่งสถานะนี้ใน join ack → คนที่เพิ่งเข้ามาไม่รู้ว่าโดนล็อก: ปุ่มพร้อมดูกดได้ปกติ + แต่กดแล้ว server ตีกลับเงียบ ๆ = "กดไม่ติด ไม่มีอะไรบอก" (คนละอันกับ relay lobby-preplay-open + ที่ส่งเฉพาะตอน host กด เปิด/ปิด — คนเข้าทีหลังไม่เคยได้ยิน) */ + preplayOpen: !!space.preplayOpen, + missionLiveBeginsAt: space.missionLiveBeginsAt || null, + missionLive: isMinigameLiveForRejoinResume(space), /* rejoin กลางเกม live → client ข้าม how-to เข้าเล่นต่อ */ + }; + if (space.jsLayout) joinCb.jsLayout = space.jsLayout; /* MG5: ส่ง pattern ที่สุ่มไว้ให้ client (initial + late-join) */ + 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); + /* กันค้างตอน reconnect: event "จบมินิเกม" (balloon-boss-over/stack-over/space-shooter-over) + host ยิงครั้งเดียวผ่าน socket.to — ถ้า client หลุดตอนนั้นจะค้างถาวร (ไม่มี replay). + เก็บ over ล่าสุดไว้ → resend ให้ socket ที่เพิ่ง (re)join ภายใน 45s ถ้ายังอยู่ในแมปมินิเกม + (gameType !== 'lobby' = ยังไม่ทรานซิชันกลับ). delay เล็กน้อยให้ client applyMapAndStart เสร็จก่อน */ + const lmo = space.lastMinigameOver; + if (lmo && lmo.event && (Date.now() - lmo.atMs) < 45000 && mapDataOut && mapDataOut.gameType !== 'lobby') { + const targetSid = socket.id; + setTimeout(() => { + try { + const sp = spaces.get(spaceId); + if (sp && sp.lastMinigameOver === lmo && io.sockets.sockets.get(targetSid)) { + io.to(targetSid).emit(lmo.event, lmo.payload); + } + } catch (_e) { /* ignore */ } + }, 400); + } + if (space.hostId !== prevHostId) io.to(spaceId).emit('host-changed', { hostId: space.hostId }); /* host เปลี่ยน (เช่น host ตัวจริงกลับเข้ามา) → sync ทุกคน */ + 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 }); + } + } + /* Card 3 Ban: ผู้เล่นกลับเข้า LobbyB หลังเกมจบ + มีการ์ด Ban ค้าง → เริ่มโหวตแบนทันที + (คน rejoin คนแรกเป็นคนเปิดโหวต; คนที่ rejoin ทีหลังขณะโหวตเปิดอยู่ → ส่งสถานะให้เห็นด้วย) */ + if (serverMapIsPostCaseLobbyB(space)) { + /* จับเวลา rejoin ล่าสุด → maybeOpenPendingBanVote รอ socket "นิ่ง" 2s ก่อนเปิดโหวต (กันเปิดกลาง transition) */ + space._lastLobbyBRejoinAt = Date.now(); + /* ผู้เล่นกลับเข้า LobbyB แล้ว → อัปเดตจำนวนคนที่พร้อมให้ทุกคน (host ใช้เปิดปุ่มเริ่มเมื่อครบ) */ + emitDetectiveLobbyBPresence(spaceId, space); + console.log('[ban-debug] join-space rejoin: postLobbyB=true pendingBan=', !!space.pendingBanVoteCard, 'pickVote=', !!space.pickVote, 'by', socket.id); + if (space.pendingBanVoteCard && !space.pickVote) { + /* เปิดโหวต Ban เฉพาะเมื่อทุกคนกลับเข้า LobbyB ครบ (ไม่ใช่หน่วงเวลาคงที่ตอนคนยังเข้าไม่ครบ) + — maybeOpenPendingBanVote เช็ค present/total + safety timeout เอง */ + maybeOpenPendingBanVote(spaceId, space); + } else if (space.pickVote && !space.pickVote.resolved) { + const pv = space.pickVote; + /* คนเพิ่ง reconnect ระหว่างโหวตยังเปิด (LobbyB transition ยังไม่นิ่งตอนเปิดโหวต) → เดิมไม่อยู่ใน + candidate snapshot = "เล่น 4 คนแต่โหวตได้ 3". rebuild candidates จาก peers ปัจจุบัน แล้ว + broadcast ให้ทุกคนเห็นครบ (client เก็บ selection เดิมไว้เมื่อ purpose เดิม) */ + pv.candidates = pickVoteCandidates(space); + autoVotePickBots(space); + const pvPayload = { purpose: pv.purpose, candidates: pv.candidates.map(pickVoteCandidatePayload), endsAt: pv.endsAt, durationMs: pickVoteMs() }; + space.peers.forEach((_p, pid) => { const sk = io.sockets.sockets.get(pid); if (sk) sk.emit('player-pick-vote-open', pvPayload); }); + emitPickVoteUpdate(spaceId, space); + } + } + }); + + /** ผู้เล่นเดินชนไอคอนคำถามพิเศษ — ใครชนก่อนเป็นคนเปิดเซสชันให้ทั้งห้อง */ + /** 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); + }); + + /** Test Mode (Ctrl+Alt+W): บังคับให้รอบโหวตชี้คนร้ายถัดไป "นับเป็นโหวตผิด" 1 ครั้ง → ใช้เทสต์การ์ด 5 (จับผิดตัว/โหวตใหม่) */ + socket.on('test-force-wrong-vote', () => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !serverMapIsPostCaseLobbyB(space) || !space.peers.has(socket.id)) return; + space.testForceWrongVote = true; + console.log('[test] force-wrong-vote armed by', socket.id); + }); + + /** client แจ้งว่าเกม "live จริง" แล้ว (พ้น howto/นับถอยหลัง) → ปล่อยไอคอนคำถามพิเศษ (หน่วง 2 วิ + นับถอยหลัง) ครั้งเดียว/รอบ */ + socket.on('special-quiz-arm', () => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !space.peers.has(socket.id)) return; + const sq = space.specialQuiz; + if (!sq || sq.triggered || sq.done || sq.appeared || sq.armScheduled) return; + sq.armScheduled = true; + scheduleSpecialQuizIconAppearAndExpiry(sid, space); + }); + + 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); + /* [achv b5] คนที่แตะไอคอน ตำรวจ/ทนาย = คนเก็บไอเทมช่วยเหลือ (trigger ครั้งเดียว/ไอคอน) */ + if (started) { + const bp = space.peers.get(socket.id); + if (bp && bp.playerKey) submitAchievementProgress([{ playerKey: bp.playerKey, id: 'b5_deep_scanner', inc: 1 }]); + } + 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 }); + /* รอบจบไปแล้ว (resume แล้ว) — ตอบ ok เฉย ๆ */ + if (!space.specialQuizAwaitContinue) return reply({ ok: true }); + /* เก็บ ack ของผู้เล่นคนนี้ แล้วเช็ค: ครบทุกคน → resume พร้อมกัน (ไม่ resume ทันทีคนเดียว) */ + if (!space.specialQuizContinueAcks) space.specialQuizContinueAcks = new Set(); + space.specialQuizContinueAcks.add(socket.id); + reply({ ok: true }); + maybeFinishSpecialQuizContinue(sid, space); + }); + + /** เปลี่ยนสีเสื้อ/ผิวใน 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; + } + /* ชนกับ "คนจริง" คนอื่น → ปฏิเสธ; ชนกับ "บอท" เท่านั้น → ดันบอทไปสีอื่น (ให้สอดคล้องกับตอน join) + กันอาการ "เปลี่ยนสีไม่ได้" เมื่อบอทยึดสีไว้ */ + let humanTaken = false; + space.peers.forEach((op, oid) => { + if (oid === socket.id) return; + if (parseInt(op.lobbyColorThemeIndex, 10) === themeIndex) humanTaken = true; + }); + if (humanTaken) return reply({ ok: false, error: 'สีเสื้อนี้มีคนใช้แล้ว' }); + if (space.lobbyBotThemeBySlot && typeof space.lobbyBotThemeBySlot === 'object') { + Object.keys(space.lobbyBotThemeBySlot).forEach((k) => { + const slot = space.lobbyBotThemeBySlot[k]; + if (slot && parseInt(slot.themeIndex, 10) === themeIndex) slot.themeIndex = 0; /* syncLobbyBotThemeSlots จะหาสีใหม่ให้ */ + }); + } + peer.lobbyColorThemeIndex = themeIndex; + peer.lobbySkinToneIndex = skinIndex; + emitLobbyTintSync(sid, space); + reply({ + ok: true, + lobbyColorThemeIndex: themeIndex, + lobbySkinToneIndex: skinIndex, + lobbyBotThemes: space.lobbyBotThemeBySlot ? syncLobbyBotThemeSlots(space) : [], + }); + }); + + /* บอท lobby เดินตรงกันทุก client: host เป็นเจ้าของตำแหน่ง → relay ให้คนอื่นในห้อง + (กัน desync จากการที่แต่ละเครื่องสุ่มเดินบอทแยกกัน) */ + /* DEPRECATED: server เป็นเจ้าของบอท lobby เองแล้ว (stepLobbyBotsForSpace/lobbyBotSweepTick) + → ไม่ relay ตำแหน่งบอทจาก client อีก (กัน host เวอร์ชันเก่าฉีดตำแหน่งชนกับ server tick = บอทกระตุก/2 แหล่ง) */ + socket.on('lobby-bot-move', () => { /* no-op: server-authoritative */ }); + + /* fill-bot ใน 7 มินิเกม: host เป็นเจ้าของ state ของบอท → relay ให้คนอื่นในห้อง + (กัน desync จากการที่แต่ละเครื่องจำลองบอทแยกกัน) */ + socket.on('fill-bot-state', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || space.hostId !== socket.id) return; /* เฉพาะ host เท่านั้น */ + if (!data || !Array.isArray(data.bots) || data.bots.length > 16) return; + /* พ่วง hostId — ผู้รับ (non-host เท่านั้น เพราะ socket.to ไม่ส่งกลับผู้ส่ง) ใช้ยืนยันว่า "ตัวเองไม่ใช่ host" + แก้ playHostId ที่ผิดตอน race LobbyB→play (กัน 2 จอคิดว่าเป็น host → sim บอทคนละชุด) */ + const humans = (Array.isArray(data.humans) ? data.humans.filter((h) => h != null).slice(0, 50).map(String) : undefined); + socket.to(sid).emit('fill-bot-state', { bots: data.bots, hostId: space.hostId, humans }); + }); + + /* MG7 balloon_boss — relay กระสุนของผู้เล่น/บอท ให้ทุกคนในห้องเห็นตรงกัน (ทุกคนยิงได้ ไม่จำกัด host) + payload: { sx, sy, vx, vy, ownerId, t0 } — t0 = serverNow ตอนยิง ให้ผู้รับ fast-forward ตำแหน่งให้ตรง */ + socket.on('balloon-boss-shot', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !data) return; + if (isSpecialQuizPausedServer(space)) return; /* คำถามพิเศษ → ไม่ relay กระสุน (กันค้างยิงรัวหลังตอบ) */ + const ok = ['sx', 'sy', 'vx', 'vy'].every((k) => typeof data[k] === 'number' && Number.isFinite(data[k])); + if (!ok) return; + socket.to(sid).emit('balloon-boss-shot', { + sx: data.sx, sy: data.sy, vx: data.vx, vy: data.vy, + ownerId: data.ownerId, t0: (typeof data.t0 === 'number' ? data.t0 : Date.now()), + /* [P5] forward the shooter's aim so the receiver can pin the arrow ring to it (fixes arrow/bullet + desync on a viewer's screen). Redundant with atan2(vy,vx) today since vx/vy = dir*spd, but + explicit = authoritative and future-proof if the velocity ever stops being a pure direction. */ + aim: (typeof data.aim === 'number' && Number.isFinite(data.aim)) ? data.aim : undefined, + }); + }); + + /* MG7 balloon_boss — host เป็นคนตัดสินจบเกม แล้ว relay ให้ทุกคนจบพร้อมกัน */ + socket.on('balloon-boss-over', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !data) return; + const reason = (data.reason === 'victory' || data.reason === 'all_dead' || data.reason === 'time') ? data.reason : 'all_dead'; + const scores = Array.isArray(data.scores) + ? data.scores.filter((s) => s && s.id != null).map((s) => ({ id: s.id, score: Math.max(0, Number(s.score) || 0), eliminated: !!s.eliminated })) + : []; + const overPayload = { reason, scores }; + socket.to(sid).emit('balloon-boss-over', overPayload); + /* เก็บไว้ resend ให้คน reconnect (host ยิงครั้งเดียว — หลุดตอนนั้นจะค้างถาวร) */ + space.lastMinigameOver = { event: 'balloon-boss-over', payload: overPayload, atMs: Date.now() }; + }); + + /* MG7 balloon_boss — host เป็นเจ้าของ HP บอส: relay ดาเมจรวม + สถานะผู้เล่น (ลูกโป่ง/ตาย) เป็นระยะ + → ทุกเครื่องแสดง HP บอสตรงกับ host เป๊ะ (เดิมแต่ละเครื่องรวม balloonBossBossDmg จาก move เองทยอยมา = bar เพี้ยน) */ + socket.on('balloon-boss-sync', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || space.hostId !== socket.id || !data) return; /* host เท่านั้น */ + const bossDmg = Math.max(0, Number(data.bossDmg) || 0); + const anchor = Math.max(0, Number(data.anchor) || 0); /* forward anchor host → clients adopt (มุมลูกศรวงกลม sync) */ + const hostT = (typeof data.t === 'number' && Number.isFinite(data.t) && data.t > 0) ? data.t : undefined; /* [P5 v2] นาฬิกา host → clients ใช้เป็นฐานเวลาวงลูกศร (หมุน sync ต่อเนื่อง ไม่พึ่ง serverNow ต่อเครื่อง) */ + const players = Array.isArray(data.players) + ? data.players.filter((p) => p && p.id != null).slice(0, 12).map((p) => ({ + id: p.id, + d: Math.max(0, Number(p.d) || 0), + b: Math.max(0, Number(p.b) || 0), + e: !!p.e, + })) + : []; + socket.to(sid).emit('balloon-boss-sync', { bossDmg, players, anchor, t: hostT }); + }); + + /* MG3 stack (shared tower) — relay ผลการวาง (เจ้าของตา/บอทของ host) ให้ทุกคนวางบล็อกเดียวกัน + เลื่อนตาพร้อมกัน */ + socket.on('stack-drop', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !data) return; + if (space.stackSession && space.stackSession.active) return; /* server-auth: client ไม่เป็นเจ้าของดรอป */ + const num = (v) => (typeof v === 'number' && Number.isFinite(v)) ? v : 0; + const themeOf = (v) => { const n = Math.floor(Number(v)); return (n >= 1 && n <= 8) ? n : null; }; + socket.to(sid).emit('stack-drop', { + placedCx: num(data.placedCx), placedW: num(data.placedW), + miss: !!data.miss, perfect: !!data.perfect, + pts: num(data.pts), progressDelta: num(data.progressDelta), landOffsetTiles: num(data.landOffsetTiles), + overlap: num(data.overlap), + supportRatio: (data.supportRatio == null ? null : num(data.supportRatio)), + delta: num(data.delta), + seat: Math.max(1, Math.min(8, Math.floor(num(data.seat)) || 1)), heavy: !!data.heavy, + theme: themeOf(data.theme), /* สี block ที่วาง (host เป็นเจ้าของ) — เดิมถูกตัดทิ้ง → client freeze สีตัวเอง = เพี้ยน */ + actorId: data.actorId != null ? String(data.actorId).slice(0, 64) : '', isBot: !!data.isBot, + }); + }); + /* MG1 quiz — host ขอจบเกมก่อนครบข้อ (ทุกคนตอบผิดหมด=ผีหมด) → endQuizGame โชว์ผล/รอกดรับการ์ด (ไม่ auto) */ + socket.on('quiz-end-request', () => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || space.hostId !== socket.id) return; /* host เท่านั้น */ + if (!space.detectiveMinigameActive || !space.quizSession || !space.quizSession.active) return; + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'quiz') return; + endQuizGame(sid, space, 'จบเกม — ทุกคนตอบผิดหมด'); + }); + + /* MG3 stack — relay เฟสแกว่ง (เจ้าของตา → คนอื่น mirror บล็อกที่กำลังแกว่ง) */ + socket.on('stack-swing', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !data) return; + if (space.stackSession && space.stackSession.active) return; /* server-auth: server แกว่งเอง */ + const ph = Number(data.phase); + if (!Number.isFinite(ph)) return; + socket.to(sid).emit('stack-swing', { phase: ph }); + }); + + /* MG3 stack (server-auth) — รับ "เจตนากดวาง" จากผู้เล่นจริง → server คำนวณ ณ เฟสปัจจุบัน + emit ผลให้ทุกเครื่อง */ + socket.on('stack-drop-req', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space) return; + const s = space.stackSession; + if (!s || !s.active || s.ended) return; + if (Date.now() < s.busyUntil) return; /* กำลังแอนิเมชันดรอปก่อนหน้า */ + const entry = stackCurrentTurnEntry(s); + if (!entry || entry.kind !== 'human' || String(entry.id) !== String(socket.id)) return; /* ต้องเป็นตาของคนนี้ */ + const md = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (!md || md.gameType !== 'stack') return; + /* ใช้เฟสที่ผู้เล่นเห็นจริงตอนกด (client ส่งมา) → บล็อกลงตรงจุดที่เห็นแกว่ง ไม่เยื้องตาม half-RTT/latency. + sanity bound กว้าง (STACK_DROP_PHASE_TOL = 2π) เหลือไว้กันค่า garbage เท่านั้น (ดู const) */ + let dropPhase = s.phase; + const cph = data && Number(data.phase); + const usedClient = Number.isFinite(cph) && Math.abs(cph - s.phase) <= STACK_DROP_PHASE_TOL; + if (usedClient) dropPhase = cph; + const hit = stackComputeDropServer(s, md, dropPhase); + try { console.log('[mg3-drop-dbg] cph=' + (Number.isFinite(cph) ? cph.toFixed(3) : '-') + ' sphase=' + s.phase.toFixed(3) + ' diff=' + (Number.isFinite(cph) ? Math.abs(cph - s.phase).toFixed(3) : '-') + ' used=' + (usedClient ? 'CLIENT' : 'server') + ' raw=' + (s.swingAmp * Math.sin(dropPhase)).toFixed(3) + ' overlap=' + hit.overlap.toFixed(3) + ' thresh=' + (hit.missThreshold != null ? hit.missThreshold.toFixed(3) : '?') + ' miss=' + hit.miss + ' perfect=' + hit.perfect + ' layers=' + s.layers.length + ' amp=' + s.swingAmp.toFixed(2)); } catch (eDbg) { /* ignore */ } + stackApplyHitServer(s, hit, { id: entry.id }); + const peer = space.peers.get(socket.id); + const theme = stackHumanThemeServer(peer) || (entry.theme || null); + io.to(sid).emit('stack-drop', { + placedCx: hit.placedCx, placedW: hit.placedW, miss: hit.miss, perfect: hit.perfect, + pts: hit.pts, progressDelta: hit.progressDelta, landOffsetTiles: hit.landOffsetTiles, + overlap: hit.overlap, supportRatio: hit.supportRatio, delta: hit.delta, + seat: entry.seat, heavy: false, theme, + actorId: entry.id, isBot: false, + phase: dropPhase, + teamMissesLeft: s.teamMissesLeft, + }); + s.busyUntil = Date.now() + STACK_DROP_BUSY_MS; + if (s.teamMissesLeft != null && s.teamMissesLeft <= 0) { + stackSessionEnd(sid, space, 'misses_exhausted'); + return; + } + stackAdvanceTurnServer(s); + emitStackStateServer(sid, space); + }); + /* MG3 stack — host เป็นเจ้าของลำดับตา+index relay ให้ non-host (กัน host/client มองลำดับตาไม่ตรง) */ + socket.on('stack-state', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || space.hostId !== socket.id || !data) return; /* host เท่านั้น */ + if (space.stackSession && space.stackSession.active) return; /* server-auth: server เป็นเจ้าของลำดับตา */ + const themeOf = (v) => { const n = Math.floor(Number(v)); return (n >= 1 && n <= 8) ? n : null; }; + const order = Array.isArray(data.order) + ? data.order.filter((e) => e && (e.kind === 'human' || e.kind === 'bot')).slice(0, 8).map((e) => ({ + kind: e.kind === 'bot' ? 'bot' : 'human', + seat: Math.max(1, Math.min(8, Math.floor(Number(e.seat)) || 1)), + id: e.id != null ? String(e.id).slice(0, 64) : null, + botId: e.botId != null ? String(e.botId).slice(0, 64) : null, + theme: themeOf(e.theme), /* สีต่อ seat (host เป็นเจ้าของ) — เดิมถูกตัดทิ้ง → client คำนวณเอง = เพี้ยน */ + })) + : []; + const idx = Math.max(0, Math.floor(Number(data.idx)) || 0); + /* สีของ block ที่วางทั้งหอ (host authoritative) — ต้อง relay ด้วย ไม่งั้นถูก allowlist ตัดทิ้ง */ + const lthemes = Array.isArray(data.lthemes) + ? data.lthemes.slice(0, 200).map((v) => { const n = Math.floor(Number(v)); return (n >= 1 && n <= 8) ? n : 0; }) + : []; + /* hostId (= host จริงที่ server ยืนยัน) → client ที่เข้าใจผิดว่าตัวเองเป็น host (dual-host) แก้ playHostId แล้วหยุด sim เอง */ + socket.to(sid).emit('stack-state', { order, idx, hostId: socket.id, lthemes }); + }); + + /* MG3 stack — host ตัดสินจบรอบ relay ให้ทุกคนจบพร้อมกัน */ + socket.on('stack-over', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space) return; + if (space.stackSession && space.stackSession.active) return; /* server-auth: server ตัดสินจบเกม */ + const outcome = (data && typeof data.outcome === 'string') ? data.outcome.slice(0, 32) : 'time_up'; + const overPayload = { outcome }; + socket.to(sid).emit('stack-over', overPayload); + space.lastMinigameOver = { event: 'stack-over', payload: overPayload, atMs: Date.now() }; + }); + + /* MG6 space_shooter — host ตัดสินจบเกม + แนบคะแนนสุดท้าย → ทุกคนจบ/อันดับตรงกัน */ + socket.on('space-shooter-over', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !data) return; + const kind = (data.kind === 'all_dead') ? 'all_dead' : 'time_up'; + const scores = Array.isArray(data.scores) + ? data.scores.filter((s) => s && s.id != null).map((s) => ({ id: s.id, score: Math.max(0, Number(s.score) || 0), hits: Math.max(0, Number(s.hits) || 0), eliminated: !!s.eliminated })) + : []; + const overPayload = { kind, scores }; + socket.to(sid).emit('space-shooter-over', overPayload); + space.lastMinigameOver = { event: 'space-shooter-over', payload: overPayload, atMs: Date.now() }; + }); + + /* MG6 space_shooter — relay กระสุนผู้เล่น/บอท ให้ทุกคนเห็นตรงกัน (host เห็นยังไง client เห็นแบบนั้น) */ + socket.on('space-shooter-shot', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !data) return; + if (isSpecialQuizPausedServer(space)) return; /* คำถามพิเศษ → ไม่ relay กระสุน (กันค้างยิงรัวหลังตอบ) */ + const ok = ['x', 'y', 'vy'].every((k) => typeof data[k] === 'number' && Number.isFinite(data[k])); + if (!ok) return; + socket.to(sid).emit('space-shooter-shot', { + x: data.x, y: data.y, vy: data.vy, + ownerId: data.ownerId, t0: (typeof data.t0 === 'number' ? data.t0 : Date.now()), + }); + }); + + /* MG6 — host คิดการชน "บอท × อุกาบาต" (asteroid deterministic + ตำแหน่งบอทจาก server) แล้วรายงานจำนวนชน + server เป็นคนตัดสินตายจริง (>= SS_MAX_ASTEROID_HITS) → กันบอททะลุอุกาบาตไม่มีวันตาย และสถานะตายตรงกันทุกเครื่อง */ + socket.on('space-shooter-bot-hit', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || space.hostId !== socket.id || !data || typeof data.id !== 'string') return; /* เชื่อเฉพาะ host */ + const s = space.spaceShooterSession; + if (!s || !s.active || s.ended) return; + if (isSpecialQuizPausedServer(space)) return; + const b = s.bots.get(data.id); + if (!b || b.eliminated) return; + const hits = Math.max(0, Math.min(99, Number(data.hits) || 0)); + b.ssHits = Math.max(b.ssHits || 0, hits); /* monotonic */ + if (b.ssHits >= SS_MAX_ASTEROID_HITS) { + b.eliminated = true; + /* step loop ข้ามบอทที่ตายแล้ว (ไม่ emit ต่อ) → ส่ง bot-move แจ้ง elim เดี๋ยวนี้ครั้งเดียวให้ทุกเครื่องรู้ */ + io.to(sid).emit('space-shooter-bot-move', { bots: [{ id: data.id, cx: b.cx, cy: b.cy, dir: 'up', score: b.score, elim: true, hits: b.ssHits || 0 }] }); + } + }); + + /* MG5 — ผู้เล่นสลับแท็บ (hidden): browser throttle setInterval เหลือ ~1Hz → emit move ~1/วิ = คนอื่นเห็นตกเป็นก้อน. + แก้: client แจ้ง hidden พร้อม state ปัจจุบัน → server จำลองการตก (jsPhysicsIntegrate) แทน ใน stepJumpSurviveBots + แล้ว broadcast ผ่าน jump-survive-bot-move (full rate) → คนอื่นเห็นลื่น. visible → hidden:false → client คุมเอง. */ + socket.on('jump-survive-hidden', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || !data) return; + const s = space.jumpSurviveSession; + if (!s || !s.active || s.ended) return; + if (!s.players[socket.id]) s.players[socket.id] = {}; + const st = s.players[socket.id]; + if (data.hidden) { + if (st.eliminated) return; + st.hidden = true; + st.x = Number.isFinite(data.x) ? data.x : (Number.isFinite(st.x) ? st.x : 1); + st.y = Number.isFinite(data.y) ? data.y : (Number.isFinite(st.y) ? st.y : 1); + st.vy = Number.isFinite(data.vy) ? data.vy : 0; + st.onGround = !!data.onGround; + st.dir = data.direction || st.dir || 'down'; + } else { + st.hidden = false; + } + }); + + /** เปลี่ยนชื่อที่แสดงใน 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: 'ไม่พบผู้เล่น' }); + /* ใช้ clampNickname ตัวเดียวกับ join-space — เดิม cap ที่ 32 ตรงนี้ที่เดียว ไม่ตรงกับ client (24) + และ join-space ไม่ cap เลย = 3 ค่าคนละอัน */ + let safe = clampNickname(nickname); + if (!safe) return reply({ ok: false, error: 'ชื่อว่าง' }); + 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); + } + } + /* [2026-07-23] roster ถาวรต้องตามชื่อใหม่ด้วย ไม่งั้นเปลี่ยนชื่อกลางคดีแล้ว rejoin ไม่ได้ */ + if (space.caseRosterEverNicknames && space.caseRosterEverNicknames.has(oldNorm)) { + space.caseRosterEverNicknames.delete(oldNorm); + space.caseRosterEverNicknames.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; + const claimPeer = space.peers.get(socket.id); + if (claimPeer && claimPeer.playerKey) space.hostPlayerKey = claimPeer.playerKey; + 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; + /* เก็บตัวตนตัวป่วนแบบถาวร (playerKey/nickname) — socket.id เปลี่ยนหลังเล่นมินิเกม ใช้ id ตรง ๆ ไม่ได้ */ + if (accept) { + const dPeer = space.peers.get(socket.id); + space.disruptorPlayerKey = (dPeer && dPeer.playerKey) ? dPeer.playerKey : ''; + space.disruptorNickname = dPeer ? normalizeLobbyNickname(dPeer.nickname) : ''; + } else { + space.disruptorPlayerKey = ''; + space.disruptorNickname = ''; + } + openSuspectPhaseForRoom(sid, space, accept); + 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 กดเริ่มสืบสวนได้' }); + } + /* ต้องรอผู้เล่นทุกคนในคดีกลับเข้า LobbyB ให้ครบก่อน host ถึงเริ่มมินิเกมรอบถัดไปได้ + (เดิม: หลังจบเกม client บางคนยังโหลด LobbyB ไม่เสร็จ แต่ host กดเริ่มได้เลย → คนนั้นหลุดรอบ) */ + const waitInfo = detectiveLobbyBMissingParticipants(space); + if (waitInfo.missing > 0) { + try { const hp = space.peers.get(socket.id); glog(sid, space, 'lobbyb-gate', '[TC144] BLOCK เริ่มสืบสวน — รอผู้เล่นกลับเข้า LobbyB ครบ (' + waitInfo.present + '/' + waitInfo.total + ') — gate ทำงาน (pass)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } + return reply({ + ok: false, + waitingPlayers: true, + present: waitInfo.present, + total: waitInfo.total, + error: 'รอผู้เล่นกลับเข้าห้องให้ครบก่อน (' + waitInfo.present + '/' + waitInfo.total + ')', + }); + } + /* ต้องโหวต Ban ให้เสร็จก่อนถึงเริ่มมินิเกมรอบถัดไปได้ (มีการ์ด Ban ค้าง = ยังไม่ได้เลือก/โหวตยังไม่จบ) */ + if (space.pendingBanVoteCard || (space.pickVote && space.pickVote.purpose === 'ban' && !space.pickVote.resolved)) { + try { const hp = space.peers.get(socket.id); glog(sid, space, 'lobbyb-gate', '[TC-BAN] BLOCK เริ่มสืบสวน — ต้องโหวตแบนให้เสร็จก่อน (gate ทำงาน)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } + return reply({ ok: false, waitingBanVote: true, error: 'ต้องโหวตแบนผู้เล่นให้เสร็จก่อนเริ่มมินิเกมถัดไป' }); + } + /* TC156: gate จนกว่าทุกคนอ่าน story (cutscene) ครบ — เฉพาะรอบแรก (storyReadComplete เป็น true หลังครบ/รอบถัดไป) */ + if (!space.storyReadComplete) { + const stRead = detectiveStoryReadStatus(space); + if (!stRead.allRead) { + try { const hp = space.peers.get(socket.id); glog(sid, space, 'story-gate', '[TC156] BLOCK เริ่มสืบสวน — รอทุกคนอ่าน story ครบ (' + stRead.present + '/' + stRead.total + ') — gate ทำงาน (pass)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } + return reply({ ok: false, waitingStory: true, present: stRead.present, total: stRead.total, error: 'รอผู้เล่นอ่านเนื้อเรื่องให้ครบก่อน (' + stRead.present + '/' + stRead.total + ')' }); + } + space.storyReadComplete = true; + } + 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: 'ยังไม่ได้สุ่มมินิเกมสำหรับการ์ดนี้' }); + } + /* จำกัดสืบผู้ต้องสงสัยละไม่เกิน 3 ครั้ง (#1) — นับจาก max หลักฐานของผู้ต้องสงสัยนี้ข้ามผู้เล่น + (แต่ละรอบทุกคนได้ +1) → กันเล่นครั้งที่ 4 + กันการ์ดพิเศษโผล่รอบที่ 4 (#2) */ + let timesPlayedThisSuspect = 0; + space.peers.forEach((_p, pid) => { + const prog = playerSuspectProgressFromEvidence(space, pid); + const n = Array.isArray(prog) ? (prog[selectedIndex] | 0) : 0; + if (n > timesPlayedThisSuspect) timesPlayedThisSuspect = n; + }); + if (timesPlayedThisSuspect >= 3) { + return reply({ ok: false, error: 'สืบผู้ต้องสงสัยคนนี้ครบ 3 ครั้งแล้ว — เลือกคนอื่น' }); + } + /* Card 3 Ban — ก่อนเริ่มมินิเกมรอบหน้า โหวตเลือกผู้เล่น 1 คนที่ห้ามเล่น */ + const pend = findPendingSpecialCard(space, 3); + if (pend && !space.pickVote) { + pend.consumed = true; + io.to(sid).emit('special-card-applied', { card: specialCardClientPayload(pend.card), context: 'ban' }); + try { const hp = space.peers.get(socket.id); glog(sid, space, 'vote-gate', '[TC157] เริ่ม vote ban (ผู้เล่นเข้า LobbyB ครบ ' + waitInfo.present + '/' + waitInfo.total + ') — มินิเกมจะเริ่มหลังโหวตจบเท่านั้น (pass)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } + startPlayerPickVote(sid, space, 'ban', (targetId) => { + const sp = spaces.get(sid); + if (!sp) return; + sp.bannedPlayerId = targetId || null; + sp.bannedPlayerKey = resolveBanTargetPlayerKey(sp, targetId); /* ตัวตนถาวร — id จะเปลี่ยนก่อนเกมเริ่ม */ + 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); + try { const hp = space.peers.get(socket.id); glog(sid, space, 'lobbyb-gate', '[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ ' + waitInfo.total + ' คน · ผู้ต้องสงสัย #' + (selectedIndex + 1), (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } + 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: 'ไม่อยู่ในห้อง' }); + } + /* client รายงานคะแนน/รอด ของตัวเอง (ใช้กับเกมที่ server ไม่ได้นับคะแนน เช่น stack/quiz_carry) */ + const finPeer = space.peers.get(socket.id); + if (finPeer && data && typeof data === 'object') { + /* คะแนน client รายงานเอง (stack/quiz_carry) — clamp ด้วย sanity cap กัน overflow/ค่าขยะ/spoof สูงเกินจริง + หมายเหตุ: ยังเป็น client-trusted ภายในเพดาน (anti-cheat สมบูรณ์ต้องจำลองเกมฝั่ง server — เกินสโคปตอนนี้) */ + if (Number.isFinite(Number(data.score))) { + let rs = Math.max(0, Math.floor(Number(data.score))); + if (rs > REPORTED_MINI_SCORE_MAX) { + console.warn('[mini-score] clamp reportedMiniScore', socket.id, rs, '->', REPORTED_MINI_SCORE_MAX); + rs = REPORTED_MINI_SCORE_MAX; + } + finPeer.reportedMiniScore = rs; + } + if (data.survived === false) finPeer.reportedMiniSurvived = false; + } + 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: 'ไม่ได้อยู่ในมินิเกมสืบสวน' }); + } + /* MG2 วิ่งหลบ (gauntlet): ไม่จบเกมตอนคนแรกเข้าเส้นชัย — ให้คนที่เข้าเส้นแล้ว "รอในฉาก" + จนกว่าทุกคนที่ยังไม่ตาย/ไม่ถูกแบนจะเข้าเส้นครบ แล้วค่อยจบพร้อมกัน (returnDetective แจกการ์ด + ทุกคนพร้อมกันตอนทุกคนยังอยู่ในห้อง) · endGauntletGame timer เป็น fallback ถ้าใครไม่เข้าเส้น */ + const finMd = (space.mapId && maps.get(space.mapId)) || space.mapData; + if (finMd && finMd.gameType === 'gauntlet' && space.detectiveMinigameActive) { + if (finPeer) finPeer.detectiveFinished = true; + const allDone = [...space.peers.values()].every((p) => + p.gauntletEliminated || p.bannedSpectator || p.detectiveFinished + ); + if (!allDone) { + /* ยังมีคนวิ่งอยู่ → ให้ client นี้รอในฉาก (ไม่เด้งออก) จะกลับ LobbyB พร้อมกันตอนเกมจบ */ + return reply({ ok: true, holdInScene: true }); + } + /* ครบทุกคนแล้ว → จบเกมให้ทุกคนด้านล่าง */ + } + // เล่นมินิเกม 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 เปิดพิจารณาคดีได้' }); + /* [TC179-followup] ใช้ team progress (ตัวเลขเดียวกับที่การ์ดโชว์ทุกจอ) เป็นหลัก — หลักฐานแจก "รายคน" + คนที่หลุด/โดนบอทแทนตอนมินิเกมของผู้ต้องสงสัยคนนั้นจะไม่มีใบนั้น → host เห็นการ์ด ✓ ครบแต่กด + "ชี้ตัวคนร้าย" ไม่ได้ (เดิมเช็คแต่ prog ส่วนตัว). max(ส่วนตัว, ทีม) = ไม่บล็อกทั้งสองทาง */ + const prog = playerSuspectProgressFromEvidence(space, socket.id); + const teamProg = Array.isArray(space.suspectTeamProgress) ? space.suspectTeamProgress : [0, 0, 0]; + if (![0, 1, 2].every((i) => Math.max((prog[i] || 0), (teamProg[i] || 0)) >= 1)) { + return reply({ ok: false, error: 'ต้องสืบครบทั้ง 3 ผู้ต้องสงสัยก่อน (เก็บหลักฐานอย่างน้อย 1 ใบต่อคน)' }); + } + /* ชี้ตัวคนร้าย = เข้าพิจารณาคดี → ต้องรอผู้เล่นในคดีกลับเข้า LobbyB ครบก่อน (กันคนตกรอบโหวต) */ + const accuseWait = detectiveLobbyBMissingParticipants(space); + if (accuseWait.missing > 0) { + try { const hp = space.peers.get(socket.id); glog(sid, space, 'lobbyb-gate', '[TC144] BLOCK ชี้ตัวคนร้าย — รอผู้เล่นเข้า LobbyB ครบ (' + accuseWait.present + '/' + accuseWait.total + ') — gate ทำงาน (pass)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } + return reply({ ok: false, waitingPlayers: true, present: accuseWait.present, total: accuseWait.total, error: 'รอผู้เล่นกลับเข้าห้องให้ครบก่อน (' + accuseWait.present + '/' + accuseWait.total + ')' }); + } + 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 = {}; + const d3AlreadySubmitted = space.testimonySubmitted[socket.id] !== undefined; + space.testimonySubmitted[socket.id] = picks.slice(0, required); + /* [achv d3] นับหลักฐานที่ "ส่งมอบให้ทีมวิเคราะห์" บนหน้า es3 (ห้องสรุปหลักฐาน) — นับครั้งแรกของรอบนี้เท่านั้น กันนับซ้ำ */ + if (!d3AlreadySubmitted) { + const d3p = space.peers.get(socket.id); + if (d3p && d3p.playerKey && picks.length > 0) { + submitAchievementProgress([{ playerKey: d3p.playerKey, id: 'd3_the_backbone', inc: picks.length }]); + } + } + 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: 'เลือกผู้ต้องสงสัยไม่ถูกต้อง' }); + /* รอบจับผิดตัว: ห้ามโหวตผู้ต้องสงสัยที่โหวตผิดไปแล้ว */ + if (Number.isInteger(space.trialRevoteExcluded) && idx === space.trialRevoteExcluded) { + return reply({ ok: false, error: 'ผู้ต้องสงสัยนี้ถูกโหวตไปแล้ว (ผิด) — เลือกคนอื่น', excluded: true }); + } + space.trialVotes = space.trialVotes || {}; + space.trialVotes[socket.id] = idx; + space.trialVoteAt = space.trialVoteAt || {}; + space.trialVoteAt[socket.id] = Date.now(); /* [achv c4] เวลาโหวต → เช็ค "10 วิสุดท้าย" */ + emitTrialVoteUpdate(sid, space); + const totalTV = trialEligibleVoterTotal(space); + const votedNow = Object.keys(space.trialVotes).length; + try { glog(sid, space, 'trial-vote', '[TC180] ยืนยันโหวต ' + votedNow + '/' + totalTV + (votedNow >= totalTV && totalTV > 0 ? ' — ครบ → reveal (pass)' : ' — รออีก')); } catch (e) { /* ignore */ } + maybeRevealTrialIfAllVoted(sid, space, 'trial-vote'); /* [S6] ใช้ตัวเดียวกับเส้นทาง disconnect */ + 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; + /* [2026-07-23] เดิมเขียน Math.max(humans, total - bots) แต่บรรทัดบนเพิ่งตั้ง total = humans + bots + → total - bots === humans เสมอ = "ค่าที่ host เลือกถูกทิ้ง แล้ว cap หดเหลือจำนวนคนที่ต่ออยู่ตอนนั้น" + ผลคือห้องเต็มถาวร: ใครกลับจากมินิเกมเข้า LobbyB ก็โดน 'ห้องเต็มแล้ว (N/N)' แล้วเด้งออก + ที่ถูกคือ cap = จำนวนช่องทั้งหมด (บอทเป็นแค่ตัวเติมช่องว่าง ต้องหลีกให้คนจริง) */ + space.maxPlayers = total; + 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: 'ไม่พบผู้เล่นในห้อง' }); + } + /* [2026-07-19] เก็บ playerKey/nickname ของคนที่ถูก kick "ก่อน" ลบ peer — ใช้ prune roster คดี */ + const kickedPeer = space.peers.get(tid) || {}; + const kickedKey = (typeof kickedPeer.playerKey === 'string') ? kickedPeer.playerKey : ''; + const kickedNick = kickedPeer.nickname || ''; + 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 }); + /* [2026-07-19] host ดึงคนออก = ตั้งใจถอดถาวร → ตัดออกจาก roster คดีทันที + re-emit presence + ไม่งั้น TC144 gate ตอนเลือกผู้ต้องสงสัยจะค้าง "รอผู้เล่น (x/y)" ตลอด host กด Start ต่อไม่ได้ + (host-kick เดิมไม่เคย prune roster / ไม่ arm grace / ไม่ re-emit presence เลย) */ + pruneDetectiveParticipantNow(sid, space, kickedKey, kickedNick, true); /* [2026-07-23] true = ถอดถาวร ห้าม rejoin */ + cb && cb({ ok: true }); + }); + + /* พร้อมแล้ว — ไม่บังคับจำนวนคนในห้อง */ + socket.on('set-ready', ({ ready }) => { + for (const [sid, space] of spaces) { + if (space.peers.has(socket.id)) { + /* ล็อก: host กำลังเปิด wizard เลือกระดับ/คดี → ปฏิเสธ set-ready (กัน client หลุด flow ตอน host เลือกอยู่) */ + if (space.preplayOpen && space.hostId !== socket.id) { + const p = space.peers.get(socket.id); + io.to(socket.id).emit('peer-ready', { id: socket.id, ready: !!(p && p.ready) }); /* ย้ำสถานะเดิมกลับให้ผู้ส่ง */ + try { glog(sid, space, 'preplay-lock', '[TC154] REJECT set-ready ระหว่าง host เลือกคดี — feature ทำงาน (pass): client ยกเลิกพร้อมไม่ได้', (p && p.nickname) || String(socket.id).slice(0, 6)); } catch (e) { /* ignore */ } + break; + } + const p = space.peers.get(socket.id); + if (p) p.ready = !!ready; + io.to(sid).emit('peer-ready', { id: socket.id, ready: !!ready }); + break; + } + } + }); + + /* LobbyA: host เปิด/ปิด wizard เลือกระดับ/คดี → เก็บ state ที่ server + relay ทั้งห้อง (ล็อกปุ่มพร้อมฝั่ง client) */ + socket.on('lobby-preplay-open', ({ open } = {}) => { + for (const [sid, space] of spaces) { + if (space.peers.has(socket.id) && space.hostId === socket.id) { + space.preplayOpen = !!open; + io.to(sid).emit('lobby-preplay-open', { open: !!open }); + try { const hp = space.peers.get(socket.id); glog(sid, space, 'preplay-lock', '[TC154] ' + (open ? 'host เปิด wizard เลือกคดี → ล็อก set-ready ทั้งห้อง' : 'host ปิด wizard → ปลดล็อก set-ready') + ' (' + space.peers.size + ' คนในห้อง)', (hp && hp.nickname) || 'host'); } catch (e) { /* ignore */ } + break; + } + } + }); + + /* TC156: client อ่าน story (cutscene) จบ → บันทึกว่าคนนี้พร้อม · ครบทุกคน → ปลดล็อกเลือกผู้ต้องสงสัย */ + socket.on('story-read-done', () => { + for (const [sid, space] of spaces) { + if (!space.peers.has(socket.id)) continue; + if (!serverMapIsPostCaseLobbyB(space)) break; + const p = space.peers.get(socket.id); + const key = detectiveParticipantKey(p); + const nick = p && p.nickname ? normalizeLobbyNickname(p.nickname) : ''; + if (!key) break; + if (!space.storyReadKeys) space.storyReadKeys = new Set(); + const already = space.storyReadKeys.has(key); + space.storyReadKeys.add(key); + /* อ่าน story จบ = โหลด LobbyB เสร็จแน่นอน → นับ lobbyb-loaded ด้วย (reliable กว่า event lobbyb-loaded เดี่ยว) */ + if (!space.lobbyBLoadedKeys) space.lobbyBLoadedKeys = new Set(); + space.lobbyBLoadedKeys.add(key); + const st = detectiveStoryReadStatus(space); + if (!already) { try { glog(sid, space, 'story-gate', '[TC156] อ่าน story จบ ' + st.present + '/' + st.total, nick); } catch (e) { /* ignore */ } } + if (st.allRead && !space.storyReadComplete) { + space.storyReadComplete = true; + if (space.storyReadTimer) { clearTimeout(space.storyReadTimer); space.storyReadTimer = null; } + try { glog(sid, space, 'story-gate', '[TC156] ทุกคนอ่าน story ครบ (' + st.total + ') → ปลดล็อกเลือกผู้ต้องสงสัย (pass)'); } catch (e) { /* ignore */ } + } + emitDetectiveStoryReadPresence(sid, space); + /* piggyback เพิ่ม lobbyBLoadedKeys แล้ว → ต้อง broadcast lobbyb-presence ใหม่ด้วย + ไม่งั้น client ค้างโชว์ "รอผู้เล่น (2/3)" (ค่าเก่า) → ปุ่ม host ถูก disable ทั้งที่ครบแล้ว */ + emitDetectiveLobbyBPresence(sid, space); + break; + } + }); + + /* client โหลดฉาก LobbyB เสร็จจริง → นับเป็น "present" ได้ (กัน host เริ่มก่อนคนโหลดเสร็จ = ค้างตอนจบ) */ + socket.on('lobbyb-loaded', () => { + for (const [sid, space] of spaces) { + if (!space.peers.has(socket.id)) continue; + if (!serverMapIsPostCaseLobbyB(space)) break; + const p = space.peers.get(socket.id); + const key = detectiveParticipantKey(p); + const nick = p && p.nickname ? normalizeLobbyNickname(p.nickname) : ''; + if (!key) break; + if (!space.lobbyBLoadedKeys) space.lobbyBLoadedKeys = new Set(); + if (!space.lobbyBLoadedKeys.has(key)) { + space.lobbyBLoadedKeys.add(key); + const info = detectiveLobbyBMissingParticipants(space); + try { glog(sid, space, 'lobbyb-gate', '[TC144] โหลด LobbyB เสร็จ ' + info.present + '/' + info.total, nick); } catch (e) { /* ignore */ } + } + emitDetectiveLobbyBPresence(sid, space); + break; + } + }); + + /* client โหลดฉากเสร็จ (ทุกฉาก lobby/minigame) → เอา badge "กำลังโหลด" ออก (คนอื่นเห็นว่าพร้อมแล้ว) */ + socket.on('client-ready', () => { + for (const [sid, space] of spaces) { + if (!space.peers.has(socket.id)) continue; + const p = space.peers.get(socket.id); + if (p && p.netReady !== true) { + p.netReady = true; + io.to(sid).emit('peer-net', { id: socket.id, nr: true }); + } + 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) { + space.preplayOpen = false; /* host เริ่มเกมแล้ว → ปลดล็อก (client เปลี่ยนฉากอยู่แล้ว แต่เคลียร์ state กัน stuck) */ + 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' }); + } + /* detective LobbyA→LobbyB: ไม่บังคับยืนโซนส้ม (หน้าจอเลือกคดีบังแผนที่ ขยับตัวไม่ได้) */ + if (!isLobbyADetectiveStart && !lobbyHostStandingInStartArea(space, socket.id)) { + return reply({ ok: false, error: 'ยืนในพื้นที่เริ่มเกม (สีส้มในเอดิเตอร์) ก่อนกดเริ่ม' }); + } + if (isLobbyADetectiveStart) { + space.lobbyAMapId = space.mapId; /* item1: จำ map LobbyA เดิมไว้ (ก่อนทับด้วย LobbyB) เพื่อ restore ตอน post-case reset */ + space.lobbyAMapData = space.mapData; + space.postCaseLobbyAReset = false; /* เริ่มคดีใหม่ → เปิด guard reset→LobbyA อีกครั้ง */ + assignDetectiveSuspectCardMinigames(space); + space.mapId = POST_CASE_LOBBY_SPACE_ID; + space.mapData = maps.get(POST_CASE_LOBBY_SPACE_ID); + const caseNickWl = new Set(); + const caseKeyWl = new Set(); + space.peers.forEach((p) => { + caseNickWl.add(normalizeLobbyNickname(p.nickname)); /* ชื่อ — ใช้ whitelist rejoin (คงเดิม) */ + const k = detectiveParticipantKey(p); + if (k) caseKeyWl.add(k); /* playerKey — ใช้ presence/story/gate (กันชื่อซ้ำนับเป็นคนเดียว) */ + }); + space.caseParticipantNicknames = caseNickWl; + space.caseParticipantKeys = caseKeyWl; + /* [2026-07-23] roster "ถาวร" สำหรับด่าน rejoin เท่านั้น — ห้ามหด + ต่างจาก caseParticipant* ข้างบนที่เป็น "ตัวนับว่ายังรอใครอยู่" ซึ่งถูก prune ได้ + (host kick / หลุดตอนเฟสเลือกผู้ต้องสงสัย server.js:13391 / grace หมดเวลา) + ต้นตอบั๊ก 07-23: เดินจาก play.html กลับ room-lobby.html = disconnect เหมือนกัน + → โดน prune ทันทีตอน suspectPhaseActive → join กลับมาไม่อยู่ใน whitelist + → 'ห้องนี้เริ่มคดีแล้ว ไม่รับผู้เล่นเพิ่ม' + เด้งออกกลางคดี */ + space.caseRosterEverKeys = new Set(caseKeyWl); + space.caseRosterEverNicknames = new Set(caseNickWl); + /* TC156: gate เลือกผู้ต้องสงสัยจนกว่าทุกคนอ่าน story (cutscene) ครบ — reset ที่ initial LobbyA→LobbyB เท่านั้น + (รอบ investigate ถัดไปไม่มี cutscene → storyReadComplete คงเป็น true) · safety timer กัน deadlock ถ้า client ไม่ emit */ + space.storyReadKeys = new Set(); + space.storyReadComplete = false; + space.lobbyBLoadedKeys = new Set(); /* เริ่มคดี → ทุกคนต้องส่ง lobbyb-loaded ใหม่ ก่อนนับ present */ + armLobbyBLoadedSafety(sid, space); /* backstop กัน deadlock */ + if (space.storyReadTimer) clearTimeout(space.storyReadTimer); + space.storyReadTimer = setTimeout(() => { + const sp = spaces.get(sid); + if (sp && !sp.storyReadComplete) { + sp.storyReadComplete = true; + try { glog(sid, sp, 'story-gate', '[TC156] safety-timeout 90s → force ปลดล็อก (client บางคนไม่ส่ง story-read-done)'); } catch (e) { /* ignore */ } + try { emitDetectiveStoryReadPresence(sid, sp); } catch (e) { /* ignore */ } + } + }, 90000); + space.joinLocked = true; + space.troublesomeOfferSent = false; + space.troublesomeTargetId = null; + space.troublesomeResponseReceived = false; + space.troublesomeAccept = false; + space.disruptorPlayerKey = ''; + space.disruptorNickname = ''; + 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); + /* [TC190] backstop กัน deadlock "หน้าเลือกผู้ต้องสงสัยไม่ขึ้น": + openSuspectPhaseForRoom เข้าถึงได้ "ทางเดียว" = client ส่ง troublesome-eligible → + scheduleTroublesomeRoll → timer. ตัว troublesomeMaxTimer (8 วิ) ที่ดูเหมือน safety net + ถูก arm อยู่ "ข้างใน" scheduleTroublesomeRoll ที่สัญญาณนั้นเรียกเอง + สองบรรทัดข้างบนเพิ่งล้าง + timer เป็น null → ถ้าไม่มีใครส่งเลย (ติด overlay how-to/mic, หรือ room-lobby.js:4543 ซ่อน + mic overlay แล้วไม่เรียก tryTroublesomeLobbyGate ซ้ำ) = ห้องค้างถาวร ไม่มีอะไรกู้ + (พิสูจน์แล้ว: justice-mg-all.js --probe eligible-loss → ค้าง 40 วิ ไม่มีวันเปิด). + arm ตรงนี้ = เฟสที่ห้อง "มาถึง LobbyB" จริง (บทเรียน §4: backstop ที่ arm ผิดเฟสจะ no-op เงียบ) + — storyReadTimer ข้างบนรู้บทเรียนนี้อยู่แล้ว, gate นี้แค่ไม่เคยได้. 45 วิ = พ้น worst case + ที่เคยวัดได้ (6 คน join ใช้ 10-16 วิ) เยอะพอจะไม่แย่งทางปกติ. */ + if (space.troublesomeDeadlockTimer) clearTimeout(space.troublesomeDeadlockTimer); + space.troublesomeDeadlockTimer = setTimeout(() => { + const sp = spaces.get(sid); + if (!sp || sp.troublesomeOfferSent) return; /* เปิดไปแล้วตามทางปกติ */ + if (!serverMapIsPostCaseLobbyB(sp)) return; /* ไม่ได้อยู่ LobbyB แล้ว */ + try { glog(sid, sp, 'suspect-gate', '[TC190] safety-timeout 45s → force เปิดเฟสเลือกผู้ต้องสงสัย (ไม่มี client ส่ง troublesome-eligible เลย) — backstop ทำงาน (pass)'); } catch (e) { /* ignore */ } + /* force=true ข้ามเงื่อนไข "ทุกคน eligible"; ถ้า pool ว่าง attemptTroublesomeRoll + fallback เป็น peerIds ทั้งหมดอยู่แล้ว (10638) จึงปลอดภัยเมื่อไม่มีใคร eligible */ + attemptTroublesomeRoll(sid, true); + }, 45000); + space.peers.forEach((p) => { + const sp = pickSpawnForJoin(space.mapData, typeof p.spawnJoinOrder === 'number' ? p.spawnJoinOrder : 0); + 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; + /* MG5: สุ่ม pattern แพลตฟอร์ม 1 อัน/เกม (เหมือน detective starter) */ + space.jsLayout = null; space._jsEffMd = null; + if (md && md.gameType === 'jump_survive') pickJumpSurviveLayoutForSpace(space); + 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 (space.jsLayout) gameStartPayload.jsLayout = space.jsLayout; + 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; + /* เก็บข้อมูลก่อนลบ peer — ใช้ทำ grace 10 วิ (รอ reconnect) + bot takeover ระหว่างมินิเกม */ + const leftPeerInfo = space.peers.get(socket.id) || {}; + const leftPlayerKey = (typeof leftPeerInfo.playerKey === 'string') ? leftPeerInfo.playerKey : ''; + const leftNick = (leftPeerInfo.nickname || '').trim() || 'ผู้เล่น'; + /* คนที่หลุดตาย/ตกรอบไปแล้วหรือยัง (ตอนหลุด) — ใช้ตัดสินว่า bot ที่มาแทนควร spawn ใหม่ไหม (คนตายแล้วไม่ควรฟื้น) */ + let leftWasEliminated = false; + try { + const md8 = (space.mapId && maps.get(space.mapId)) || space.mapData; + const gt8 = md8 && md8.gameType; + leftWasEliminated = minigamePlayerScoreSurvived(space, leftPeerInfo, gt8).survived === false; + } catch (e) { /* ignore */ } + /* สี (ธีม/สกิน) ของคนที่ออก — ใช้ให้ bot ที่มาแทนใช้สีเดียวกัน (b) */ + const leftThemeIndex = parseInt(leftPeerInfo.lobbyColorThemeIndex, 10); + const leftSkinIndex = parseInt(leftPeerInfo.lobbySkinToneIndex, 10); + /* MG1 (quiz live): หลุด → แปลง entity เดิม (socket.id) เป็นบอท server "ในเกมทันที" + เก็บ avatar เดิมไว้ = สี+ตำแหน่งเดิมอัตโนมัติ + inherit คะแนน/สถานะตกรอบ (ผีถ้าตอบผิด) */ + let mg1Takeover = false; + try { + const md9 = (space.mapId && maps.get(space.mapId)) || space.mapData; + const sessT = space.quizSession; + if (sessT && sessT.active && sessT.bots && md9 && md9.gameType === 'quiz') { + const st = (sessT.players && sessT.players[socket.id]) || {}; + const px = Number(leftPeerInfo.x), py = Number(leftPeerInfo.y); + sessT.bots.set(socket.id, { + tier: quizBotTierForSlot(sessT.bots.size), + x: Number.isFinite(px) ? px : 1, y: Number.isFinite(py) ? py : 1, + dir: leftPeerInfo.direction || 'down', isWalking: false, + score: Math.max(0, Number(st.score) || 0), + eliminated: !!(st.cannotTrue && st.cannotFalse), + choice: null, answerRight: false, tx: null, ty: null, wanderTX: null, wanderTY: null, retargetAt: 0, + takeover: true, takeoverPlayerKey: leftPlayerKey || '', + }); + mg1Takeover = true; + } + } catch (e) { /* ignore */ } + /* MG2 (gauntlet live): หลุดกลาง run → แปลง peer เป็นบอท server "ในที่เดิม" (ตำแหน่ง/คะแนน/สถานะเดิม) + เก็บ avatar เดิมไว้ + เล่นต่อเป็นบอท (host หลุดก็ไม่หยุด race) */ + let mg2Takeover = false; + try { + const md9b = (space.mapId && maps.get(space.mapId)) || space.mapData; + const grT = space.gauntletRun; + /* takeover ตราบที่ run ยังมีอยู่ — รวมช่วง how-to / pre-run hold (crownRunHeld) ด้วย เหมือน MG1 + (เดิมข้าม crownRunHeld → host หลุดตอน how-to = avatar หายไปเลย ไม่มีบอทรับช่วง; ตอนนี้แปลงเป็นบอททันที + บอทจะค้างในเลนตอน how-to แล้ววิ่งเองเมื่อเกมเริ่ม — emitGauntletSync ส่งบอท takeover ระหว่าง hold อยู่แล้ว) */ + if (grT && md9b && md9b.gameType === 'gauntlet') { + if (!grT.bots) grT.bots = new Map(); + const px = Number(leftPeerInfo.x), py = Number(leftPeerInfo.y); + const themeIdx = parseInt(leftPeerInfo.lobbyColorThemeIndex, 10); + const skinIdx = parseInt(leftPeerInfo.lobbySkinToneIndex, 10); + grT.bots.set(socket.id, { + tier: 'avg', + /* คงตำแหน่ง float เดิม (อย่า floor — เดิม floor ทำให้บอทสแนปไปทับเลน/ตัวอื่น) เหมือน MG1 */ + x: Number.isFinite(px) ? px : 1, + y: Number.isFinite(py) ? py : 1, + dir: leftPeerInfo.direction || 'right', + gauntletJumpTicks: 0, + gauntletScore: Math.max(0, Number(leftPeerInfo.gauntletScore) || 0), + gauntletEliminated: !!leftPeerInfo.gauntletEliminated, + /* เก็บ identity เดิมไว้ → sync ส่งชื่อ+"(บอท)"+สี ให้ client คงไว้ (กันชื่อ/สีเพี้ยนตอน re-create) */ + nickname: leftNick, + characterId: leftPeerInfo.characterId != null ? leftPeerInfo.characterId : null, + lobbyColorThemeIndex: Number.isFinite(themeIdx) ? themeIdx : null, + lobbySkinToneIndex: Number.isFinite(skinIdx) ? skinIdx : null, + takeover: true, + takeoverPlayerKey: leftPlayerKey || '', + }); + mg2Takeover = true; + } + } catch (e) { /* ignore */ } + /* MG3 (stack live): หลุดกลางรอบ → แปลง turn-order entry เป็นบอท server "ในที่เดิม" (ใช้ socket.id เป็น botId + → client เก็บ avatar เดิมไว้) inherit คะแนนสะสม แล้วบอทเล่นตาแทนต่อ (host หลุดก็ไม่ค้างตา) */ + let mg3Takeover = false; + try { + const md9c = (space.mapId && maps.get(space.mapId)) || space.mapData; + const s3 = space.stackSession; + if (s3 && s3.active && !s3.ended && md9c && md9c.gameType === 'stack') { + const entry = (s3.turnOrder || []).find((e) => e.kind === 'human' && String(e.id) === String(socket.id)); + if (entry) { + const inheritedPts = Math.max(0, s3.humanPts[socket.id] | 0); + s3.bots.set(socket.id, { tier: 'avg', score: inheritedPts, takeover: true, takeoverPlayerKey: leftPlayerKey || '' }); + entry.kind = 'bot'; + entry.botId = socket.id; + entry.id = null; + mg3Takeover = true; + } + } + } catch (e) { /* ignore */ } + /* MG4 (quiz_carry live): หลุดกลางเกม → แปลง entity เดิม (socket.id) เป็นบอท server "ในที่เดิม" + เก็บ avatar เดิม + inherit ป้ายที่ถือ/คะแนน → บอท BFS เล่นต่อ (host หลุดก็ไม่หยุด) */ + let mg4Takeover = false; + try { + const md9d = (space.mapId && maps.get(space.mapId)) || space.mapData; + const sC = space.quizCarrySession; + if (sC && sC.active && !sC.ended && md9d && md9d.gameType === 'quiz_carry') { + const st = sC.players[socket.id] || {}; + const px = Number(leftPeerInfo.x), py = Number(leftPeerInfo.y); + sC.bots.set(socket.id, { + tier: 'avg', + x: Number.isFinite(px) ? px : 1, y: Number.isFinite(py) ? py : 1, + dir: leftPeerInfo.direction || 'down', isWalking: false, + held: (st.held == null ? null : st.held), + score: Math.max(0, Number(st.score) || 0), + botPath: [], pathfindAfter: 0, wantIdx: null, + wanderTX: null, wanderTY: null, retargetAt: 0, + takeover: true, takeoverPlayerKey: leftPlayerKey || '', + }); + delete sC.players[socket.id]; + mg4Takeover = true; + } + } catch (e) { /* ignore */ } + /* MG5 (jump_survive live): หลุดกลางเกม → แปลง entity เดิม (socket.id) เป็นบอท server "ในที่เดิม" + เก็บ avatar/ตำแหน่งเดิม + inherit สถานะตาย → บอทกระโดดเอาตัวรอดต่อ (host หลุดก็ไม่หยุด) */ + let mg5Takeover = false; + try { + const md9e = (space.mapId && maps.get(space.mapId)) || space.mapData; + const sJ = space.jumpSurviveSession; + if (sJ && sJ.active && !sJ.ended && md9e && md9e.gameType === 'jump_survive') { + const st = sJ.players[socket.id] || {}; + const wasDead = !!st.eliminated || !!leftPeerInfo.jumpSurviveEliminated; + const px = Number(leftPeerInfo.x), py = Number(leftPeerInfo.y); + sJ.bots.set(socket.id, { + tier: 'avg', + x: Number.isFinite(px) ? px : 1, y: Number.isFinite(py) ? py : 1, + vy: 0, onGround: false, dir: leftPeerInfo.direction || 'down', walking: false, + eliminated: wasDead, + wishX: 0, wishNext: Date.now() + 200, nextJumpAi: Date.now() + 200, + takeover: true, takeoverPlayerKey: leftPlayerKey || '', + }); + delete sJ.players[socket.id]; + mg5Takeover = true; + } + } catch (e) { /* ignore */ } + /* MG6 (space_shooter live): หลุดกลางเกม → แปลงเป็นบอท server "ในที่เดิม" (คะแนนเดิม) ยิงต่อ */ + let mg6Takeover = false; + try { + const md9f = (space.mapId && maps.get(space.mapId)) || space.mapData; + const sS = space.spaceShooterSession; + if (sS && sS.active && !sS.ended && md9f && md9f.gameType === 'space_shooter') { + const ts = jsTileSize(md9f); + const mh = (md9f.height || 15) * ts; + const cx = Number.isFinite(Number(leftPeerInfo.x)) ? (Number(leftPeerInfo.x) + 0.5) * ts : (md9f.width || 20) * ts * 0.5; + const cy = ssClampCy(Number.isFinite(Number(leftPeerInfo.y)) ? (Number(leftPeerInfo.y) + 0.5) * ts : mh - ts * 1.5, mh); + const st = sS.players[socket.id] || {}; + sS.bots.set(socket.id, { + tier: 'avg', cx, cy, dir: 'up', + score: Math.max(0, Number(leftPeerInfo.spaceShooterScore) || 0), + eliminated: !!st.eliminated || !!leftPeerInfo.spaceShooterEliminated, + fireCd: 0.3 + Math.random() * 0.3, + wanderVx: Math.random() < 0.5 ? -1 : 1, wanderVy: Math.random() < 0.5 ? -1 : 1, wanderNext: Date.now() + 500, + takeover: true, takeoverPlayerKey: leftPlayerKey || '', + }); + delete sS.players[socket.id]; + mg6Takeover = true; + } + } catch (e) { /* ignore */ } + /* MG7 (balloon_boss live): หลุดกลางเกม → แปลงเป็นบอท server "ในที่เดิม" (คะแนน/ดาเมจ/ลูกโป่ง/ตายเดิม) ยิงต่อ */ + let mg7Takeover = false; + try { + const md9g = (space.mapId && maps.get(space.mapId)) || space.mapData; + const sB = space.balloonBossSession; + if (sB && sB.active && !sB.ended && md9g && md9g.gameType === 'balloon_boss') { + const ts = jsTileSize(md9g); + const mw = (md9g.width || 20) * ts, mh = (md9g.height || 15) * ts; + const cx = Number.isFinite(Number(leftPeerInfo.x)) ? (Number(leftPeerInfo.x) + 0.5) * ts : mw * 0.5; + const cy = Number.isFinite(Number(leftPeerInfo.y)) ? (Number(leftPeerInfo.y) + 0.5) * ts : mh - ts * 1.4; + const cl = bbClampWorld(cx, cy, mw, mh); + sB.bots.set(socket.id, { + tier: 'avg', homeCx: cl.cx, homeCy: cl.cy, cx: cl.cx, cy: cl.cy, + score: Math.max(0, Number(leftPeerInfo.balloonBossScore) || 0), + dmg: Math.max(0, Number(leftPeerInfo.balloonBossBossDmg) || 0), + balloons: Math.max(0, Number(leftPeerInfo.balloonBossBalloons) || balloonBossBalloonsForMap(md9g)), + eliminated: !!leftPeerInfo.balloonBossEliminated, + fireCd: 0.6 + Math.random() * 0.8, phase: Math.random() * Math.PI * 2, + takeover: true, takeoverPlayerKey: leftPlayerKey || '', + }); + mg7Takeover = true; + } + } catch (e) { /* ignore */ } + /* MG3–MG7 หลุดช่วง how-to / นับถอยหลัง (ยังไม่มี session live) → เก็บ pending takeover ไว้ + แล้ว seed เป็นบอทตอนเกมเริ่ม + แจ้ง client เก็บ avatar (*-player-to-bot) เหมือน MG1/MG2 + (เดิม: ไม่มี session = ไม่ takeover = avatar หายเลยตอน how-to) */ + let shellTakeoverEvent = null; + try { + if (!mg1Takeover && !mg2Takeover && !mg3Takeover && !mg4Takeover && !mg5Takeover && !mg6Takeover && !mg7Takeover && space.detectiveMinigameActive) { + const gtShell = missionShellGameTypeForSpace(space); + const liveActive = !!( + (gtShell === 'space_shooter' && space.spaceShooterSession && space.spaceShooterSession.active) || + (gtShell === 'jump_survive' && space.jumpSurviveSession && space.jumpSurviveSession.active) || + (gtShell === 'balloon_boss' && space.balloonBossSession && space.balloonBossSession.active) || + (gtShell === 'quiz_carry' && space.quizCarrySession && space.quizCarrySession.active) || + (gtShell === 'stack' && space.stackSession && space.stackSession.active) + ); + if (gtShell && !liveActive) { + recordPendingMissionTakeover(space, gtShell, socket.id, leftPeerInfo, leftPlayerKey, leftNick, leftWasEliminated); + shellTakeoverEvent = + gtShell === 'space_shooter' ? 'space-shooter-player-to-bot' : + gtShell === 'jump_survive' ? 'jump-survive-player-to-bot' : + gtShell === 'balloon_boss' ? 'balloon-boss-player-to-bot' : + gtShell === 'quiz_carry' ? 'quiz-carry-player-to-bot' : + gtShell === 'stack' ? 'stack-player-to-bot' : null; + } + } + } catch (e) { /* ignore */ } + 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', quizCarryLobbySyncPayload(space)); + } + } + if (space.gauntletCrownLobbyReady && typeof space.gauntletCrownLobbyReady === 'object') { + delete space.gauntletCrownLobbyReady[socket.id]; + if (isCrownLobbyShellSpace(space)) { + io.to(sid).emit('gauntlet-crown-lobby-sync', crownLobbySyncPayload(space)); + } + } + if (space.qbwSolved) space.qbwSolved.delete(socket.id); // Quiz Battle (ฉากเดิน) — เอาคะแนนคนออกแล้ว sync ใหม่ด้านล่าง + space.peers.delete(socket.id); + if (space.peers.size === 0) { + stopGauntletTicker(space); + stopStackTicker(space); + space.stackSession = null; + clearSpaceQuizTimers(space); + space.quizSession = null; + if (space.quizCarrySession) space.quizCarrySession.active = false; + space.quizCarrySession = null; + if (space.quizCarryWatchdog) { clearInterval(space.quizCarryWatchdog); space.quizCarryWatchdog = null; } + if (space.jumpSurviveSession) space.jumpSurviveSession.active = false; + space.jumpSurviveSession = null; + if (space.spaceShooterSession) space.spaceShooterSession.active = false; + space.spaceShooterSession = null; + if (space.balloonBossSession) space.balloonBossSession.active = false; + space.balloonBossSession = null; + space.pendingMissionTakeovers = []; /* ห้องว่าง → ล้าง pending takeover ค้าง (กัน seed บอทผีรอบหน้า) */ + space.emptySince = Date.now(); + /* ระหว่างมินิเกมสืบสวน / คดีล็อก — อย่าลบห้องเมื่อเปลี่ยนหน้า room-lobby → play.html (รอ join กลับ) */ + const keepCaseSpace = !!(space.detectiveMinigameActive + || (space.postCaseKeepAliveUntil && Date.now() < space.postCaseKeepAliveUntil) /* item1: ห้อง reset→LobbyA รอ client reload กลับ */ + || (space.joinLocked && space.caseParticipantNicknames && space.caseParticipantNicknames.size > 0)); + /* ห้อง Quiz Battle — เก็บไว้เมื่อว่าง ให้ refresh กลับเข้าห้องเดิมได้ (TTL prune จะเก็บกวาดเมื่อว่างเกิน 45 วิ) */ + const spaceMd = (space.mapId && maps.get(space.mapId)) || space.mapData; + const keepQuizBattleRoom = !!(spaceMd && spaceMd.gameType === 'quiz_battle'); + if (keepCaseSpace || keepQuizBattleRoom) { + space.hostId = null; + return; + } + spaces.delete(sid); + } else if (space.serverOwnedRoom || quizBattleRoomNumberOf(space) > 0) { + /* [2026-07-22] ห้อง Quiz Battle = server เป็นเจ้าของ — ไม่ต้องย้าย host ให้ใคร คนออกก็ออกไป */ + if (space.hostId) { space.hostId = null; io.to(sid).emit('host-changed', { hostId: null }); } + } else { + if (wasHost) { + /* host หลุดกลางเปิด wizard เลือกคดี → ปลดล็อกปุ่มพร้อมให้คนที่เหลือ (กัน stuck lock) */ + if (space.preplayOpen) { space.preplayOpen = false; io.to(sid).emit('lobby-preplay-open', { open: false }); try { glog(sid, space, 'preplay-lock', '[TC154] host หลุดกลางเปิด wizard → auto-ปลดล็อก set-ready (กัน stuck)', leftNick); } catch (e) { /* ignore */ } } + const firstPeer = pickNextHostPeer(space); /* คนอยู่นานสุด (spawnJoinOrder ต่ำสุด) — แทน Map-first */ + if (firstPeer) { + space.hostId = firstPeer.id; /* ย้าย pointer host ชั่วคราวให้คนที่เหลือ (liveness) */ + /* อย่าเขียนทับ hostPlayerKey ถ้ามีอยู่แล้ว — host ตัวจริงต้อง reclaim ได้เมื่อ rejoin จาก play.html + (เปลี่ยนหน้า lobby→play→lobbyB ทำให้ socket disconnect ชั่วคราว ไม่ใช่ออกจากห้องจริง + ถ้าเขียนทับ key → host/client สลับกันถาวรหลังเข้า minigame) */ + if (!space.hostPlayerKey && firstPeer.playerKey) space.hostPlayerKey = firstPeer.playerKey; + io.to(sid).emit('host-changed', { hostId: firstPeer.id }); + glog(sid, space, 'host-change', 'host ใหม่ → ' + ((firstPeer.nickname) || String(firstPeer.id).slice(0, 6)), leftNick); + } + } + if (mg2Takeover) { + /* MG2: ไม่ลบ avatar — แปลงเป็นบอทในที่เดิม (สี/ตำแหน่ง/คะแนนเดิม) ให้ client เก็บ entity ไว้ */ + io.to(sid).emit('gauntlet-player-to-bot', { id: socket.id, nickname: leftNick }); + glog(sid, space, 'leave', 'หลุด MG2 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); + } else if (mg1Takeover) { + /* MG1: ไม่ลบ avatar — แปลงเป็นบอทในที่เดิม (สี/ตำแหน่ง/สถานะเดิม) ให้ client เก็บ entity ไว้ */ + io.to(sid).emit('quiz-player-to-bot', { id: socket.id, nickname: leftNick }); + glog(sid, space, 'leave', 'หลุด MG1 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); + } else if (mg3Takeover) { + /* MG3: ไม่ลบ avatar — แปลง seat เป็นบอท server เล่นตาแทน (สี/ตำแหน่งเดิม) + sync ลำดับตาใหม่ */ + io.to(sid).emit('stack-player-to-bot', { id: socket.id, nickname: leftNick }); + emitStackStateServer(sid, space); + glog(sid, space, 'leave', 'หลุด MG3 → บอทแทนในเกม · เหลือคน ' + space.peers.size, leftNick); + } else if (mg4Takeover) { + /* MG4: ไม่ลบ avatar — แปลงเป็นบอท server (ตำแหน่ง/ป้ายที่ถือ/คะแนนเดิม) BFS เล่นต่อ */ + io.to(sid).emit('quiz-carry-player-to-bot', { id: socket.id, nickname: leftNick }); + glog(sid, space, 'leave', 'หลุด MG4 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); + } else if (mg5Takeover) { + /* MG5: ไม่ลบ avatar — แปลงเป็นบอท server (ตำแหน่ง/สถานะเดิม) กระโดดเอาตัวรอดต่อ */ + io.to(sid).emit('jump-survive-player-to-bot', { id: socket.id, nickname: leftNick }); + glog(sid, space, 'leave', 'หลุด MG5 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); + } else if (mg6Takeover) { + /* MG6: ไม่ลบ avatar — แปลงเป็นบอท server (ตำแหน่ง/คะแนนเดิม) ยิงต่อ */ + io.to(sid).emit('space-shooter-player-to-bot', { id: socket.id, nickname: leftNick }); + glog(sid, space, 'leave', 'หลุด MG6 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); + } else if (mg7Takeover) { + /* MG7: ไม่ลบ avatar — แปลงเป็นบอท server (ตำแหน่ง/คะแนน/ดาเมจ/ลูกโป่งเดิม) ยิงบอสต่อ */ + io.to(sid).emit('balloon-boss-player-to-bot', { id: socket.id, nickname: leftNick }); + glog(sid, space, 'leave', 'หลุด MG7 → บอทแทนในเกม (สี/ตำแหน่งเดิม) · เหลือคน ' + space.peers.size, leftNick); + } else if (shellTakeoverEvent) { + /* how-to/นับถอยหลัง: client เก็บ avatar + ติดป้าย (บอท) — บอท server จะ seed ตอนเกมเริ่ม (pendingMissionTakeovers) */ + io.to(sid).emit(shellTakeoverEvent, { id: socket.id, nickname: leftNick }); + glog(sid, space, 'leave', 'หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน ' + space.peers.size, leftNick); + } else { + io.to(sid).emit('user-left', { id: socket.id }); + glog(sid, space, 'leave', 'ออกจากห้อง · เหลือคน ' + space.peers.size, leftNick); + } + if (space.qbwSolved) qbwBroadcastScores(sid, space); // Quiz Battle ฉากเดิน — อัปเดตอันดับหลังมีคนออก + emitDetectiveLobbyBPresence(sid, space); /* คนออกจาก LobbyB → อัปเดตจำนวนที่พร้อมให้ host */ + /* กำลังรอกด «เล่นต่อ» (special-quiz) แล้วมีคนออก — เอา ack คนนั้นออกแล้วเช็คใหม่ (กันค้างรอคนที่ไม่อยู่แล้ว) */ + if (space.specialQuizAwaitContinue) { + if (space.specialQuizContinueAcks) space.specialQuizContinueAcks.delete(socket.id); + maybeFinishSpecialQuizContinue(sid, space); + } + /* [2026-07-27 · S6] กำลังโหวตพิจารณาคดีแล้วมีคนออก — ทิ้งโหวตของคนนั้น แล้ว "เช็คครบใหม่" + เดิมไม่มีอะไร re-check ตรงนี้ → เคส "คนอื่นโหวตครบก่อน แล้วคนที่ยังไม่โหวตหลุด" + ต้องรอ trialVoteSec หมดเวลาเอง (วัดได้ 27.5-29.2 วิ) ทั้งที่ไม่มีใครเหลือให้รอแล้ว */ + if (space.trialPhase === 'voting') { + if (space.trialVotes) delete space.trialVotes[socket.id]; + if (space.trialVoteAt) delete space.trialVoteAt[socket.id]; + emitTrialVoteUpdate(sid, space); + maybeRevealTrialIfAllVoted(sid, space, 'disconnect'); + } + /* ห้องส่งหลักฐาน (testimony) — คนหลุดระหว่างเฟส → ลบ submit/ready ที่ค้าง แล้ว re-emit สถานะ + เพื่อให้ "จำนวนที่ต้องส่ง/ready" ลดตามจริง (peers.delete ไปแล้วด้านบน) ไม่งั้น host รอคนที่ไม่อยู่ = ไปต่อไม่ได้ */ + if (space.testimonyActive) { + if (space.testimonySubmitted) delete space.testimonySubmitted[socket.id]; + if (space.testimonyReadyNext) delete space.testimonyReadyNext[socket.id]; + emitTestimonyStatus(sid, space); + if (space.testimonyRevealed) { + io.to(sid).emit('testimony-ready-update', { + ready: Object.keys(space.testimonyReadyNext || {}), + totalPlayers: testimonyTotalParticipants(space), + hostId: space.hostId, + }); + } + } + /* หลุดในสาย "คดี" — ครอบทั้ง 3 จังหวะ: + (1) ระหว่างมินิเกม (2) ระหว่าง LobbyB (3) ช่วงเปลี่ยนหน้า minigame→LobbyB (detectiveMinigameActive เพิ่งเป็น false) + ทั้งหมดให้เวลา reconnect 10 วิ (กันเปลี่ยนหน้า lobbyB→play→lobbyB ที่ socket หลุดชั่วคราว) + ครบเวลายังไม่กลับ → ลบออกจาก roster คดี + เติมบอทสีตามคนที่ออก (ไม่งั้น LobbyB ค้าง "รอผู้เล่น x/y" เริ่มเกมต่อไม่ได้) */ + const inCaseFlowForBot = space.detectiveMinigameActive + || serverMapIsPostCaseLobbyB(space) + || (space.joinLocked && space.caseParticipantNicknames && space.caseParticipantNicknames.size > 0); + if (space.suspectPhaseActive) { + /* [2026-07-19] หลุดระหว่างเฟส "เลือกผู้ต้องสงสัย" → ตัดออกจาก roster ทันที (user: เล่นต่อทันที) + ครอบเคส playerKey ว่างด้วย (เดิม grace block ข้ามถ้า leftPlayerKey ว่าง → gate ค้างถาวร). + เฟสนี้อยู่ใน LobbyB ยังไม่เข้าเกม จึงไม่ต้องรอ grace 22วิ/ไม่ต้องมีบอทแทน — แค่ให้ gate ลด + แล้ว host เริ่มต่อได้เลย. ช่วง "เล่นมินิเกมจริง" ยังใช้ grace+บอทเหมือนเดิม (else-if ล่าง) */ + pruneDetectiveParticipantNow(sid, space, leftPlayerKey, leftNick); + } else if (inCaseFlowForBot && leftPlayerKey) { + io.to(sid).emit('peer-disconnect-grace', { id: socket.id, nickname: leftNick, seconds: DISCONNECT_GRACE_SEC }); + if (!space.graceReconnect || typeof space.graceReconnect !== 'object') space.graceReconnect = {}; + if (!space.graceTimers || typeof space.graceTimers !== 'object') space.graceTimers = {}; + if (space.graceTimers[leftPlayerKey]) clearTimeout(space.graceTimers[leftPlayerKey]); + space.graceReconnect[leftPlayerKey] = Date.now(); + space.graceTimers[leftPlayerKey] = setTimeout(() => { + const sp = spaces.get(sid); + if (!sp) return; + if (sp.graceTimers) delete sp.graceTimers[leftPlayerKey]; + /* กลับมาแล้ว (playerKey อยู่ใน peers) → ไม่ต้อง takeover */ + let back = false; + if (sp.peers) sp.peers.forEach((p) => { if (p && p.playerKey === leftPlayerKey) back = true; }); + if (sp.graceReconnect) delete sp.graceReconnect[leftPlayerKey]; + if (back) return; + /* คนนี้กลายเป็นบอทถาวร → เอาออกจาก roster คดี เพื่อ LobbyB ไม่รอคนนี้อีก (เดิมยังนับ → ค้าง "รอผู้เล่น (x/y)") */ + if (sp.caseParticipantNicknames && leftNick) { + try { sp.caseParticipantNicknames.delete(normalizeLobbyNickname(leftNick)); } catch (e) { /* ignore */ } + /* เอา participantKey ออกจาก roster gate ด้วย (playerKey — กัน gate รอคนที่ออกถาวร) */ + try { + if (sp.caseParticipantKeys) { + if (leftPlayerKey && /^[a-zA-Z0-9_-]{8,128}$/.test(leftPlayerKey)) sp.caseParticipantKeys.delete('k:' + leftPlayerKey); + sp.caseParticipantKeys.delete('n:' + normalizeLobbyNickname(leftNick)); + } + } catch (e) { /* ignore */ } + emitDetectiveLobbyBPresence(sid, sp); + } + /* คนออกถาวร → ลด expectedActive (จำนวนคนที่ต้องเข้าครบก่อนเริ่ม) + re-emit lobby-sync + แก้บั๊ก: host หลุดในหน้า how-to ก่อนกด START → host ใหม่กดไม่ได้เพราะค้าง "รอผู้เล่นครบ (x/y)" (y ไม่ลด) */ + if (typeof sp.minigameExpectedActiveCount === 'number' && sp.minigameExpectedActiveCount > 0) { + sp.minigameExpectedActiveCount = Math.max(0, sp.minigameExpectedActiveCount - 1); + } + if (isCrownLobbyShellSpace(sp)) io.to(sid).emit('gauntlet-crown-lobby-sync', crownLobbySyncPayload(sp)); + /* (c) persist + (b) สี: เพิ่มจำนวนบอทถาวร 1 ตัว (อยู่ยาวจนจบคดี: LobbyB → เกมอื่น → vote) + แล้ว pin "สีของคนที่ออก" ให้ slot บอทใหม่ → บอทที่มาแทนสีตรงคนเดิม + ส่งสีให้ทุกเครื่องตรงกัน */ + try { + const oldBotCount = effectiveBotSlotCount(sp); + if (oldBotCount < LOBBY_SLOT_TOTAL) { + sp.botSlotCount = oldBotCount + 1; + if (leftThemeIndex >= 1 && leftThemeIndex <= LOBBY_THEME_COUNT) { + if (!sp.lobbyBotThemeBySlot || typeof sp.lobbyBotThemeBySlot !== 'object') sp.lobbyBotThemeBySlot = {}; + sp.lobbyBotThemeBySlot[oldBotCount] = { + themeIndex: leftThemeIndex, + skinToneIndex: (leftSkinIndex >= 1 && leftSkinIndex <= LOBBY_SKIN_COUNT) ? leftSkinIndex : pickLobbySkinIndexForSlot(oldBotCount), + }; + } + emitLobbyTintSync(sid, sp); + } + } catch (e) { /* ignore */ } + io.to(sid).emit('peer-bot-takeover', { nickname: leftNick, eliminated: leftWasEliminated }); + glog(sid, sp, 'bot-takeover', 'หลุดเกิน ' + DISCONNECT_GRACE_SEC + 'วิ → บอทแทน' + (leftWasEliminated ? ' (ตกรอบแล้ว ไม่ spawn)' : ' (สีตามคนออก)'), leftNick); + }, DISCONNECT_GRACE_SEC * 1000); + } + } + }); + + /* client แจ้งว่าเส้นชัยมาถึงแล้ว (BG ถึง finish) → เข้าโหมดจบ: หยุด obstacle + collision */ + socket.on('gauntlet-finish-phase', () => { + 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; + const gr = space.gauntletRun; + if (!gr) return; + /* server-authority: ยังไม่เริ่ม run (crownRunHeld) หรือยิงเร็วผิดปกติ → ปฏิเสธ (กัน client ปลอม/desync) */ + if (gr.crownRunHeld) return; + if (gr.liveStartMs != null && (Date.now() - gr.liveStartMs) < GAUNTLET_FINISH_MIN_RUN_MS) return; + if (gr.finishPhase) return; + gr.finishPhase = true; + gr.obstacles = []; + /* คนแรกเข้าเส้นชัย → หยุด collision (obstacles ว่าง) + สั่ง client เคลียร์อุปสรรคจากจอ "จังหวะเดียวกัน" + กันอาการ "หายแล้วยังชน" (visual เคลียร์ตรงกับตอน collision หยุด ไม่เร็วไป) */ + io.to(sid).emit('gauntlet-finish-clear'); + emitGauntletSync(sid, space); + }); + + 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; + }); + + /* MG5 jump_survive — relay การกระโดดของคนจริงให้เพื่อนในห้อง (เสียงเท่านั้น ไม่แตะ physics; mg5 ไม่มี jump-seq ใน move sync) */ + socket.on('js-jump', () => { + 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 !== 'jump_survive') return; + socket.to(sid).emit('js-jump', { id: socket.id }); + }); + + 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; + /* G2: เลื่อนเวลาเริ่มจริงไปอนาคต = ค้างที่ start (hold) → ทั้ง bg(client) + obstacle/bot(server) gate ด้วย liveStartMs เดียวกัน */ + gr.liveStartMs = Date.now() + GAUNTLET_PRERUN_HOLD_MS; /* G1/G2: เวลาเริ่ม run จริง (server) — broadcast ให้ทุกเครื่อง anchor scroll เดียวกัน */ + 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', quizCarryLobbySyncPayload(space)); + }); + + 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', quizCarryLobbySyncPayload(space)); + }); + + 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()].filter((id) => !isBannedPeerForRun(space, id)); + /* ต้องเข้าเกมครบก่อน (กัน host เริ่มก่อน client โหลดเสร็จ) */ + const expectedActive = space.minigameExpectedActiveCount || 0; + if (expectedActive > 0 && humanIds.length < expectedActive) { + return reply({ ok: false, waitingPlayers: true, present: humanIds.length, total: expectedActive, error: 'รอผู้เล่นเข้าเกมให้ครบก่อน (' + humanIds.length + '/' + expectedActive + ')' }); + } + const allReady = humanIds.length > 0 && humanIds.every((id) => rm[id]); + if (!allReady) return reply({ ok: false, error: 'ยังมีผู้เล่นที่ยังไม่ Ready' }); + const liveBeginsAt = Date.now() + LIVE_COUNTDOWN_MS; + space.missionLiveBeginsAt = liveBeginsAt; + io.to(sid).emit('quiz-carry-lobby-started', { liveBeginsAt }); + reply({ ok: true }); + /* MG4 server-auth: เกมจริง (detective) → server เริ่ม engine เองหลัง 3-2-1 (เหมือน MG1 ใน performCrownLobbyStart) */ + setTimeout(() => { + const sp = spaces.get(sid); + if (!sp || !sp.detectiveMinigameActive) return; + const md2 = (sp.mapId && maps.get(sp.mapId)) || sp.mapData; + if (md2 && md2.gameType === 'quiz_carry') startQuizCarryGame(sid, sp); + }, LIVE_COUNTDOWN_MS); + }); + + /* host เลือกคำถาม quiz_carry → relay index ให้คนอื่นในห้องใช้ข้อเดียวกัน (กันสุ่มไม่ตรงกัน) + server-auth (เกมจริง): server เป็นเจ้าของคำถามผ่าน quiz-carry-phase → ข้าม relay */ + socket.on('quiz-carry-question', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || space.hostId !== socket.id) return; /* เฉพาะ host */ + if (space.quizCarrySession && space.quizCarrySession.active) return; + const idx = Number(data && data.idx); + if (!Number.isInteger(idx) || idx < 0) return; + socket.to(sid).emit('quiz-carry-question', { idx }); + }); + + /* host จบเซสชัน quiz_carry → ให้คนอื่นโชว์สรุปพร้อมกัน (server-auth ใช้ quiz-carry-ended แทน) */ + socket.on('quiz-carry-session-complete', () => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space || space.hostId !== socket.id) return; + if (space.quizCarrySession && space.quizCarrySession.active) return; + socket.to(sid).emit('quiz-carry-session-complete', {}); + }); + + /* MG4 server-auth: client ส่ง intent "หยิบ/ส่ง" (กด F / ปุ่ม GRAB) → server ตัดสินจากตำแหน่งล่าสุด */ + socket.on('quiz-carry-grab', (data) => { + const sid = socket.data.spaceId; + const space = sid ? spaces.get(sid) : null; + if (!space) return; + const s = space.quizCarrySession; + if (!s || !s.active || s.ended) return; + const changed = quizCarryServerTryInteract(sid, space, socket.id, false, data); + /* กดแล้วไม่เกิดอะไร (หยิบไม่ได้/ส่งไม่ได้) → ยืนยัน held จริงกลับไปให้ "คนกด" เพื่อแก้ optimistic prediction + ที่อาจค้างผิด (เช่น ทำนายว่าหยิบได้แต่ server ปฏิเสธเพราะคนอื่นถืออยู่) */ + if (!changed) { + const hp = s.players[socket.id]; + io.to(socket.id).emit('quiz-carry-held', { id: socket.id, held: hp ? (hp.held == null ? null : hp.held) : null }); + } + }); + + /* ตอบเวลา server ปัจจุบัน → client ใช้คำนวณ clock offset (ชดเชย latency) ให้เวลาในเกมตรงกันทุกเครื่อง */ + socket.on('time-sync', (cb) => { + if (typeof cb === 'function') cb({ t: Date.now() }); + }); + + 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', crownLobbySyncPayload(space)); + }); + + 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', crownLobbySyncPayload(space)); + /* หมายเหตุ: host (ผู้เล่นจริง) เป็นคนกด START เอง — ไม่ auto-start (ตามที่ต้องการให้ host คุมจังหวะเริ่ม) */ + }); + + 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()].filter((id) => !isBannedPeerForRun(space, id)); + /* ต้องเข้าเกมครบก่อน (กัน host เริ่มก่อน client โหลดเสร็จ → เกมไม่ sync/จบไม่พร้อมกัน) */ + const expectedActive = space.minigameExpectedActiveCount || 0; + if (expectedActive > 0 && humanIds.length < expectedActive) { + return reply({ ok: false, waitingPlayers: true, present: humanIds.length, total: expectedActive, error: 'รอผู้เล่นเข้าเกมให้ครบก่อน (' + humanIds.length + '/' + expectedActive + ')' }); + } + const allReady = humanIds.length > 0 && humanIds.every((id) => rm[id]); + if (!allReady) return reply({ ok: false, error: 'ยังมีผู้เล่นที่ยังไม่ Ready' }); + performCrownLobbyStart(sid, space); + 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; + } else if (quizBattlePathModeActiveServer(md) && !quizBattlePathGateAllowsServer(md, space, socket.id, 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; + } + } + let moveForceEmit = false; + if (p && md && md.gameType === 'space_shooter' && data) { + if (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 (data.spaceShooterHits != null) { + const nh = Math.floor(Number(data.spaceShooterHits)); + if (Number.isFinite(nh) && nh >= 0) { + const prevH = Math.max(0, p.spaceShooterHits | 0); + if (nh > prevH) { p.spaceShooterHits = nh; moveForceEmit = true; } + } + } + /* ตาย/รอด มาจากเจ้าของยาน → ต้อง relay แม้ตำแหน่งไม่เปลี่ยน (ยานหยุดตอนตาย) + ไม่งั้นยานคนตายจะค้างนิ่งบนจออีกฝั่ง (ดูเหมือนขยับไม่ได้) + ตายไม่ตรงกัน */ + if (data.spaceShooterEliminated && !p.spaceShooterEliminated) { + p.spaceShooterEliminated = true; + moveForceEmit = true; + const sSm = space.spaceShooterSession; + if (sSm && sSm.active && !sSm.ended) { + if (!sSm.players[socket.id]) sSm.players[socket.id] = {}; + sSm.players[socket.id].eliminated = true; + } + } + } + if (p && md && md.gameType === 'jump_survive' && data) { + /* MG5: การตายมาจากเจ้าตัว → relay แม้ตำแหน่งไม่เปลี่ยน (ตอนตายไม่ขยับ) ไม่งั้นคนอื่นไม่รู้ = "ไม่ยอมตาย" */ + if (data.jumpSurviveEliminated && !p.jumpSurviveEliminated) { + p.jumpSurviveEliminated = true; + moveForceEmit = true; + /* MG5 server-auth: บันทึกการตายของคนจริงเข้า session (server เป็นเจ้าของรอด/เกรด + เช็ค all_dead) */ + const sJm = space.jumpSurviveSession; + if (sJm && sJm.active && !sJm.ended) { + if (!sJm.players[socket.id]) sJm.players[socket.id] = {}; + sJm.players[socket.id].eliminated = true; + } + } + /* isWalking ของเจ้าตัว → relay ให้คนอื่นเห็น anim วิ่ง (เดิม out ไม่ใส่ jsWalking = client อื่นไม่เคยได้ → anim ไม่ขึ้น); + เปลี่ยนสถานะ (เริ่มเดิน/หยุด) → force emit แม้ตำแหน่งไม่เปลี่ยน */ + const jwIn = !!data.jsWalking; + if (jwIn !== !!p.jumpSurviveWalking) moveForceEmit = true; + p.jumpSurviveWalking = jwIn; + } + if (p && md && md.gameType === 'balloon_boss' && data) { + const bbDefaultBalloons = balloonBossBalloonsForMap(md); + 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); + /* เพดานเพิ่ม/แพ็กเก็ต = เผื่อยิงโดนหลายนัดต่อ move (ดาเมจ/นัดตั้งได้ถึง 100) */ + const bbDmgCap = Math.max(8, bbDamagePerHit() * 3); + if (nd <= prevD + bbDmgCap) 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) { p.balloonBossEliminated = true; moveForceEmit = true; } + else if (data.balloonBossEliminated) p.balloonBossEliminated = true; + } + /* server-auth (เกมจริง): server เป็นเจ้าของ held/score ผ่าน quiz-carry-held/result → ไม่รับค่าจาก client (กัน spoof/desync) */ + const quizCarrySrvAuth = !!(space.quizCarrySession && space.quizCarrySession.active); + if (!quizCarrySrvAuth && p && md && md.gameType === 'quiz_carry' && data && data.quizCarryHeld !== undefined) { + /* relay ของที่ผู้เล่น (มนุษย์) ถืออยู่ → client อื่นเห็น "ใครถืออะไร"; force emit แม้ยืนนิ่ง (หยิบ/วางขณะไม่ขยับ) */ + const prevHeld = (p.quizCarryHeld === undefined ? null : p.quizCarryHeld); + const rawHeld = data.quizCarryHeld; + const nh = (rawHeld == null) ? null : Math.floor(Number(rawHeld)); + const newHeld = (nh == null || !Number.isFinite(nh)) ? null : nh; + if (newHeld !== prevHeld) moveForceEmit = true; + p.quizCarryHeld = newHeld; + } + if (!quizCarrySrvAuth && p && md && md.gameType === 'quiz_carry' && data && data.quizCarryScore !== undefined) { + /* คะแนน carry ของผู้เล่น (มนุษย์) → relay ให้ทุกเครื่องเห็นคะแนนตรงกัน */ + const sc = Math.max(0, Math.floor(Number(data.quizCarryScore) || 0)); + if (sc !== (p.quizCarryScore | 0)) moveForceEmit = true; + p.quizCarryScore = sc; + } + /* L2 server clamp (เฉพาะ lobby): จำกัดระยะต่อ move ตามเวลาจริง → กันเครื่องเร็ว/โกงวิ่งเกินความเร็ว + เผื่อ jitter/lag burst ด้วย factor 3 + ฐาน 0.5 (move ปกติ ~12.5Hz เดินแค่ ~0.72u อยู่ใต้เพดานสบาย) + ไม่แตะมินิเกม (ฟิสิกส์ตก/กระโดดเร็วกว่านี้) */ + if (p && md && md.gameType === 'lobby') { + const tnow = Date.now(); + const tlast = (typeof p.lastLobbyMoveTs === 'number') ? p.lastLobbyMoveTs : 0; + let edt = tlast ? (tnow - tlast) / 1000 : 0.1; + if (!(edt > 0)) edt = 0.1; + if (edt > 0.5) edt = 0.5; + p.lastLobbyMoveTs = tnow; + const maxDist = LOBBY_MOVE_SPEED_PER_SEC * edt * 3 + 0.5; + const ddx = Number(nx) - Number(p.x); + const ddy = Number(ny) - Number(p.y); + if (Number.isFinite(ddx) && Number.isFinite(ddy)) { + const dlen = Math.hypot(ddx, ddy); + if (dlen > maxDist && dlen > 0) { + const s = maxDist / dlen; + nx = Number(p.x) + ddx * s; + ny = Number(p.y) + ddy * s; + } + } + } + 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 || moveForceEmit) { + const out = { id: socket.id, x: nx, y: ny, direction: p.direction, characterId: p.characterId, lobbyColorThemeIndex: p.lobbyColorThemeIndex || null, lobbySkinToneIndex: p.lobbySkinToneIndex || null }; + if (md && md.gameType === 'space_shooter') { + out.spaceShooterScore = Math.max(0, p.spaceShooterScore | 0); + out.spaceShooterHits = Math.max(0, p.spaceShooterHits | 0); + out.spaceShooterEliminated = !!p.spaceShooterEliminated; + } + if (md && md.gameType === 'balloon_boss') { + const bbDef = balloonBossBalloonsForMap(md); + 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 (md && md.gameType === 'quiz_carry') { + out.quizCarryHeld = (p.quizCarryHeld === undefined ? null : p.quizCarryHeld); + out.quizCarryScore = Math.max(0, p.quizCarryScore | 0); + } + if (md && md.gameType === 'jump_survive') { + out.jumpSurviveEliminated = !!p.jumpSurviveEliminated; + out.jsWalking = !!p.jumpSurviveWalking; /* anim วิ่งให้คนอื่นเห็น */ + } + 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 diff --git a/www/html/Main-Lobby/index.html b/www/html/Main-Lobby/index.html index ec454ad..74acb97 100644 --- a/www/html/Main-Lobby/index.html +++ b/www/html/Main-Lobby/index.html @@ -231,7 +231,7 @@ - + - - - + + + + + + Quiz Battle — JD JUSTICE DIVERS + + + + + + + + + +
+
+ + + +
+ +
+ +
+

+
+
+
+ +
+ + + + + diff --git a/www/html/Quiz-Battle/quiz-battle.js b/www/html/Quiz-Battle/quiz-battle.js index 47e22e8..e99178f 100644 --- a/www/html/Quiz-Battle/quiz-battle.js +++ b/www/html/Quiz-Battle/quiz-battle.js @@ -1,254 +1,352 @@ -(function () { - 'use strict'; - - var QB_IMAGE = typeof appPath === 'function' ? appPath('/Quiz-Battle/IMAGE') : '/Quiz-Battle/IMAGE'; - var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game'; - var CHAR_KEY = 'gameCharacterId'; - var LOBBY_IDLE_DOWN_PREFIX = 'jdCharLobbyIdleDown:'; - var VOICE_STATE_KEY = 'qbVoiceMicOn'; - - /* ฉากเดินตอบแบบ ZEP — แต่ละห้อง(หัวข้อ) = 1 space บนแผนที่ quiz_battle ของหัวข้อนั้น (qbroom1..10) */ - function mapIdForRoom(n) { return 'qbroom' + n; } - var QB_MAX_PLAYERS = 50; - var ROOM_NAME_PREFIX = 'Quiz Battle ห้อง '; - - if (localStorage.getItem('isLoggedIn') !== 'true') { - window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/'; - return; - } - - var grid = document.getElementById('qb-grid'); - var toast = document.getElementById('qb-toast'); - - function pad2(n) { return n < 10 ? '0' + n : String(n); } - function setToast(msg) { if (toast) toast.textContent = msg || ''; } - - function getSelectedCharacterId() { - try { return (localStorage.getItem(CHAR_KEY) || '').trim(); } catch (e) { return ''; } - } - - function getPlayerName() { - var keys = ['gameNickname', 'nickname', 'displayName', 'playerName', 'userName']; - for (var i = 0; i < keys.length; i++) { - try { var v = (localStorage.getItem(keys[i]) || '').trim(); if (v) return v.slice(0, 24); } catch (e) { /* ignore */ } - } - return 'ผู้เล่น' + Math.floor(1000 + Math.random() * 9000); - } - - function characterDownUrls(id) { - var enc = encodeURIComponent(id); - return [BASE + '/img/characters/' + enc + '_down.png', BASE + '/img/characters/' + enc + '_down_0.png']; - } - - function renderAvatarForId(av, cid, fallbackAvatar) { - try { - var savedLobbyAvatar = localStorage.getItem(LOBBY_IDLE_DOWN_PREFIX + cid) || ''; - if (savedLobbyAvatar && savedLobbyAvatar.indexOf('data:image/') === 0) { - av.onerror = function () { av.onerror = null; av.src = fallbackAvatar; }; - av.src = savedLobbyAvatar; - return; - } - } catch (e0) { /* ignore */ } - var urls = characterDownUrls(cid); - var avStep = 0; - av.onerror = function () { - avStep += 1; - if (avStep === 1) av.src = urls[1]; - else { av.onerror = null; av.src = fallbackAvatar; } - }; - av.src = urls[0]; - } - - function applyProfileAvatar() { - var av = document.getElementById('lobby-profile-avatar'); - if (!av) return; - var fallbackAvatar = typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png'; - var cid = getSelectedCharacterId(); - if (cid) { renderAvatarForId(av, cid, fallbackAvatar); return; } - // ไม่มี id ใน localStorage → ดึงจาก API เหมือน lobby (กัน avatar ไม่ขึ้น/ขึ้นรูป default) - fetch(BASE + '/api/characters', { credentials: 'same-origin', cache: 'no-store' }) - .then(function (r) { return r.json(); }) - .then(function (list) { - if (!Array.isArray(list) || !list.length) { av.onerror = null; av.src = fallbackAvatar; return; } - var last = list[list.length - 1]; - var id = last && last.id ? String(last.id).trim() : ''; - if (!id) { av.onerror = null; av.src = fallbackAvatar; return; } - renderAvatarForId(av, id, fallbackAvatar); - }) - .catch(function () { av.onerror = null; av.src = fallbackAvatar; }); - } - - /* ===================== เข้าฉากเดิน (ZEP walkable) ===================== */ - var entering = false; - - function roomName(n) { return ROOM_NAME_PREFIX + n; } - - function gotoPlay(spaceId) { - var nick = getPlayerName(); - var url = BASE + '/play.html?space=' + encodeURIComponent(spaceId) + - '&map=' + encodeURIComponent(mapIdForRoom(currentRoomNo)) + - '&nick=' + encodeURIComponent(nick); - try { localStorage.setItem('lastCreatedSpaceName', roomName(currentRoomNo)); } catch (e) { /* ignore */ } - window.location.href = url; - } - - var currentRoomNo = 0; - - /** หา space สาธารณะของห้องนี้ที่ยังมีคนอยู่ ถ้าไม่มีให้สร้างใหม่ แล้วเข้าเล่น */ - function startQuizForRoom(n) { - if (entering) return; - entering = true; - currentRoomNo = n; - setToast('กำลังเข้าห้อง ' + n + '…'); - var wantName = roomName(n); - fetch(BASE + '/api/spaces', { cache: 'no-store' }) - .then(function (r) { return r.ok ? r.json() : []; }) - .then(function (list) { - var found = null; - if (Array.isArray(list)) { - for (var i = 0; i < list.length; i++) { - if (list[i] && list[i].spaceName === wantName && (list[i].peerCount || 0) < QB_MAX_PLAYERS) { found = list[i]; break; } - } - } - if (found) { gotoPlay(found.spaceId); return null; } - // สร้างห้องใหม่บนแผนที่ quiz_battle - return fetch(BASE + '/api/spaces', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ mapId: mapIdForRoom(n), name: wantName, maxPlayers: QB_MAX_PLAYERS }) - }).then(function (r) { return r.json(); }).then(function (res) { - if (res && res.ok && res.spaceId) { gotoPlay(res.spaceId); } - else { setToast((res && res.error) || 'สร้างห้องไม่สำเร็จ'); entering = false; } - }); - }) - .catch(function () { setToast('เชื่อมต่อเซิร์ฟเวอร์ไม่ได้ ลองอีกครั้ง'); entering = false; }); - } - - /** อัปเดตจำนวนคนในแต่ละห้องจากรายชื่อ space (poll) */ - function refreshRoomCounts() { - fetch(BASE + '/api/spaces', { cache: 'no-store' }) - .then(function (r) { return r.ok ? r.json() : []; }) - .then(function (list) { - var counts = {}; - if (Array.isArray(list)) { - list.forEach(function (s) { - if (!s || !s.spaceName) return; - if (s.spaceName.indexOf(ROOM_NAME_PREFIX) === 0) { - var n = parseInt(s.spaceName.slice(ROOM_NAME_PREFIX.length), 10); - if (n >= 1 && n <= 10) counts[n] = (s.peerCount || 0); - } - }); - } - for (var i = 1; i <= 10; i++) { - var el = document.getElementById('qb-count-' + i); - if (el) el.textContent = (counts[i] || 0) + '/' + QB_MAX_PLAYERS; - } - }) - .catch(function () { /* keep last */ }); - } - - function getVoiceIconPath(micOn) { - var imgPath = micOn ? '/Game/img/btn-mic-on.png' : '/Game/img/btn-mic-mute.png'; - return typeof appPath === 'function' ? appPath(imgPath) : imgPath; - } - function setVoiceButtonIcon(btn, micOn) { - if (!btn) return; - var img = btn.querySelector('#btn-voice-icon-img') || btn.querySelector('img'); - if (img) { img.src = getVoiceIconPath(micOn); img.alt = micOn ? 'ปิดเสียง' : 'เปิดเสียง'; } - btn.title = micOn ? 'ปิดเสียงพูด' : 'เปิดเสียงพูด'; - btn.setAttribute('aria-label', micOn ? 'ปิดเสียงพูด' : 'เปิดเสียงพูด'); - } - function bindVoiceButton() { - var btnVoice = document.getElementById('btn-voice'); - if (!btnVoice) return; - var micOn = false; - try { micOn = localStorage.getItem(VOICE_STATE_KEY) === '1'; } catch (e) { /* ignore */ } - setVoiceButtonIcon(btnVoice, micOn); - btnVoice.addEventListener('click', function () { - micOn = !micOn; - try { localStorage.setItem(VOICE_STATE_KEY, micOn ? '1' : '0'); } catch (e2) { /* ignore */ } - setVoiceButtonIcon(btnVoice, micOn); - }); - } - - function buildGrid() { - if (!grid) return; - grid.innerHTML = ''; - for (var i = 1; i <= 10; i++) { - var card = document.createElement('div'); - card.className = 'qb-card'; - card.setAttribute('data-room', String(i)); - card.setAttribute('role', 'listitem'); - card.setAttribute('aria-label', 'ห้อง ' + i); - - var roomImg = document.createElement('img'); - roomImg.className = 'qb-card-room'; - roomImg.src = QB_IMAGE + '/Room-' + pad2(i) + '.png'; - roomImg.alt = ''; - roomImg.decoding = 'async'; - - var startBtn = document.createElement('button'); - startBtn.type = 'button'; - startBtn.className = 'qb-card-start'; - startBtn.setAttribute('aria-label', 'เริ่มเกมห้อง ' + i); - startBtn.addEventListener('click', (function (roomNo) { - return function (ev) { ev.preventDefault(); ev.stopPropagation(); startQuizForRoom(roomNo); }; - })(i)); - - var startImg = document.createElement('img'); - startImg.src = QB_IMAGE + '/btn-start-quiz.png'; - startImg.alt = 'เริ่มเกม'; - startImg.decoding = 'async'; - startBtn.appendChild(startImg); - - var countEl = document.createElement('span'); - countEl.className = 'qb-card-count'; - countEl.id = 'qb-count-' + i; - countEl.textContent = '0/' + QB_MAX_PLAYERS; - - card.appendChild(roomImg); - card.appendChild(startBtn); - card.appendChild(countEl); - grid.appendChild(card); - } - setToast('กดเริ่มเกมในห้องเพื่อเข้าฉากเดินตอบคำถาม'); - } - - function syncMainScale() { - var mainEl = document.querySelector('.qb-main'); - if (!mainEl) return; - var availableW = Math.max(1, mainEl.clientWidth); - var availableH = Math.max(1, mainEl.clientHeight); - var baseWidth = (271 * 5) + (18 * 4); - var baseHeight = 60 + 151 + 12 + (351 * 2) + 14 + 8 + 24; - var scaleW = availableW / baseWidth; - var scaleH = availableH / baseHeight; - var scale = Math.max(0.22, Math.min(1, Math.min(scaleW, scaleH))); - mainEl.style.setProperty('--qb-main-scale', scale.toFixed(4)); - } - - function syncUiScale() { - var uiEl = document.querySelector('.qb-ui'); - if (!uiEl) return; - var vw = Math.max(1, window.innerWidth || 0); - var vh = Math.max(1, window.innerHeight || 0); - var scale = Math.max(0.34, Math.min(1, Math.min(vw / 1920, vh / 1080))); - uiEl.style.setProperty('--qb-ui-scale', scale.toFixed(4)); - } - - document.getElementById('btn-back')?.addEventListener('click', function () { - window.location.href = typeof appPath === 'function' ? appPath('/Main-Lobby/') : '/Main-Lobby/'; - }); - - applyProfileAvatar(); - buildGrid(); - bindVoiceButton(); - refreshRoomCounts(); - setInterval(refreshRoomCounts, 5000); - syncUiScale(); - syncMainScale(); - window.addEventListener('resize', function () { syncUiScale(); syncMainScale(); }); - window.addEventListener('orientationchange', function () { - setTimeout(function () { syncUiScale(); syncMainScale(); }, 120); - }); -})(); +(function () { + 'use strict'; + + var QB_IMAGE = typeof appPath === 'function' ? appPath('/Quiz-Battle/IMAGE') : '/Quiz-Battle/IMAGE'; + var BASE = typeof appPath === 'function' ? appPath('/Game') : '/Game'; + var CHAR_KEY = 'gameCharacterId'; + var LOBBY_IDLE_DOWN_PREFIX = 'jdCharLobbyIdleDown:'; + var VOICE_STATE_KEY = 'qbVoiceMicOn'; + + /* ฉากเดินตอบแบบ ZEP — แต่ละห้อง(หัวข้อ) = 1 space บนแผนที่ quiz_battle ของหัวข้อนั้น (qbroom1..10) */ + function mapIdForRoom(n) { return 'qbroom' + n; } + var QB_MAX_PLAYERS = 50; /* เพดานแข็ง (fallback ก่อน config โหลด) */ + var ROOM_NAME_PREFIX = 'Quiz Battle ห้อง '; + /* [2026-07-23] จำนวนคนต่อห้องปรับได้จาก Admin — เก็บค่าจริงต่อห้อง (เติมจาก refreshRoomModes) */ + var qbRoomMax = {}; + function roomMax(n) { var v = qbRoomMax[String(n)]; return (v >= 1) ? Math.min(50, v) : QB_MAX_PLAYERS; } + + /* [2026-07-23] หัวข้อห้อง — การ์ดใหม่ประกอบเอง (กรอบ+ไอคอน+ชื่อเป็นข้อความ) → ชื่อเปลี่ยนตาม Admin ได้ + default = ค่าเดิม (โชว์ทันทีก่อน config โหลด) · เมื่อ refreshRoomModes ได้ title จาก /api → เขียนทับ */ + var DEFAULT_TITLES = ['', 'กฎหมายใกล้ตัว', 'กฎหมายสิทธิพื้นฐาน', 'กฎหมายจราจร', 'คดีเกี่ยวกับทรัพย์', 'คดีหมิ่นประมาท', 'คดีทางเพศ', 'คดีอาชญากรรม', 'อาชญากรรมออนไลน์', 'กระบวนการยุติธรรม', 'งานบริการกระทรวงยุติธรรม']; + function roomTitle(n) { + var m = qbModeState[String(n)]; + if (m && typeof m.title === 'string' && m.title.trim()) return m.title.trim(); + return DEFAULT_TITLES[n] || ('ห้อง ' + n); + } + function applyRoomTitle(n) { + var el = document.getElementById('qb-title-' + n); + if (el) { var t = roomTitle(n); if (el.textContent !== t) el.textContent = t; } + } + + if (localStorage.getItem('isLoggedIn') !== 'true') { + window.location.href = typeof appPath === 'function' ? appPath('/Login/') : '/Login/'; + return; + } + + var grid = document.getElementById('qb-grid'); + var toast = document.getElementById('qb-toast'); + + function pad2(n) { return n < 10 ? '0' + n : String(n); } + function setToast(msg) { if (toast) toast.textContent = msg || ''; } + + function getSelectedCharacterId() { + try { return (localStorage.getItem(CHAR_KEY) || '').trim(); } catch (e) { return ''; } + } + + function getPlayerName() { + var keys = ['gameNickname', 'nickname', 'displayName', 'playerName', 'userName']; + for (var i = 0; i < keys.length; i++) { + try { var v = (localStorage.getItem(keys[i]) || '').trim(); if (v) return v.slice(0, 24); } catch (e) { /* ignore */ } + } + return 'ผู้เล่น' + Math.floor(1000 + Math.random() * 9000); + } + + function characterDownUrls(id) { + var enc = encodeURIComponent(id); + return [BASE + '/img/characters/' + enc + '_down.png', BASE + '/img/characters/' + enc + '_down_0.png']; + } + + function renderAvatarForId(av, cid, fallbackAvatar) { + try { + var savedLobbyAvatar = localStorage.getItem(LOBBY_IDLE_DOWN_PREFIX + cid) || ''; + if (savedLobbyAvatar && savedLobbyAvatar.indexOf('data:image/') === 0) { + av.onerror = function () { av.onerror = null; av.src = fallbackAvatar; }; + av.src = savedLobbyAvatar; + return; + } + } catch (e0) { /* ignore */ } + var urls = characterDownUrls(cid); + var avStep = 0; + av.onerror = function () { + avStep += 1; + if (avStep === 1) av.src = urls[1]; + else { av.onerror = null; av.src = fallbackAvatar; } + }; + av.src = urls[0]; + } + + function applyProfileAvatar() { + var av = document.getElementById('lobby-profile-avatar'); + if (!av) return; + var fallbackAvatar = typeof appPath === 'function' ? appPath('/Main-Menu/char-main.png') : '/Main-Menu/char-main.png'; + var cid = getSelectedCharacterId(); + if (cid) { renderAvatarForId(av, cid, fallbackAvatar); return; } + // ไม่มี id ใน localStorage → ดึงจาก API เหมือน lobby (กัน avatar ไม่ขึ้น/ขึ้นรูป default) + fetch(BASE + '/api/characters', { credentials: 'same-origin', cache: 'no-store' }) + .then(function (r) { return r.json(); }) + .then(function (list) { + if (!Array.isArray(list) || !list.length) { av.onerror = null; av.src = fallbackAvatar; return; } + var last = list[list.length - 1]; + var id = last && last.id ? String(last.id).trim() : ''; + if (!id) { av.onerror = null; av.src = fallbackAvatar; return; } + renderAvatarForId(av, id, fallbackAvatar); + }) + .catch(function () { av.onerror = null; av.src = fallbackAvatar; }); + } + + /* ===================== เข้าฉากเดิน (ZEP walkable) ===================== */ + var entering = false; + + function roomName(n) { return ROOM_NAME_PREFIX + n; } + + function gotoPlay(spaceId) { + var nick = getPlayerName(); + var url = BASE + '/play.html?space=' + encodeURIComponent(spaceId) + + '&map=' + encodeURIComponent(mapIdForRoom(currentRoomNo)) + + '&nick=' + encodeURIComponent(nick); + try { localStorage.setItem('lastCreatedSpaceName', roomName(currentRoomNo)); } catch (e) { /* ignore */ } + window.location.href = url; + } + + var currentRoomNo = 0; + + /** หา space สาธารณะของห้องนี้ที่ยังมีคนอยู่ ถ้าไม่มีให้สร้างใหม่ แล้วเข้าเล่น */ + function startQuizForRoom(n) { + if (entering) return; + /* โหมดถูกปิด/หมดเวลา → ไม่ต้องสร้างห้องเปล่าทิ้งไว้ (server ก็ปฏิเสธอยู่ดี) */ + if (!roomIsOpen(n)) { + var mm = qbModeState[String(n)] || {}; + setToast(mm.expired ? ('ห้องนี้หมดเวลาเล่นแล้ว (ถึง ' + (mm.openUntil || '') + ' น.)') : 'ห้องนี้ปิดชั่วคราว'); + return; + } + entering = true; + currentRoomNo = n; + setToast('กำลังเข้าห้อง ' + n + '…'); + var wantName = roomName(n); + fetch(BASE + '/api/spaces', { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : []; }) + .then(function (list) { + var found = null; + if (Array.isArray(list)) { + for (var i = 0; i < list.length; i++) { + if (list[i] && list[i].spaceName === wantName && (list[i].peerCount || 0) < roomMax(n)) { found = list[i]; break; } + } + } + if (found) { gotoPlay(found.spaceId); return null; } + // สร้างห้องใหม่บนแผนที่ quiz_battle (server บังคับ cap ตาม config อีกชั้นตอน join) + return fetch(BASE + '/api/spaces', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mapId: mapIdForRoom(n), name: wantName, maxPlayers: roomMax(n) }) + }).then(function (r) { return r.json(); }).then(function (res) { + if (res && res.ok && res.spaceId) { gotoPlay(res.spaceId); } + else { setToast((res && res.error) || 'สร้างห้องไม่สำเร็จ'); entering = false; } + }); + }) + .catch(function () { setToast('เชื่อมต่อเซิร์ฟเวอร์ไม่ได้ ลองอีกครั้ง'); entering = false; }); + } + + /** อัปเดตจำนวนคนในแต่ละห้องจากรายชื่อ space (poll) */ + function refreshRoomCounts() { + fetch(BASE + '/api/spaces', { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : []; }) + .then(function (list) { + var counts = {}; + if (Array.isArray(list)) { + list.forEach(function (s) { + if (!s || !s.spaceName) return; + if (s.spaceName.indexOf(ROOM_NAME_PREFIX) === 0) { + var n = parseInt(s.spaceName.slice(ROOM_NAME_PREFIX.length), 10); + if (n >= 1 && n <= 10) counts[n] = (s.peerCount || 0); + } + }); + } + for (var i = 1; i <= 10; i++) { + var el = document.getElementById('qb-count-' + i); + if (el) el.textContent = (counts[i] || 0) + '/' + roomMax(i); + } + }) + .catch(function () { /* keep last */ }); + } + + function getVoiceIconPath(micOn) { + var imgPath = micOn ? '/Game/img/btn-mic-on.png' : '/Game/img/btn-mic-mute.png'; + return typeof appPath === 'function' ? appPath(imgPath) : imgPath; + } + function setVoiceButtonIcon(btn, micOn) { + if (!btn) return; + var img = btn.querySelector('#btn-voice-icon-img') || btn.querySelector('img'); + if (img) { img.src = getVoiceIconPath(micOn); img.alt = micOn ? 'ปิดเสียง' : 'เปิดเสียง'; } + btn.title = micOn ? 'ปิดเสียงพูด' : 'เปิดเสียงพูด'; + btn.setAttribute('aria-label', micOn ? 'ปิดเสียงพูด' : 'เปิดเสียงพูด'); + } + function bindVoiceButton() { + var btnVoice = document.getElementById('btn-voice'); + if (!btnVoice) return; + var micOn = false; + try { micOn = localStorage.getItem(VOICE_STATE_KEY) === '1'; } catch (e) { /* ignore */ } + setVoiceButtonIcon(btnVoice, micOn); + btnVoice.addEventListener('click', function () { + micOn = !micOn; + try { localStorage.setItem(VOICE_STATE_KEY, micOn ? '1' : '0'); } catch (e2) { /* ignore */ } + setVoiceButtonIcon(btnVoice, micOn); + }); + } + + function buildGrid() { + if (!grid) return; + grid.innerHTML = ''; + for (var i = 1; i <= 10; i++) { + var card = document.createElement('div'); + card.className = 'qb-card'; + card.setAttribute('data-room', String(i)); + card.setAttribute('role', 'listitem'); + card.setAttribute('aria-label', 'ห้อง ' + i); + + /* [2026-07-23] การ์ดประกอบเอง: กรอบ (room-frame) + ไอคอน (room-icon-N) + ชื่อ (ข้อความ) + แทน Room-NN.png เดิมที่ฝังชื่อในรูป → เปลี่ยนชื่อตาม Admin ได้ */ + var roomImg = document.createElement('img'); + roomImg.className = 'qb-card-room'; + roomImg.src = QB_IMAGE + '/room-frame.png'; + roomImg.alt = ''; + roomImg.decoding = 'async'; + roomImg.loading = 'lazy'; /* [2026-07-23] การ์ด 10 ห้อง — บนมือถือเห็นไม่ครบจอ โหลดเท่าที่เห็นพอ */ + + var iconImg = document.createElement('img'); + iconImg.className = 'qb-card-icon'; + iconImg.src = QB_IMAGE + '/room-icon-' + i + '.png'; + iconImg.alt = ''; + iconImg.decoding = 'async'; + iconImg.loading = 'lazy'; + + var titleEl = document.createElement('div'); + titleEl.className = 'qb-card-title'; + titleEl.id = 'qb-title-' + i; + titleEl.textContent = roomTitle(i); + + var startBtn = document.createElement('button'); + startBtn.type = 'button'; + startBtn.className = 'qb-card-start'; + startBtn.setAttribute('aria-label', 'เริ่มเกมห้อง ' + i); + startBtn.addEventListener('click', (function (roomNo) { + return function (ev) { ev.preventDefault(); ev.stopPropagation(); startQuizForRoom(roomNo); }; + })(i)); + + var startImg = document.createElement('img'); + startImg.src = QB_IMAGE + '/btn-start-quiz.png'; + startImg.alt = 'เริ่มเกม'; + startImg.decoding = 'async'; + startImg.loading = 'lazy'; + startBtn.appendChild(startImg); + + var countEl = document.createElement('span'); + countEl.className = 'qb-card-count'; + countEl.id = 'qb-count-' + i; + countEl.textContent = '0/' + roomMax(i); + + /* ป้ายสถานะโหมด (ปิด / หมดเวลา / ถึงวันที่…) — เติมค่าโดย refreshRoomModes() */ + var lockEl = document.createElement('span'); + lockEl.className = 'qb-card-lock'; + lockEl.id = 'qb-lock-' + i; + lockEl.hidden = true; + + card.appendChild(roomImg); + card.appendChild(iconImg); + card.appendChild(titleEl); + card.appendChild(startBtn); + card.appendChild(countEl); + card.appendChild(lockEl); + grid.appendChild(card); + } + setToast('กดเริ่มเกมในห้องเพื่อเข้าฉากเดินตอบคำถาม'); + } + + /* ===== โหมดที่แอดมินเปิด/ปิด + วันหมดเขต (เวลาไทย) [2026-07-22] ===== + ตัวตัดสินจริงอยู่ที่ server (join-space ปฏิเสธห้องที่ปิด) — ตรงนี้แค่บอกผู้เล่นให้รู้ก่อนกด */ + var qbModeState = {}; + + function roomIsOpen(n) { + var m = qbModeState[String(n)]; + return !m || m.open !== false; + } + + function applyRoomModeUi(n, m) { + var card = grid && grid.querySelector('.qb-card[data-room="' + n + '"]'); + var lock = document.getElementById('qb-lock-' + n); + var btn = card && card.querySelector('.qb-card-start'); + var open = !m || m.open !== false; + if (card) card.classList.toggle('is-locked', !open); + if (btn) { + btn.disabled = !open; + btn.setAttribute('aria-disabled', open ? 'false' : 'true'); + } + if (!lock) return; + if (open) { + if (m && m.openUntil) { lock.hidden = false; lock.className = 'qb-card-lock is-until'; lock.textContent = 'ถึง ' + m.openUntil + ' น.'; } + else { lock.hidden = true; lock.textContent = ''; } + } else { + lock.hidden = false; + lock.className = 'qb-card-lock is-closed'; + lock.textContent = (m && m.expired) ? ('หมดเวลาแล้ว · ' + (m.openUntil || '')) : 'ปิดชั่วคราว'; + } + } + + function refreshRoomModes() { + fetch(BASE + '/api/quiz-battle-modes', { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (j) { + if (!j || !j.modes) return; + qbModeState = j.modes; + for (var i = 1; i <= 10; i++) { + var mm = j.modes[String(i)] || {}; + if (mm.maxPlayers >= 1) qbRoomMax[String(i)] = Math.min(50, mm.maxPlayers | 0); + applyRoomModeUi(i, mm); + applyRoomTitle(i); /* [2026-07-23] ชื่อห้องจาก Admin → เขียนทับข้อความบนการ์ด */ + /* อัปเดตป้าย X/max ให้ตัวหารตรง config (ตัวเศษ = คนจริง อัปโดย refreshRoomCounts) */ + var cEl = document.getElementById('qb-count-' + i); + if (cEl) { var cur = (cEl.textContent || '').split('/')[0] || '0'; cEl.textContent = cur + '/' + roomMax(i); } + } + }) + .catch(function () { /* อ่านไม่ได้ → ปล่อยเปิดไว้ ให้ server เป็นคนปฏิเสธเอง */ }); + } + + function syncMainScale() { + var mainEl = document.querySelector('.qb-main'); + if (!mainEl) return; + var availableW = Math.max(1, mainEl.clientWidth); + var availableH = Math.max(1, mainEl.clientHeight); + var baseWidth = (271 * 5) + (18 * 4); + var baseHeight = 60 + 151 + 12 + (351 * 2) + 14 + 8 + 24; + var scaleW = availableW / baseWidth; + var scaleH = availableH / baseHeight; + var scale = Math.max(0.22, Math.min(1, Math.min(scaleW, scaleH))); + mainEl.style.setProperty('--qb-main-scale', scale.toFixed(4)); + } + + function syncUiScale() { + var uiEl = document.querySelector('.qb-ui'); + if (!uiEl) return; + var vw = Math.max(1, window.innerWidth || 0); + var vh = Math.max(1, window.innerHeight || 0); + var scale = Math.max(0.34, Math.min(1, Math.min(vw / 1920, vh / 1080))); + uiEl.style.setProperty('--qb-ui-scale', scale.toFixed(4)); + } + + document.getElementById('btn-back')?.addEventListener('click', function () { + window.location.href = typeof appPath === 'function' ? appPath('/Main-Lobby/') : '/Main-Lobby/'; + }); + + applyProfileAvatar(); + buildGrid(); + bindVoiceButton(); + refreshRoomCounts(); + setInterval(refreshRoomCounts, 5000); + refreshRoomModes(); + setInterval(refreshRoomModes, 30000); /* เผื่อแอดมินปิดโหมดระหว่างที่ค้างหน้านี้อยู่ */ + syncUiScale(); + syncMainScale(); + window.addEventListener('resize', function () { syncUiScale(); syncMainScale(); }); + window.addEventListener('orientationchange', function () { + setTimeout(function () { syncUiScale(); syncMainScale(); }, 120); + }); +})(); diff --git a/www/html/Quiz-Battle/style.css b/www/html/Quiz-Battle/style.css index 69e0966..ceae34c 100644 --- a/www/html/Quiz-Battle/style.css +++ b/www/html/Quiz-Battle/style.css @@ -257,6 +257,43 @@ body { user-select: none; } +/* [2026-07-23] การ์ดประกอบเอง: ไอคอน (วางบน) + ชื่อเป็นข้อความ (เปลี่ยนตาม Admin) */ +.qb-card-icon { + position: absolute; + left: 50%; + top: calc(52px * var(--qb-main-scale)); /* ~15% ของการ์ด 351px */ + transform: translateX(-50%); + width: calc(150px * var(--qb-main-scale)); + height: calc(150px * var(--qb-main-scale)); + object-fit: contain; + pointer-events: none; + user-select: none; + z-index: 2; +} + +.qb-card-title { + position: absolute; + left: 50%; + top: calc(214px * var(--qb-main-scale)); /* ~61% — ใต้ไอคอน ตรงกับแถบชื่อใน mock เดิม */ + transform: translateX(-50%); + width: calc(240px * var(--qb-main-scale)); + text-align: center; + font-family: 'NotoSansThaiMedium', 'NotoSansThai', 'Kanit', 'Sarabun', system-ui, sans-serif; + font-size: calc(24px * var(--qb-main-scale)); + font-weight: 600; + line-height: 1.1; + color: #ffffff; + text-shadow: + 0 0 calc(12px * var(--qb-main-scale)) rgba(87, 217, 255, 0.7), + 0 0 calc(3px * var(--qb-main-scale)) rgba(0, 0, 0, 0.7); + pointer-events: none; + user-select: none; + z-index: 3; + /* ชื่อยาว (เช่น "งานบริการกระทรวงยุติธรรม") → ย่อลงพอดี ไม่ล้นการ์ด */ + overflow-wrap: break-word; + word-break: break-word; +} + .qb-card-start { position: absolute; left: 50%; @@ -634,3 +671,35 @@ body { + +/* ===== โหมดที่แอดมินปิด / มีวันหมดเขต (เวลาไทย) [2026-07-22] ===== */ +.qb-card-lock { + position: absolute; + left: 50%; + bottom: calc(96px * var(--qb-main-scale)); + transform: translateX(-50%); + max-width: calc(230px * var(--qb-main-scale)); + padding: calc(6px * var(--qb-main-scale)) calc(12px * var(--qb-main-scale)); + border-radius: 999px; + font-family: 'NotoSansThaiMedium', 'NotoSansThai', 'Kanit', 'Sarabun', system-ui, sans-serif; + font-size: calc(17px * var(--qb-main-scale)); + line-height: 1.15; + text-align: center; + white-space: nowrap; + pointer-events: none; + z-index: 4; +} +.qb-card-lock.is-until { + background: rgba(8, 20, 40, 0.82); + border: 1px solid rgba(120, 220, 255, 0.55); + color: #bfe9ff; +} +.qb-card-lock.is-closed { + background: rgba(48, 8, 18, 0.9); + border: 1px solid rgba(255, 120, 150, 0.75); + color: #ffc2d0; +} +/* ห้องที่ปิด: หรี่ทั้งใบ + ปุ่มกดไม่ได้ (server ปฏิเสธซ้ำอีกชั้นอยู่แล้ว) */ +.qb-card.is-locked .qb-card-room { filter: grayscale(0.85) brightness(0.5); } +.qb-card.is-locked .qb-card-start { filter: grayscale(1) brightness(0.55); cursor: not-allowed; } +.qb-card.is-locked .qb-card-start:hover { transform: none; } diff --git a/www/html/gamelog/data/log.jsonl b/www/html/gamelog/data/log.jsonl index 022b9d3..318f291 100644 --- a/www/html/gamelog/data/log.jsonl +++ b/www/html/gamelog/data/log.jsonl @@ -10227,3 +10227,1732 @@ {"t":1784716266158,"cat":"LobbyB","room":"trialshot-237492-1784716236911","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:9.2,16.6->9.2,16.7 b1:24.1,11.4->25.3,10.3 b2:13.0,6.7->14.5,7.1] [+26225ms]","src":"client"} {"t":1784716270210,"cat":"LobbyB","room":"trialshot-237492-1784716236911","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:19.8,12.5->21.7,11.4 b1:28.4,7.3->27.4,8.2 b2:28.8,8.1->29.0,6.9] [+30275ms]","src":"client"} {"t":1784716274224,"cat":"LobbyB","room":"trialshot-237492-1784716236911","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:18.7,7.3->16.5,7.1 b1:14.9,13.8->12.7,13.4 b2:16.2,5.2->16.2,5.3] [+34291ms]","src":"client"} +{"t":1784777155119,"cat":"LobbyA","room":"Quiz Battle ห้อง 1","who":"สุดซอยแจ้งวัฒนะ","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1784777205324,"cat":"LobbyA","room":"บางบอน","who":"สุดซอยแจ้งวัฒนะ","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1784777206182,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+54ms]","src":"client"} +{"t":1784777206183,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"lobby-open","detail":"space=บางบอน-1784777201859 [+0ms]","src":"client"} +{"t":1784777206187,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"preload-start","detail":" [+70ms]","src":"client"} +{"t":1784777206380,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"manifest-done","detail":" [+70ms]","src":"client"} +{"t":1784777206384,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"colors-resolved","detail":" [+74ms]","src":"client"} +{"t":1784777206387,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"mychar-tinted","detail":"roomJoinReady=false [+74ms]","src":"client"} +{"t":1784777206392,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"preload-occupants-done","detail":" [+75ms]","src":"client"} +{"t":1784777206415,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"preload-ready(occupants)","detail":" [+75ms]","src":"client"} +{"t":1784777206416,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"socket-connect","detail":" [+215ms]","src":"client"} +{"t":1784777209050,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"join-ok","detail":"peers=1 host=me map=mlsbbxfe bots=5 [+2723ms]","src":"client"} +{"t":1784777209083,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2739ms]","src":"client"} +{"t":1784777209102,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้ง","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=บางบอน-1784777201859 mapName=LobbyA [+3201ms]","src":"client"} +{"t":1784777209118,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้ง","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,14.0->9.9,15.8 b1:16.0,14.0->17.6,14.7 b2:22.0,13.0->20.2,13.2 b3:23.0,8.0->24.6,8.7 b4:15.0,5.0->16.7,5.6] [+3229ms]","src":"client"} +{"t":1784777214742,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้ง","ev":"bot-pos","detail":"map=35x20 n=5 [b0:9.9,15.8->9.8,17.6 b1:17.6,14.7->19.3,15.5 b2:20.2,13.2->18.4,13.5 b3:24.6,8.7->26.3,9.5 b4:16.7,5.6->18.4,6.2] [+9636ms]","src":"client"} +{"t":1784777225804,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้ง","ev":"bot-pos","detail":"map=35x20 n=5 [b0:13.4,15.8->12.9,15.5 b1:29.8,6.7->29.8,6.6 b2:7.8,4.3->7.8,4.3 b3:29.8,11.6->29.8,11.6 b4:27.0,9.4->27.0,9.4] [+20687ms]","src":"client"} +{"t":1784777232821,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้ง","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.2,4.2->9.7,4.0 b1:18.3,4.3->18.3,4.3 b2:17.0,4.3->17.0,4.3 b3:6.2,13.5->6.2,13.5 b4:12.4,9.8->11.8,9.8] [+27596ms]","src":"client"} +{"t":1784777249957,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้ง","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,4.7->10.0,4.7 b1:20.4,8.4->19.9,8.5 b2:14.7,10.6->14.7,10.6 b3:13.0,5.2->13.0,5.2 b4:16.2,4.4->16.2,4.4] [+44309ms]","src":"client"} +{"t":1784777261344,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้ง","ev":"bot-pos","detail":"map=35x20 n=5 [b0:13.0,5.6->13.5,5.9 b1:6.0,11.7->6.0,11.7 b2:14.7,10.6->14.7,10.6 b3:6.5,4.3->6.5,4.3 b4:13.5,7.5->13.1,7.8] [+56229ms]","src":"client"} +{"t":1784777272298,"cat":"LobbyA","room":"บางบอน-1784777201859","who":"สุดซอยแจ้งวัฒนะ","ev":"socket-connect","detail":" [+67198ms]","src":"client"} +{"t":1784785065092,"cat":"LobbyA","room":"Quiz Battle ห้อง 7","who":"Q","ev":"join","detail":"สี#6 · คน 1 + บอท 0","src":"server"} +{"t":1784786088951,"cat":"LobbyA","room":"Quiz Battle ห้อง 1","who":"Q","ev":"join","detail":"สี#6 · คน 1 + บอท 0","src":"server"} +{"t":1784786100612,"cat":"LobbyA","room":"Quiz Battle ห้อง 1","who":"Q","ev":"join","detail":"สี#6 · คน 1 + บอท 0","src":"server"} +{"t":1784810076326,"cat":"LobbyA","room":"","who":"ผู้เล่น","ev":"lobby-open","detail":"space=null [+0ms]","src":"client"} +{"t":1784875508166,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875504327 [+1ms]","src":"client"} +{"t":1784875508245,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+36ms]","src":"client"} +{"t":1784875508261,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"preload-start","detail":" [+40ms]","src":"client"} +{"t":1784875508270,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"manifest-done","detail":" [+40ms]","src":"client"} +{"t":1784875508286,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+41ms]","src":"client"} +{"t":1784875508287,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+41ms]","src":"client"} +{"t":1784875508318,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+41ms]","src":"client"} +{"t":1784875508325,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+41ms]","src":"client"} +{"t":1784875508469,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"socket-connect","detail":" [+336ms]","src":"client"} +{"t":1784875508483,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"join","detail":"สี#5 · คน 1 + บอท 3","src":"server"} +{"t":1784875510649,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mlsbbxfe bots=3 [+2551ms]","src":"client"} +{"t":1784875510661,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2551ms]","src":"client"} +{"t":1784875510719,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875504327 mapName=LobbyA [+2610ms]","src":"client"} +{"t":1784875510776,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:10.0,14.0->9.9,12.2 b1:16.0,14.0->15.8,14.0 b2:22.0,13.0->22.7,11.4] [+2615ms]","src":"client"} +{"t":1784875520421,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:9.3,4.4->9.3,4.3 b1:16.7,13.2->18.3,13.1 b2:27.3,5.2->27.4,5.1] [+12281ms]","src":"client"} +{"t":1784875524405,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:30.0,10.1->30.0,10.1 b1:11.8,4.5->12.0,4.4 b2:6.7,16.0->6.3,15.9] [+16298ms]","src":"client"} +{"t":1784875526329,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"lobby-open","detail":"space=ฟ้า [+7ms]","src":"client"} +{"t":1784875526479,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"manifest-done","detail":" [+186ms]","src":"client"} +{"t":1784875526479,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+186ms]","src":"client"} +{"t":1784875526481,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"preload-start","detail":" [+185ms]","src":"client"} +{"t":1784875526516,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+186ms]","src":"client"} +{"t":1784875526518,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+186ms]","src":"client"} +{"t":1784875526560,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+186ms]","src":"client"} +{"t":1784875527795,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"socket-connect","detail":" [+363ms]","src":"client"} +{"t":1784875527810,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1784875528552,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:16.7,7.6->17.6,7.4 b1:20.4,12.7->20.4,12.7 b2:10.6,13.6->11.5,13.2] [+20301ms]","src":"client"} +{"t":1784875528812,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2521ms]","src":"client"} +{"t":1784875528813,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mlsbbxfe bots=5 [+2520ms]","src":"client"} +{"t":1784875528917,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=ฟ้า mapName=LobbyA [+2617ms]","src":"client"} +{"t":1784875528940,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,14.0->8.8,13.5 b1:16.0,14.0->17.3,13.7 b2:22.0,13.0->20.9,12.3 b3:23.0,8.0->21.7,8.2 b4:15.0,5.0->13.8,5.5] [+2653ms]","src":"client"} +{"t":1784875531591,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host เปิด wizard เลือกคดี → ล็อก set-ready ทั้งห้อง (1 คนในห้อง)","src":"server"} +{"t":1784875531785,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"mobile-center-btn","detail":"[TC177] แตะปุ่ม START\/center โดยตรง (มือถือ ไม่ใช้ F) [+23494ms]","src":"client"} +{"t":1784875532505,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:22.9,6.6->21.9,6.8 b1:8.6,11.0->7.9,10.3 b2:22.9,14.4->22.3,15.2] [+24336ms]","src":"client"} +{"t":1784875536402,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"lobby-open","detail":"space=ฟ้า [+0ms]","src":"client"} +{"t":1784875536469,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"preload-start","detail":" [+26ms]","src":"client"} +{"t":1784875536470,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"manifest-done","detail":" [+26ms]","src":"client"} +{"t":1784875536483,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+27ms]","src":"client"} +{"t":1784875536495,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+27ms]","src":"client"} +{"t":1784875536496,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+27ms]","src":"client"} +{"t":1784875536536,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+27ms]","src":"client"} +{"t":1784875536628,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"socket-connect","detail":" [+215ms]","src":"client"} +{"t":1784875536763,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:6.0,11.0->6.0,11.0 b1:6.0,17.2->6.0,17.4 b2:20.0,17.9->20.0,17.9] [+28385ms]","src":"client"} +{"t":1784875538597,"cat":"LobbyB","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1784875539056,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+30947ms]","src":"client"} +{"t":1784875540623,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:14.9,11.2->14.9,11.2 b1:17.4,5.4->17.9,5.8 b2:27.7,13.2->27.7,13.2] [+32502ms]","src":"client"} +{"t":1784875540943,"cat":"LobbyB","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+32813ms]","src":"client"} +{"t":1784875541528,"cat":"LobbyB","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"suspect-open-evt","detail":"idx=0 postCase=true [+33409ms]","src":"client"} +{"t":1784875545183,"cat":"LobbyB","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:12.7,16.9->12.6,16.9 b1:19.5,10.2->19.0,9.5 b2:27.7,13.1->27.7,13.1] [+37091ms]","src":"client"} +{"t":1784875546057,"cat":"LobbyB","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"story-gate","detail":"[TC156] อ่าน story จบ 1/1","src":"server"} +{"t":1784875546057,"cat":"LobbyB","room":"แจ้งวัฒนะซอย 7","who":"","ev":"story-gate","detail":"[TC156] ทุกคนอ่าน story ครบ (1) → ปลดล็อกเลือกผู้ต้องสงสัย (pass)","src":"server"} +{"t":1784875546544,"cat":"LobbyB","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+38429ms]","src":"client"} +{"t":1784875549355,"cat":"LobbyB","room":"แจ้งวัฒนะซอย 7-1784875504327","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:19.5,16.9->19.5,16.9 b1:8.2,7.7->8.2,7.7 b2:12.7,5.4->12.7,5.4] [+41104ms]","src":"client"} +{"t":1784875552240,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-4 คำศัพท์ในกระบวนการยุติธรรม (การ์ด 2)","src":"server"} +{"t":1784875552253,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1784875552337,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1784875553364,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"join","detail":"สี#5 · คน 1 + บอท 3","src":"server"} +{"t":1784875561189,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"lobby-open","detail":"space=ฟ้า [+0ms]","src":"client"} +{"t":1784875561270,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"preload-start","detail":" [+23ms]","src":"client"} +{"t":1784875561276,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"manifest-done","detail":" [+23ms]","src":"client"} +{"t":1784875561286,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+24ms]","src":"client"} +{"t":1784875561298,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+24ms]","src":"client"} +{"t":1784875561316,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+24ms]","src":"client"} +{"t":1784875561329,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+24ms]","src":"client"} +{"t":1784875561445,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"socket-connect","detail":" [+249ms]","src":"client"} +{"t":1784875561448,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1784875564598,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mlsbbxfe bots=5 [+2022ms]","src":"client"} +{"t":1784875564598,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2023ms]","src":"client"} +{"t":1784875564888,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=ฟ้า mapName=LobbyA [+2084ms]","src":"client"} +{"t":1784875564960,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,14.0->10.7,14.0 b1:16.0,14.0->15.3,14.0 b2:22.0,13.0->22.7,13.1 b3:23.0,8.0->22.4,7.7 b4:15.0,5.0->15.6,5.2] [+2088ms]","src":"client"} +{"t":1784875569152,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:23.7,9.8->21.5,7.2 b1:15.1,16.5->17.6,17.2 b2:18.0,4.2->17.9,4.1 b3:6.5,15.3->6.2,15.5 b4:29.3,10.8->29.5,10.9] [+7844ms]","src":"client"} +{"t":1784875573832,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-read","detail":"q=1 deck=29 rounds=0/10","src":"server"} +{"t":1784875574329,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:20.4,7.0->16.9,4.1 b1:10.8,14.7->6.4,12.9 b2:15.6,5.3->12.6,6.8 b3:16.3,13.1->26.0,10.8 b4:26.9,10.1->23.8,9.1] [+13029ms]","src":"client"} +{"t":1784875575096,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+13910ms]","src":"client"} +{"t":1784875578347,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:17.7,13.0->17.7,13.0 b1:23.0,11.3->23.9,11.7 b2:16.3,5.0->16.3,5.8 b3:6.4,5.8->6.4,5.8 b4:26.5,6.3->25.6,6.7] [+17043ms]","src":"client"} +{"t":1784875582350,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.4,13.0->19.5,13.0 b1:18.3,15.9->17.4,15.8 b2:16.1,9.6->16.1,9.6 b3:8.5,14.3->9.3,14.0 b4:16.8,9.9->16.8,9.9] [+21044ms]","src":"client"} +{"t":1784875586379,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:24.5,8.3->25.0,7.6 b1:6.1,13.3->6.1,13.3 b2:6.8,8.9->6.4,8.8 b3:17.8,6.7->16.9,6.6 b4:6.3,4.3->6.1,4.2] [+25078ms]","src":"client"} +{"t":1784875590467,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:29.5,5.3->29.5,5.3 b1:20.4,10.5->20.7,10.7 b2:8.6,14.6->8.7,14.7 b3:29.8,11.7->29.8,11.7 b4:11.5,4.5->11.5,4.7] [+29129ms]","src":"client"} +{"t":1784875591833,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-round","detail":"resolve 1/10 q=1 winner=-(หมดเวลา ไม่มีคนถูก) correctIdx=7 willEnd=false","src":"server"} +{"t":1784875594334,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-read","detail":"q=2 deck=28 rounds=1/10","src":"server"} +{"t":1784875594464,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:15.4,13.9->14.6,13.4 b1:13.6,13.7->14.0,12.9 b2:22.6,17.6->22.6,17.7 b3:20.3,9.8->20.2,9.8 b4:21.0,5.5->21.0,5.5] [+33160ms]","src":"client"} +{"t":1784875598507,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:6.3,7.1->6.3,7.1 b1:18.7,4.4->18.7,4.4 b2:15.7,9.5->16.6,9.0 b3:11.5,4.2->11.5,4.2 b4:17.2,5.0->16.2,4.9] [+37191ms]","src":"client"} +{"t":1784875602549,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:21.3,9.9->20.3,10.2 b1:14.0,12.7->14.9,13.0 b2:19.2,4.1->19.2,4.1 b3:14.8,9.2->15.4,9.6 b4:29.8,8.6->29.8,8.7] [+41227ms]","src":"client"} +{"t":1784875606558,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:6.3,15.2->6.3,15.2 b1:24.3,13.4->24.6,12.5 b2:19.5,7.1->18.5,7.1 b3:14.1,8.9->13.1,8.8 b4:11.9,9.6->10.9,9.6] [+45260ms]","src":"client"} +{"t":1784875610618,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.6,10.8->14.6,10.6 b1:6.2,14.5->6.2,14.5 b2:22.2,6.8->21.3,7.2 b3:22.6,17.6->22.6,17.6 b4:14.6,11.8->14.6,11.9] [+49296ms]","src":"client"} +{"t":1784875612334,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-round","detail":"resolve 2/10 q=2 winner=-(หมดเวลา ไม่มีคนถูก) correctIdx=7 willEnd=false","src":"server"} +{"t":1784875614501,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:16.5,7.1->15.5,7.0 b1:19.5,4.7->19.9,4.5 b2:14.8,10.4->14.8,10.3 b3:22.6,17.6->22.6,17.6 b4:6.5,11.7->6.5,11.7] [+53322ms]","src":"client"} +{"t":1784875614835,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-read","detail":"q=3 deck=27 rounds=2/10","src":"server"} +{"t":1784875618677,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:9.0,6.2->9.0,6.2 b1:6.5,17.8->7.3,17.5 b2:26.5,5.4->26.6,5.4 b3:23.2,9.3->24.1,8.8 b4:6.5,11.5->6.5,11.5] [+57352ms]","src":"client"} +{"t":1784875622588,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.3,6.1->15.0,6.1 b1:25.5,8.0->25.5,8.0 b2:15.5,6.4->15.1,6.4 b3:25.7,9.5->25.9,9.5 b4:24.8,9.7->24.8,9.8] [+61397ms]","src":"client"} +{"t":1784875626034,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-round","detail":"resolve 3/10 q=3 winner=[TC169] BOT __pv_bot_0 correctIdx=4 willEnd=false","src":"server"} +{"t":1784875626736,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:25.8,5.8->25.8,5.8 b1:6.0,9.6->6.0,9.6 b2:21.3,7.7->22.2,8.0 b3:26.5,5.2->26.5,5.2 b4:8.8,7.0->7.8,6.8] [+65431ms]","src":"client"} +{"t":1784875628535,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-read","detail":"q=4 deck=26 rounds=3/10","src":"server"} +{"t":1784875630792,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:20.7,12.9->20.7,12.9 b1:15.0,12.8->15.0,12.8 b2:20.4,12.7->20.4,12.7 b3:20.7,5.0->20.7,5.0 b4:21.6,13.5->20.9,12.9] [+69481ms]","src":"client"} +{"t":1784875634828,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:13.3,12.0->13.3,12.0 b1:6.1,16.4->6.1,16.4 b2:20.1,12.7->20.1,12.7 b3:6.3,8.3->6.3,8.4 b4:20.4,11.1->20.4,11.1] [+73514ms]","src":"client"} +{"t":1784875638871,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:13.4,5.4->13.4,5.4 b1:20.6,10.5->20.4,10.6 b2:8.8,13.1->8.8,13.1 b3:6.3,8.5->6.3,8.5 b4:15.0,5.4->14.5,5.6] [+77559ms]","src":"client"} +{"t":1784875642925,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:23.5,10.5->22.6,10.1 b1:20.4,10.9->20.4,10.9 b2:6.2,14.8->6.2,14.8 b3:18.6,9.8->18.6,9.8 b4:21.0,10.5->21.9,10.9] [+81600ms]","src":"client"} +{"t":1784875646535,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-round","detail":"resolve 4/10 q=4 winner=-(หมดเวลา ไม่มีคนถูก) correctIdx=11 willEnd=false","src":"server"} +{"t":1784875646976,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.5,10.4->11.3,9.9 b1:16.0,13.5->15.1,13.7 b2:22.8,16.8->23.7,17.0 b3:6.2,5.6->6.2,5.6 b4:17.9,4.3->17.0,4.2] [+85651ms]","src":"client"} +{"t":1784875649036,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-read","detail":"q=5 deck=25 rounds=4/10","src":"server"} +{"t":1784875651000,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:29.6,8.0->29.6,8.0 b1:6.4,15.4->6.4,15.4 b2:12.8,5.4->13.7,5.7 b3:26.6,8.7->27.6,8.9 b4:9.0,17.7->9.0,17.7] [+89681ms]","src":"client"} +{"t":1784875655078,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:29.6,9.1->29.5,10.0 b1:22.1,15.6->21.3,15.8 b2:20.2,11.3->20.2,11.3 b3:23.9,7.7->23.0,7.4 b4:9.0,17.7->9.0,17.7] [+93711ms]","src":"client"} +{"t":1784875659075,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:28.9,12.7->28.9,12.7 b1:9.6,17.9->9.6,17.9 b2:21.9,11.2->22.8,11.1 b3:14.7,10.2->14.2,11.0 b4:29.5,7.0->29.5,7.0] [+97757ms]","src":"client"} +{"t":1784875663154,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:23.7,5.3->23.7,5.3 b1:24.0,15.3->23.4,14.7 b2:6.0,6.4->6.0,6.3 b3:14.2,5.3->14.2,5.3 b4:24.2,5.2->24.0,5.2] [+101785ms]","src":"client"} +{"t":1784875667086,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-round","detail":"resolve 5/10 q=5 winner=-(หมดเวลา ไม่มีคนถูก) correctIdx=7 willEnd=false","src":"server"} +{"t":1784875667117,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:23.7,5.3->23.7,5.3 b1:29.8,7.5->29.8,7.5 b2:17.1,9.9->18.0,9.9 b3:14.2,5.3->14.0,5.3 b4:6.0,13.8->6.0,13.8] [+105817ms]","src":"client"} +{"t":1784875669587,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-read","detail":"q=6 deck=24 rounds=5/10","src":"server"} +{"t":1784875671176,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:29.6,6.6->29.6,6.6 b1:8.0,14.9->7.0,15.1 b2:22.5,5.2->23.5,5.2 b3:13.0,5.3->13.0,5.3 b4:7.7,6.2->7.9,5.3] [+109862ms]","src":"client"} +{"t":1784875675215,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:29.9,11.9->29.9,11.9 b1:12.4,11.5->13.1,11.0 b2:30.0,5.2->30.0,5.2 b3:22.5,5.2->22.6,5.2 b4:8.8,4.2->8.8,4.2] [+113893ms]","src":"client"} +{"t":1784875679112,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:29.9,7.5->29.9,7.5 b1:6.3,7.3->6.3,7.4 b2:20.7,5.5->19.7,5.6 b3:25.8,6.7->26.8,6.9 b4:11.5,4.6->11.5,4.6] [+117931ms]","src":"client"} +{"t":1784875683362,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:26.9,5.1->26.9,5.1 b1:9.7,4.4->9.7,4.4 b2:27.4,9.3->26.4,9.4 b3:25.1,5.6->24.2,5.1 b4:6.1,15.8->6.1,15.8] [+121970ms]","src":"client"} +{"t":1784875687198,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:26.9,5.1->26.9,5.1 b1:18.0,8.8->18.8,9.3 b2:20.5,11.8->20.5,11.9 b3:16.5,4.2->16.5,4.2 b4:9.5,6.7->9.3,6.3] [+126012ms]","src":"client"} +{"t":1784875687590,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-round","detail":"resolve 6/10 q=6 winner=-(หมดเวลา ไม่มีคนถูก) correctIdx=13 willEnd=false","src":"server"} +{"t":1784875690091,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-read","detail":"q=7 deck=23 rounds=6/10","src":"server"} +{"t":1784875691453,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:12.7,13.3->12.7,13.3 b1:6.0,16.3->6.0,16.4 b2:8.1,4.2->7.8,4.2 b3:16.5,4.1->16.5,4.1 b4:15.9,15.2->15.9,15.2] [+130049ms]","src":"client"} +{"t":1784875695401,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:15.1,10.1->15.8,9.6 b1:6.0,16.4->6.0,16.4 b2:10.4,6.8->10.7,7.4 b3:16.2,5.2->16.2,5.2 b4:11.7,7.6->12.4,8.3] [+134080ms]","src":"client"} +{"t":1784875698668,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-round","detail":"resolve 7/10 q=7 winner=[TC169] BOT __pv_bot_0 correctIdx=4 willEnd=false","src":"server"} +{"t":1784875699447,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.7,14.7->10.0,15.4 b1:13.8,13.5->13.5,14.4 b2:13.8,5.3->13.4,5.3 b3:16.8,9.9->16.1,9.9 b4:14.4,5.8->14.1,5.3] [+138136ms]","src":"client"} +{"t":1784875701169,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-read","detail":"q=8 deck=22 rounds=7/10","src":"server"} +{"t":1784875703462,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:15.0,11.6->15.0,11.6 b1:6.3,5.9->6.3,5.9 b2:13.1,5.3->13.1,5.3 b3:15.0,10.9->15.0,10.9 b4:14.1,5.5->14.1,5.5] [+142164ms]","src":"client"} +{"t":1784875707502,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.4,16.4->23.3,16.8 b1:6.3,8.0->6.3,8.0 b2:18.4,13.1->19.3,13.1 b3:8.7,7.4->7.9,6.9 b4:6.4,9.9->6.4,9.9] [+146199ms]","src":"client"} +{"t":1784875711536,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:24.4,16.8->24.4,16.8 b1:17.2,8.7->18.3,8.7 b2:15.7,6.2->14.7,6.2 b3:25.5,8.4->25.5,8.4 b4:20.1,11.5->20.1,12.5] [+150235ms]","src":"client"} +{"t":1784875715451,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.6,13.4->18.8,13.3 b1:28.4,5.3->28.4,5.3 b2:27.4,13.7->26.6,13.4 b3:29.5,9.8->30.0,9.5 b4:6.2,13.7->6.2,13.6] [+154267ms]","src":"client"} +{"t":1784875719171,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-round","detail":"resolve 8/10 q=8 winner=-(หมดเวลา ไม่มีคนถูก) correctIdx=1 willEnd=false","src":"server"} +{"t":1784875719629,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:17.2,13.3->17.2,13.3 b1:15.0,10.5->14.9,11.4 b2:11.9,14.2->12.7,14.6 b3:14.0,7.6->13.6,6.9 b4:15.4,9.9->14.9,10.1] [+158310ms]","src":"client"} +{"t":1784875721672,"cat":"MG4","room":"แจ้งวัฒนะซอย 7","who":"","ev":"mg4-read","detail":"q=9 deck=21 rounds=8/10","src":"server"} +{"t":1784875723684,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:29.4,5.2->29.8,5.2 b1:24.4,8.3->24.1,9.3 b2:27.9,13.8->27.9,13.8 b3:7.2,7.7->6.2,7.7 b4:9.1,14.0->10.1,13.9] [+162347ms]","src":"client"} +{"t":1784875727703,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.0,9.9->19.1,9.9 b1:6.5,8.0->6.4,7.9 b2:28.2,6.2->28.3,5.1 b3:6.1,5.5->6.0,4.4 b4:19.3,9.4->19.9,9.9] [+166388ms]","src":"client"} +{"t":1784875731733,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.2,9.9->19.2,9.9 b1:29.5,6.2->28.7,6.1 b2:28.4,5.1->28.4,5.1 b3:6.0,4.4->6.0,4.4 b4:27.8,8.5->27.5,7.5] [+170430ms]","src":"client"} +{"t":1784875735798,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:7.5,5.3->7.8,6.2 b1:13.3,12.2->14.3,12.2 b2:23.8,7.6->23.0,8.3 b3:12.0,4.7->12.5,5.1 b4:26.3,10.8->26.2,11.8] [+174465ms]","src":"client"} +{"t":1784875739828,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:6.6,4.1->6.4,4.1 b1:14.8,12.5->14.8,12.5 b2:12.7,14.5->12.6,13.5 b3:6.1,10.6->6.1,10.6 b4:24.5,16.5->24.5,16.5] [+178504ms]","src":"client"} +{"t":1784875743773,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.2,9.9->20.0,9.9 b1:18.6,4.4->18.6,4.4 b2:11.9,6.2->12.8,6.7 b3:6.1,10.7->6.1,10.7 b4:29.8,6.3->29.8,6.3] [+182546ms]","src":"client"} +{"t":1784875746605,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"preplay-lock","detail":"[TC154] host เปิด wizard เลือกคดี → ล็อก set-ready ทั้งห้อง (1 คนในห้อง)","src":"server"} +{"t":1784875746736,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"mobile-center-btn","detail":"[TC177] แตะปุ่ม START\/center โดยตรง (มือถือ ไม่ใช้ F) [+185421ms]","src":"client"} +{"t":1784875747768,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:13.9,8.3->13.0,8.0 b1:15.8,9.7->15.8,9.7 b2:14.7,12.1->14.7,12.7 b3:15.6,5.3->15.6,5.3 b4:29.8,6.2->29.8,6.2] [+186579ms]","src":"client"} +{"t":1784875748118,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1784875749058,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"mobile-center-btn","detail":"[TC177] แตะปุ่ม START\/center โดยตรง (มือถือ ไม่ใช้ F) [+187871ms]","src":"client"} +{"t":1784875749057,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"preplay-lock","detail":"[TC154] host เปิด wizard เลือกคดี → ล็อก set-ready ทั้งห้อง (1 คนในห้อง)","src":"server"} +{"t":1784875750004,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1784875751789,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:6.5,4.9->7.3,4.8 b1:25.4,5.3->25.4,5.3 b2:14.5,13.7->15.4,13.2 b3:26.3,8.9->27.3,9.0 b4:6.4,6.3->6.3,6.3] [+190603ms]","src":"client"} +{"t":1784875755964,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:6.2,11.8->6.2,11.8 b1:25.4,5.3->25.4,5.3 b2:28.5,5.2->28.5,5.2 b3:18.9,9.9->19.2,9.9 b4:18.1,4.6->17.2,4.9] [+194641ms]","src":"client"} +{"t":1784875759876,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:16.9,14.2->17.9,14.4 b1:26.8,5.3->26.8,5.3 b2:24.2,12.7->24.4,11.7 b3:19.8,9.9->19.8,9.9 b4:18.1,10.0->18.0,10.0] [+198681ms]","src":"client"} +{"t":1784875764041,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.9,7.6->19.8,6.6 b1:22.1,15.3->21.2,15.1 b2:10.9,8.6->10.0,8.8 b3:6.3,8.6->6.3,8.6 b4:14.3,7.9->14.6,6.9] [+202727ms]","src":"client"} +{"t":1784875768148,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:17.3,9.6->17.3,9.6 b1:15.2,16.7->15.2,16.7 b2:6.3,16.7->6.4,15.7 b3:6.3,8.5->6.3,8.5 b4:14.9,6.3->14.3,7.0] [+206766ms]","src":"client"} +{"t":1784875772109,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:6.6,16.6->6.6,16.6 b1:7.2,10.8->7.4,11.8 b2:26.1,13.1->25.3,12.6 b3:25.9,16.0->25.9,16.0 b4:28.5,5.2->28.7,5.2] [+210807ms]","src":"client"} +{"t":1784875776170,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.9,11.2->14.9,11.2 b1:18.3,9.8->18.4,9.8 b2:20.4,12.8->19.9,13.1 b3:6.5,13.8->6.5,13.8 b4:19.0,7.7->18.1,7.9] [+214847ms]","src":"client"} +{"t":1784875780196,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:26.3,9.5->25.8,9.7 b1:16.9,6.7->16.5,5.9 b2:6.2,10.6->6.2,10.5 b3:16.6,9.3->17.4,8.9 b4:6.4,11.5->6.4,11.5] [+218887ms]","src":"client"} +{"t":1784875784092,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:23.9,16.0->23.9,16.0 b1:13.7,14.9->13.7,14.9 b2:6.2,10.4->6.2,10.4 b3:16.3,6.8->15.5,7.1 b4:22.1,17.9->22.8,17.9] [+222904ms]","src":"client"} +{"t":1784875788839,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:16.3,15.2->15.4,15.6 b1:28.0,6.0->29.1,5.8 b2:14.8,8.9->15.9,8.9 b3:6.1,9.3->6.1,9.2 b4:6.3,4.2->6.3,4.1] [+227436ms]","src":"client"} +{"t":1784875792784,"cat":"LobbyA","room":"ฟ้า","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=5 [b0:26.9,14.5->26.9,14.5 b1:11.2,14.3->11.2,14.3 b2:18.0,9.7->18.0,9.7 b3:14.9,12.8->14.9,12.9 b4:6.3,5.3->6.3,5.3] [+231454ms]","src":"client"} +{"t":1784875932175,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+1ms]","src":"client"} +{"t":1784875932256,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+16ms]","src":"client"} +{"t":1784875932256,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-start","detail":" [+21ms]","src":"client"} +{"t":1784875932257,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+22ms]","src":"client"} +{"t":1784875932256,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+22ms]","src":"client"} +{"t":1784875932257,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"manifest-done","detail":" [+21ms]","src":"client"} +{"t":1784875932298,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+22ms]","src":"client"} +{"t":1784875932328,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+22ms]","src":"client"} +{"t":1784875932418,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"socket-connect","detail":" [+285ms]","src":"client"} +{"t":1784875932499,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"join","detail":"สี#5 · คน 1 + บอท 4","src":"server"} +{"t":1784875933674,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mlsbbxfe bots=4 [+1550ms]","src":"client"} +{"t":1784875933715,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+1551ms]","src":"client"} +{"t":1784875933780,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+1621ms]","src":"client"} +{"t":1784875933797,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:10.0,14.0->10.7,13.7 b1:16.0,14.0->15.5,13.4 b2:22.0,13.0->21.3,12.8 b3:23.0,8.0->23.1,7.3] [+1626ms]","src":"client"} +{"t":1784875940007,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:11.0,10.9->7.2,7.8 b1:9.5,9.3->6.4,8.6 b2:13.9,9.4->6.4,7.0 b3:23.6,5.9->23.7,5.1] [+7878ms]","src":"client"} +{"t":1784875948316,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:17.1,11.0->23.5,11.1 b1:11.2,10.6->13.0,12.6 b2:18.1,12.4->22.4,15.5 b3:23.5,6.3->23.3,7.2] [+16025ms]","src":"client"} +{"t":1784875952337,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:15.3,5.1->15.3,5.1 b1:25.5,5.1->25.5,5.1 b2:13.1,9.3->13.1,9.3 b3:6.3,17.3->6.2,17.5] [+20031ms]","src":"client"} +{"t":1784875956350,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:18.5,6.0->19.2,6.7 b1:11.2,16.6->11.2,16.6 b2:12.3,10.9->11.4,11.2 b3:6.2,17.6->6.2,17.6] [+24078ms]","src":"client"} +{"t":1784875960360,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:9.8,17.9->9.8,17.9 b1:16.3,13.6->15.4,13.3 b2:10.7,6.6->10.7,6.6 b3:10.7,4.6->9.7,4.4] [+28102ms]","src":"client"} +{"t":1784875964400,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:15.7,8.5->16.6,8.3 b1:21.0,7.2->21.9,7.2 b2:18.7,9.5->17.9,9.1 b3:6.6,4.1->6.6,4.1] [+32145ms]","src":"client"} +{"t":1784875968432,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:23.5,5.0->23.5,5.0 b1:7.9,10.8->7.9,10.8 b2:23.1,11.3->23.9,11.7 b3:14.8,5.4->15.7,5.5] [+36179ms]","src":"client"} +{"t":1784875972491,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:7.7,7.3->7.8,6.3 b1:16.5,4.3->16.5,4.2 b2:6.3,6.9->6.3,6.7 b3:14.9,11.0->14.9,10.9] [+40206ms]","src":"client"} +{"t":1784875976506,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:29.6,5.6->29.6,5.6 b1:9.0,7.0->8.8,7.8 b2:19.7,4.3->19.7,4.3 b3:14.9,10.5->14.9,10.5] [+44234ms]","src":"client"} +{"t":1784875980564,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:22.3,12.6->21.9,13.4 b1:10.7,11.2->11.4,11.0 b2:9.5,7.8->9.1,8.6 b3:14.8,12.7->14.8,12.7] [+48280ms]","src":"client"} +{"t":1784875984587,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->6.2,12.2 b1:13.3,10.5->17.2,9.7 b2:8.3,10.4->22.5,5.6 b3:14.8,12.7->14.8,12.7] [+52317ms]","src":"client"} +{"t":1784875988667,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->24.2,7.7 b1:13.3,10.5->9.5,10.3 b2:8.3,10.4->27.2,13.7 b3:14.8,12.7->26.7,13.9] [+56360ms]","src":"client"} +{"t":1784875992676,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->22.5,5.7 b1:13.3,10.5->6.2,11.2 b2:8.3,10.4->27.2,13.7 b3:14.8,12.7->26.7,13.9] [+60401ms]","src":"client"} +{"t":1784875996729,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->20.2,10.2 b1:13.3,10.5->14.9,12.6 b2:8.3,10.4->29.4,6.7 b3:14.8,12.7->29.9,9.9] [+64453ms]","src":"client"} +{"t":1784876000809,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->14.3,6.7 b1:13.3,10.5->14.6,12.4 b2:8.3,10.4->28.2,8.5 b3:14.8,12.7->12.3,5.1] [+68536ms]","src":"client"} +{"t":1784876004848,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->10.4,17.6 b1:13.3,10.5->20.0,8.7 b2:8.3,10.4->11.3,14.6 b3:14.8,12.7->21.4,11.3] [+72579ms]","src":"client"} +{"t":1784876008866,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->20.2,11.3 b1:13.3,10.5->8.2,5.8 b2:8.3,10.4->16.8,9.5 b3:14.8,12.7->29.9,8.9] [+76612ms]","src":"client"} +{"t":1784876012947,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->29.6,11.5 b1:13.3,10.5->6.1,4.6 b2:8.3,10.4->29.8,5.0 b3:14.8,12.7->13.0,5.1] [+80662ms]","src":"client"} +{"t":1784876016949,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->29.6,11.5 b1:13.3,10.5->6.1,7.8 b2:8.3,10.4->20.3,12.5 b3:14.8,12.7->20.8,5.3] [+84686ms]","src":"client"} +{"t":1784876020994,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->20.5,12.5 b1:13.3,10.5->10.4,9.8 b2:8.3,10.4->20.9,11.9 b3:14.8,12.7->25.1,16.4] [+88719ms]","src":"client"} +{"t":1784876025061,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->12.6,15.1 b1:13.3,10.5->7.7,4.4 b2:8.3,10.4->13.8,9.4 b3:14.8,12.7->12.0,5.1] [+92755ms]","src":"client"} +{"t":1784876029069,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->27.4,9.6 b1:13.3,10.5->21.9,13.1 b2:8.3,10.4->6.4,11.8 b3:14.8,12.7->6.5,5.7] [+96786ms]","src":"client"} +{"t":1784876033086,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->7.4,7.0 b1:13.3,10.5->6.1,11.2 b2:8.3,10.4->14.5,10.2 b3:14.8,12.7->19.7,8.8] [+100817ms]","src":"client"} +{"t":1784876037109,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->12.7,5.1 b1:13.3,10.5->6.1,6.9 b2:8.3,10.4->18.0,13.2 b3:14.8,12.7->10.8,10.1] [+104849ms]","src":"client"} +{"t":1784876041149,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->16.4,4.7 b1:13.3,10.5->6.1,6.2 b2:8.3,10.4->11.7,5.8 b3:14.8,12.7->8.6,7.5] [+108885ms]","src":"client"} +{"t":1784876045192,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->16.4,4.6 b1:13.3,10.5->14.8,12.6 b2:8.3,10.4->22.8,8.3 b3:14.8,12.7->20.2,11.2] [+112931ms]","src":"client"} +{"t":1784876049267,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->29.6,11.9 b1:13.3,10.5->14.8,10.9 b2:8.3,10.4->25.8,12.0 b3:14.8,12.7->19.5,4.9] [+116970ms]","src":"client"} +{"t":1784876053288,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->29.6,12.0 b1:13.3,10.5->6.4,7.8 b2:8.3,10.4->28.5,12.0 b3:14.8,12.7->6.4,9.0] [+121010ms]","src":"client"} +{"t":1784876057303,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->8.7,15.3 b1:13.3,10.5->6.4,6.7 b2:8.3,10.4->12.0,4.9 b3:14.8,12.7->6.4,9.2] [+125047ms]","src":"client"} +{"t":1784876061343,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->9.2,17.6 b1:13.3,10.5->16.9,13.2 b2:8.3,10.4->11.6,5.8 b3:14.8,12.7->11.6,6.5] [+129091ms]","src":"client"} +{"t":1784876065389,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->29.5,5.3 b1:13.3,10.5->9.9,5.6 b2:8.3,10.4->21.2,14.2 b3:14.8,12.7->25.7,14.5] [+133126ms]","src":"client"} +{"t":1784876069427,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->6.4,16.6 b1:13.3,10.5->27.2,11.2 b2:8.3,10.4->30.0,6.7 b3:14.8,12.7->20.5,12.2] [+137165ms]","src":"client"} +{"t":1784876073488,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->28.8,12.3 b1:13.3,10.5->9.9,14.6 b2:8.3,10.4->25.6,15.9 b3:14.8,12.7->29.6,11.5] [+141210ms]","src":"client"} +{"t":1784876077529,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->24.7,11.3 b1:13.3,10.5->28.6,12.7 b2:8.3,10.4->13.9,12.9 b3:14.8,12.7->28.4,9.5] [+145246ms]","src":"client"} +{"t":1784876081567,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->23.4,5.2 b1:13.3,10.5->28.6,12.1 b2:8.3,10.4->8.2,7.2 b3:14.8,12.7->24.7,5.1] [+149281ms]","src":"client"} +{"t":1784876085609,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->25.9,9.7 b1:13.3,10.5->18.3,13.1 b2:8.3,10.4->8.2,7.2 b3:14.8,12.7->21.9,8.6] [+153320ms]","src":"client"} +{"t":1784876089665,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->25.0,5.0 b1:13.3,10.5->16.1,16.6 b2:8.3,10.4->17.1,15.3 b3:14.8,12.7->18.1,9.9] [+157363ms]","src":"client"} +{"t":1784876093668,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->9.8,8.1 b1:13.3,10.5->16.7,13.2 b2:8.3,10.4->8.6,7.2 b3:14.8,12.7->28.7,12.8] [+161382ms]","src":"client"} +{"t":1784876097689,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->6.0,7.2 b1:13.3,10.5->26.9,14.9 b2:8.3,10.4->8.6,12.6 b3:14.8,12.7->19.5,13.7] [+165412ms]","src":"client"} +{"t":1784876101709,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->12.1,8.0 b1:13.3,10.5->16.4,8.9 b2:8.3,10.4->14.5,10.9 b3:14.8,12.7->14.1,5.3] [+169442ms]","src":"client"} +{"t":1784876105769,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->14.7,5.1 b1:13.3,10.5->20.8,6.4 b2:8.3,10.4->6.4,12.2 b3:14.8,12.7->15.1,5.3] [+173487ms]","src":"client"} +{"t":1784876109828,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->6.2,10.2 b1:13.3,10.5->23.9,7.6 b2:8.3,10.4->7.5,10.4 b3:14.8,12.7->28.0,6.9] [+177520ms]","src":"client"} +{"t":1784876113827,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->18.4,17.3 b1:13.3,10.5->22.0,8.2 b2:8.3,10.4->22.6,17.7 b3:14.8,12.7->8.2,12.0] [+181554ms]","src":"client"} +{"t":1784876117908,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->28.3,9.0 b1:13.3,10.5->6.1,9.5 b2:8.3,10.4->19.6,4.9 b3:14.8,12.7->24.8,5.1] [+185590ms]","src":"client"} +{"t":1784876121888,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->27.8,13.7 b1:13.3,10.5->29.6,5.1 b2:8.3,10.4->22.9,8.3 b3:14.8,12.7->26.7,12.3] [+189628ms]","src":"client"} +{"t":1784876125931,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->27.8,13.2 b1:13.3,10.5->9.8,15.6 b2:8.3,10.4->17.6,18.0 b3:14.8,12.7->6.3,14.7] [+193657ms]","src":"client"} +{"t":1784876130015,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->19.1,9.5 b1:13.3,10.5->22.5,10.9 b2:8.3,10.4->17.6,18.0 b3:14.8,12.7->19.9,14.5] [+197693ms]","src":"client"} +{"t":1784876134008,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->16.7,6.8 b1:13.3,10.5->9.0,6.0 b2:8.3,10.4->6.4,8.6 b3:14.8,12.7->24.0,9.6] [+201738ms]","src":"client"} +{"t":1784876138089,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->21.4,13.3 b1:13.3,10.5->25.7,9.3 b2:8.3,10.4->29.7,10.5 b3:14.8,12.7->19.8,14.6] [+205775ms]","src":"client"} +{"t":1784876142088,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->12.3,10.1 b1:13.3,10.5->16.0,9.6 b2:8.3,10.4->21.5,7.1 b3:14.8,12.7->19.4,15.6] [+209818ms]","src":"client"} +{"t":1784876146107,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->22.5,13.2 b1:13.3,10.5->15.6,5.5 b2:8.3,10.4->28.1,5.4 b3:14.8,12.7->11.6,9.5] [+213852ms]","src":"client"} +{"t":1784876150148,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->7.4,12.7 b1:13.3,10.5->9.1,7.0 b2:8.3,10.4->29.7,5.6 b3:14.8,12.7->13.6,5.0] [+217881ms]","src":"client"} +{"t":1784876154218,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->28.8,12.7 b1:13.3,10.5->7.2,7.7 b2:8.3,10.4->28.6,11.6 b3:14.8,12.7->7.6,5.0] [+221909ms]","src":"client"} +{"t":1784876158254,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->27.1,11.8 b1:13.3,10.5->18.3,16.2 b2:8.3,10.4->28.4,12.7 b3:14.8,12.7->6.1,15.2] [+225943ms]","src":"client"} +{"t":1784876162248,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->23.2,9.8 b1:13.3,10.5->29.8,8.6 b2:8.3,10.4->20.9,12.3 b3:14.8,12.7->6.1,13.0] [+229990ms]","src":"client"} +{"t":1784876166309,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->20.3,13.0 b1:13.3,10.5->20.3,12.4 b2:8.3,10.4->8.0,10.9 b3:14.8,12.7->14.4,13.7] [+234023ms]","src":"client"} +{"t":1784876170386,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->14.8,11.4 b1:13.3,10.5->13.8,5.0 b2:8.3,10.4->6.1,14.8 b3:14.8,12.7->30.0,6.1] [+238058ms]","src":"client"} +{"t":1784876174388,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->25.9,15.8 b1:13.3,10.5->29.7,8.7 b2:8.3,10.4->21.8,5.9 b3:14.8,12.7->30.0,5.4] [+242091ms]","src":"client"} +{"t":1784876178401,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->6.0,6.5 b1:13.3,10.5->29.7,8.8 b2:8.3,10.4->22.4,5.4 b3:14.8,12.7->18.9,15.0] [+246134ms]","src":"client"} +{"t":1784876182428,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->6.0,6.4 b1:13.3,10.5->10.1,14.3 b2:8.3,10.4->16.4,4.2 b3:14.8,12.7->25.6,15.3] [+250167ms]","src":"client"} +{"t":1784876186468,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->13.6,5.3 b1:13.3,10.5->21.1,5.0 b2:8.3,10.4->14.3,5.1 b3:14.8,12.7->6.2,5.3] [+254194ms]","src":"client"} +{"t":1784876190480,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->6.2,15.5 b1:13.3,10.5->17.1,10.0 b2:8.3,10.4->6.3,11.4 b3:14.8,12.7->13.7,12.5] [+258228ms]","src":"client"} +{"t":1784876194537,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->6.2,15.5 b1:13.3,10.5->17.1,10.0 b2:8.3,10.4->6.3,13.8 b3:14.8,12.7->11.1,8.0] [+262262ms]","src":"client"} +{"t":1784876198568,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->14.9,12.1 b1:13.3,10.5->29.5,8.1 b2:8.3,10.4->14.9,11.2 b3:14.8,12.7->16.9,16.1] [+266299ms]","src":"client"} +{"t":1784876202609,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->17.0,6.4 b1:13.3,10.5->28.5,12.9 b2:8.3,10.4->24.1,5.1 b3:14.8,12.7->14.2,17.9] [+270348ms]","src":"client"} +{"t":1784876206672,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->11.7,4.0 b1:13.3,10.5->25.7,13.6 b2:8.3,10.4->24.3,5.1 b3:14.8,12.7->22.4,10.4] [+274381ms]","src":"client"} +{"t":1784876210720,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->19.9,9.9 b1:13.3,10.5->10.6,13.8 b2:8.3,10.4->6.4,17.0 b3:14.8,12.7->6.5,11.6] [+278442ms]","src":"client"} +{"t":1784876214721,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->11.3,5.6 b1:13.3,10.5->6.0,11.4 b2:8.3,10.4->22.6,17.9 b3:14.8,12.7->14.6,12.5] [+282454ms]","src":"client"} +{"t":1784876218826,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->28.7,12.9 b1:13.3,10.5->6.0,9.7 b2:8.3,10.4->14.7,11.2 b3:14.8,12.7->6.1,14.6] [+286496ms]","src":"client"} +{"t":1784876222829,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->28.7,12.9 b1:13.3,10.5->6.0,14.7 b2:8.3,10.4->26.5,13.5 b3:14.8,12.7->9.8,12.4] [+290533ms]","src":"client"} +{"t":1784876226861,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->20.4,10.9 b1:13.3,10.5->6.6,13.2 b2:8.3,10.4->19.2,9.0 b3:14.8,12.7->23.7,5.1] [+294566ms]","src":"client"} +{"t":1784876230866,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.7->6.2,17.6 b1:13.3,10.5->10.4,4.1 b2:8.3,10.4->12.6,7.1 b3:14.8,12.7->6.5,8.1] [+298600ms]","src":"client"} +{"t":1784876234901,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:7.3,4.9->7.3,4.9 b1:17.8,9.9->17.8,9.9 b2:19.6,5.8->20.6,5.9 b3:7.4,9.0->8.2,9.6] [+302633ms]","src":"client"} +{"t":1784876239018,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:22.9,7.6->22.3,6.9 b1:11.4,13.0->10.7,13.6 b2:29.8,8.1->29.7,7.1 b3:17.9,13.4->17.9,13.4] [+306693ms]","src":"client"} +{"t":1784876242988,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.1,4.3->16.1,4.3 b1:22.3,18.0->22.8,18.0 b2:29.4,5.5->29.4,5.5 b3:17.9,13.4->17.9,13.4] [+310699ms]","src":"client"} +{"t":1784876247020,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.1,4.9->16.1,4.9 b1:30.0,10.5->30.0,10.5 b2:26.4,5.1->26.4,5.1 b3:26.0,14.1->25.0,14.3] [+314737ms]","src":"client"} +{"t":1784876251106,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.1,4.9->16.1,4.9 b1:7.7,7.8->7.7,7.8 b2:6.2,9.6->6.2,9.6 b3:14.7,11.0->14.7,11.0] [+318820ms]","src":"client"} +{"t":1784876255088,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:14.6,11.0->14.6,11.5 b1:28.4,5.1->28.4,5.1 b2:6.0,6.7->6.0,6.7 b3:22.1,5.3->22.1,5.3] [+322824ms]","src":"client"} +{"t":1784876257991,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+2ms]","src":"client"} +{"t":1784876258099,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+24ms]","src":"client"} +{"t":1784876258100,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-start","detail":" [+29ms]","src":"client"} +{"t":1784876258119,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"colors-resolved","detail":" [+30ms]","src":"client"} +{"t":1784876258119,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"manifest-done","detail":" [+29ms]","src":"client"} +{"t":1784876258133,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"mychar-tinted","detail":"roomJoinReady=false [+31ms]","src":"client"} +{"t":1784876258159,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-occupants-done","detail":" [+31ms]","src":"client"} +{"t":1784876258161,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-ready(occupants)","detail":" [+31ms]","src":"client"} +{"t":1784876258255,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"socket-connect","detail":" [+313ms]","src":"client"} +{"t":1784876258255,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1784876259295,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:29.5,9.8->29.5,9.8 b1:7.0,7.0->6.3,6.6 b2:16.3,9.4->17.7,9.7 b3:22.3,5.5->22.9,6.0] [+327167ms]","src":"client"} +{"t":1784876261625,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"join-ok","detail":"peers=2 host=other map=mlsbbxfe bots=4 [+2289ms]","src":"client"} +{"t":1784876261642,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+2397ms]","src":"client"} +{"t":1784876261642,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2289ms]","src":"client"} +{"t":1784876261705,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.0,14.0->29.5,9.8 b1:22.0,13.0->8.9,7.5 b2:23.0,8.0->14.2,8.7 b3:15.0,5.0->22.1,5.3] [+2401ms]","src":"client"} +{"t":1784876263576,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:29.5,9.8->29.5,9.8 b1:6.4,8.8->6.2,11.8 b2:21.8,13.1->25.1,16.2 b3:25.2,7.9->26.1,8.6] [+331433ms]","src":"client"} +{"t":1784876265067,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:26.7,10.8->27.0,10.9 b1:17.9,9.8->23.1,6.9 b2:15.8,7.4->12.0,4.7 b3:18.9,4.8->18.0,4.3] [+7129ms]","src":"client"} +{"t":1784876268601,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1784876269626,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+0ms]","src":"client"} +{"t":1784876269702,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+20ms]","src":"client"} +{"t":1784876269703,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-start","detail":" [+24ms]","src":"client"} +{"t":1784876269704,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"manifest-done","detail":" [+25ms]","src":"client"} +{"t":1784876269732,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"colors-resolved","detail":" [+25ms]","src":"client"} +{"t":1784876269732,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"mychar-tinted","detail":"roomJoinReady=false [+25ms]","src":"client"} +{"t":1784876269770,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-ready(occupants)","detail":" [+26ms]","src":"client"} +{"t":1784876269771,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-occupants-done","detail":" [+26ms]","src":"client"} +{"t":1784876269856,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"socket-connect","detail":" [+231ms]","src":"client"} +{"t":1784876269858,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1784876270382,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"host-change","detail":"host ใหม่ → ฟ้า","src":"server"} +{"t":1784876270383,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1784876271228,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+0ms]","src":"client"} +{"t":1784876271306,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-start","detail":" [+12ms]","src":"client"} +{"t":1784876271306,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+10ms]","src":"client"} +{"t":1784876271307,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+13ms]","src":"client"} +{"t":1784876271309,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"manifest-done","detail":" [+12ms]","src":"client"} +{"t":1784876271309,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+13ms]","src":"client"} +{"t":1784876271369,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+13ms]","src":"client"} +{"t":1784876271370,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+13ms]","src":"client"} +{"t":1784876271450,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"socket-connect","detail":" [+231ms]","src":"client"} +{"t":1784876271454,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"join","detail":"สี#5 · คน 2 + บอท 4","src":"server"} +{"t":1784876272642,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"join-ok","detail":"peers=2 host=me map=mlsbbxfe bots=4 [+1423ms]","src":"client"} +{"t":1784876272676,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+1424ms]","src":"client"} +{"t":1784876272710,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+1481ms]","src":"client"} +{"t":1784876272749,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.0,14.0->8.9,7.6 b1:22.0,13.0->13.0,7.4 b2:23.0,8.0->9.9,14.9 b3:15.0,5.0->26.3,14.7] [+1487ms]","src":"client"} +{"t":1784876273562,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"join-ok","detail":"peers=2 host=other map=mlsbbxfe bots=4 [+2216ms]","src":"client"} +{"t":1784876273569,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2216ms]","src":"client"} +{"t":1784876273865,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+2262ms]","src":"client"} +{"t":1784876273878,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.0,14.0->10.5,11.2 b1:22.0,13.0->12.9,5.1 b2:23.0,8.0->6.2,14.0 b3:15.0,5.0->26.3,14.7] [+2265ms]","src":"client"} +{"t":1784876280028,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.6,7.7->29.9,5.1 b1:16.8,8.6->17.2,5.3 b2:21.7,11.9->26.9,11.1 b3:15.9,8.1->11.8,6.1] [+8794ms]","src":"client"} +{"t":1784876282276,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:25.9,5.7->29.9,5.1 b1:15.7,10.4->17.6,4.3 b2:25.4,11.9->25.6,10.0 b3:12.0,6.5->13.4,6.3] [+12514ms]","src":"client"} +{"t":1784876287401,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.4,11.2->6.4,17.0 b1:17.6,8.9->20.0,7.3 b2:21.9,10.9->18.3,9.8 b3:18.6,11.5->26.0,16.8] [+17624ms]","src":"client"} +{"t":1784876287870,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:25.1,6.4->29.9,5.1 b1:17.1,6.8->17.6,4.3 b2:24.0,11.3->26.1,10.4 b3:14.2,7.2->12.9,6.2] [+16386ms]","src":"client"} +{"t":1784876291278,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:23.1,5.1->23.1,5.1 b1:11.1,7.8->11.1,7.8 b2:25.2,16.0->24.3,15.6 b3:21.0,13.1->20.3,12.8] [+21654ms]","src":"client"} +{"t":1784876291675,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:23.1,5.1->23.1,5.1 b1:12.9,7.8->13.8,7.9 b2:22.2,14.6->21.4,14.2 b3:20.3,11.8->20.3,11.5] [+20442ms]","src":"client"} +{"t":1784876295453,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:18.7,9.7->18.0,9.9 b1:14.1,12.5->13.2,12.1 b2:18.7,8.4->19.8,8.5 b3:6.2,5.4->6.2,5.4] [+25695ms]","src":"client"} +{"t":1784876295850,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.6,9.9->16.1,9.9 b1:11.0,11.2->10.2,10.8 b2:17.8,9.8->17.0,10.0 b3:6.2,5.4->6.2,5.4] [+24481ms]","src":"client"} +{"t":1784876300263,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:18.4,12.6->20.3,11.5 b1:6.2,13.4->6.2,13.5 b2:12.4,13.1->14.9,10.7 b3:6.1,13.4->6.1,13.5] [+29925ms]","src":"client"} +{"t":1784876300756,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.6,13.2->17.8,13.1 b1:6.2,13.2->6.2,13.5 b2:9.7,15.4->11.7,14.5 b3:6.1,13.2->6.1,13.4] [+29338ms]","src":"client"} +{"t":1784876304704,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:21.1,7.3->22.2,5.2 b1:6.2,13.4->6.2,13.5 b2:18.9,8.8->21.8,6.8 b3:6.1,13.4->6.1,13.5] [+34951ms]","src":"client"} +{"t":1784876306308,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:19.6,9.3->22.2,5.2 b1:6.2,13.4->6.2,13.5 b2:18.6,10.1->26.9,5.0 b3:6.1,13.4->6.1,13.5] [+34948ms]","src":"client"} +{"t":1784876308602,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:17.0,16.4->14.3,16.5 b1:14.8,12.5->14.8,12.5 b2:22.1,11.8->25.5,14.2 b3:15.2,13.9->20.3,14.1] [+38967ms]","src":"client"} +{"t":1784876312807,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:14.7,11.1->14.7,11.1 b1:27.0,5.2->27.1,5.2 b2:13.2,12.8->12.7,13.6 b3:16.7,13.1->15.8,13.1] [+42995ms]","src":"client"} +{"t":1784876313685,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"host-change","detail":"host ใหม่ → ฟ้า","src":"server"} +{"t":1784876313685,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1784876314728,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+0ms]","src":"client"} +{"t":1784876314808,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"manifest-done","detail":" [+20ms]","src":"client"} +{"t":1784876314808,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-start","detail":" [+20ms]","src":"client"} +{"t":1784876314808,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+17ms]","src":"client"} +{"t":1784876314809,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+21ms]","src":"client"} +{"t":1784876314826,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+21ms]","src":"client"} +{"t":1784876314840,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+22ms]","src":"client"} +{"t":1784876314872,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+22ms]","src":"client"} +{"t":1784876314949,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"socket-connect","detail":" [+226ms]","src":"client"} +{"t":1784876314954,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"join","detail":"สี#5 · คน 2 + บอท 4","src":"server"} +{"t":1784876316697,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"join-ok","detail":"peers=2 host=me map=mlsbbxfe bots=4 [+1878ms]","src":"client"} +{"t":1784876316704,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+1879ms]","src":"client"} +{"t":1784876316705,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+1945ms]","src":"client"} +{"t":1784876316776,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.0,14.0->14.7,10.4 b1:22.0,13.0->27.2,5.2 b2:23.0,8.0->20.3,12.0 b3:15.0,5.0->27.2,6.6] [+1950ms]","src":"client"} +{"t":1784876317906,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:18.2,8.0->19.8,7.1 b1:27.2,5.2->27.2,5.2 b2:19.4,15.5->19.3,15.7 b3:29.7,5.5->29.8,5.2] [+47159ms]","src":"client"} +{"t":1784876322247,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1784876323326,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+1ms]","src":"client"} +{"t":1784876323405,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+22ms]","src":"client"} +{"t":1784876323405,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-start","detail":" [+27ms]","src":"client"} +{"t":1784876323417,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"colors-resolved","detail":" [+28ms]","src":"client"} +{"t":1784876323417,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"mychar-tinted","detail":"roomJoinReady=false [+28ms]","src":"client"} +{"t":1784876323418,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"manifest-done","detail":" [+27ms]","src":"client"} +{"t":1784876323463,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-occupants-done","detail":" [+28ms]","src":"client"} +{"t":1784876323464,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-ready(occupants)","detail":" [+29ms]","src":"client"} +{"t":1784876323562,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"socket-connect","detail":" [+226ms]","src":"client"} +{"t":1784876323565,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1784876325249,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+1920ms]","src":"client"} +{"t":1784876325304,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"loading-hide","detail":"เริ่มเล่นได้ [+1849ms]","src":"client"} +{"t":1784876325306,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"join-ok","detail":"peers=2 host=other map=mlsbbxfe bots=4 [+1848ms]","src":"client"} +{"t":1784876325400,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.0,14.0->6.4,15.4 b1:22.0,13.0->29.9,5.2 b2:23.0,8.0->6.2,4.7 b3:15.0,5.0->6.2,7.3] [+1941ms]","src":"client"} +{"t":1784876325688,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:15.6,9.7->19.5,5.1 b1:27.2,5.2->27.2,5.2 b2:16.0,6.4->12.6,5.8 b3:10.8,8.4->6.2,4.1] [+10820ms]","src":"client"} +{"t":1784876329908,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.7,15.0->22.6,17.6 b1:27.3,6.3->25.6,7.7 b2:9.4,5.6->7.9,6.0 b3:10.2,6.1->12.0,4.3] [+15185ms]","src":"client"} +{"t":1784876332019,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1784876332875,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+0ms]","src":"client"} +{"t":1784876332936,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+17ms]","src":"client"} +{"t":1784876332959,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"manifest-done","detail":" [+20ms]","src":"client"} +{"t":1784876332959,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"colors-resolved","detail":" [+21ms]","src":"client"} +{"t":1784876332959,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-start","detail":" [+20ms]","src":"client"} +{"t":1784876332965,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"mychar-tinted","detail":"roomJoinReady=false [+21ms]","src":"client"} +{"t":1784876333005,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-ready(occupants)","detail":" [+22ms]","src":"client"} +{"t":1784876333006,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-occupants-done","detail":" [+22ms]","src":"client"} +{"t":1784876333114,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"socket-connect","detail":" [+226ms]","src":"client"} +{"t":1784876333111,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1784876336609,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.3->25.5,13.1 b1:23.4,7.5->18.8,8.9 b2:10.6,7.6->12.4,10.0 b3:13.1,7.2->16.6,8.8] [+21744ms]","src":"client"} +{"t":1784876336748,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"join-ok","detail":"peers=2 host=other map=mlsbbxfe bots=4 [+2401ms]","src":"client"} +{"t":1784876336783,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2401ms]","src":"client"} +{"t":1784876336922,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+2463ms]","src":"client"} +{"t":1784876336928,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.0,14.0->18.1,4.3 b1:22.0,13.0->6.6,9.9 b2:23.0,8.0->26.4,5.3 b3:15.0,5.0->21.1,5.2] [+2467ms]","src":"client"} +{"t":1784876341714,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1784876342561,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+0ms]","src":"client"} +{"t":1784876342636,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"colors-resolved","detail":" [+20ms]","src":"client"} +{"t":1784876342636,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"manifest-done","detail":" [+20ms]","src":"client"} +{"t":1784876342636,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+17ms]","src":"client"} +{"t":1784876342637,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-start","detail":" [+19ms]","src":"client"} +{"t":1784876342644,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"mychar-tinted","detail":"roomJoinReady=false [+20ms]","src":"client"} +{"t":1784876342701,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-occupants-done","detail":" [+21ms]","src":"client"} +{"t":1784876342714,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-ready(occupants)","detail":" [+21ms]","src":"client"} +{"t":1784876342796,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"socket-connect","detail":" [+230ms]","src":"client"} +{"t":1784876342800,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1784876343687,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.3->18.1,4.3 b1:23.4,7.5->6.6,9.9 b2:10.6,7.6->26.4,5.3 b3:13.1,7.2->21.1,5.2] [+28815ms]","src":"client"} +{"t":1784876343804,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"join-ok","detail":"peers=2 host=other map=mlsbbxfe bots=4 [+1248ms]","src":"client"} +{"t":1784876343806,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"loading-hide","detail":"เริ่มเล่นได้ [+1249ms]","src":"client"} +{"t":1784876343881,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+1318ms]","src":"client"} +{"t":1784876343896,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.0,14.0->6.2,10.9 b1:22.0,13.0->16.7,16.2 b2:23.0,8.0->19.2,6.7 b3:15.0,5.0->15.0,8.1] [+1329ms]","src":"client"} +{"t":1784876348250,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:13.3,11.5->14.9,11.6 b1:7.9,13.4->6.1,13.2 b2:8.6,10.5->8.8,10.9 b3:21.2,12.1->25.1,15.2] [+5577ms]","src":"client"} +{"t":1784876351157,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1784876352017,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+1ms]","src":"client"} +{"t":1784876352085,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+17ms]","src":"client"} +{"t":1784876352087,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-start","detail":" [+20ms]","src":"client"} +{"t":1784876352098,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"manifest-done","detail":" [+20ms]","src":"client"} +{"t":1784876352098,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"mychar-tinted","detail":"roomJoinReady=false [+21ms]","src":"client"} +{"t":1784876352100,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"colors-resolved","detail":" [+21ms]","src":"client"} +{"t":1784876352134,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-occupants-done","detail":" [+22ms]","src":"client"} +{"t":1784876352148,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-ready(occupants)","detail":" [+22ms]","src":"client"} +{"t":1784876352148,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.3->14.5,12.6 b1:23.4,7.5->18.2,4.1 b2:10.6,7.6->26.4,5.3 b3:13.1,7.2->21.1,5.2] [+37261ms]","src":"client"} +{"t":1784876352235,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"socket-connect","detail":" [+227ms]","src":"client"} +{"t":1784876352239,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1784876355385,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"join-ok","detail":"peers=2 host=other map=mlsbbxfe bots=4 [+1856ms]","src":"client"} +{"t":1784876355396,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"loading-hide","detail":"เริ่มเล่นได้ [+1857ms]","src":"client"} +{"t":1784876355789,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+1914ms]","src":"client"} +{"t":1784876355865,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.0,14.0->8.1,17.0 b1:22.0,13.0->18.6,13.1 b2:23.0,8.0->29.6,7.0 b3:15.0,5.0->24.6,10.2] [+1917ms]","src":"client"} +{"t":1784876360847,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1784876361745,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+0ms]","src":"client"} +{"t":1784876361811,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+16ms]","src":"client"} +{"t":1784876361811,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"manifest-done","detail":" [+20ms]","src":"client"} +{"t":1784876361812,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-start","detail":" [+19ms]","src":"client"} +{"t":1784876361826,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"colors-resolved","detail":" [+20ms]","src":"client"} +{"t":1784876361841,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"mychar-tinted","detail":"roomJoinReady=false [+20ms]","src":"client"} +{"t":1784876361891,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-occupants-done","detail":" [+21ms]","src":"client"} +{"t":1784876361892,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-ready(occupants)","detail":" [+21ms]","src":"client"} +{"t":1784876361972,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"socket-connect","detail":" [+215ms]","src":"client"} +{"t":1784876361974,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1784876363606,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.0,14.0->19.4,4.2 b1:22.0,13.0->26.7,10.0 b2:23.0,8.0->24.3,5.4 b3:15.0,5.0->19.9,9.9] [+1851ms]","src":"client"} +{"t":1784876363645,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"loading-hide","detail":"เริ่มเล่นได้ [+1779ms]","src":"client"} +{"t":1784876363645,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"join-ok","detail":"peers=2 host=other map=mlsbbxfe bots=4 [+1778ms]","src":"client"} +{"t":1784876363724,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+1849ms]","src":"client"} +{"t":1784876370125,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1784876371048,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+0ms]","src":"client"} +{"t":1784876371125,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-start","detail":" [+19ms]","src":"client"} +{"t":1784876371125,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+17ms]","src":"client"} +{"t":1784876371127,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"manifest-done","detail":" [+20ms]","src":"client"} +{"t":1784876371127,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"colors-resolved","detail":" [+20ms]","src":"client"} +{"t":1784876371127,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"mychar-tinted","detail":"roomJoinReady=false [+21ms]","src":"client"} +{"t":1784876371175,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-occupants-done","detail":" [+21ms]","src":"client"} +{"t":1784876371188,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-ready(occupants)","detail":" [+21ms]","src":"client"} +{"t":1784876371260,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"socket-connect","detail":" [+224ms]","src":"client"} +{"t":1784876371263,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ฟ้า","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1784876372235,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"loading-hide","detail":"เริ่มเล่นได้ [+1188ms]","src":"client"} +{"t":1784876372236,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"join-ok","detail":"peers=2 host=other map=mlsbbxfe bots=4 [+1187ms]","src":"client"} +{"t":1784876372300,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=แจ้งวัฒนะซอย 7-1784875928356 mapName=LobbyA [+1257ms]","src":"client"} +{"t":1784876372301,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"bot-pos","detail":"map=35x20 n=4 [b0:16.0,14.0->23.4,16.8 b1:22.0,13.0->18.5,17.6 b2:23.0,8.0->15.5,5.2 b3:15.0,5.0->17.2,8.1] [+1259ms]","src":"client"} +{"t":1784876374303,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"host-change","detail":"host ใหม่ → ฟ้า","src":"server"} +{"t":1784876374303,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7","who":"ผู้เล่น","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1784876377008,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:20.8,14.3->6.2,10.8 b1:23.4,7.5->22.7,17.6 b2:10.6,7.6->24.1,5.3 b3:13.1,7.2->18.7,9.6] [+62125ms]","src":"client"} +{"t":1784876377726,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+0ms]","src":"client"} +{"t":1784876377795,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"manifest-done","detail":" [+20ms]","src":"client"} +{"t":1784876377795,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-start","detail":" [+20ms]","src":"client"} +{"t":1784876377795,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+17ms]","src":"client"} +{"t":1784876377809,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"colors-resolved","detail":" [+20ms]","src":"client"} +{"t":1784876377824,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"mychar-tinted","detail":"roomJoinReady=false [+21ms]","src":"client"} +{"t":1784876377855,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-ready(occupants)","detail":" [+21ms]","src":"client"} +{"t":1784876377857,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"preload-occupants-done","detail":" [+21ms]","src":"client"} +{"t":1784876377943,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ฟ้า","ev":"socket-connect","detail":" [+211ms]","src":"client"} +{"t":1784876382088,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=4 [b0:13.7,12.6->6.2,10.9 b1:20.5,11.8->16.7,16.2 b2:15.1,7.1->19.2,6.7 b3:14.4,7.8->15.0,8.1] [+67169ms]","src":"client"} +{"t":1784876390148,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"lobby-open","detail":"space=แจ้งวัฒนะซอย 7-1784875928356 [+0ms]","src":"client"} +{"t":1784876390229,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+17ms]","src":"client"} +{"t":1784876390229,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-start","detail":" [+20ms]","src":"client"} +{"t":1784876390238,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+21ms]","src":"client"} +{"t":1784876390239,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"manifest-done","detail":" [+21ms]","src":"client"} +{"t":1784876390239,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+21ms]","src":"client"} +{"t":1784876390287,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+22ms]","src":"client"} +{"t":1784876390288,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+22ms]","src":"client"} +{"t":1784876390371,"cat":"LobbyA","room":"แจ้งวัฒนะซอย 7-1784875928356","who":"ผู้เล่น","ev":"socket-connect","detail":" [+233ms]","src":"client"} +{"t":1785154485919,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"lobby-open","detail":"space=fah [+0ms]","src":"client"} +{"t":1785154487318,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"preload-start","detail":" [+284ms]","src":"client"} +{"t":1785154487487,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+345ms]","src":"client"} +{"t":1785154487498,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"manifest-done","detail":" [+308ms]","src":"client"} +{"t":1785154487770,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+350ms]","src":"client"} +{"t":1785154487770,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+364ms]","src":"client"} +{"t":1785154487770,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+357ms]","src":"client"} +{"t":1785154488460,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"socket-connect","detail":" [+2793ms]","src":"client"} +{"t":1785154488501,"cat":"LobbyA","room":"Fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785154492459,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mlsbbxfe bots=5 [+6820ms]","src":"client"} +{"t":1785154492488,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+6843ms]","src":"client"} +{"t":1785154493177,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=fah mapName=LobbyA [+7532ms]","src":"client"} +{"t":1785154493354,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,14.0->11.8,14.2 b1:16.0,14.0->17.3,15.3 b2:22.0,13.0->20.2,12.9 b3:23.0,8.0->21.3,8.6 b4:15.0,5.0->13.2,5.1] [+7690ms]","src":"client"} +{"t":1785154497421,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:20.1,10.1->20.1,10.1 b1:11.4,7.1->10.5,6.6 b2:20.2,11.4->20.2,11.4 b3:16.5,9.7->16.5,9.7 b4:28.9,12.1->28.9,12.1] [+11760ms]","src":"client"} +{"t":1785154504693,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+18992ms]","src":"client"} +{"t":1785154504941,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:20.1,10.0->20.1,10.0 b1:6.7,7.3->6.7,7.3 b2:20.2,11.0->20.2,11.0 b3:16.5,9.7->16.5,9.7 b4:28.9,12.1->28.4,12.0] [+19316ms]","src":"client"} +{"t":1785154519279,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:20.1,10.0->20.1,10.0 b1:6.7,7.3->6.7,7.3 b2:20.2,11.0->20.2,10.9 b3:16.5,9.7->16.5,9.7 b4:28.4,12.0->27.8,12.0] [+33648ms]","src":"client"} +{"t":1785154529871,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:20.1,11.2->20.1,11.2 b1:11.4,4.0->11.4,4.0 b2:8.9,14.7->8.8,14.7 b3:25.7,5.1->25.7,5.1 b4:20.3,10.6->20.3,10.6] [+44246ms]","src":"client"} +{"t":1785154533918,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.7,5.4->14.7,5.4 b1:15.0,11.8->15.0,11.8 b2:18.9,13.2->18.9,13.2 b3:18.8,6.4->18.7,6.5 b4:6.1,13.4->6.1,13.6] [+48287ms]","src":"client"} +{"t":1785154537941,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.1,17.9->14.1,17.9 b1:17.5,14.3->17.8,13.4 b2:21.0,17.8->21.0,17.8 b3:6.4,9.4->6.4,9.4 b4:6.1,14.1->6.1,14.1] [+52306ms]","src":"client"} +{"t":1785154538460,"cat":"LobbyA","room":"Fah","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host เปิด wizard เลือกคดี → ล็อก set-ready ทั้งห้อง (1 คนในห้อง)","src":"server"} +{"t":1785154542006,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.2,13.2->19.2,13.2 b1:18.8,13.4->18.8,13.4 b2:11.2,16.2->11.6,15.5 b3:28.7,7.5->29.6,7.0 b4:19.9,4.7->19.9,4.7] [+56365ms]","src":"client"} +{"t":1785154545998,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:27.8,14.0->27.8,14.0 b1:28.5,12.2->28.5,12.2 b2:6.8,9.5->6.8,9.5 b3:16.2,13.2->17.1,13.4 b4:20.9,6.8->21.3,7.7] [+60371ms]","src":"client"} +{"t":1785154550103,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.7,15.7->13.9,15.8 b1:20.5,10.9->20.5,10.8 b2:28.0,5.2->28.0,5.2 b3:22.2,10.8->21.4,10.4 b4:23.4,5.1->23.4,5.1] [+64456ms]","src":"client"} +{"t":1785154554141,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:6.3,17.3->6.3,17.3 b1:6.7,8.9->7.4,8.6 b2:13.0,8.5->12.1,8.7 b3:26.0,16.8->26.0,16.8 b4:15.7,9.0->15.6,9.9] [+68471ms]","src":"client"} +{"t":1785154555224,"cat":"LobbyB","room":"Fah","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1785154555696,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+70072ms]","src":"client"} +{"t":1785154557466,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+71830ms]","src":"client"} +{"t":1785154558146,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.6,7.5->14.2,7.9 b1:14.9,10.4->14.9,10.4 b2:12.2,9.5->12.7,9.4 b3:14.7,12.6->14.8,12.3 b4:19.1,9.0->19.1,9.0] [+72520ms]","src":"client"} +{"t":1785154558253,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"suspect-open-evt","detail":"idx=0 postCase=true [+72620ms]","src":"client"} +{"t":1785154563329,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.6,6.1->20.0,6.1 b1:8.0,8.6->8.0,8.6 b2:24.6,7.9->24.6,7.9 b3:14.8,11.5->14.8,11.5 b4:10.8,9.2->11.2,9.5] [+77698ms]","src":"client"} +{"t":1785154567350,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:8.1,8.5->8.1,8.5 b1:15.0,12.8->15.2,13.7 b2:20.3,8.2->19.5,8.7 b3:14.8,12.4->14.8,12.4 b4:15.1,6.1->15.1,6.1] [+81722ms]","src":"client"} +{"t":1785154571382,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:8.1,8.5->8.1,8.5 b1:23.0,6.3->23.0,6.3 b2:9.4,16.9->9.4,16.9 b3:14.8,12.4->14.9,11.8 b4:11.9,10.2->11.9,10.2] [+85752ms]","src":"client"} +{"t":1785154575427,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:13.4,8.9->13.7,9.7 b1:27.7,9.1->27.7,9.1 b2:9.4,16.9->9.9,16.9 b3:16.8,5.5->16.8,5.5 b4:11.6,7.7->12.5,7.4] [+89798ms]","src":"client"} +{"t":1785154579450,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:12.1,15.7->12.1,15.7 b1:19.2,11.2->19.2,11.1 b2:11.6,16.9->11.6,16.9 b3:26.5,13.3->26.2,12.4 b4:16.5,5.2->16.5,5.2] [+93828ms]","src":"client"} +{"t":1785154580162,"cat":"LobbyB","room":"Fah","who":"","ev":"lobbyb-gate","detail":"[TC166] safety 25s → force นับ present จาก peer ที่อยู่จริง (+1)","src":"server"} +{"t":1785154583488,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.8,17.0->22.8,17.0 b1:27.8,10.6->27.8,10.6 b2:19.4,13.8->19.5,14.8 b3:22.0,6.3->22.0,6.3 b4:8.4,7.2->8.4,7.2] [+97862ms]","src":"client"} +{"t":1785154587527,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.5,14.5->9.5,14.4 b1:25.6,3.3->25.4,3.3 b2:22.1,10.8->21.2,11.0 b3:8.3,12.4->8.3,12.4 b4:14.7,11.2->14.9,11.7] [+101901ms]","src":"client"} +{"t":1785154591566,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:7.4,13.3->7.4,13.3 b1:25.3,3.3->25.3,3.3 b2:8.3,14.0->8.3,14.0 b3:20.5,16.7->21.5,16.8 b4:25.1,16.9->24.1,16.9] [+105936ms]","src":"client"} +{"t":1785154592093,"cat":"LobbyB","room":"Fah","who":"ผู้เล่น","ev":"story-gate","detail":"[TC156] อ่าน story จบ 1/1","src":"server"} +{"t":1785154592093,"cat":"LobbyB","room":"Fah","who":"","ev":"story-gate","detail":"[TC156] ทุกคนอ่าน story ครบ (1) → ปลดล็อกเลือกผู้ต้องสงสัย (pass)","src":"server"} +{"t":1785154592593,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+106928ms]","src":"client"} +{"t":1785154595638,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.9,11.5->14.9,11.5 b1:25.3,3.0->25.3,3.0 b2:6.7,9.2->6.7,9.2 b3:19.9,8.1->20.3,9.0 b4:19.3,10.0->19.1,10.3] [+109987ms]","src":"client"} +{"t":1785154599663,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.9,11.4->14.9,11.4 b1:21.0,15.2->20.0,14.9 b2:6.7,9.2->6.7,9.2 b3:20.1,5.4->20.1,5.4 b4:14.8,13.3->14.8,13.3] [+114016ms]","src":"client"} +{"t":1785154603697,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.4,16.9->22.4,16.9 b1:21.5,14.7->21.3,13.7 b2:19.5,11.9->19.3,12.5 b3:23.0,9.9->23.0,10.0 b4:16.4,7.8->17.2,7.1] [+118042ms]","src":"client"} +{"t":1785154607720,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:12.0,7.7->11.4,6.7 b1:27.8,15.0->27.8,15.0 b2:15.6,15.8->16.6,15.6 b3:19.3,10.7->19.3,10.7 b4:21.6,5.1->21.6,5.1] [+122068ms]","src":"client"} +{"t":1785154611749,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:13.1,7.0->13.4,7.1 b1:27.8,14.7->27.7,13.9 b2:28.9,12.4->28.9,12.4 b3:15.4,6.7->15.0,6.4 b4:19.3,11.2->19.3,10.9] [+126103ms]","src":"client"} +{"t":1785154615782,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,9.3->11.0,9.2 b1:19.7,9.0->19.3,9.5 b2:23.7,11.6->24.6,11.2 b3:8.0,5.5->8.0,5.5 b4:8.5,8.0->8.0,8.3] [+130136ms]","src":"client"} +{"t":1785154619842,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.8,9.0->14.8,9.0 b1:17.0,14.9->17.0,14.9 b2:22.9,6.7->22.1,6.5 b3:27.7,16.1->27.7,16.2 b4:21.9,5.6->21.9,5.6] [+134199ms]","src":"client"} +{"t":1785154623864,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:7.6,12.0->7.4,12.4 b1:15.6,13.3->15.6,13.3 b2:12.6,12.9->13.6,12.9 b3:12.5,17.0->12.5,17.0 b4:21.9,5.8->21.9,5.8] [+138220ms]","src":"client"} +{"t":1785154626059,"cat":"MG1","room":"Fah","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-1 จริงหรือไม่ (การ์ด 2)","src":"server"} +{"t":1785154626074,"cat":"MG1","room":"Fah","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785154626183,"cat":"MG1","room":"Fah","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1785154627814,"cat":"MG1","room":"Fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785154727005,"cat":"MG1","room":"Fah","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 0/1/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785154727014,"cat":"MG1","room":"Fah","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785154740302,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"lobby-open","detail":"space=fah [+1ms]","src":"client"} +{"t":1785154740429,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+46ms]","src":"client"} +{"t":1785154740444,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"manifest-done","detail":" [+56ms]","src":"client"} +{"t":1785154740445,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"preload-start","detail":" [+53ms]","src":"client"} +{"t":1785154740456,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+58ms]","src":"client"} +{"t":1785154740499,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+59ms]","src":"client"} +{"t":1785154740520,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+60ms]","src":"client"} +{"t":1785154740521,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+61ms]","src":"client"} +{"t":1785154740525,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=true [+78ms]","src":"client"} +{"t":1785154740701,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"socket-connect","detail":" [+366ms]","src":"client"} +{"t":1785154740774,"cat":"LobbyB","room":"Fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785154743101,"cat":"LobbyB","room":"Fah","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785154743141,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2829ms]","src":"client"} +{"t":1785154743143,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mn8nx46h bots=5 [+2826ms]","src":"client"} +{"t":1785154743229,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=true pickIdx=1 postCase=true clientMapId=mn8nx46h spaceId=fah mapName=LobbyB [+2945ms]","src":"client"} +{"t":1785154743344,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,15.0->10.9,14.9 b1:15.0,15.0->14.3,15.6 b2:21.0,15.0->20.1,15.0 b3:23.0,12.0->22.3,11.6 b4:24.0,9.0->23.3,8.5] [+3048ms]","src":"client"} +{"t":1785154754597,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.6,12.3->19.9,11.9 b1:11.8,16.7->11.8,16.7 b2:5.3,15.9->5.3,15.9 b3:15.0,6.7->15.4,7.0 b4:9.1,10.9->8.7,11.3] [+14282ms]","src":"client"} +{"t":1785154779242,"cat":"MG4","room":"Fah","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-4 คำศัพท์ในกระบวนการยุติธรรม (การ์ด 1)","src":"server"} +{"t":1785154779248,"cat":"MG4","room":"Fah","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785154837322,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.4,9.0->22.7,8.6 b1:11.8,16.7->11.8,16.7 b2:5.3,15.9->5.3,15.9 b3:18.7,8.7->19.1,9.0 b4:7.2,13.9->7.2,14.2] [+97019ms]","src":"client"} +{"t":1785154869976,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"socket-connect","detail":" [+129674ms]","src":"client"} +{"t":1785154869977,"cat":"MG4","room":"Fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785154869980,"cat":"MG4","room":"Fah","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 1/1/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785154873303,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mn8nx46h bots=5 [+132999ms]","src":"client"} +{"t":1785154873378,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=true pickIdx=0 postCase=true clientMapId=mn8nx46h spaceId=fah mapName=LobbyB [+133046ms]","src":"client"} +{"t":1785154873472,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->9.8,14.3 b1:19.1,13.4->15.6,14.5 b2:5.0,17.0->20.5,14.5 b3:24.1,5.4->22.8,12.7 b4:9.0,15.5->23.5,8.5] [+133086ms]","src":"client"} +{"t":1785154873509,"cat":"LobbyB","room":"Fah","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785154874313,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+133994ms]","src":"client"} +{"t":1785154876596,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+136295ms]","src":"client"} +{"t":1785154878090,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->15.0,12.0 b1:19.1,13.4->21.4,6.0 b2:5.0,17.0->16.2,14.0 b3:24.1,5.4->16.5,5.1 b4:9.0,15.5->14.7,11.0] [+137795ms]","src":"client"} +{"t":1785154882112,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->23.0,7.8 b1:19.1,13.4->27.7,15.7 b2:5.0,17.0->21.2,16.9 b3:24.1,5.4->17.5,13.8 b4:9.0,15.5->5.1,9.0] [+141825ms]","src":"client"} +{"t":1785154886168,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->22.2,9.0 b1:19.1,13.4->11.8,14.7 b2:5.0,17.0->7.2,11.5 b3:24.1,5.4->28.3,12.9 b4:9.0,15.5->25.7,17.0] [+145874ms]","src":"client"} +{"t":1785154891384,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->25.6,13.6 b1:19.1,13.4->16.5,5.3 b2:5.0,17.0->14.7,9.8 b3:24.1,5.4->23.0,14.2 b4:9.0,15.5->28.0,11.5] [+151093ms]","src":"client"} +{"t":1785154895441,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->15.3,15.7 b1:19.1,13.4->20.9,17.0 b2:5.0,17.0->27.8,10.0 b3:24.1,5.4->20.4,10.7 b4:9.0,15.5->27.7,5.1] [+155148ms]","src":"client"} +{"t":1785154900945,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->25.7,3.4 b1:19.1,13.4->13.7,13.5 b2:5.0,17.0->19.2,11.2 b3:24.1,5.4->10.2,13.0 b4:9.0,15.5->27.7,5.2] [+160651ms]","src":"client"} +{"t":1785154905693,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->19.9,6.4 b1:19.1,13.4->20.7,13.4 b2:5.0,17.0->19.2,12.6 b3:24.1,5.4->19.5,12.0 b4:9.0,15.5->25.7,16.7] [+165402ms]","src":"client"} +{"t":1785154910016,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->7.3,12.1 b1:19.1,13.4->15.0,9.2 b2:5.0,17.0->19.4,14.8 b3:24.1,5.4->14.4,16.8 b4:9.0,15.5->21.7,6.7] [+169724ms]","src":"client"} +{"t":1785154914038,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->9.5,5.1 b1:19.1,13.4->15.0,11.9 b2:5.0,17.0->15.6,9.0 b3:24.1,5.4->14.8,9.9 b4:9.0,15.5->25.4,8.3] [+173745ms]","src":"client"} +{"t":1785154918073,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->11.3,13.8 b1:19.1,13.4->10.6,8.5 b2:5.0,17.0->12.2,15.4 b3:24.1,5.4->14.8,9.9 b4:9.0,15.5->20.4,11.9] [+177786ms]","src":"client"} +{"t":1785154922112,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->14.8,11.8 b1:19.1,13.4->7.2,14.4 b2:5.0,17.0->9.9,15.6 b3:24.1,5.4->8.3,6.4 b4:9.0,15.5->17.4,16.6] [+181827ms]","src":"client"} +{"t":1785154926163,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->14.8,9.4 b1:19.1,13.4->9.5,5.6 b2:5.0,17.0->24.8,16.2 b3:24.1,5.4->8.8,4.2 b4:9.0,15.5->27.6,16.8] [+185871ms]","src":"client"} +{"t":1785154930186,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->9.6,5.3 b1:19.1,13.4->6.4,10.8 b2:5.0,17.0->22.7,17.0 b3:24.1,5.4->8.8,4.3 b4:9.0,15.5->15.2,15.5] [+189897ms]","src":"client"} +{"t":1785154934242,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->7.4,13.8 b1:19.1,13.4->14.4,16.7 b2:5.0,17.0->27.4,13.5 b3:24.1,5.4->24.1,8.4 b4:9.0,15.5->7.1,14.7] [+193936ms]","src":"client"} +{"t":1785154938288,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->5.8,16.6 b1:19.1,13.4->8.9,16.5 b2:5.0,17.0->13.6,5.9 b3:24.1,5.4->28.6,7.7 b4:9.0,15.5->21.3,12.5] [+197978ms]","src":"client"} +{"t":1785154942292,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.2,6.9->5.7,16.6 b1:19.1,13.4->9.3,6.7 b2:5.0,17.0->24.4,11.0 b3:24.1,5.4->28.6,7.7 b4:9.0,15.5->17.8,8.5] [+201991ms]","src":"client"} +{"t":1785154946309,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:9.1,11.3->8.2,11.2 b1:21.8,14.6->21.6,14.7 b2:18.9,8.5->18.0,8.3 b3:27.1,5.1->27.1,5.1 b4:29.0,8.8->29.0,8.8] [+206025ms]","src":"client"} +{"t":1785154950370,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.8,9.2->14.8,9.2 b1:16.5,13.2->17.5,13.4 b2:8.4,7.3->8.4,8.0 b3:19.2,12.8->19.2,13.0 b4:23.4,13.2->22.5,13.6] [+210059ms]","src":"client"} +{"t":1785154954391,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.8,9.2->14.8,9.2 b1:18.1,7.9->17.4,7.9 b2:10.0,11.6->9.2,11.5 b3:5.3,16.1->5.3,16.1 b4:12.4,14.5->12.4,14.5] [+214098ms]","src":"client"} +{"t":1785154954863,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+214562ms]","src":"client"} +{"t":1785154958433,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.8,11.1->14.8,11.1 b1:14.6,11.2->14.6,11.2 b2:7.1,11.3->7.1,11.3 b3:19.3,12.9->20.3,12.6 b4:12.2,6.7->13.0,7.2] [+218136ms]","src":"client"} +{"t":1785154962467,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:8.4,5.7->8.4,5.7 b1:18.3,13.1->18.3,13.1 b2:15.2,17.0->15.2,17.0 b3:28.9,7.8->28.9,7.8 b4:28.3,12.3->27.4,12.5] [+222168ms]","src":"client"} +{"t":1785154966496,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:24.9,10.8->24.9,10.8 b1:16.8,14.4->16.8,14.4 b2:28.6,8.8->28.6,8.8 b3:11.8,6.2->11.7,6.1 b4:16.5,13.1->17.3,13.1] [+226210ms]","src":"client"} +{"t":1785154970532,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:24.2,16.7->24.2,16.7 b1:28.5,12.9->28.5,12.9 b2:24.7,9.1->24.1,9.6 b3:11.5,9.1->12.2,8.9 b4:27.6,8.6->26.9,8.7] [+230235ms]","src":"client"} +{"t":1785154974563,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:15.5,17.0->15.5,17.0 b1:16.8,7.5->16.8,7.4 b2:18.3,13.3->18.4,13.3 b3:19.0,14.4->19.9,14.2 b4:19.3,10.6->19.3,10.6] [+234264ms]","src":"client"} +{"t":1785154978636,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,11.4->9.0,11.4 b1:13.3,16.5->13.3,16.5 b2:19.9,7.0->20.9,7.0 b3:23.3,16.6->23.3,16.6 b4:27.9,16.6->27.9,16.6] [+238321ms]","src":"client"} +{"t":1785154982634,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:7.4,12.2->7.4,12.2 b1:11.0,16.8->10.9,16.8 b2:19.4,17.0->19.2,17.0 b3:19.4,14.6->18.5,14.1 b4:24.6,16.8->23.6,16.9] [+242334ms]","src":"client"} +{"t":1785154986667,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:12.2,5.7->12.2,5.8 b1:10.9,16.8->10.9,16.8 b2:19.1,17.0->19.1,17.0 b3:7.1,12.5->7.1,12.6 b4:9.9,16.4->9.9,16.4] [+246376ms]","src":"client"} +{"t":1785154990709,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:8.4,6.6->8.4,6.6 b1:28.6,7.4->28.6,7.2 b2:17.8,6.3->16.9,6.4 b3:7.1,11.5->7.1,11.5 b4:27.8,16.8->27.8,16.8] [+250423ms]","src":"client"} +{"t":1785154994739,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.7,10.6->14.7,10.7 b1:15.9,13.3->15.5,13.2 b2:8.2,8.0->8.2,8.0 b3:7.1,11.5->7.1,11.5 b4:9.8,17.0->8.8,17.0] [+254453ms]","src":"client"} +{"t":1785154998777,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:13.4,5.4->13.4,5.4 b1:7.3,12.2->7.4,12.1 b2:16.3,6.9->17.3,6.6 b3:7.1,11.9->7.1,11.9 b4:22.5,6.7->23.5,6.4] [+258484ms]","src":"client"} +{"t":1785155002814,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:13.4,5.4->13.4,5.4 b1:12.9,13.8->13.8,14.0 b2:14.3,11.4->14.7,11.6 b3:7.7,10.2->8.0,9.4 b4:23.1,6.5->22.3,6.9] [+262523ms]","src":"client"} +{"t":1785155006851,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.6,12.5->14.6,12.5 b1:19.5,10.1->19.5,10.1 b2:27.7,13.7->27.7,13.7 b3:16.3,8.9->16.1,8.9 b4:7.7,13.8->7.5,12.9] [+266562ms]","src":"client"} +{"t":1785155008658,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=true [+268354ms]","src":"client"} +{"t":1785155010877,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.2,12.4->9.2,12.4 b1:8.2,8.2->8.2,8.2 b2:28.0,15.4->28.0,15.4 b3:19.8,6.4->19.5,7.3 b4:14.4,12.4->14.5,12.7] [+270592ms]","src":"client"} +{"t":1785155014923,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.0,6.2->14.5,6.1 b1:21.7,5.4->22.0,6.1 b2:8.9,4.2->8.9,4.2 b3:16.0,8.8->16.0,8.8 b4:12.3,16.5->11.4,16.7] [+274636ms]","src":"client"} +{"t":1785155018959,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:14.6,6.1->14.6,6.1 b1:10.3,6.4->10.3,6.4 b2:8.9,4.6->8.9,4.6 b3:16.7,8.8->16.7,8.8 b4:22.8,8.7->22.8,8.7] [+278668ms]","src":"client"} +{"t":1785155022999,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:15.4,8.8->15.4,8.8 b1:24.6,11.9->25.5,12.4 b2:23.5,11.7->24.3,12.2 b3:16.7,8.8->16.7,8.8 b4:16.9,14.8->17.6,15.5] [+282700ms]","src":"client"} +{"t":1785155027040,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:15.2,8.8->15.2,8.8 b1:27.9,13.5->27.9,13.4 b2:7.0,13.6->7.0,13.6 b3:14.6,10.3->14.6,9.8 b4:19.0,15.6->18.6,14.8] [+286743ms]","src":"client"} +{"t":1785155029286,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"lobby-open","detail":"space=fah [+0ms]","src":"client"} +{"t":1785155029387,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"preload-start","detail":" [+61ms]","src":"client"} +{"t":1785155029387,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+53ms]","src":"client"} +{"t":1785155029449,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"manifest-done","detail":" [+62ms]","src":"client"} +{"t":1785155029450,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+64ms]","src":"client"} +{"t":1785155029464,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+67ms]","src":"client"} +{"t":1785155029464,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=true [+83ms]","src":"client"} +{"t":1785155029464,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+65ms]","src":"client"} +{"t":1785155029464,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+66ms]","src":"client"} +{"t":1785155029656,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"socket-connect","detail":" [+354ms]","src":"client"} +{"t":1785155029739,"cat":"LobbyB","room":"Fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785155031941,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mn8nx46h bots=5 [+2645ms]","src":"client"} +{"t":1785155031955,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2653ms]","src":"client"} +{"t":1785155047430,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"lobby-open","detail":"space=ฟ้า-1785155043512 [+0ms]","src":"client"} +{"t":1785155047482,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+24ms]","src":"client"} +{"t":1785155047529,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"preload-start","detail":" [+27ms]","src":"client"} +{"t":1785155047543,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+31ms]","src":"client"} +{"t":1785155047543,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+30ms]","src":"client"} +{"t":1785155047544,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+29ms]","src":"client"} +{"t":1785155047544,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"manifest-done","detail":" [+28ms]","src":"client"} +{"t":1785155047564,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+33ms]","src":"client"} +{"t":1785155047714,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"socket-connect","detail":" [+275ms]","src":"client"} +{"t":1785155047716,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785155051859,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mlsbbxfe bots=5 [+4440ms]","src":"client"} +{"t":1785155051865,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+4441ms]","src":"client"} +{"t":1785155051928,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=ฟ้า-1785155043512 mapName=LobbyA [+4493ms]","src":"client"} +{"t":1785155051942,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,14.0->9.7,13.4 b1:16.0,14.0->15.6,13.5 b2:22.0,13.0->21.4,12.9 b3:23.0,8.0->23.0,8.7 b4:15.0,5.0->15.4,5.5] [+4509ms]","src":"client"} +{"t":1785155059699,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:26.6,10.0->26.4,10.5 b1:14.5,12.6->14.5,12.6 b2:21.1,9.6->21.4,9.2 b3:29.7,5.7->29.7,5.7 b4:29.9,7.0->29.9,7.0] [+12283ms]","src":"client"} +{"t":1785155066564,"cat":"LobbyA","room":"Quiz Battle ห้อง 1","who":"ผู้เล่น5399","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785155073167,"cat":"LobbyA","room":"ฟ้า","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host เปิด wizard เลือกคดี → ล็อก set-ready ทั้งห้อง (1 คนในห้อง)","src":"server"} +{"t":1785155083842,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:16.7,13.2->16.7,13.2 b1:13.9,13.5->13.6,14.0 b2:18.2,9.0->17.7,9.3 b3:25.0,7.3->24.5,7.5 b4:27.5,8.3->27.0,8.5] [+36413ms]","src":"client"} +{"t":1785155086831,"cat":"LobbyB","room":"ฟ้า","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1785155087348,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+39916ms]","src":"client"} +{"t":1785155087887,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:28.0,16.0->28.0,16.7 b1:7.3,14.3->7.3,14.3 b2:12.3,9.9->12.2,9.8 b3:14.4,10.8->14.8,10.4 b4:19.2,12.2->19.2,12.2] [+40463ms]","src":"client"} +{"t":1785155088496,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+41059ms]","src":"client"} +{"t":1785155090012,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"suspect-open-evt","detail":"idx=0 postCase=true [+42590ms]","src":"client"} +{"t":1785155090220,"cat":"LobbyB","room":"ฟ้า","who":"ผู้เล่น","ev":"story-gate","detail":"[TC156] อ่าน story จบ 1/1","src":"server"} +{"t":1785155090221,"cat":"LobbyB","room":"ฟ้า","who":"","ev":"story-gate","detail":"[TC156] ทุกคนอ่าน story ครบ (1) → ปลดล็อกเลือกผู้ต้องสงสัย (pass)","src":"server"} +{"t":1785155092806,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:28.2,6.3->27.7,6.6 b1:7.3,14.2->7.3,14.2 b2:14.6,7.6->15.1,7.3 b3:14.8,10.4->14.8,10.4 b4:19.2,12.0->19.2,12.0] [+44675ms]","src":"client"} +{"t":1785155092944,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+44978ms]","src":"client"} +{"t":1785155096570,"cat":"MG3","room":"ฟ้า","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-3 Tower block (การ์ด 1)","src":"server"} +{"t":1785155096574,"cat":"MG3","room":"ฟ้า","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785155100027,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:28.9,6.7->28.9,6.7 b1:9.0,13.5->8.9,14.0 b2:19.1,9.6->19.1,9.6 b3:9.6,8.9->9.1,9.0 b4:19.8,10.2->20.3,10.1] [+52537ms]","src":"client"} +{"t":1785155120858,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:28.9,6.7->28.9,6.7 b1:8.1,16.7->8.1,16.7 b2:19.1,9.7->19.1,9.8 b3:8.1,9.3->8.1,9.3 b4:19.2,10.2->19.2,10.2] [+73351ms]","src":"client"} +{"t":1785155121012,"cat":"MG3","room":"ฟ้า","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1785155122642,"cat":"MG3","room":"ฟ้า","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785155138856,"cat":"LobbyA","room":"Quiz Battle ห้อง 1","who":"ผู้เล่น3490","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785155215462,"cat":"LobbyA","room":"Quiz Battle ห้อง 2","who":"ผู้เล่น4322","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785155244841,"cat":"MG3","room":"ฟ้า","who":"","ev":"stack-score","detail":"[TC183] จบ MG3 outcome=time_up team=290 layers=17 progress=58% · ต่อคน: ผู้เล่=40 bot=50 bot=50 bot=60 bot=50 bot=40","src":"server"} +{"t":1785155258327,"cat":"MG3","room":"ฟ้า","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 1/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785155258332,"cat":"MG3","room":"ฟ้า","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785155271674,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"lobby-open","detail":"space=ฟ้า-1785155043512 [+0ms]","src":"client"} +{"t":1785155271780,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"preload-start","detail":" [+32ms]","src":"client"} +{"t":1785155271782,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+25ms]","src":"client"} +{"t":1785155271788,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"manifest-done","detail":" [+33ms]","src":"client"} +{"t":1785155271789,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+35ms]","src":"client"} +{"t":1785155271860,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+37ms]","src":"client"} +{"t":1785155271860,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=true [+54ms]","src":"client"} +{"t":1785155271861,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+35ms]","src":"client"} +{"t":1785155271862,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+36ms]","src":"client"} +{"t":1785155272016,"cat":"LobbyA","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"socket-connect","detail":" [+351ms]","src":"client"} +{"t":1785155272102,"cat":"LobbyB","room":"ฟ้า","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785155274394,"cat":"LobbyB","room":"ฟ้า","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785155274436,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mn8nx46h bots=5 [+2734ms]","src":"client"} +{"t":1785155274499,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2736ms]","src":"client"} +{"t":1785155274588,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=true pickIdx=0 postCase=true clientMapId=mn8nx46h spaceId=ฟ้า-1785155043512 mapName=LobbyB [+2850ms]","src":"client"} +{"t":1785155274638,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,15.0->10.6,14.8 b1:15.0,15.0->15.6,14.7 b2:21.0,15.0->20.5,14.6 b3:23.0,12.0->22.4,11.8 b4:24.0,9.0->23.7,9.6] [+2967ms]","src":"client"} +{"t":1785155285589,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:26.6,9.3->27.0,9.0 b1:23.6,10.3->23.6,10.3 b2:14.8,11.6->14.8,11.7 b3:13.3,8.1->12.8,8.0 b4:25.5,14.9->25.5,14.9] [+13868ms]","src":"client"} +{"t":1785155343467,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:28.9,6.8->28.9,6.7 b1:24.2,7.1->24.3,6.6 b2:14.8,12.6->14.8,12.7 b3:8.5,7.2->8.5,7.1 b4:25.5,14.9->25.5,14.9] [+71773ms]","src":"client"} +{"t":1785155426994,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"socket-connect","detail":" [+155302ms]","src":"client"} +{"t":1785155427008,"cat":"LobbyB","room":"ฟ้า","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785155433963,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mn8nx46h bots=5 [+162279ms]","src":"client"} +{"t":1785155433998,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=true pickIdx=0 postCase=true clientMapId=mn8nx46h spaceId=ฟ้า-1785155043512 mapName=LobbyB [+162321ms]","src":"client"} +{"t":1785155434066,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:16.2,5.4->10.8,15.0 b1:6.2,15.2->14.3,14.7 b2:16.3,6.9->20.2,14.9 b3:18.8,13.1->22.3,12.3 b4:7.3,13.5->24.6,9.5] [+162342ms]","src":"client"} +{"t":1785155441330,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.8,15.0->11.4,14.9 b1:14.3,14.7->13.7,14.5 b2:20.2,14.9->19.6,14.7 b3:22.3,12.3->21.7,12.5 b4:24.6,9.5->25.0,10.0] [+169656ms]","src":"client"} +{"t":1785155446415,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:11.4,14.9->12.0,14.9 b1:13.7,14.5->13.2,14.3 b2:19.6,14.7->19.1,14.6 b3:21.7,12.5->21.2,12.8 b4:25.0,10.0->25.4,10.3] [+174732ms]","src":"client"} +{"t":1785155450726,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:9.5,10.4->9.5,10.4 b1:25.5,12.4->25.5,12.4 b2:19.9,7.3->19.3,7.2 b3:14.6,12.2->14.6,12.5 b4:28.9,6.4->28.9,6.4] [+179042ms]","src":"client"} +{"t":1785155477628,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:7.0,13.9->7.0,13.9 b1:26.0,15.2->26.0,15.2 b2:19.5,14.0->20.0,14.1 b3:12.1,5.9->12.1,5.9 b4:6.2,10.8->6.2,10.8] [+205938ms]","src":"client"} +{"t":1785155479320,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"socket-connect","detail":" [+207640ms]","src":"client"} +{"t":1785155479322,"cat":"LobbyB","room":"ฟ้า","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785155483322,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mn8nx46h bots=5 [+211648ms]","src":"client"} +{"t":1785155483419,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=true pickIdx=1 postCase=true clientMapId=mn8nx46h spaceId=ฟ้า-1785155043512 mapName=LobbyB [+211722ms]","src":"client"} +{"t":1785155483477,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:9.5,11.6->10.5,15.1 b1:25.9,8.4->15.5,15.0 b2:20.1,10.0->20.5,14.9 b3:14.0,10.1->23.4,11.7 b4:19.1,11.3->23.8,8.5] [+211758ms]","src":"client"} +{"t":1785155491830,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.5,15.1->11.1,15.1 b1:15.5,15.0->16.1,15.1 b2:20.5,14.9->20.0,14.7 b3:23.4,11.7->23.9,11.4 b4:23.8,8.5->23.6,8.0] [+218421ms]","src":"client"} +{"t":1785155532122,"cat":"LobbyB","room":"ฟ้า-1785155043512","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:11.1,15.1->11.6,15.2 b1:16.1,15.1->16.6,15.1 b2:20.0,14.7->19.4,14.6 b3:23.9,11.4->24.3,11.1 b4:23.6,8.0->23.4,7.5] [+259683ms]","src":"client"} +{"t":1785155715139,"cat":"LobbyA","room":"Quiz Battle ห้อง 2","who":"ผู้เล่น432","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785155904824,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"lobby-open","detail":"space=fah-1785155901092 [+0ms]","src":"client"} +{"t":1785155904936,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"preload-start","detail":" [+64ms]","src":"client"} +{"t":1785155904937,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=false howtoOk=true micOk=true sent=false [+55ms]","src":"client"} +{"t":1785155904970,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"manifest-done","detail":" [+66ms]","src":"client"} +{"t":1785155904975,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+68ms]","src":"client"} +{"t":1785155904989,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+69ms]","src":"client"} +{"t":1785155905012,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+72ms]","src":"client"} +{"t":1785155905012,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+71ms]","src":"client"} +{"t":1785155905150,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"socket-connect","detail":" [+315ms]","src":"client"} +{"t":1785155905166,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785155906871,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mlsbbxfe bots=5 [+2037ms]","src":"client"} +{"t":1785155906885,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2039ms]","src":"client"} +{"t":1785155907032,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=fah-1785155901092 mapName=LobbyA [+2206ms]","src":"client"} +{"t":1785155907075,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,14.0->11.0,13.8 b1:16.0,14.0->16.9,13.5 b2:22.0,13.0->21.1,13.2 b3:23.0,8.0->22.8,7.1 b4:15.0,5.0->15.7,5.7] [+2241ms]","src":"client"} +{"t":1785155926284,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:20.3,12.1->20.3,12.1 b1:20.7,11.7->20.7,11.7 b2:6.1,17.0->6.1,17.0 b3:21.3,5.3->21.3,5.3 b4:23.6,15.3->23.1,15.0] [+21452ms]","src":"client"} +{"t":1785155946836,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:29.7,7.0->29.7,7.0 b1:21.9,8.6->22.1,9.1 b2:23.8,5.2->23.2,5.3 b3:6.2,12.5->6.2,12.5 b4:15.5,13.0->15.5,13.0] [+41994ms]","src":"client"} +{"t":1785155949251,"cat":"LobbyA","room":"fah","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host เปิด wizard เลือกคดี → ล็อก set-ready ทั้งห้อง (1 คนในห้อง)","src":"server"} +{"t":1785155950879,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:18.4,13.1->17.6,13.1 b1:7.5,4.0->7.5,4.0 b2:25.4,16.7->25.4,16.7 b3:7.8,9.8->8.8,9.5 b4:11.3,4.3->11.3,4.3] [+46050ms]","src":"client"} +{"t":1785155952895,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1785155953437,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+48600ms]","src":"client"} +{"t":1785155954589,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+49770ms]","src":"client"} +{"t":1785155954902,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:12.1,9.5->13.1,9.6 b1:12.7,13.8->12.7,13.8 b2:28.8,12.7->28.8,12.7 b3:22.4,16.2->22.2,16.6 b4:17.3,16.7->17.3,16.7] [+50069ms]","src":"client"} +{"t":1785155955287,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-open-evt","detail":"idx=0 postCase=true [+50459ms]","src":"client"} +{"t":1785155955593,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"story-gate","detail":"[TC156] อ่าน story จบ 1/1","src":"server"} +{"t":1785155955593,"cat":"LobbyB","room":"fah","who":"","ev":"story-gate","detail":"[TC156] ทุกคนอ่าน story ครบ (1) → ปลดล็อกเลือกผู้ต้องสงสัย (pass)","src":"server"} +{"t":1785155961052,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+55400ms]","src":"client"} +{"t":1785155961134,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:17.8,8.3->18.4,8.2 b1:12.7,13.8->12.7,13.8 b2:28.8,12.7->28.8,12.7 b3:22.2,16.6->22.2,16.6 b4:17.3,16.7->17.3,16.7] [+55423ms]","src":"client"} +{"t":1785155964955,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:25.3,4.8->25.3,4.8 b1:15.9,7.0->15.4,7.1 b2:19.1,12.6->19.1,12.6 b3:11.0,11.2->11.0,11.2 b4:21.8,16.9->21.8,16.9] [+60112ms]","src":"client"} +{"t":1785155968988,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:25.4,4.1->25.4,4.0 b1:11.8,9.9->11.8,9.9 b2:19.2,11.6->19.1,10.8 b3:11.4,6.1->11.4,6.1 b4:14.9,10.4->14.9,10.5] [+64135ms]","src":"client"} +{"t":1785155969777,"cat":"MG2","room":"fah","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-2 วิ่งหลบสิ่งกีดขวาง (การ์ด 2)","src":"server"} +{"t":1785155969791,"cat":"MG2","room":"fah","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785155969868,"cat":"MG2","room":"fah","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1785155971088,"cat":"MG2","room":"fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785156031049,"cat":"MG2","room":"fah","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 0/1/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785156031052,"cat":"MG2","room":"fah","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785156056053,"cat":"LobbyB","room":"fah","who":"","ev":"lobbyb-gate","detail":"[TC166] safety 25s → force นับ present จาก peer ที่อยู่จริง (+1)","src":"server"} +{"t":1785156064930,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"lobby-open","detail":"space=fah-1785155901092 [+1ms]","src":"client"} +{"t":1785156064973,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+25ms]","src":"client"} +{"t":1785156065007,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"preload-start","detail":" [+28ms]","src":"client"} +{"t":1785156065032,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+30ms]","src":"client"} +{"t":1785156065032,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"manifest-done","detail":" [+29ms]","src":"client"} +{"t":1785156065032,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+30ms]","src":"client"} +{"t":1785156065051,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+31ms]","src":"client"} +{"t":1785156065067,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+32ms]","src":"client"} +{"t":1785156065081,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=true [+44ms]","src":"client"} +{"t":1785156065365,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"socket-connect","detail":" [+411ms]","src":"client"} +{"t":1785156065367,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785156067016,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mn8nx46h bots=5 [+2089ms]","src":"client"} +{"t":1785156067050,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2092ms]","src":"client"} +{"t":1785156067164,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=true pickIdx=1 postCase=true clientMapId=mn8nx46h spaceId=fah-1785155901092 mapName=LobbyB [+2232ms]","src":"client"} +{"t":1785156067206,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,15.0->10.5,15.3 b1:15.0,15.0->14.9,14.4 b2:21.0,15.0->20.4,14.9 b3:23.0,12.0->23.5,11.7 b4:24.0,9.0->23.5,8.8] [+2273ms]","src":"client"} +{"t":1785156078753,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:11.6,14.9->12.1,14.7 b1:14.7,13.3->14.6,12.8 b2:19.4,14.7->18.8,14.5 b3:24.4,11.0->24.8,10.7 b4:22.4,8.5->21.9,8.3] [+13813ms]","src":"client"} +{"t":1785156107580,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:28.8,8.5->28.8,8.4 b1:13.1,5.3->13.1,5.3 b2:7.2,11.8->7.2,11.8 b3:28.7,6.2->28.7,6.2 b4:13.8,10.3->13.6,10.8] [+42636ms]","src":"client"} +{"t":1785156126364,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"socket-connect","detail":" [+61430ms]","src":"client"} +{"t":1785156126365,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785156127639,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mn8nx46h bots=5 [+62712ms]","src":"client"} +{"t":1785156127654,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=true pickIdx=1 postCase=true clientMapId=mn8nx46h spaceId=fah-1785155901092 mapName=LobbyB [+62740ms]","src":"client"} +{"t":1785156127711,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:25.8,17.0->10.7,14.8 b1:17.5,5.4->14.4,15.3 b2:28.9,6.4->21.7,14.9 b3:19.1,10.4->22.8,11.3 b4:24.6,10.1->23.3,8.8] [+62779ms]","src":"client"} +{"t":1785156135190,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:18.3,13.0->18.3,13.0 b1:28.7,12.3->28.7,12.2 b2:27.5,13.0->27.5,13.0 b3:20.3,5.1->20.3,5.1 b4:12.0,5.1->12.0,5.1] [+70255ms]","src":"client"} +{"t":1785156150139,"cat":"MG2","room":"fah","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-2 วิ่งหลบสิ่งกีดขวาง (การ์ด 2)","src":"server"} +{"t":1785156150144,"cat":"MG2","room":"fah","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785156245984,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"socket-connect","detail":" [+180906ms]","src":"client"} +{"t":1785156246149,"cat":"MG2","room":"fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785156246158,"cat":"MG2","room":"fah","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 0/2/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785156247753,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785156247770,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+182807ms]","src":"client"} +{"t":1785156251229,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+186283ms]","src":"client"} +{"t":1785156263387,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+198436ms]","src":"client"} +{"t":1785156274743,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mn8nx46h bots=5 [+209811ms]","src":"client"} +{"t":1785156274768,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=true pickIdx=1 postCase=true clientMapId=mn8nx46h spaceId=fah-1785155901092 mapName=LobbyB [+209838ms]","src":"client"} +{"t":1785156275503,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:18.3,13.0->9.4,14.5 b1:24.3,6.7->14.3,14.8 b2:27.5,13.0->20.8,14.3 b3:21.3,6.3->22.3,11.9 b4:12.0,5.1->23.3,9.3] [+210573ms]","src":"client"} +{"t":1785156280585,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:26.7,6.6->26.7,6.6 b1:26.1,5.4->26.1,5.4 b2:24.8,11.8->24.6,11.3 b3:23.4,12.4->23.7,12.8 b4:27.9,15.4->27.9,14.9] [+215655ms]","src":"client"} +{"t":1785156293771,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.7,12.6->19.7,12.6 b1:26.1,5.4->26.1,5.4 b2:17.0,8.8->17.1,8.8 b3:21.1,17.0->21.1,17.0 b4:21.6,15.9->21.6,15.9] [+228833ms]","src":"client"} +{"t":1785156297994,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:19.0,7.1->19.9,7.3 b1:8.5,7.8->8.0,7.8 b2:5.7,15.4->5.2,16.1 b3:16.1,6.2->16.1,5.4 b4:25.3,13.4->27.1,13.0] [+233025ms]","src":"client"} +{"t":1785156302001,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:22.5,16.8->22.5,16.8 b1:17.4,13.4->18.3,13.5 b2:12.2,5.6->11.9,6.2 b3:8.0,6.2->8.0,6.2 b4:27.3,8.0->27.5,7.3] [+237058ms]","src":"client"} +{"t":1785156306045,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:27.9,13.0->27.9,13.0 b1:8.1,11.5->8.1,11.5 b2:8.5,12.6->7.4,12.6 b3:7.1,13.4->7.1,13.6 b4:17.6,5.5->17.6,5.5] [+241109ms]","src":"client"} +{"t":1785156306705,"cat":"MG2","room":"fah","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-2 วิ่งหลบสิ่งกีดขวาง (การ์ด 2)","src":"server"} +{"t":1785156306717,"cat":"MG2","room":"fah","who":"ผู้เล่น","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785156306858,"cat":"MG2","room":"fah","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1785156308460,"cat":"MG2","room":"fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785156380034,"cat":"MG2","room":"fah","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 0/3/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785156380036,"cat":"MG2","room":"fah","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785156403632,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"lobby-open","detail":"space=fah-1785155901092 [+2ms]","src":"client"} +{"t":1785156403755,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"preload-start","detail":" [+54ms]","src":"client"} +{"t":1785156403755,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=false [+44ms]","src":"client"} +{"t":1785156403767,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"manifest-done","detail":" [+55ms]","src":"client"} +{"t":1785156403783,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+57ms]","src":"client"} +{"t":1785156403792,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+58ms]","src":"client"} +{"t":1785156403792,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+59ms]","src":"client"} +{"t":1785156403822,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+60ms]","src":"client"} +{"t":1785156403831,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=true sent=true [+79ms]","src":"client"} +{"t":1785156403969,"cat":"LobbyA","room":"fah-1785155901092","who":"ผู้เล่น","ev":"socket-connect","detail":" [+345ms]","src":"client"} +{"t":1785156404063,"cat":"LobbyB","room":"fah","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785156405036,"cat":"LobbyB","room":"fah","who":"","ev":"lobbyb-gate","detail":"[TC166] safety 25s → force นับ present จาก peer ที่อยู่จริง (+1)","src":"server"} +{"t":1785156406619,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mn8nx46h bots=5 [+2987ms]","src":"client"} +{"t":1785156406643,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+3010ms]","src":"client"} +{"t":1785156406781,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=true pickIdx=1 postCase=true clientMapId=mn8nx46h spaceId=fah-1785155901092 mapName=LobbyB [+3123ms]","src":"client"} +{"t":1785156410828,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:10.0,15.0->10.7,14.9 b1:15.0,15.0->14.5,14.5 b2:21.0,15.0->21.7,15.2 b3:23.0,12.0->23.2,11.3 b4:24.0,9.0->24.6,8.7] [+6724ms]","src":"client"} +{"t":1785156414599,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:25.9,6.1->25.5,6.4 b1:13.4,7.2->13.9,7.4 b2:27.5,16.4->27.5,16.4 b3:13.9,8.9->13.4,9.0 b4:19.1,10.6->19.1,10.6] [+10943ms]","src":"client"} +{"t":1785156421499,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:12.0,16.9->12.0,16.9 b1:19.5,9.7->19.5,9.6 b2:21.7,15.1->21.2,15.0 b3:7.1,11.1->7.1,11.1 b4:26.9,6.3->26.5,6.7] [+17846ms]","src":"client"} +{"t":1785156450492,"cat":"LobbyB","room":"fah-1785155901092","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=5 [b0:17.4,13.2->17.3,13.2 b1:20.0,9.0->20.5,8.9 b2:18.4,8.7->18.4,8.7 b3:5.4,16.7->5.4,16.9 b4:10.0,6.1->10.0,6.1] [+46839ms]","src":"client"} +{"t":1785440752051,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"lobby-open","detail":"space=flowtest-19-45-50-1785440749143 [+8ms]","src":"client"} +{"t":1785440752091,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"preload-start","detail":" [+16ms]","src":"client"} +{"t":1785440752094,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"manifest-done","detail":" [+17ms]","src":"client"} +{"t":1785440752094,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+17ms]","src":"client"} +{"t":1785440752115,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+17ms]","src":"client"} +{"t":1785440752134,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+18ms]","src":"client"} +{"t":1785440752141,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+18ms]","src":"client"} +{"t":1785440752195,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"socket-connect","detail":" [+187ms]","src":"client"} +{"t":1785440752218,"cat":"LobbyA","room":"flowtest-19-45-50","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 3","src":"server"} +{"t":1785440753715,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mlsbbxfe bots=3 [+1711ms]","src":"client"} +{"t":1785440753719,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+1712ms]","src":"client"} +{"t":1785440753749,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=flowtest-19-45-50-1785440749143 mapName=LobbyA [+1741ms]","src":"client"} +{"t":1785440753751,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:10.0,14.0->10.7,13.8 b1:16.0,14.0->16.7,13.8 b2:22.0,13.0->21.4,13.4] [+1744ms]","src":"client"} +{"t":1785440753919,"cat":"LobbyB","room":"flowtest-19-45-50","who":"ผู้เล่น","ev":"preplay-lock","detail":"[TC154] host ปิด wizard → ปลดล็อก set-ready (1 คนในห้อง)","src":"server"} +{"t":1785440754382,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+2372ms]","src":"client"} +{"t":1785440756248,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"suspect-gate","detail":"postCase=true howtoOk=true micOk=false sent=false [+4242ms]","src":"client"} +{"t":1785440759181,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:14.9,9.8->14.9,9.8 b1:15.2,8.1->15.2,8.1 b2:27.5,10.5->27.5,10.4] [+7177ms]","src":"client"} +{"t":1785440760404,"cat":"LobbyA","room":"PlayAll 1 1785440760975","who":"play1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785440764167,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:29.0,7.7->29.0,7.7 b1:15.5,9.0->15.5,9.0 b2:26.1,5.5->26.1,5.5] [+12163ms]","src":"client"} +{"t":1785440765474,"cat":"LobbyB","room":"flowtest-19-45-50","who":"ผู้เล่น","ev":"story-gate","detail":"[TC156] อ่าน story จบ 1/1","src":"server"} +{"t":1785440765475,"cat":"LobbyB","room":"flowtest-19-45-50","who":"","ev":"story-gate","detail":"[TC156] ทุกคนอ่าน story ครบ (1) → ปลดล็อกเลือกผู้ต้องสงสัย (pass)","src":"server"} +{"t":1785440765933,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+13926ms]","src":"client"} +{"t":1785440768231,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:21.4,10.9->22.3,10.5 b1:26.4,16.6->26.9,16.9 b2:7.3,11.3->7.2,11.4] [+16224ms]","src":"client"} +{"t":1785440769383,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"suspect-open-evt","detail":"idx=0 postCase=true [+17377ms]","src":"client"} +{"t":1785440772267,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:28.8,7.0->28.8,7.0 b1:27.4,15.2->27.4,14.2 b2:8.8,11.1->9.7,10.8] [+20261ms]","src":"client"} +{"t":1785440776302,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:8.7,8.7->8.3,8.8 b1:27.3,5.5->27.3,5.5 b2:5.4,9.4->5.5,9.4] [+24296ms]","src":"client"} +{"t":1785440780340,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:6.1,9.1->6.1,9.1 b1:8.4,8.6->8.4,8.6 b2:6.0,9.4->6.0,9.4] [+28336ms]","src":"client"} +{"t":1785440782963,"cat":"LobbyB","room":"flowtest-19-45-50","who":"","ev":"trial-vote","detail":"[TC180] ยืนยันโหวต 4/4 — ครบ → reveal (pass)","src":"server"} +{"t":1785440782964,"cat":"LobbyB","room":"flowtest-19-45-50","who":"","ev":"trial-vote","detail":"[S6] โหวตครบ 4/4 (trial-vote) → reveal ทันที ไม่ต้องรอหมดเวลา","src":"server"} +{"t":1785440783019,"cat":"LobbyA","room":"flowtest-19-45-50","who":"","ev":"post-case","detail":"[item1] reset ห้องกลับ LobbyA (postCaseDestination=lobbya) — รอ client reload","src":"server"} +{"t":1785440783067,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"trial-reveal-dur","detail":"[TC174] โชว์ผลโหวต 7000ms (admin ปรับได้) ก่อนไปหน้าผลเต็ม [+31058ms]","src":"client"} +{"t":1785440784357,"cat":"LobbyB","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:18.5,15.0->18.5,15.0 b1:25.7,9.4->26.7,8.8 b2:24.3,11.0->23.2,11.0] [+32351ms]","src":"client"} +{"t":1785440786264,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"lobby-open","detail":"space=flowtest-19-45-50-1785440749143 [+9ms]","src":"client"} +{"t":1785440786284,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"preload-start","detail":" [+17ms]","src":"client"} +{"t":1785440786304,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"manifest-done","detail":" [+18ms]","src":"client"} +{"t":1785440786306,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"colors-resolved","detail":" [+19ms]","src":"client"} +{"t":1785440786310,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"mychar-tinted","detail":"roomJoinReady=false [+19ms]","src":"client"} +{"t":1785440786313,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"preload-occupants-done","detail":" [+19ms]","src":"client"} +{"t":1785440786321,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"preload-ready(occupants)","detail":" [+20ms]","src":"client"} +{"t":1785440786391,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"socket-connect","detail":" [+137ms]","src":"client"} +{"t":1785440786394,"cat":"LobbyA","room":"flowtest-19-45-50","who":"ผู้เล่น","ev":"join","detail":"สี#1 · คน 1 + บอท 3","src":"server"} +{"t":1785440787121,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"join-ok","detail":"peers=1 host=me map=mlsbbxfe bots=3 [+865ms]","src":"client"} +{"t":1785440787129,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"loading-hide","detail":"เริ่มเล่นได้ [+865ms]","src":"client"} +{"t":1785440787144,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"suspect-join","detail":"phaseActive=false pickIdx=0 postCase=false clientMapId=mlsbbxfe spaceId=flowtest-19-45-50-1785440749143 mapName=LobbyA [+891ms]","src":"client"} +{"t":1785440787166,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:10.0,14.0->10.2,13.4 b1:16.0,14.0->15.4,14.3 b2:22.0,13.0->22.1,12.4] [+895ms]","src":"client"} +{"t":1785440791631,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:13.3,5.3->13.3,5.3 b1:14.5,16.4->15.0,16.3 b2:16.3,5.9->15.7,5.8] [+5378ms]","src":"client"} +{"t":1785440795676,"cat":"LobbyA","room":"flowtest-19-45-50-1785440749143","who":"ผู้เล่น","ev":"bot-pos","detail":"map=35x20 n=3 [b0:16.4,9.6->16.4,9.6 b1:6.5,10.5->6.5,10.5 b2:21.5,8.2->20.8,7.7] [+9423ms]","src":"client"} +{"t":1785440796848,"cat":"LobbyA","room":"PlayAll 2 1785440797599","who":"play2","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785440820565,"cat":"LobbyA","room":"PlayAll 3 1785440821269","who":"play3","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785440857562,"cat":"LobbyA","room":"PlayAll 4 1785440858317","who":"play4","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785440884728,"cat":"LobbyA","room":"PlayAll 5 1785440885486","who":"play5","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785440915496,"cat":"LobbyA","room":"PlayAll 6 1785440916219","who":"play6","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785440952194,"cat":"LobbyA","room":"PlayAll 7 1785440952565","who":"play7","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785440975417,"cat":"LobbyA","room":"PlayAll 8 1785440976167","who":"play8","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785440993686,"cat":"LobbyA","room":"PlayAll 9 1785440994458","who":"play9","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441012787,"cat":"LobbyA","room":"PlayAll 10 1785441013351","who":"play10","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441061368,"cat":"LobbyA","room":"PlayAll 1 1785441062053","who":"play1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441080106,"cat":"LobbyA","room":"PlayAll 2 1785441080886","who":"play2","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441098379,"cat":"LobbyA","room":"PlayAll 3 1785441099115","who":"play3","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441125789,"cat":"LobbyA","room":"PlayAll 4 1785441126514","who":"play4","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441153330,"cat":"LobbyA","room":"PlayAll 5 1785441154109","who":"play5","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441171603,"cat":"LobbyA","room":"PlayAll 6 1785441172371","who":"play6","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441199393,"cat":"LobbyA","room":"PlayAll 7 1785441200132","who":"play7","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441214424,"cat":"LobbyA","room":"PlayAll 8 1785441215239","who":"play8","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441241236,"cat":"LobbyA","room":"PlayAll 9 1785441241989","who":"play9","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785441268994,"cat":"LobbyA","room":"PlayAll 10 1785441269763","who":"play10","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465622377,"cat":"LobbyA","room":"multi1-1785465622121","who":"P1_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465635101,"cat":"LobbyA","room":"multi1-1785465622121","who":"OBSmjac","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465635675,"cat":"LobbyA","room":"multi2-1785465635429","who":"P2_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465648491,"cat":"LobbyA","room":"multi2-1785465635429","who":"OBSrusy","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465649059,"cat":"LobbyA","room":"multi3-1785465648826","who":"P3_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465661775,"cat":"LobbyA","room":"multi3-1785465648826","who":"OBS8i2o","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465662342,"cat":"LobbyA","room":"multi4-1785465662112","who":"P4_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465675144,"cat":"LobbyA","room":"multi4-1785465662112","who":"OBSb98f","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465675698,"cat":"LobbyA","room":"multi5-1785465675461","who":"P5_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465688402,"cat":"LobbyA","room":"multi5-1785465675461","who":"OBSba5q","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465688966,"cat":"LobbyA","room":"multi6-1785465688737","who":"P6_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465701648,"cat":"LobbyA","room":"multi6-1785465688737","who":"OBSxw3h","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465702219,"cat":"LobbyA","room":"multi7-1785465701974","who":"P7_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465714897,"cat":"LobbyA","room":"multi7-1785465701974","who":"OBShewd","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465715458,"cat":"LobbyA","room":"multi8-1785465715222","who":"P8_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465728165,"cat":"LobbyA","room":"multi8-1785465715222","who":"OBS8xoh","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465728722,"cat":"LobbyA","room":"multi9-1785465728494","who":"P9_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465741445,"cat":"LobbyA","room":"multi9-1785465728494","who":"OBScip9","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465742008,"cat":"LobbyA","room":"multi10-1785465741779","who":"P10_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465754875,"cat":"LobbyA","room":"multi10-1785465741779","who":"OBSkrsl","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465755627,"cat":"LobbyA","room":"multi1-1785465755339","who":"P1_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465755900,"cat":"LobbyA","room":"multi1-1785465755339","who":"P1_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465768352,"cat":"LobbyA","room":"multi1-1785465755339","who":"OBSgl5n","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465768776,"cat":"LobbyA","room":"multi1-1785465755339","who":"WATCH1","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465769860,"cat":"LobbyA","room":"multi2-1785465769622","who":"P2_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465770158,"cat":"LobbyA","room":"multi2-1785465769622","who":"P2_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465782866,"cat":"LobbyA","room":"multi2-1785465769622","who":"OBS4elv","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465783300,"cat":"LobbyA","room":"multi2-1785465769622","who":"WATCH2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465784658,"cat":"LobbyA","room":"multi3-1785465784430","who":"P3_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465784912,"cat":"LobbyA","room":"multi3-1785465784430","who":"P3_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465797455,"cat":"LobbyA","room":"multi3-1785465784430","who":"OBSjn1k","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465797884,"cat":"LobbyA","room":"multi3-1785465784430","who":"WATCH3","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465799254,"cat":"LobbyA","room":"multi4-1785465799019","who":"P4_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465799521,"cat":"LobbyA","room":"multi4-1785465799019","who":"P4_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465812189,"cat":"LobbyA","room":"multi4-1785465799019","who":"OBSyv8m","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465812630,"cat":"LobbyA","room":"multi4-1785465799019","who":"WATCH4","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465813935,"cat":"LobbyA","room":"multi5-1785465813716","who":"P5_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465814185,"cat":"LobbyA","room":"multi5-1785465813716","who":"P5_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465826787,"cat":"LobbyA","room":"multi5-1785465813716","who":"OBS833m","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465827232,"cat":"LobbyA","room":"multi5-1785465813716","who":"WATCH5","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465828584,"cat":"LobbyA","room":"multi6-1785465828368","who":"P6_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465828852,"cat":"LobbyA","room":"multi6-1785465828368","who":"P6_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465841362,"cat":"LobbyA","room":"multi6-1785465828368","who":"OBShs7r","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465841783,"cat":"LobbyA","room":"multi6-1785465828368","who":"WATCH6","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465843141,"cat":"LobbyA","room":"multi7-1785465842916","who":"P7_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465843389,"cat":"LobbyA","room":"multi7-1785465842916","who":"P7_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465856038,"cat":"LobbyA","room":"multi7-1785465842916","who":"OBSg0h1","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465856489,"cat":"LobbyA","room":"multi7-1785465842916","who":"WATCH7","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465857836,"cat":"LobbyA","room":"multi8-1785465857620","who":"P8_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465858086,"cat":"LobbyA","room":"multi8-1785465857620","who":"P8_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465870485,"cat":"LobbyA","room":"multi8-1785465857620","who":"OBSrqeu","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465870907,"cat":"LobbyA","room":"multi8-1785465857620","who":"WATCH8","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465872266,"cat":"LobbyA","room":"multi9-1785465872023","who":"P9_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465872539,"cat":"LobbyA","room":"multi9-1785465872023","who":"P9_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465884869,"cat":"LobbyA","room":"multi9-1785465872023","who":"OBS90uc","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465885319,"cat":"LobbyA","room":"multi9-1785465872023","who":"WATCH9","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465886093,"cat":"LobbyA","room":"multi10-1785465885843","who":"P10_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465886348,"cat":"LobbyA","room":"multi10-1785465885843","who":"P10_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465898908,"cat":"LobbyA","room":"multi10-1785465885843","who":"OBS1jb0","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465899348,"cat":"LobbyA","room":"multi10-1785465885843","who":"WATCH10","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465900900,"cat":"LobbyA","room":"multi1-1785465900625","who":"P1_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465901179,"cat":"LobbyA","room":"multi1-1785465900625","who":"P1_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465901435,"cat":"LobbyA","room":"multi1-1785465900625","who":"P1_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465913063,"cat":"LobbyA","room":"multi1-1785465900625","who":"OBShc3e","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465913490,"cat":"LobbyA","room":"multi1-1785465900625","who":"WATCH1","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465914818,"cat":"LobbyA","room":"multi2-1785465914616","who":"P2_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465915070,"cat":"LobbyA","room":"multi2-1785465914616","who":"P2_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465915322,"cat":"LobbyA","room":"multi2-1785465914616","who":"P2_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465927519,"cat":"LobbyA","room":"multi2-1785465914616","who":"OBSntia","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465927931,"cat":"LobbyA","room":"multi2-1785465914616","who":"WATCH2","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465929331,"cat":"LobbyA","room":"multi3-1785465929053","who":"P3_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465929612,"cat":"LobbyA","room":"multi3-1785465929053","who":"P3_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465929875,"cat":"LobbyA","room":"multi3-1785465929053","who":"P3_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465941218,"cat":"LobbyA","room":"multi3-1785465929053","who":"OBSs9mm","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465941648,"cat":"LobbyA","room":"multi3-1785465929053","who":"WATCH3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465943017,"cat":"LobbyA","room":"multi4-1785465942767","who":"P4_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465943274,"cat":"LobbyA","room":"multi4-1785465942767","who":"P4_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465943555,"cat":"LobbyA","room":"multi4-1785465942767","who":"P4_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465955797,"cat":"LobbyA","room":"multi4-1785465942767","who":"OBS696l","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465956246,"cat":"LobbyA","room":"multi4-1785465942767","who":"WATCH4","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465957601,"cat":"LobbyA","room":"multi5-1785465957372","who":"P5_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465957854,"cat":"LobbyA","room":"multi5-1785465957372","who":"P5_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465958124,"cat":"LobbyA","room":"multi5-1785465957372","who":"P5_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465970654,"cat":"LobbyA","room":"multi5-1785465957372","who":"OBSw0w7","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465971097,"cat":"LobbyA","room":"multi5-1785465957372","who":"WATCH5","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465972450,"cat":"LobbyA","room":"multi6-1785465972242","who":"P6_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465972707,"cat":"LobbyA","room":"multi6-1785465972242","who":"P6_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465972979,"cat":"LobbyA","room":"multi6-1785465972242","who":"P6_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465984625,"cat":"LobbyA","room":"multi6-1785465972242","who":"OBS0g2a","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465985051,"cat":"LobbyA","room":"multi6-1785465972242","who":"WATCH6","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465986391,"cat":"LobbyA","room":"multi7-1785465986175","who":"P7_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785465986649,"cat":"LobbyA","room":"multi7-1785465986175","who":"P7_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785465986926,"cat":"LobbyA","room":"multi7-1785465986175","who":"P7_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785465999340,"cat":"LobbyA","room":"multi7-1785465986175","who":"OBS6v5i","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785465999766,"cat":"LobbyA","room":"multi7-1785465986175","who":"WATCH7","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466001113,"cat":"LobbyA","room":"multi8-1785466000887","who":"P8_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466001351,"cat":"LobbyA","room":"multi8-1785466000887","who":"P8_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466001623,"cat":"LobbyA","room":"multi8-1785466000887","who":"P8_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466014256,"cat":"LobbyA","room":"multi8-1785466000887","who":"OBS25t7","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466014658,"cat":"LobbyA","room":"multi8-1785466000887","who":"WATCH8","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466016023,"cat":"LobbyA","room":"multi9-1785466015785","who":"P9_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466016272,"cat":"LobbyA","room":"multi9-1785466015785","who":"P9_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466016523,"cat":"LobbyA","room":"multi9-1785466015785","who":"P9_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466029186,"cat":"LobbyA","room":"multi9-1785466015785","who":"OBS084i","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466029600,"cat":"LobbyA","room":"multi9-1785466015785","who":"WATCH9","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466030946,"cat":"LobbyA","room":"multi10-1785466030715","who":"P10_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466031219,"cat":"LobbyA","room":"multi10-1785466030715","who":"P10_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466031492,"cat":"LobbyA","room":"multi10-1785466030715","who":"P10_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466043545,"cat":"LobbyA","room":"multi10-1785466030715","who":"OBSoshr","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466043988,"cat":"LobbyA","room":"multi10-1785466030715","who":"WATCH10","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466045495,"cat":"LobbyA","room":"multi1-1785466045253","who":"P1_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466045748,"cat":"LobbyA","room":"multi1-1785466045253","who":"P1_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466046011,"cat":"LobbyA","room":"multi1-1785466045253","who":"P1_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466046267,"cat":"LobbyA","room":"multi1-1785466045253","who":"P1_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466058416,"cat":"LobbyA","room":"multi1-1785466045253","who":"OBS88fd","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466058853,"cat":"LobbyA","room":"multi1-1785466045253","who":"WATCH1","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466060203,"cat":"LobbyA","room":"multi2-1785466059985","who":"P2_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466060445,"cat":"LobbyA","room":"multi2-1785466059985","who":"P2_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466060701,"cat":"LobbyA","room":"multi2-1785466059985","who":"P2_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466060962,"cat":"LobbyA","room":"multi2-1785466059985","who":"P2_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466071703,"cat":"LobbyA","room":"multi2-1785466059985","who":"OBSx1lu","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466072116,"cat":"LobbyA","room":"multi2-1785466059985","who":"WATCH2","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466073480,"cat":"LobbyA","room":"multi3-1785466073247","who":"P3_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466073732,"cat":"LobbyA","room":"multi3-1785466073247","who":"P3_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466073992,"cat":"LobbyA","room":"multi3-1785466073247","who":"P3_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466074256,"cat":"LobbyA","room":"multi3-1785466073247","who":"P3_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466086537,"cat":"LobbyA","room":"multi3-1785466073247","who":"OBS1yt1","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466086967,"cat":"LobbyA","room":"multi3-1785466073247","who":"WATCH3","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466088325,"cat":"LobbyA","room":"multi4-1785466088094","who":"P4_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466088599,"cat":"LobbyA","room":"multi4-1785466088094","who":"P4_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466088878,"cat":"LobbyA","room":"multi4-1785466088094","who":"P4_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466089168,"cat":"LobbyA","room":"multi4-1785466088094","who":"P4_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466100733,"cat":"LobbyA","room":"multi4-1785466088094","who":"OBSzvg8","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466101180,"cat":"LobbyA","room":"multi4-1785466088094","who":"WATCH4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466102256,"cat":"LobbyA","room":"multi5-1785466102039","who":"P5_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466102523,"cat":"LobbyA","room":"multi5-1785466102039","who":"P5_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466102780,"cat":"LobbyA","room":"multi5-1785466102039","who":"P5_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466103037,"cat":"LobbyA","room":"multi5-1785466102039","who":"P5_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466115136,"cat":"LobbyA","room":"multi5-1785466102039","who":"OBSk07s","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466115562,"cat":"LobbyA","room":"multi5-1785466102039","who":"WATCH5","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466116914,"cat":"LobbyA","room":"multi6-1785466116680","who":"P6_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466117171,"cat":"LobbyA","room":"multi6-1785466116680","who":"P6_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466117427,"cat":"LobbyA","room":"multi6-1785466116680","who":"P6_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466117678,"cat":"LobbyA","room":"multi6-1785466116680","who":"P6_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466129837,"cat":"LobbyA","room":"multi6-1785466116680","who":"OBSyu34","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466130250,"cat":"LobbyA","room":"multi6-1785466116680","who":"WATCH6","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466131593,"cat":"LobbyA","room":"multi7-1785466131365","who":"P7_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466131855,"cat":"LobbyA","room":"multi7-1785466131365","who":"P7_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466132113,"cat":"LobbyA","room":"multi7-1785466131365","who":"P7_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466132372,"cat":"LobbyA","room":"multi7-1785466131365","who":"P7_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466144869,"cat":"LobbyA","room":"multi7-1785466131365","who":"OBSbohl","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466145294,"cat":"LobbyA","room":"multi7-1785466131365","who":"WATCH7","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466146657,"cat":"LobbyA","room":"multi8-1785466146434","who":"P8_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466146912,"cat":"LobbyA","room":"multi8-1785466146434","who":"P8_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466147168,"cat":"LobbyA","room":"multi8-1785466146434","who":"P8_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466147418,"cat":"LobbyA","room":"multi8-1785466146434","who":"P8_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466159762,"cat":"LobbyA","room":"multi8-1785466146434","who":"OBS8i43","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466160206,"cat":"LobbyA","room":"multi8-1785466146434","who":"WATCH8","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466161570,"cat":"LobbyA","room":"multi9-1785466161345","who":"P9_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466161816,"cat":"LobbyA","room":"multi9-1785466161345","who":"P9_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466162065,"cat":"LobbyA","room":"multi9-1785466161345","who":"P9_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466162341,"cat":"LobbyA","room":"multi9-1785466161345","who":"P9_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466174247,"cat":"LobbyA","room":"multi9-1785466161345","who":"OBS2ku8","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466174687,"cat":"LobbyA","room":"multi9-1785466161345","who":"WATCH9","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466176024,"cat":"LobbyA","room":"multi10-1785466175808","who":"P10_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466176269,"cat":"LobbyA","room":"multi10-1785466175808","who":"P10_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466176530,"cat":"LobbyA","room":"multi10-1785466175808","who":"P10_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466176792,"cat":"LobbyA","room":"multi10-1785466175808","who":"P10_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466189517,"cat":"LobbyA","room":"multi10-1785466175808","who":"OBSte13","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466189931,"cat":"LobbyA","room":"multi10-1785466175808","who":"WATCH10","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466191442,"cat":"LobbyA","room":"multi1-1785466191182","who":"P1_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466191692,"cat":"LobbyA","room":"multi1-1785466191182","who":"P1_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466191947,"cat":"LobbyA","room":"multi1-1785466191182","who":"P1_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466192194,"cat":"LobbyA","room":"multi1-1785466191182","who":"P1_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466192434,"cat":"LobbyA","room":"multi1-1785466191182","who":"P1_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466203987,"cat":"LobbyA","room":"multi1-1785466191182","who":"OBStg5p","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466204424,"cat":"LobbyA","room":"multi1-1785466191182","who":"WATCH1","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466205782,"cat":"LobbyA","room":"multi2-1785466205551","who":"P2_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466206034,"cat":"LobbyA","room":"multi2-1785466205551","who":"P2_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466206298,"cat":"LobbyA","room":"multi2-1785466205551","who":"P2_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466206557,"cat":"LobbyA","room":"multi2-1785466205551","who":"P2_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466206816,"cat":"LobbyA","room":"multi2-1785466205551","who":"P2_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466218418,"cat":"LobbyA","room":"multi2-1785466205551","who":"OBSo4cz","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466218842,"cat":"LobbyA","room":"multi2-1785466205551","who":"WATCH2","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466220194,"cat":"LobbyA","room":"multi3-1785466219969","who":"P3_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466220444,"cat":"LobbyA","room":"multi3-1785466219969","who":"P3_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466220699,"cat":"LobbyA","room":"multi3-1785466219969","who":"P3_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466220969,"cat":"LobbyA","room":"multi3-1785466219969","who":"P3_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466221220,"cat":"LobbyA","room":"multi3-1785466219969","who":"P3_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466233539,"cat":"LobbyA","room":"multi3-1785466219969","who":"OBSnxri","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466233969,"cat":"LobbyA","room":"multi3-1785466219969","who":"WATCH3","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466235312,"cat":"LobbyA","room":"multi4-1785466235088","who":"P4_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466235561,"cat":"LobbyA","room":"multi4-1785466235088","who":"P4_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466235803,"cat":"LobbyA","room":"multi4-1785466235088","who":"P4_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466236053,"cat":"LobbyA","room":"multi4-1785466235088","who":"P4_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466236306,"cat":"LobbyA","room":"multi4-1785466235088","who":"P4_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466248395,"cat":"LobbyA","room":"multi4-1785466235088","who":"OBSf4nz","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466248802,"cat":"LobbyA","room":"multi4-1785466235088","who":"WATCH4","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466250148,"cat":"LobbyA","room":"multi5-1785466249921","who":"P5_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466250402,"cat":"LobbyA","room":"multi5-1785466249921","who":"P5_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466250666,"cat":"LobbyA","room":"multi5-1785466249921","who":"P5_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466250929,"cat":"LobbyA","room":"multi5-1785466249921","who":"P5_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466251193,"cat":"LobbyA","room":"multi5-1785466249921","who":"P5_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466262196,"cat":"LobbyA","room":"multi5-1785466249921","who":"OBSzauf","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466262607,"cat":"LobbyA","room":"multi5-1785466249921","who":"WATCH5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466263943,"cat":"LobbyA","room":"multi6-1785466263722","who":"P6_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466264198,"cat":"LobbyA","room":"multi6-1785466263722","who":"P6_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466264455,"cat":"LobbyA","room":"multi6-1785466263722","who":"P6_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466264718,"cat":"LobbyA","room":"multi6-1785466263722","who":"P6_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466264975,"cat":"LobbyA","room":"multi6-1785466263722","who":"P6_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466276312,"cat":"LobbyA","room":"multi6-1785466263722","who":"OBSwjy9","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466276733,"cat":"LobbyA","room":"multi6-1785466263722","who":"WATCH6","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466278096,"cat":"LobbyA","room":"multi7-1785466277862","who":"P7_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466278347,"cat":"LobbyA","room":"multi7-1785466277862","who":"P7_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466278590,"cat":"LobbyA","room":"multi7-1785466277862","who":"P7_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466278848,"cat":"LobbyA","room":"multi7-1785466277862","who":"P7_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466279103,"cat":"LobbyA","room":"multi7-1785466277862","who":"P7_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466290612,"cat":"LobbyA","room":"multi7-1785466277862","who":"OBSr1qh","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466291025,"cat":"LobbyA","room":"multi7-1785466277862","who":"WATCH7","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466292363,"cat":"LobbyA","room":"multi8-1785466292138","who":"P8_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466292617,"cat":"LobbyA","room":"multi8-1785466292138","who":"P8_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466292858,"cat":"LobbyA","room":"multi8-1785466292138","who":"P8_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466293116,"cat":"LobbyA","room":"multi8-1785466292138","who":"P8_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466293378,"cat":"LobbyA","room":"multi8-1785466292138","who":"P8_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466304484,"cat":"LobbyA","room":"multi8-1785466292138","who":"OBSpvei","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466304917,"cat":"LobbyA","room":"multi8-1785466292138","who":"WATCH8","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466306268,"cat":"LobbyA","room":"multi9-1785466306048","who":"P9_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466306516,"cat":"LobbyA","room":"multi9-1785466306048","who":"P9_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466306769,"cat":"LobbyA","room":"multi9-1785466306048","who":"P9_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466307019,"cat":"LobbyA","room":"multi9-1785466306048","who":"P9_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466307283,"cat":"LobbyA","room":"multi9-1785466306048","who":"P9_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466319495,"cat":"LobbyA","room":"multi9-1785466306048","who":"OBSdm0s","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466319925,"cat":"LobbyA","room":"multi9-1785466306048","who":"WATCH9","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466321281,"cat":"LobbyA","room":"multi10-1785466321053","who":"P10_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466321529,"cat":"LobbyA","room":"multi10-1785466321053","who":"P10_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466321789,"cat":"LobbyA","room":"multi10-1785466321053","who":"P10_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466322050,"cat":"LobbyA","room":"multi10-1785466321053","who":"P10_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466322313,"cat":"LobbyA","room":"multi10-1785466321053","who":"P10_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466333905,"cat":"LobbyA","room":"multi10-1785466321053","who":"OBS7sfn","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466334336,"cat":"LobbyA","room":"multi10-1785466321053","who":"WATCH10","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466335855,"cat":"LobbyA","room":"multi1-1785466335601","who":"P1_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466336104,"cat":"LobbyA","room":"multi1-1785466335601","who":"P1_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466336362,"cat":"LobbyA","room":"multi1-1785466335601","who":"P1_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466336612,"cat":"LobbyA","room":"multi1-1785466335601","who":"P1_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466336869,"cat":"LobbyA","room":"multi1-1785466335601","who":"P1_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466337127,"cat":"LobbyA","room":"multi1-1785466335601","who":"P1_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466348794,"cat":"LobbyA","room":"multi1-1785466335601","who":"OBSseya","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466349227,"cat":"LobbyA","room":"multi1-1785466335601","who":"WATCH1","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466350598,"cat":"LobbyA","room":"multi2-1785466350357","who":"P2_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466350866,"cat":"LobbyA","room":"multi2-1785466350357","who":"P2_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466351122,"cat":"LobbyA","room":"multi2-1785466350357","who":"P2_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466351393,"cat":"LobbyA","room":"multi2-1785466350357","who":"P2_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466351660,"cat":"LobbyA","room":"multi2-1785466350357","who":"P2_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466351926,"cat":"LobbyA","room":"multi2-1785466350357","who":"P2_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466363707,"cat":"LobbyA","room":"multi2-1785466350357","who":"OBSxjg3","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466364134,"cat":"LobbyA","room":"multi2-1785466350357","who":"WATCH2","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466365483,"cat":"LobbyA","room":"multi3-1785466365256","who":"P3_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466365739,"cat":"LobbyA","room":"multi3-1785466365256","who":"P3_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466366003,"cat":"LobbyA","room":"multi3-1785466365256","who":"P3_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466366261,"cat":"LobbyA","room":"multi3-1785466365256","who":"P3_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466366507,"cat":"LobbyA","room":"multi3-1785466365256","who":"P3_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466366761,"cat":"LobbyA","room":"multi3-1785466365256","who":"P3_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466378953,"cat":"LobbyA","room":"multi3-1785466365256","who":"OBShn5x","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466379371,"cat":"LobbyA","room":"multi3-1785466365256","who":"WATCH3","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466380744,"cat":"LobbyA","room":"multi4-1785466380514","who":"P4_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466381011,"cat":"LobbyA","room":"multi4-1785466380514","who":"P4_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466381261,"cat":"LobbyA","room":"multi4-1785466380514","who":"P4_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466381527,"cat":"LobbyA","room":"multi4-1785466380514","who":"P4_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466381792,"cat":"LobbyA","room":"multi4-1785466380514","who":"P4_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466382047,"cat":"LobbyA","room":"multi4-1785466380514","who":"P4_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466393701,"cat":"LobbyA","room":"multi4-1785466380514","who":"OBSlvtt","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466394136,"cat":"LobbyA","room":"multi4-1785466380514","who":"WATCH4","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466395492,"cat":"LobbyA","room":"multi5-1785466395268","who":"P5_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466395750,"cat":"LobbyA","room":"multi5-1785466395268","who":"P5_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466396008,"cat":"LobbyA","room":"multi5-1785466395268","who":"P5_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466396267,"cat":"LobbyA","room":"multi5-1785466395268","who":"P5_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466396522,"cat":"LobbyA","room":"multi5-1785466395268","who":"P5_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466396772,"cat":"LobbyA","room":"multi5-1785466395268","who":"P5_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466408766,"cat":"LobbyA","room":"multi5-1785466395268","who":"OBS2vwd","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466409206,"cat":"LobbyA","room":"multi5-1785466395268","who":"WATCH5","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466410601,"cat":"LobbyA","room":"multi6-1785466410369","who":"P6_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466410862,"cat":"LobbyA","room":"multi6-1785466410369","who":"P6_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466411113,"cat":"LobbyA","room":"multi6-1785466410369","who":"P6_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466411367,"cat":"LobbyA","room":"multi6-1785466410369","who":"P6_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466411615,"cat":"LobbyA","room":"multi6-1785466410369","who":"P6_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466411880,"cat":"LobbyA","room":"multi6-1785466410369","who":"P6_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466423610,"cat":"LobbyA","room":"multi6-1785466410369","who":"OBSwm2s","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466424031,"cat":"LobbyA","room":"multi6-1785466410369","who":"WATCH6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466425386,"cat":"LobbyA","room":"multi7-1785466425162","who":"P7_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466425634,"cat":"LobbyA","room":"multi7-1785466425162","who":"P7_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466425890,"cat":"LobbyA","room":"multi7-1785466425162","who":"P7_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466426151,"cat":"LobbyA","room":"multi7-1785466425162","who":"P7_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466426401,"cat":"LobbyA","room":"multi7-1785466425162","who":"P7_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466426651,"cat":"LobbyA","room":"multi7-1785466425162","who":"P7_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466438542,"cat":"LobbyA","room":"multi7-1785466425162","who":"OBSgo2a","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466438962,"cat":"LobbyA","room":"multi7-1785466425162","who":"WATCH7","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466440293,"cat":"LobbyA","room":"multi8-1785466440077","who":"P8_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466440550,"cat":"LobbyA","room":"multi8-1785466440077","who":"P8_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466440800,"cat":"LobbyA","room":"multi8-1785466440077","who":"P8_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466441049,"cat":"LobbyA","room":"multi8-1785466440077","who":"P8_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466441307,"cat":"LobbyA","room":"multi8-1785466440077","who":"P8_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466441563,"cat":"LobbyA","room":"multi8-1785466440077","who":"P8_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466453382,"cat":"LobbyA","room":"multi8-1785466440077","who":"OBSud3u","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466453801,"cat":"LobbyA","room":"multi8-1785466440077","who":"WATCH8","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466455153,"cat":"LobbyA","room":"multi9-1785466454925","who":"P9_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466455390,"cat":"LobbyA","room":"multi9-1785466454925","who":"P9_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466455640,"cat":"LobbyA","room":"multi9-1785466454925","who":"P9_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466455895,"cat":"LobbyA","room":"multi9-1785466454925","who":"P9_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466456153,"cat":"LobbyA","room":"multi9-1785466454925","who":"P9_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466456420,"cat":"LobbyA","room":"multi9-1785466454925","who":"P9_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466468021,"cat":"LobbyA","room":"multi9-1785466454925","who":"OBSg388","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466468444,"cat":"LobbyA","room":"multi9-1785466454925","who":"WATCH9","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466469195,"cat":"LobbyA","room":"multi10-1785466468969","who":"P10_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466469447,"cat":"LobbyA","room":"multi10-1785466468969","who":"P10_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466469706,"cat":"LobbyA","room":"multi10-1785466468969","who":"P10_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466469977,"cat":"LobbyA","room":"multi10-1785466468969","who":"P10_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466470238,"cat":"LobbyA","room":"multi10-1785466468969","who":"P10_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466470490,"cat":"LobbyA","room":"multi10-1785466468969","who":"P10_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466482256,"cat":"LobbyA","room":"multi10-1785466468969","who":"OBScnwn","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466482678,"cat":"LobbyA","room":"multi10-1785466468969","who":"WATCH10","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466484224,"cat":"LobbyA","room":"multi1-1785466483956","who":"P1_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466484478,"cat":"LobbyA","room":"multi1-1785466483956","who":"P1_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466484743,"cat":"LobbyA","room":"multi1-1785466483956","who":"P1_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466485005,"cat":"LobbyA","room":"multi1-1785466483956","who":"P1_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466485268,"cat":"LobbyA","room":"multi1-1785466483956","who":"P1_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466485510,"cat":"LobbyA","room":"multi1-1785466483956","who":"P1_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466485772,"cat":"LobbyA","room":"multi1-1785466483956","who":"P1_6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466486033,"cat":"LobbyA","room":"multi1-1785466483956","who":"P1_7","ev":"join","detail":"สี#8 · คน 8 + บอท 0","src":"server"} +{"t":1785466486284,"cat":"LobbyA","room":"multi1-1785466483956","who":"P1_8","ev":"join","detail":"สี#8 · คน 9 + บอท 0","src":"server"} +{"t":1785466486555,"cat":"LobbyA","room":"multi1-1785466483956","who":"P1_9","ev":"join","detail":"สี#1 · คน 10 + บอท 0","src":"server"} +{"t":1785466493131,"cat":"LobbyA","room":"multi1-1785466483956","who":"OBSghzq","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466493615,"cat":"LobbyA","room":"multi1-1785466483956","who":"WATCH1","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466494993,"cat":"LobbyA","room":"multi2-1785466494770","who":"P2_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466495241,"cat":"LobbyA","room":"multi2-1785466494770","who":"P2_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466495493,"cat":"LobbyA","room":"multi2-1785466494770","who":"P2_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466495745,"cat":"LobbyA","room":"multi2-1785466494770","who":"P2_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466496002,"cat":"LobbyA","room":"multi2-1785466494770","who":"P2_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466496267,"cat":"LobbyA","room":"multi2-1785466494770","who":"P2_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466496536,"cat":"LobbyA","room":"multi2-1785466494770","who":"P2_6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466496814,"cat":"LobbyA","room":"multi2-1785466494770","who":"P2_7","ev":"join","detail":"สี#8 · คน 8 + บอท 0","src":"server"} +{"t":1785466497071,"cat":"LobbyA","room":"multi2-1785466494770","who":"P2_8","ev":"join","detail":"สี#8 · คน 9 + บอท 0","src":"server"} +{"t":1785466497334,"cat":"LobbyA","room":"multi2-1785466494770","who":"P2_9","ev":"join","detail":"สี#1 · คน 10 + บอท 0","src":"server"} +{"t":1785466503755,"cat":"LobbyA","room":"multi2-1785466494770","who":"OBS2j5a","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466504229,"cat":"LobbyA","room":"multi2-1785466494770","who":"WATCH2","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466505611,"cat":"LobbyA","room":"multi3-1785466505367","who":"P3_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466505879,"cat":"LobbyA","room":"multi3-1785466505367","who":"P3_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466506136,"cat":"LobbyA","room":"multi3-1785466505367","who":"P3_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466506394,"cat":"LobbyA","room":"multi3-1785466505367","who":"P3_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466506651,"cat":"LobbyA","room":"multi3-1785466505367","who":"P3_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466506905,"cat":"LobbyA","room":"multi3-1785466505367","who":"P3_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466507163,"cat":"LobbyA","room":"multi3-1785466505367","who":"P3_6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466507422,"cat":"LobbyA","room":"multi3-1785466505367","who":"P3_7","ev":"join","detail":"สี#8 · คน 8 + บอท 0","src":"server"} +{"t":1785466507693,"cat":"LobbyA","room":"multi3-1785466505367","who":"P3_8","ev":"join","detail":"สี#8 · คน 9 + บอท 0","src":"server"} +{"t":1785466507958,"cat":"LobbyA","room":"multi3-1785466505367","who":"P3_9","ev":"join","detail":"สี#1 · คน 10 + บอท 0","src":"server"} +{"t":1785466514637,"cat":"LobbyA","room":"multi3-1785466505367","who":"OBS3gda","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466515106,"cat":"LobbyA","room":"multi3-1785466505367","who":"WATCH3","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466516470,"cat":"LobbyA","room":"multi4-1785466516247","who":"P4_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466516716,"cat":"LobbyA","room":"multi4-1785466516247","who":"P4_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466516970,"cat":"LobbyA","room":"multi4-1785466516247","who":"P4_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466517228,"cat":"LobbyA","room":"multi4-1785466516247","who":"P4_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466517485,"cat":"LobbyA","room":"multi4-1785466516247","who":"P4_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466517744,"cat":"LobbyA","room":"multi4-1785466516247","who":"P4_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466518001,"cat":"LobbyA","room":"multi4-1785466516247","who":"P4_6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466518262,"cat":"LobbyA","room":"multi4-1785466516247","who":"P4_7","ev":"join","detail":"สี#8 · คน 8 + บอท 0","src":"server"} +{"t":1785466518518,"cat":"LobbyA","room":"multi4-1785466516247","who":"P4_8","ev":"join","detail":"สี#8 · คน 9 + บอท 0","src":"server"} +{"t":1785466518783,"cat":"LobbyA","room":"multi4-1785466516247","who":"P4_9","ev":"join","detail":"สี#1 · คน 10 + บอท 0","src":"server"} +{"t":1785466525518,"cat":"LobbyA","room":"multi4-1785466516247","who":"OBSnow7","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466525979,"cat":"LobbyA","room":"multi4-1785466516247","who":"WATCH4","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466527348,"cat":"LobbyA","room":"multi5-1785466527122","who":"P5_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466527597,"cat":"LobbyA","room":"multi5-1785466527122","who":"P5_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466527839,"cat":"LobbyA","room":"multi5-1785466527122","who":"P5_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466528094,"cat":"LobbyA","room":"multi5-1785466527122","who":"P5_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466528351,"cat":"LobbyA","room":"multi5-1785466527122","who":"P5_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466528600,"cat":"LobbyA","room":"multi5-1785466527122","who":"P5_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466528857,"cat":"LobbyA","room":"multi5-1785466527122","who":"P5_6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466529124,"cat":"LobbyA","room":"multi5-1785466527122","who":"P5_7","ev":"join","detail":"สี#8 · คน 8 + บอท 0","src":"server"} +{"t":1785466529387,"cat":"LobbyA","room":"multi5-1785466527122","who":"P5_8","ev":"join","detail":"สี#8 · คน 9 + บอท 0","src":"server"} +{"t":1785466529645,"cat":"LobbyA","room":"multi5-1785466527122","who":"P5_9","ev":"join","detail":"สี#1 · คน 10 + บอท 0","src":"server"} +{"t":1785466536188,"cat":"LobbyA","room":"multi5-1785466527122","who":"OBS2nc6","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466536652,"cat":"LobbyA","room":"multi5-1785466527122","who":"WATCH5","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466538035,"cat":"LobbyA","room":"multi6-1785466537796","who":"P6_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466538310,"cat":"LobbyA","room":"multi6-1785466537796","who":"P6_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466538591,"cat":"LobbyA","room":"multi6-1785466537796","who":"P6_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466538860,"cat":"LobbyA","room":"multi6-1785466537796","who":"P6_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466539116,"cat":"LobbyA","room":"multi6-1785466537796","who":"P6_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466539368,"cat":"LobbyA","room":"multi6-1785466537796","who":"P6_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466539623,"cat":"LobbyA","room":"multi6-1785466537796","who":"P6_6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466539897,"cat":"LobbyA","room":"multi6-1785466537796","who":"P6_7","ev":"join","detail":"สี#8 · คน 8 + บอท 0","src":"server"} +{"t":1785466540150,"cat":"LobbyA","room":"multi6-1785466537796","who":"P6_8","ev":"join","detail":"สี#8 · คน 9 + บอท 0","src":"server"} +{"t":1785466540403,"cat":"LobbyA","room":"multi6-1785466537796","who":"P6_9","ev":"join","detail":"สี#1 · คน 10 + บอท 0","src":"server"} +{"t":1785466546998,"cat":"LobbyA","room":"multi6-1785466537796","who":"OBSfn4t","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466547465,"cat":"LobbyA","room":"multi6-1785466537796","who":"WATCH6","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466548825,"cat":"LobbyA","room":"multi7-1785466548605","who":"P7_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466549075,"cat":"LobbyA","room":"multi7-1785466548605","who":"P7_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466549332,"cat":"LobbyA","room":"multi7-1785466548605","who":"P7_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466549582,"cat":"LobbyA","room":"multi7-1785466548605","who":"P7_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466549841,"cat":"LobbyA","room":"multi7-1785466548605","who":"P7_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466550097,"cat":"LobbyA","room":"multi7-1785466548605","who":"P7_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466550366,"cat":"LobbyA","room":"multi7-1785466548605","who":"P7_6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466550617,"cat":"LobbyA","room":"multi7-1785466548605","who":"P7_7","ev":"join","detail":"สี#8 · คน 8 + บอท 0","src":"server"} +{"t":1785466550865,"cat":"LobbyA","room":"multi7-1785466548605","who":"P7_8","ev":"join","detail":"สี#8 · คน 9 + บอท 0","src":"server"} +{"t":1785466551141,"cat":"LobbyA","room":"multi7-1785466548605","who":"P7_9","ev":"join","detail":"สี#1 · คน 10 + บอท 0","src":"server"} +{"t":1785466557689,"cat":"LobbyA","room":"multi7-1785466548605","who":"OBSx9uk","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466558156,"cat":"LobbyA","room":"multi7-1785466548605","who":"WATCH7","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466559533,"cat":"LobbyA","room":"multi8-1785466559317","who":"P8_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466559781,"cat":"LobbyA","room":"multi8-1785466559317","who":"P8_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466560037,"cat":"LobbyA","room":"multi8-1785466559317","who":"P8_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466560294,"cat":"LobbyA","room":"multi8-1785466559317","who":"P8_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466560555,"cat":"LobbyA","room":"multi8-1785466559317","who":"P8_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466560807,"cat":"LobbyA","room":"multi8-1785466559317","who":"P8_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466561063,"cat":"LobbyA","room":"multi8-1785466559317","who":"P8_6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466561316,"cat":"LobbyA","room":"multi8-1785466559317","who":"P8_7","ev":"join","detail":"สี#8 · คน 8 + บอท 0","src":"server"} +{"t":1785466561574,"cat":"LobbyA","room":"multi8-1785466559317","who":"P8_8","ev":"join","detail":"สี#8 · คน 9 + บอท 0","src":"server"} +{"t":1785466561825,"cat":"LobbyA","room":"multi8-1785466559317","who":"P8_9","ev":"join","detail":"สี#1 · คน 10 + บอท 0","src":"server"} +{"t":1785466568547,"cat":"LobbyA","room":"multi8-1785466559317","who":"OBSuhtd","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466569017,"cat":"LobbyA","room":"multi8-1785466559317","who":"WATCH8","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466570045,"cat":"LobbyA","room":"multi9-1785466569787","who":"P9_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466570736,"cat":"LobbyA","room":"multi9-1785466569787","who":"P9_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466573660,"cat":"LobbyA","room":"multi9-1785466569787","who":"P9_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466576674,"cat":"LobbyA","room":"multi9-1785466569787","who":"P9_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466577436,"cat":"LobbyA","room":"multi9-1785466569787","who":"P9_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466577732,"cat":"LobbyA","room":"multi9-1785466569787","who":"P9_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466578002,"cat":"LobbyA","room":"multi9-1785466569787","who":"P9_6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466578292,"cat":"LobbyA","room":"multi9-1785466569787","who":"P9_7","ev":"join","detail":"สี#8 · คน 8 + บอท 0","src":"server"} +{"t":1785466578553,"cat":"LobbyA","room":"multi9-1785466569787","who":"P9_8","ev":"join","detail":"สี#8 · คน 9 + บอท 0","src":"server"} +{"t":1785466578819,"cat":"LobbyA","room":"multi9-1785466569787","who":"P9_9","ev":"join","detail":"สี#1 · คน 10 + บอท 0","src":"server"} +{"t":1785466585564,"cat":"LobbyA","room":"multi9-1785466569787","who":"OBSomml","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466586028,"cat":"LobbyA","room":"multi9-1785466569787","who":"WATCH9","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466587396,"cat":"LobbyA","room":"multi10-1785466587167","who":"P10_0","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466587657,"cat":"LobbyA","room":"multi10-1785466587167","who":"P10_1","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785466587919,"cat":"LobbyA","room":"multi10-1785466587167","who":"P10_2","ev":"join","detail":"สี#3 · คน 3 + บอท 0","src":"server"} +{"t":1785466588167,"cat":"LobbyA","room":"multi10-1785466587167","who":"P10_3","ev":"join","detail":"สี#4 · คน 4 + บอท 0","src":"server"} +{"t":1785466588425,"cat":"LobbyA","room":"multi10-1785466587167","who":"P10_4","ev":"join","detail":"สี#5 · คน 5 + บอท 0","src":"server"} +{"t":1785466588705,"cat":"LobbyA","room":"multi10-1785466587167","who":"P10_5","ev":"join","detail":"สี#6 · คน 6 + บอท 0","src":"server"} +{"t":1785466588965,"cat":"LobbyA","room":"multi10-1785466587167","who":"P10_6","ev":"join","detail":"สี#7 · คน 7 + บอท 0","src":"server"} +{"t":1785466589224,"cat":"LobbyA","room":"multi10-1785466587167","who":"P10_7","ev":"join","detail":"สี#8 · คน 8 + บอท 0","src":"server"} +{"t":1785466589479,"cat":"LobbyA","room":"multi10-1785466587167","who":"P10_8","ev":"join","detail":"สี#8 · คน 9 + บอท 0","src":"server"} +{"t":1785466589746,"cat":"LobbyA","room":"multi10-1785466587167","who":"P10_9","ev":"join","detail":"สี#1 · คน 10 + บอท 0","src":"server"} +{"t":1785466596349,"cat":"LobbyA","room":"multi10-1785466587167","who":"OBSnrum","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466596814,"cat":"LobbyA","room":"multi10-1785466587167","who":"WATCH10","ev":"join","detail":"สี#2 · คน 11 + บอท 0","src":"server"} +{"t":1785466624952,"cat":"LobbyA","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466628247,"cat":"LobbyB","room":"soak1-24727","who":"s1","ev":"story-gate","detail":"[TC156] อ่าน story จบ 1/1","src":"server"} +{"t":1785466628247,"cat":"LobbyB","room":"soak1-24727","who":"","ev":"story-gate","detail":"[TC156] ทุกคนอ่าน story ครบ (1) → ปลดล็อกเลือกผู้ต้องสงสัย (pass)","src":"server"} +{"t":1785466630588,"cat":"MG7","room":"soak1-24727","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-7 ยิง Mega Virus (การ์ด 1)","src":"server"} +{"t":1785466630594,"cat":"MG7","room":"soak1-24727","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785466632027,"cat":"MG7","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466700868,"cat":"MG7","room":"soak1-24727","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 1/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785466700870,"cat":"MG7","room":"soak1-24727","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785466702282,"cat":"LobbyB","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466702972,"cat":"LobbyB","room":"soak1-24727","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785466705300,"cat":"MG7","room":"soak1-24727","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-7 ยิง Mega Virus (การ์ด 1)","src":"server"} +{"t":1785466705305,"cat":"MG7","room":"soak1-24727","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785466706720,"cat":"MG7","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466775425,"cat":"MG7","room":"soak1-24727","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 2/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785466775429,"cat":"MG7","room":"soak1-24727","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785466776851,"cat":"LobbyB","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466777632,"cat":"LobbyB","room":"soak1-24727","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785466779951,"cat":"MG7","room":"soak1-24727","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-7 ยิง Mega Virus (การ์ด 1)","src":"server"} +{"t":1785466779954,"cat":"MG7","room":"soak1-24727","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785466781398,"cat":"MG7","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466850128,"cat":"MG7","room":"soak1-24727","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 3/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785466850130,"cat":"MG7","room":"soak1-24727","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785466851554,"cat":"LobbyB","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466852318,"cat":"LobbyB","room":"soak1-24727","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785466854645,"cat":"MG1","room":"soak1-24727","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-1 จริงหรือไม่ (การ์ด 2)","src":"server"} +{"t":1785466854648,"cat":"MG1","room":"soak1-24727","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785466856061,"cat":"MG1","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466888860,"cat":"MG1","room":"soak1-24727","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/1/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785466888862,"cat":"MG1","room":"soak1-24727","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785466890267,"cat":"LobbyB","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466891019,"cat":"LobbyB","room":"soak1-24727","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785466893339,"cat":"MG1","room":"soak1-24727","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-1 จริงหรือไม่ (การ์ด 2)","src":"server"} +{"t":1785466893344,"cat":"MG1","room":"soak1-24727","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785466894766,"cat":"MG1","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466927566,"cat":"MG1","room":"soak1-24727","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/2/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785466927568,"cat":"MG1","room":"soak1-24727","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785466929002,"cat":"LobbyB","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466929748,"cat":"LobbyB","room":"soak1-24727","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785466932064,"cat":"MG1","room":"soak1-24727","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-1 จริงหรือไม่ (การ์ด 2)","src":"server"} +{"t":1785466932069,"cat":"MG1","room":"soak1-24727","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785466933471,"cat":"MG1","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466966237,"cat":"MG1","room":"soak1-24727","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/3/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785466966238,"cat":"MG1","room":"soak1-24727","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785466967648,"cat":"LobbyB","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785466968295,"cat":"LobbyB","room":"soak1-24727","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785466970620,"cat":"MG5","room":"soak1-24727","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-5 กระโดดขึ้นแท่น (การ์ด 3)","src":"server"} +{"t":1785466970622,"cat":"MG5","room":"soak1-24727","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #3","src":"server"} +{"t":1785466972045,"cat":"MG5","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785467040490,"cat":"MG5","room":"soak1-24727","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #3 → team progress 3/3/1 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467040492,"cat":"MG5","room":"soak1-24727","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#3 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467041924,"cat":"LobbyB","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785467042671,"cat":"LobbyB","room":"soak1-24727","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467044991,"cat":"MG5","room":"soak1-24727","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-5 กระโดดขึ้นแท่น (การ์ด 3)","src":"server"} +{"t":1785467044995,"cat":"MG5","room":"soak1-24727","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #3","src":"server"} +{"t":1785467046437,"cat":"MG5","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785467114840,"cat":"MG5","room":"soak1-24727","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #3 → team progress 3/3/2 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467114843,"cat":"MG5","room":"soak1-24727","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#3 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467116279,"cat":"LobbyB","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785467116912,"cat":"LobbyB","room":"soak1-24727","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467119231,"cat":"MG5","room":"soak1-24727","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-5 กระโดดขึ้นแท่น (การ์ด 3)","src":"server"} +{"t":1785467119234,"cat":"MG5","room":"soak1-24727","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #3","src":"server"} +{"t":1785467120657,"cat":"MG5","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785467189031,"cat":"MG5","room":"soak1-24727","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #3 → team progress 3/3/3 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467189033,"cat":"MG5","room":"soak1-24727","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#3 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467190467,"cat":"LobbyB","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785467191162,"cat":"LobbyB","room":"soak1-24727","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467193139,"cat":"LobbyB","room":"soak1-24727","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785467202114,"cat":"LobbyB","room":"soak1-24727","who":"","ev":"trial-vote","detail":"[TC180] ยืนยันโหวต 1/1 — ครบ → reveal (pass)","src":"server"} +{"t":1785467202114,"cat":"LobbyB","room":"soak1-24727","who":"","ev":"trial-vote","detail":"[S6] โหวตครบ 1/1 (trial-vote) → reveal ทันที ไม่ต้องรอหมดเวลา","src":"server"} +{"t":1785467202119,"cat":"LobbyA","room":"soak1-24727","who":"","ev":"post-case","detail":"[item1] reset ห้องกลับ LobbyA (postCaseDestination=lobbya) — รอ client reload","src":"server"} +{"t":1785467205769,"cat":"LobbyA","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467208847,"cat":"LobbyB","room":"soak1b-05550","who":"s1","ev":"story-gate","detail":"[TC156] อ่าน story จบ 1/1","src":"server"} +{"t":1785467208847,"cat":"LobbyB","room":"soak1b-05550","who":"","ev":"story-gate","detail":"[TC156] ทุกคนอ่าน story ครบ (1) → ปลดล็อกเลือกผู้ต้องสงสัย (pass)","src":"server"} +{"t":1785467211167,"cat":"MG5","room":"soak1b-05550","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-5 กระโดดขึ้นแท่น (การ์ด 1)","src":"server"} +{"t":1785467211169,"cat":"MG5","room":"soak1b-05550","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785467212577,"cat":"MG5","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467280757,"cat":"MG5","room":"soak1b-05550","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 1/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467280759,"cat":"MG5","room":"soak1b-05550","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467282187,"cat":"LobbyB","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467282916,"cat":"LobbyB","room":"soak1b-05550","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467285238,"cat":"MG5","room":"soak1b-05550","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-5 กระโดดขึ้นแท่น (การ์ด 1)","src":"server"} +{"t":1785467285241,"cat":"MG5","room":"soak1b-05550","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785467286681,"cat":"MG5","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467355041,"cat":"MG5","room":"soak1b-05550","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 2/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467355044,"cat":"MG5","room":"soak1b-05550","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467356458,"cat":"LobbyB","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467357090,"cat":"LobbyB","room":"soak1b-05550","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467359402,"cat":"MG5","room":"soak1b-05550","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-5 กระโดดขึ้นแท่น (การ์ด 1)","src":"server"} +{"t":1785467359406,"cat":"MG5","room":"soak1b-05550","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785467360820,"cat":"MG5","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467429105,"cat":"MG5","room":"soak1b-05550","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 3/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467429107,"cat":"MG5","room":"soak1b-05550","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467430512,"cat":"LobbyB","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467431159,"cat":"LobbyB","room":"soak1b-05550","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467433478,"cat":"MG3","room":"soak1b-05550","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-3 Tower block (การ์ด 2)","src":"server"} +{"t":1785467433481,"cat":"MG3","room":"soak1b-05550","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785467434923,"cat":"MG3","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467531528,"cat":"MG3","room":"soak1b-05550","who":"","ev":"stack-score","detail":"[TC183] จบ MG3 outcome=time_up team=0 layers=0 progress=0% · ต่อคน: S1=0 bot=0 bot=0 bot=0 bot=0 bot=0","src":"server"} +{"t":1785467532391,"cat":"MG3","room":"soak1b-05550","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/1/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467532393,"cat":"MG3","room":"soak1b-05550","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467533822,"cat":"LobbyB","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467534515,"cat":"LobbyB","room":"soak1b-05550","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467536833,"cat":"MG3","room":"soak1b-05550","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-3 Tower block (การ์ด 2)","src":"server"} +{"t":1785467536836,"cat":"MG3","room":"soak1b-05550","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785467538239,"cat":"MG3","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467634859,"cat":"MG3","room":"soak1b-05550","who":"","ev":"stack-score","detail":"[TC183] จบ MG3 outcome=time_up team=0 layers=0 progress=0% · ต่อคน: S1=0 bot=0 bot=0 bot=0 bot=0 bot=0","src":"server"} +{"t":1785467635648,"cat":"MG3","room":"soak1b-05550","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/2/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467635650,"cat":"MG3","room":"soak1b-05550","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467637068,"cat":"LobbyB","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467637738,"cat":"LobbyB","room":"soak1b-05550","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467640052,"cat":"MG3","room":"soak1b-05550","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-3 Tower block (การ์ด 2)","src":"server"} +{"t":1785467640057,"cat":"MG3","room":"soak1b-05550","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785467641473,"cat":"MG3","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467738117,"cat":"MG3","room":"soak1b-05550","who":"","ev":"stack-score","detail":"[TC183] จบ MG3 outcome=time_up team=0 layers=0 progress=0% · ต่อคน: S1=0 bot=0 bot=0 bot=0 bot=0 bot=0","src":"server"} +{"t":1785467738965,"cat":"MG3","room":"soak1b-05550","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/3/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467738967,"cat":"MG3","room":"soak1b-05550","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467740383,"cat":"LobbyB","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467741061,"cat":"LobbyB","room":"soak1b-05550","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467743383,"cat":"MG6","room":"soak1b-05550","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-6 ยิงอุกาบาต Violent Crime (การ์ด 3)","src":"server"} +{"t":1785467743387,"cat":"MG6","room":"soak1b-05550","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #3","src":"server"} +{"t":1785467744799,"cat":"MG6","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467813805,"cat":"MG6","room":"soak1b-05550","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #3 → team progress 3/3/1 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467813806,"cat":"MG6","room":"soak1b-05550","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#3 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467815231,"cat":"LobbyB","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467815910,"cat":"LobbyB","room":"soak1b-05550","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467818221,"cat":"MG6","room":"soak1b-05550","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-6 ยิงอุกาบาต Violent Crime (การ์ด 3)","src":"server"} +{"t":1785467818224,"cat":"MG6","room":"soak1b-05550","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #3","src":"server"} +{"t":1785467819655,"cat":"MG6","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467888765,"cat":"MG6","room":"soak1b-05550","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #3 → team progress 3/3/2 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467888766,"cat":"MG6","room":"soak1b-05550","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#3 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467890179,"cat":"LobbyB","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467890886,"cat":"LobbyB","room":"soak1b-05550","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467893208,"cat":"MG6","room":"soak1b-05550","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-6 ยิงอุกาบาต Violent Crime (การ์ด 3)","src":"server"} +{"t":1785467893210,"cat":"MG6","room":"soak1b-05550","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 1 คน · ผู้ต้องสงสัย #3","src":"server"} +{"t":1785467894612,"cat":"MG6","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467963636,"cat":"MG6","room":"soak1b-05550","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #3 → team progress 3/3/3 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785467963637,"cat":"MG6","room":"soak1b-05550","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#3 → 1 key · ใบ/key=[1] (1/key · pass)","src":"server"} +{"t":1785467966087,"cat":"LobbyB","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467972830,"cat":"LobbyB","room":"soak1b-05550","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/1","src":"server"} +{"t":1785467974798,"cat":"LobbyB","room":"soak1b-05550","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 5","src":"server"} +{"t":1785467983800,"cat":"LobbyB","room":"soak1b-05550","who":"","ev":"trial-vote","detail":"[TC180] ยืนยันโหวต 6/6 — ครบ → reveal (pass)","src":"server"} +{"t":1785467983800,"cat":"LobbyB","room":"soak1b-05550","who":"","ev":"trial-vote","detail":"[S6] โหวตครบ 6/6 (trial-vote) → reveal ทันที ไม่ต้องรอหมดเวลา","src":"server"} +{"t":1785467983803,"cat":"LobbyA","room":"soak1b-05550","who":"","ev":"post-case","detail":"[item1] reset ห้องกลับ LobbyA (postCaseDestination=lobbya) — รอ client reload","src":"server"} +{"t":1785467987459,"cat":"LobbyA","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 0","src":"server"} +{"t":1785467988191,"cat":"LobbyA","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785467991520,"cat":"LobbyB","room":"soak2-87229","who":"s2","ev":"story-gate","detail":"[TC156] อ่าน story จบ 1/2","src":"server"} +{"t":1785467991520,"cat":"LobbyB","room":"soak2-87229","who":"s1","ev":"story-gate","detail":"[TC156] อ่าน story จบ 2/2","src":"server"} +{"t":1785467991520,"cat":"LobbyB","room":"soak2-87229","who":"","ev":"story-gate","detail":"[TC156] ทุกคนอ่าน story ครบ (2) → ปลดล็อกเลือกผู้ต้องสงสัย (pass)","src":"server"} +{"t":1785467993848,"cat":"MG5","room":"soak2-87229","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-5 กระโดดขึ้นแท่น (การ์ด 1)","src":"server"} +{"t":1785467993850,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785467995092,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785467995093,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785467995271,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785467995271,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785467996025,"cat":"MG5","room":"soak2-87229","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785467996201,"cat":"MG5","room":"soak2-87229","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785467996201,"cat":"MG5","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468064559,"cat":"MG5","room":"soak2-87229","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 1/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468064561,"cat":"MG5","room":"soak2-87229","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468065850,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468065850,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468066019,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468066700,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468066872,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468067591,"cat":"LobbyB","room":"soak2-87229","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468067593,"cat":"LobbyB","room":"soak2-87229","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468069902,"cat":"MG5","room":"soak2-87229","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-5 กระโดดขึ้นแท่น (การ์ด 1)","src":"server"} +{"t":1785468069905,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785468071159,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468071159,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468071332,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468071332,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468072055,"cat":"MG5","room":"soak2-87229","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468072217,"cat":"MG5","room":"soak2-87229","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468072217,"cat":"MG5","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468140533,"cat":"MG5","room":"soak2-87229","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 2/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468140536,"cat":"MG5","room":"soak2-87229","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468141835,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468141835,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468142013,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468142678,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468142852,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468143494,"cat":"LobbyB","room":"soak2-87229","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468143494,"cat":"LobbyB","room":"soak2-87229","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468145817,"cat":"MG5","room":"soak2-87229","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-5 กระโดดขึ้นแท่น (การ์ด 1)","src":"server"} +{"t":1785468145819,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785468147064,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468147064,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468147249,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468147249,"cat":"MG5","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468147977,"cat":"MG5","room":"soak2-87229","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468148158,"cat":"MG5","room":"soak2-87229","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468148158,"cat":"MG5","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468216529,"cat":"MG5","room":"soak2-87229","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 3/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468216532,"cat":"MG5","room":"soak2-87229","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468217824,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468217824,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468218008,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468218779,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468218936,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468219546,"cat":"LobbyB","room":"soak2-87229","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468219547,"cat":"LobbyB","room":"soak2-87229","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468221861,"cat":"MG4","room":"soak2-87229","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-4 คำศัพท์ในกระบวนการยุติธรรม (การ์ด 2)","src":"server"} +{"t":1785468221864,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785468223119,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468223119,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468223321,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468223322,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468224089,"cat":"MG4","room":"soak2-87229","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468224312,"cat":"MG4","room":"soak2-87229","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468224312,"cat":"MG4","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468230422,"cat":"MG4","room":"soak2-87229","who":"","ev":"mg4-read","detail":"q=1 deck=29 rounds=0/10","src":"server"} +{"t":1785468248427,"cat":"MG4","room":"soak2-87229","who":"","ev":"mg4-round","detail":"resolve 1/10 q=1 winner=-(หมดเวลา ไม่มีคนถูก) correctIdx=10 willEnd=false","src":"server"} +{"t":1785468250927,"cat":"MG4","room":"soak2-87229","who":"","ev":"mg4-read","detail":"q=2 deck=28 rounds=1/10","src":"server"} +{"t":1785468257247,"cat":"MG4","room":"soak2-87229","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/1/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468257249,"cat":"MG4","room":"soak2-87229","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468258549,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468258549,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468258704,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468259353,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468259516,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468260260,"cat":"LobbyB","room":"soak2-87229","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468260263,"cat":"LobbyB","room":"soak2-87229","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468262581,"cat":"MG4","room":"soak2-87229","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-4 คำศัพท์ในกระบวนการยุติธรรม (การ์ด 2)","src":"server"} +{"t":1785468262585,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785468263839,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468263839,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468264030,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468264030,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468264581,"cat":"MG4","room":"soak2-87229","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468264738,"cat":"MG4","room":"soak2-87229","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468264739,"cat":"MG4","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468270737,"cat":"MG4","room":"soak2-87229","who":"","ev":"mg4-read","detail":"q=1 deck=29 rounds=0/10","src":"server"} +{"t":1785468288738,"cat":"MG4","room":"soak2-87229","who":"","ev":"mg4-round","detail":"resolve 1/10 q=1 winner=-(หมดเวลา ไม่มีคนถูก) correctIdx=10 willEnd=false","src":"server"} +{"t":1785468291239,"cat":"MG4","room":"soak2-87229","who":"","ev":"mg4-read","detail":"q=2 deck=28 rounds=1/10","src":"server"} +{"t":1785468297592,"cat":"MG4","room":"soak2-87229","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/2/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468297594,"cat":"MG4","room":"soak2-87229","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468298888,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468298888,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468299045,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468299675,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468299842,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468300517,"cat":"LobbyB","room":"soak2-87229","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468300519,"cat":"LobbyB","room":"soak2-87229","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468302832,"cat":"MG4","room":"soak2-87229","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-4 คำศัพท์ในกระบวนการยุติธรรม (การ์ด 2)","src":"server"} +{"t":1785468302835,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785468304083,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468304083,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468304256,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468304256,"cat":"MG4","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468304812,"cat":"MG4","room":"soak2-87229","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468304975,"cat":"MG4","room":"soak2-87229","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468304975,"cat":"MG4","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468310977,"cat":"MG4","room":"soak2-87229","who":"","ev":"mg4-read","detail":"q=1 deck=29 rounds=0/10","src":"server"} +{"t":1785468329029,"cat":"MG4","room":"soak2-87229","who":"","ev":"mg4-round","detail":"resolve 1/10 q=1 winner=-(หมดเวลา ไม่มีคนถูก) correctIdx=0 willEnd=false","src":"server"} +{"t":1785468331528,"cat":"MG4","room":"soak2-87229","who":"","ev":"mg4-read","detail":"q=2 deck=28 rounds=1/10","src":"server"} +{"t":1785468337847,"cat":"MG4","room":"soak2-87229","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/3/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468337851,"cat":"MG4","room":"soak2-87229","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468339152,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468339152,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468339310,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468339970,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468340135,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468340733,"cat":"LobbyB","room":"soak2-87229","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468340734,"cat":"LobbyB","room":"soak2-87229","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468343054,"cat":"MG3","room":"soak2-87229","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-3 Tower block (การ์ด 3)","src":"server"} +{"t":1785468343057,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #3","src":"server"} +{"t":1785468344298,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468344298,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468344489,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468344489,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468345650,"cat":"MG3","room":"soak2-87229","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468345826,"cat":"MG3","room":"soak2-87229","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468345827,"cat":"MG3","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468442539,"cat":"MG3","room":"soak2-87229","who":"","ev":"stack-score","detail":"[TC183] จบ MG3 outcome=time_up team=0 layers=0 progress=0% · ต่อคน: S1=0 S2=0","src":"server"} +{"t":1785468442837,"cat":"MG3","room":"soak2-87229","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #3 → team progress 3/3/1 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468442840,"cat":"MG3","room":"soak2-87229","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#3 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468444127,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468444127,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468444288,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468444879,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468445041,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468445665,"cat":"LobbyB","room":"soak2-87229","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468445667,"cat":"LobbyB","room":"soak2-87229","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468447976,"cat":"MG3","room":"soak2-87229","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-3 Tower block (การ์ด 3)","src":"server"} +{"t":1785468447979,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #3","src":"server"} +{"t":1785468449217,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468449217,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468449407,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468449407,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468450544,"cat":"MG3","room":"soak2-87229","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468450742,"cat":"MG3","room":"soak2-87229","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468450742,"cat":"MG3","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468547395,"cat":"MG3","room":"soak2-87229","who":"","ev":"stack-score","detail":"[TC183] จบ MG3 outcome=time_up team=0 layers=0 progress=0% · ต่อคน: S1=0 S2=0","src":"server"} +{"t":1785468547753,"cat":"MG3","room":"soak2-87229","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #3 → team progress 3/3/2 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468547755,"cat":"MG3","room":"soak2-87229","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#3 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468549045,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468549045,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468549214,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468549889,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468550062,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468550666,"cat":"LobbyB","room":"soak2-87229","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468550668,"cat":"LobbyB","room":"soak2-87229","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468552990,"cat":"MG3","room":"soak2-87229","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-3 Tower block (การ์ด 3)","src":"server"} +{"t":1785468552993,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #3","src":"server"} +{"t":1785468554238,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468554238,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468554443,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468554444,"cat":"MG3","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468555613,"cat":"MG3","room":"soak2-87229","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468555793,"cat":"MG3","room":"soak2-87229","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468555793,"cat":"MG3","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468652505,"cat":"MG3","room":"soak2-87229","who":"","ev":"stack-score","detail":"[TC183] จบ MG3 outcome=time_up team=0 layers=0 progress=0% · ต่อคน: S1=0 S2=0","src":"server"} +{"t":1785468652811,"cat":"MG3","room":"soak2-87229","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #3 → team progress 3/3/3 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468652813,"cat":"MG3","room":"soak2-87229","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#3 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468654110,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468654111,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468654270,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468654928,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468655087,"cat":"LobbyB","room":"soak2-87229","who":"S2","ev":"join","detail":"สี#2 · คน 2 + บอท 0","src":"server"} +{"t":1785468655739,"cat":"LobbyB","room":"soak2-87229","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468655740,"cat":"LobbyB","room":"soak2-87229","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468657544,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468657544,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468657724,"cat":"LobbyB","room":"soak2-87229","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 0","src":"server"} +{"t":1785468666792,"cat":"LobbyB","room":"soak2-87229","who":"","ev":"trial-vote","detail":"[TC180] ยืนยันโหวต 1/2 — รออีก","src":"server"} +{"t":1785468666830,"cat":"LobbyB","room":"soak2-87229","who":"","ev":"trial-vote","detail":"[TC180] ยืนยันโหวต 2/2 — ครบ → reveal (pass)","src":"server"} +{"t":1785468666830,"cat":"LobbyB","room":"soak2-87229","who":"","ev":"trial-vote","detail":"[S6] โหวตครบ 2/2 (trial-vote) → reveal ทันที ไม่ต้องรอหมดเวลา","src":"server"} +{"t":1785468666835,"cat":"LobbyA","room":"soak2-87229","who":"","ev":"post-case","detail":"[item1] reset ห้องกลับ LobbyA (postCaseDestination=lobbya) — รอ client reload","src":"server"} +{"t":1785468667888,"cat":"LobbyA","room":"soak2-87229","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468670508,"cat":"LobbyA","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 1 + บอท 4","src":"server"} +{"t":1785468671296,"cat":"LobbyA","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785468674356,"cat":"LobbyB","room":"soak2b-70282","who":"s1","ev":"story-gate","detail":"[TC156] อ่าน story จบ 1/2","src":"server"} +{"t":1785468674360,"cat":"LobbyB","room":"soak2b-70282","who":"s2","ev":"story-gate","detail":"[TC156] อ่าน story จบ 2/2","src":"server"} +{"t":1785468674360,"cat":"LobbyB","room":"soak2b-70282","who":"","ev":"story-gate","detail":"[TC156] ทุกคนอ่าน story ครบ (2) → ปลดล็อกเลือกผู้ต้องสงสัย (pass)","src":"server"} +{"t":1785468676676,"cat":"MG4","room":"soak2b-70282","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-4 คำศัพท์ในกระบวนการยุติธรรม (การ์ด 1)","src":"server"} +{"t":1785468676683,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785468677929,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468677929,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468678097,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468678097,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785468678674,"cat":"MG4","room":"soak2b-70282","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468678837,"cat":"MG4","room":"soak2b-70282","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468678837,"cat":"MG4","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785468684873,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-read","detail":"q=1 deck=29 rounds=0/10","src":"server"} +{"t":1785468693230,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-round","detail":"resolve 1/10 q=1 winner=[TC169] BOT __pv_bot_0 correctIdx=2 willEnd=false","src":"server"} +{"t":1785468695729,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-read","detail":"q=2 deck=28 rounds=1/10","src":"server"} +{"t":1785468708729,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-round","detail":"resolve 2/10 q=2 winner=[TC169] BOT __pv_bot_0 correctIdx=2 willEnd=false","src":"server"} +{"t":1785468711229,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-read","detail":"q=3 deck=27 rounds=2/10","src":"server"} +{"t":1785468711622,"cat":"MG4","room":"soak2b-70282","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 1/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468711623,"cat":"MG4","room":"soak2b-70282","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468712906,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468712906,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468713081,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785468713714,"cat":"LobbyB","room":"soak2b-70282","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468713882,"cat":"LobbyB","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785468714513,"cat":"LobbyB","room":"soak2b-70282","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468714513,"cat":"LobbyB","room":"soak2b-70282","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468716835,"cat":"MG4","room":"soak2b-70282","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-4 คำศัพท์ในกระบวนการยุติธรรม (การ์ด 1)","src":"server"} +{"t":1785468716838,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785468718084,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468718084,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468718312,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468718313,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785468718950,"cat":"MG4","room":"soak2b-70282","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468719139,"cat":"MG4","room":"soak2b-70282","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468719140,"cat":"MG4","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785468725239,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-read","detail":"q=1 deck=29 rounds=0/10","src":"server"} +{"t":1785468734071,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-round","detail":"resolve 1/10 q=1 winner=[TC169] BOT __pv_bot_3 correctIdx=5 willEnd=false","src":"server"} +{"t":1785468736572,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-read","detail":"q=2 deck=28 rounds=1/10","src":"server"} +{"t":1785468747126,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-round","detail":"resolve 2/10 q=2 winner=[TC169] BOT __pv_bot_0 correctIdx=2 willEnd=false","src":"server"} +{"t":1785468749626,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-read","detail":"q=3 deck=27 rounds=2/10","src":"server"} +{"t":1785468752008,"cat":"MG4","room":"soak2b-70282","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 2/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468752010,"cat":"MG4","room":"soak2b-70282","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468753295,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468753295,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468753464,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785468754146,"cat":"LobbyB","room":"soak2b-70282","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468754309,"cat":"LobbyB","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785468754916,"cat":"LobbyB","room":"soak2b-70282","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468754916,"cat":"LobbyB","room":"soak2b-70282","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468757229,"cat":"MG4","room":"soak2b-70282","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-4 คำศัพท์ในกระบวนการยุติธรรม (การ์ด 1)","src":"server"} +{"t":1785468757231,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #1","src":"server"} +{"t":1785468758485,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468758485,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468758667,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468758668,"cat":"MG4","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785468759267,"cat":"MG4","room":"soak2b-70282","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468759461,"cat":"MG4","room":"soak2b-70282","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468759461,"cat":"MG4","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785468765455,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-read","detail":"q=1 deck=29 rounds=0/10","src":"server"} +{"t":1785468783433,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-round","detail":"resolve 1/10 q=1 winner=[TC169] BOT __pv_bot_3 correctIdx=11 willEnd=false","src":"server"} +{"t":1785468785933,"cat":"MG4","room":"soak2b-70282","who":"","ev":"mg4-read","detail":"q=2 deck=28 rounds=1/10","src":"server"} +{"t":1785468792206,"cat":"MG4","room":"soak2b-70282","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #1 → team progress 3/0/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468792208,"cat":"MG4","room":"soak2b-70282","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#1 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468793498,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468793498,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468793657,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785468794362,"cat":"LobbyB","room":"soak2b-70282","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468794526,"cat":"LobbyB","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785468795162,"cat":"LobbyB","room":"soak2b-70282","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468795162,"cat":"LobbyB","room":"soak2b-70282","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468797490,"cat":"MG3","room":"soak2b-70282","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-3 Tower block (การ์ด 2)","src":"server"} +{"t":1785468797492,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785468798732,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468798732,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468798920,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468798920,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785468800305,"cat":"MG3","room":"soak2b-70282","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468800480,"cat":"MG3","room":"soak2b-70282","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468800480,"cat":"MG3","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785468897126,"cat":"MG3","room":"soak2b-70282","who":"","ev":"stack-score","detail":"[TC183] จบ MG3 outcome=time_up team=0 layers=0 progress=0% · ต่อคน: S1=0 S2=0 bot=0 bot=0 bot=0 bot=0","src":"server"} +{"t":1785468897897,"cat":"MG3","room":"soak2b-70282","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/1/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785468897900,"cat":"MG3","room":"soak2b-70282","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785468899193,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468899193,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468899355,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785468900109,"cat":"LobbyB","room":"soak2b-70282","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785468900277,"cat":"LobbyB","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785468900957,"cat":"LobbyB","room":"soak2b-70282","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785468900958,"cat":"LobbyB","room":"soak2b-70282","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785468903263,"cat":"MG3","room":"soak2b-70282","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-3 Tower block (การ์ด 2)","src":"server"} +{"t":1785468903268,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785468904517,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785468904517,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468904689,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468904689,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785468905985,"cat":"MG3","room":"soak2b-70282","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785468906140,"cat":"MG3","room":"soak2b-70282","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785468906140,"cat":"MG3","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785469002768,"cat":"MG3","room":"soak2b-70282","who":"","ev":"stack-score","detail":"[TC183] จบ MG3 outcome=time_up team=0 layers=0 progress=0% · ต่อคน: S1=0 S2=0 bot=0 bot=0 bot=0 bot=0","src":"server"} +{"t":1785469003583,"cat":"MG3","room":"soak2b-70282","who":"","ev":"suspect-progress","detail":"[TC179] สืบผู้ต้องสงสัย #2 → team progress 3/2/0 (shared เท่ากันทุกจอ · pass)","src":"server"} +{"t":1785469003587,"cat":"MG3","room":"soak2b-70282","who":"","ev":"evidence-grant","detail":"[TC182] แจกหลักฐาน suspect#2 → 2 key · ใบ/key=[1,1] (1/key · pass)","src":"server"} +{"t":1785469004887,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785469004887,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785469005058,"cat":"LobbyB","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785469005761,"cat":"LobbyB","room":"soak2b-70282","who":"S2","ev":"leave","detail":"ออกจากห้อง · เหลือคน 1","src":"server"} +{"t":1785469005917,"cat":"LobbyB","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"} +{"t":1785469006541,"cat":"LobbyB","room":"soak2b-70282","who":"s2","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 1/2","src":"server"} +{"t":1785469006543,"cat":"LobbyB","room":"soak2b-70282","who":"s1","ev":"lobbyb-gate","detail":"[TC144] โหลด LobbyB เสร็จ 2/2","src":"server"} +{"t":1785469008868,"cat":"MG3","room":"soak2b-70282","who":"","ev":"game-start","detail":"เริ่มมินิเกม: Minigame-3 Tower block (การ์ด 2)","src":"server"} +{"t":1785469008871,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"lobbyb-gate","detail":"[TC144] เริ่มสืบสวน OK — ผู้เล่นครบ 2 คน · ผู้ต้องสงสัย #2","src":"server"} +{"t":1785469010117,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"host-change","detail":"host ใหม่ → S2","src":"server"} +{"t":1785469010118,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785469010304,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785469010304,"cat":"MG3","room":"soak2b-70282","who":"S1","ev":"join","detail":"สี#1 · คน 2 + บอท 4","src":"server"} +{"t":1785469011522,"cat":"MG3","room":"soak2b-70282","who":"S2","ev":"leave","detail":"หลุดช่วง how-to → บอทรับช่วงตอนเกมเริ่ม · เหลือคน 1","src":"server"} +{"t":1785469011705,"cat":"MG3","room":"soak2b-70282","who":"S2","ev":"join","detail":"[TC173] rejoin ก่อนเกมเริ่ม → ยกเลิก pending bot takeover 1 (กันบอทผี MG3/5/7 · pass)","src":"server"} +{"t":1785469011705,"cat":"MG3","room":"soak2b-70282","who":"S2","ev":"join","detail":"สี#6 · คน 2 + บอท 4","src":"server"}