update all

This commit is contained in:
2026-07-31 03:36:58 +00:00
parent 6dcac08598
commit de7d91b74b
72 changed files with 65947 additions and 180683 deletions
+5 -2
View File
@@ -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": ""
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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}]}
{"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}]}
+68
View File
@@ -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
}
}
}
+20 -3
View File
@@ -56,9 +56,10 @@
<div id="settings-tab" class="tab-content active">
<h2 style="font-size:1.1rem;margin-bottom:1rem;color:#c0caf5;">การตั้งค่าระบบ</h2>
<div class="form-group">
<label for="openai-key">OpenAI API Key</label>
<input type="password" id="openai-key" name="openai_api_key" placeholder="sk-..." autocomplete="off">
<small>เว้นว่างถ้าไม่ต้องการเปลี่ยน</small>
<label for="openai-key">API Key (OpenAI หรือ OpenRouter)</label>
<input type="password" id="openai-key" name="openai_api_key" placeholder="sk-... หรือ sk-or-v1-..." autocomplete="off">
<small id="key-status" style="color:#a9b1d6;">กำลังตรวจสอบ…</small>
<small>เว้นว่างถ้าไม่ต้องการเปลี่ยน · คีย์ที่ขึ้นต้น <code>sk-or-</code> ระบบจะส่งไป OpenRouter ให้อัตโนมัติ</small>
</div>
<div class="form-group">
<label for="openai-model">Model</label>
@@ -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) || 'บันทึกไม่สำเร็จ';
}
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 KiB

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 KiB

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+28015 -27892
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+64 -48
View File
@@ -4965,39 +4965,39 @@
<div id="gauntlet-name-overlay" aria-hidden="true"></div>
</div>
<button type="button" id="quiz-carry-grab-btn" class="is-hidden" aria-label="หยิบหรือส่งคำตอบ (Grab)" title="หยิบ / ส่งคำตอบ — เหมือนกด F">
<img src="/Game/img/quiz-carry/btn-grab.png" alt="" width="256" height="256" decoding="async" />
<img src="/Game/img/quiz-carry/btn-grab.png" alt="" width="256" height="256" decoding="async" / loading="lazy">
</button>
<button type="button" id="gauntlet-crown-jump-btn" class="is-hidden" aria-label="กระโดด (Jump)" title="กระโดด — เหมือน Space / W / ↑" aria-hidden="true">
<img src="/Game/img/gauntlet-assets/btn-jump.png" alt="" width="256" height="256" decoding="async" />
<img src="/Game/img/gauntlet-assets/btn-jump.png" alt="" width="256" height="256" decoding="async" / loading="lazy">
</button>
<button type="button" id="stack-tower-drop-btn" class="is-hidden" aria-label="ปล่อยบล็อก (Drop)" title="ปล่อยบล็อก — เหมือน Space / Enter" aria-hidden="true">
<img src="/Game/img/TowerBlock/btn-drop.png" alt="" width="256" height="256" decoding="async" />
<img src="/Game/img/TowerBlock/btn-drop.png" alt="" width="256" height="256" decoding="async" / loading="lazy">
</button>
<div id="quiz-map-question-panel" class="is-hidden" aria-hidden="true">
<img id="quiz-map-q-feedback-icon" class="quiz-map-q-feedback-icon is-hidden" alt="" width="64" height="64" decoding="async" />
<img id="quiz-map-q-feedback-icon" class="quiz-map-q-feedback-icon is-hidden" alt="" width="64" height="64" decoding="async" / loading="lazy">
<div id="quiz-map-question-kicker" class="quiz-map-question-kicker is-hidden" aria-hidden="true"></div>
<p id="quiz-map-question-text"></p>
<div id="quiz-map-question-subphase" class="quiz-map-question-subphase is-hidden" aria-hidden="true"></div>
<div id="quiz-map-q-feedback-score-wrap" class="is-hidden" aria-hidden="true">
<img id="quiz-map-q-feedback-score" alt="" width="120" height="48" decoding="async" />
<img id="quiz-map-q-feedback-score" alt="" width="120" height="48" decoding="async" / loading="lazy">
</div>
</div>
<div id="quiz-carry-timeup-desk-layer" class="is-hidden" aria-hidden="true">
<div class="qc-timeup-dim" aria-hidden="true"></div>
<div class="qc-timeup-desk-anchor">
<img id="quiz-carry-timeup-txt-img" class="qc-timeup-txt-img" src="/Game/img/quiz-carry/timeup-txt.png" alt="" width="480" height="120" decoding="async" />
<img id="quiz-carry-timeup-txt-img" class="qc-timeup-txt-img" src="/Game/img/quiz-carry/timeup-txt.png" alt="" width="480" height="120" decoding="async" / loading="lazy">
</div>
</div>
<div id="quiz-carry-result-end-layer" class="is-hidden" aria-hidden="true" role="img" aria-label="ผลจบภารกิจ">
<div class="qc-result-end-dim" aria-hidden="true"></div>
<img id="quiz-carry-result-end-img" class="qc-result-end-img" src="" alt="" width="900" height="520" decoding="async" />
<img id="quiz-carry-result-end-img" class="qc-result-end-img" src="" alt="" width="900" height="520" decoding="async" / loading="lazy">
</div>
</div>
<div id="play-cyber-hud" class="play-cyber-hud is-hidden" aria-hidden="true">
<div class="play-cyber-hud-mock-stage" aria-hidden="true">
<aside class="play-cyber-scoreboard" aria-label="SCORE">
<div id="play-cyber-crown-score-head" class="play-cyber-crown-score-head is-hidden" aria-hidden="true">
<img id="play-cyber-crown-score-img" class="mg2-score-label" src="/Game/img/gauntlet-assets/score.png" alt="SCORE :" width="144" height="71" decoding="async" />
<img id="play-cyber-crown-score-img" class="mg2-score-label" src="/Game/img/gauntlet-assets/score.png" alt="SCORE :" width="144" height="71" decoding="async" / loading="lazy">
</div>
<div class="play-cyber-panel-title">SCORE</div>
<ul id="play-cyber-score-list" class="play-cyber-score-list"></ul>
@@ -5005,7 +5005,7 @@
<div class="play-cyber-center-stack">
<div class="play-cyber-time-block">
<div class="play-cyber-time-head">
<img id="play-cyber-time-plaque-img" class="play-cyber-time-plaque-img is-hidden" alt="TIME" decoding="async" aria-hidden="true" />
<img id="play-cyber-time-plaque-img" class="play-cyber-time-plaque-img is-hidden" alt="TIME" decoding="async" aria-hidden="true" / loading="lazy">
<div class="play-cyber-time-label">TIME</div>
<span class="play-cyber-time-colon" aria-hidden="true">:</span>
<div id="play-cyber-time-val" class="play-cyber-time-val">0</div>
@@ -5014,7 +5014,7 @@
<div id="play-cyber-embed-zoom-hint" class="play-cyber-embed-zoom-hint is-hidden" aria-hidden="true"></div>
</div>
<div id="play-cyber-quiz-mission-q-band" class="play-cyber-quiz-mission-q-band is-hidden" aria-hidden="true">
<img id="play-cyber-quiz-mission-q-plaque" class="play-cyber-quiz-mission-q-plaque is-hidden" alt="" decoding="async" />
<img id="play-cyber-quiz-mission-q-plaque" class="play-cyber-quiz-mission-q-plaque is-hidden" alt="" decoding="async" / loading="lazy">
<p id="play-cyber-quiz-mission-q-text" class="play-cyber-quiz-mission-q-text"></p>
</div>
</div>
@@ -5022,15 +5022,15 @@
<div class="qb-room-profile-wrap play-cyber-profile-wrap" aria-hidden="true">
<div class="qb-room-profile-frame play-cyber-profile-frame">
<div class="qb-room-profile-avatar-viewport play-cyber-portrait-clip">
<img id="play-cyber-portrait-img" class="qb-room-profile-avatar play-cyber-portrait-img" alt="" decoding="async" />
<img id="play-cyber-portrait-img" class="qb-room-profile-avatar play-cyber-portrait-img" alt="" decoding="async" / loading="lazy">
</div>
<img id="play-cyber-profile-frame-overlay" class="play-cyber-profile-frame-overlay" alt="" decoding="async" aria-hidden="true" />
<img id="play-cyber-profile-frame-overlay" class="play-cyber-profile-frame-overlay" alt="" decoding="async" aria-hidden="true" / loading="lazy">
</div>
</div>
<img id="play-cyber-mic-img" class="play-cyber-mic-img is-hidden" alt="" decoding="async" aria-hidden="true" />
<img id="play-cyber-mic-img" class="play-cyber-mic-img is-hidden" alt="" decoding="async" aria-hidden="true" / loading="lazy">
<!-- ไมค์ในเกม (MG1-7) — WebRTC mesh เหมือนหน้า lobby; วางใต้ avatar ตามดีไซน์ (mg-mic). play.js เปิดแสดง (ซ่อน preview/Quiz Battle) -->
<button type="button" id="btn-voice" class="play-voice-btn" title="เปิดไมค์" aria-label="เปิด/ปิดไมค์" style="display:none;">
<img src="/Game/img/btn-mic-mute.png" alt="" id="btn-voice-icon-img" width="128" height="128" decoding="async" />
<img src="/Game/img/btn-mic-mute.png" alt="" id="btn-voice-icon-img" width="128" height="128" decoding="async" / loading="lazy">
</button>
<div id="play-cyber-self-status" class="play-cyber-self-status"></div>
<div id="play-cyber-op-widgets" class="play-cyber-op-widgets" aria-hidden="true">
@@ -5049,9 +5049,9 @@
<!-- ภารกิจคำถาม mng8a80o ช่วง live: virtual joystick (QUESTION/btn-joystick-*.png) -->
<div id="quiz-question-mission-joystick" class="quiz-q-mission-joystick is-hidden" aria-hidden="true">
<div class="quiz-q-mission-joystick-base" id="quiz-q-mission-joystick-base">
<img id="quiz-q-mission-joystick-bg" class="quiz-q-mission-joystick-bg" src="" alt="" decoding="async" />
<img id="quiz-q-mission-joystick-bg" class="quiz-q-mission-joystick-bg" src="" alt="" decoding="async" / loading="lazy">
<div class="quiz-q-mission-joystick-knob" id="quiz-q-mission-joystick-knob">
<img id="quiz-q-mission-joystick-knob-img" class="quiz-q-mission-joystick-knob-img" src="" alt="" decoding="async" />
<img id="quiz-q-mission-joystick-knob-img" class="quiz-q-mission-joystick-knob-img" src="" alt="" decoding="async" / loading="lazy">
</div>
</div>
</div>
@@ -5059,54 +5059,54 @@
<!-- MG3 Stack Tower (mnn93hpi) live HUD — DOM overlay (canvas object-fit:cover ครอป HUD ขอบจอ จึงย้ายมา DOM สเกลตาม stage) -->
<div id="mg3-tower-hud" class="mg3-tower-hud is-hidden" aria-hidden="true">
<div class="mg3-life">
<img class="mg3-life-bar" src="" alt="" decoding="async" />
<img class="mg3-life-bar" src="" alt="" decoding="async" / loading="lazy">
<div class="mg3-life-hearts" id="mg3-life-hearts"></div>
</div>
<div class="mg3-host">
<img class="mg3-host-frame" src="" alt="" decoding="async" />
<img class="mg3-host-face" id="mg3-host-av" src="" alt="" decoding="async" />
<img class="mg3-host-frame" src="" alt="" decoding="async" / loading="lazy">
<img class="mg3-host-face" id="mg3-host-av" src="" alt="" decoding="async" / loading="lazy">
</div>
<div class="mg3-terminal">
<img class="mg3-terminal-bg" src="" alt="" decoding="async" />
<img class="mg3-terminal-bg" src="" alt="" decoding="async" / loading="lazy">
<div class="mg3-terminal-log" id="mg3-terminal-log"></div>
</div>
<div class="mg3-progress">
<div class="mg3-progress-heading">
<img class="mg3-progress-label" src="" alt="" decoding="async" />
<img class="mg3-progress-label" src="" alt="" decoding="async" / loading="lazy">
<span class="mg3-progress-pct" id="mg3-progress-pct">[ 0% ]</span>
</div>
<div class="mg3-progress-bar">
<img class="mg3-progress-track" src="" alt="" decoding="async" />
<img class="mg3-progress-fill" id="mg3-progress-fill" src="" alt="" decoding="async" />
<img class="mg3-progress-track" src="" alt="" decoding="async" / loading="lazy">
<img class="mg3-progress-fill" id="mg3-progress-fill" src="" alt="" decoding="async" / loading="lazy">
</div>
</div>
</div>
<!-- MG6 Space Shooter (mnpz6rkp) live life — DOM overlay สเกล design 1920×1080 เหมือน MG3 (ไม่พึ่ง cyber HUD flex) -->
<div id="mg6-life-hud" class="mg6-life-hud is-hidden" aria-hidden="true">
<div class="mg6-life">
<img class="mg6-life-bar" src="" alt="" decoding="async" />
<img class="mg6-life-bar" src="" alt="" decoding="async" / loading="lazy">
<div class="mg6-life-hearts" id="mg6-life-hearts"></div>
</div>
</div>
<!-- Quiz Battle (qbroom*) in-game HUD — DOM overlay สเกล design 1920×1080 (header หมวด, PLAYERS, SCORE, host, EXIT, MY RESULT, RANKING) -->
<div id="qb-game-hud" class="qb-game-hud is-hidden" aria-hidden="true">
<img class="qb-hud-tag" id="qb-hud-tag" src="" alt="" decoding="async" />
<img class="qb-hud-tag" id="qb-hud-tag" src="" alt="" decoding="async" / loading="lazy">
<div class="qb-hud-players">PLAYERS : <span id="qb-hud-players-val">1/50</span></div>
<div class="qb-hud-score">SCORE : <span id="qb-hud-score-val">0</span></div>
<div class="qb-hud-host">
<div class="qb-hud-host-clip"><img class="qb-hud-host-av" id="qb-hud-host-av" src="" alt="" decoding="async" /></div>
<div class="qb-hud-host-clip"><img class="qb-hud-host-av" id="qb-hud-host-av" src="" alt="" decoding="async" / loading="lazy"></div>
</div>
<button type="button" class="qb-hud-btn qb-hud-exit" id="qb-hud-exit" aria-label="ออกจากห้อง">
<img src="/Game/img/qb-ui/btn-exit-room.png" alt="EXIT" decoding="async" />
<img src="/Game/img/qb-ui/btn-exit-room.png" alt="EXIT" decoding="async" / loading="lazy">
</button>
<button type="button" class="qb-hud-btn qb-hud-myresult" id="qb-hud-myresult" aria-label="ผลของฉัน">
<img src="/Game/img/qb-ui/btn-my-result.png" alt="MY RESULT" decoding="async" />
<img src="/Game/img/qb-ui/btn-my-result.png" alt="MY RESULT" decoding="async" / loading="lazy">
</button>
<button type="button" class="qb-hud-btn qb-hud-ranking" id="qb-hud-ranking" aria-label="อันดับ">
<img src="/Game/img/qb-ui/btn-ranking.png" alt="RANKING" decoding="async" />
<img src="/Game/img/qb-ui/btn-ranking.png" alt="RANKING" decoding="async" / loading="lazy">
</button>
<button type="button" class="qb-hud-btn qb-hud-mic" id="qb-hud-mic" aria-label="ไมโครโฟน">
<img id="qb-hud-mic-img" src="/Game/img/btn-mic-on.png" alt="mic" decoding="async" />
<img id="qb-hud-mic-img" src="/Game/img/btn-mic-on.png" alt="mic" decoding="async" / loading="lazy">
</button>
<div class="qb-hud-zoom" id="qb-hud-zoom">
<button type="button" class="qb-hud-zoom-btn" id="qb-hud-zoom-in" aria-label="ซูมเข้า">+</button>
@@ -5180,7 +5180,7 @@
<p id="quiz-carry-pregame-status" class="quiz-carry-pregame-status" aria-live="polite"></p>
<div class="quiz-carry-pregame-actions">
<button type="button" id="quiz-carry-pregame-primary" class="quiz-carry-pregame-primary-btn" aria-pressed="false" title="READY">
<img id="quiz-carry-pregame-primary-img" src="/Game/img/quiz-carry/btn-ready.png" width="220" height="56" alt="READY" decoding="async" />
<img id="quiz-carry-pregame-primary-img" src="/Game/img/quiz-carry/btn-ready.png" width="220" height="56" alt="READY" decoding="async" / loading="lazy">
</button>
</div>
</div>
@@ -5190,7 +5190,7 @@
<div id="quiz-carry-embed-countdown-inner">
<p id="quiz-carry-embed-countdown-kicker" class="quiz-carry-embed-countdown-kicker">คำถาม · Question</p>
<p id="quiz-carry-embed-countdown-q" class="quiz-carry-embed-countdown-q"></p>
<img id="quiz-carry-embed-countdown-num" src="/Game/img/QUESTION/3.png" width="200" height="200" alt="3" decoding="async" />
<img id="quiz-carry-embed-countdown-num" src="/Game/img/QUESTION/3.png" width="200" height="200" alt="3" decoding="async" / loading="lazy">
</div>
</div>
<div id="quiz-carry-embed-q-strip" class="is-hidden" role="status" aria-live="polite" aria-atomic="true" aria-hidden="true">
@@ -5212,9 +5212,9 @@
<button type="button" id="quiz-battle-mcq-close" aria-label="ปิด"></button>
<p id="quiz-battle-mcq-text" class="quiz-battle-mcq-text"></p>
<div class="quiz-battle-mcq-actions">
<button type="button" class="quiz-battle-choice" data-idx="0"><span class="qbc-letter">A</span><span class="qbc-text"></span><img class="qbc-mark" alt="" decoding="async" /></button>
<button type="button" class="quiz-battle-choice" data-idx="1"><span class="qbc-letter">B</span><span class="qbc-text"></span><img class="qbc-mark" alt="" decoding="async" /></button>
<button type="button" class="quiz-battle-choice" data-idx="2"><span class="qbc-letter">C</span><span class="qbc-text"></span><img class="qbc-mark" alt="" decoding="async" /></button>
<button type="button" class="quiz-battle-choice" data-idx="0"><span class="qbc-letter">A</span><span class="qbc-text"></span><img class="qbc-mark" alt="" decoding="async" / loading="lazy"></button>
<button type="button" class="quiz-battle-choice" data-idx="1"><span class="qbc-letter">B</span><span class="qbc-text"></span><img class="qbc-mark" alt="" decoding="async" / loading="lazy"></button>
<button type="button" class="quiz-battle-choice" data-idx="2"><span class="qbc-letter">C</span><span class="qbc-text"></span><img class="qbc-mark" alt="" decoding="async" / loading="lazy"></button>
</div>
</div>
</div>
@@ -5222,7 +5222,7 @@
<div class="gch-backdrop gch-legacy-backdrop" aria-hidden="true"></div>
<div class="gch-shell">
<!-- legacy (Mega Virus / แมปอื่น) -->
<img class="gch-bg gch-legacy-only" src="/Game/img/gauntlet-assets/popup-Howto.png" alt="" width="920" height="520" decoding="async" />
<img class="gch-bg gch-legacy-only" src="/Game/img/gauntlet-assets/popup-Howto.png" alt="" width="920" height="520" decoding="async" / loading="lazy">
<div class="gch-inner gch-inner--art gch-legacy-only">
<div class="gch-art-footer">
<p id="gauntlet-crown-howto-status" class="gch-status is-hidden" aria-live="polite"></p>
@@ -5234,13 +5234,13 @@
<div class="gch-mg2-stage gch-mg2-only" aria-hidden="true">
<div class="layer mg-overlay" aria-hidden="true"></div>
<div class="layer howto-popup">
<img class="gch-bg" src="/Game/img/gauntlet-assets/popup-Howto.png" alt="HOW TO PLAY" width="1500" height="880" decoding="async" />
<img class="gch-bg" src="/Game/img/gauntlet-assets/popup-Howto.png" alt="HOW TO PLAY" width="1500" height="880" decoding="async" / loading="lazy">
</div>
<div class="layer howto-status">
<p id="gauntlet-crown-howto-status-mg2" class="gch-status is-hidden" aria-live="polite"></p>
</div>
<button type="button" class="layer howto-btn btn btn-gch-ready" id="btn-gch-ready-mg2" title="READY">
<img id="btn-gch-ready-mg2-img" src="/Game/img/gauntlet-assets/btn-ready.png" alt="READY" width="299" height="120" decoding="async" />
<img id="btn-gch-ready-mg2-img" src="/Game/img/gauntlet-assets/btn-ready.png" alt="READY" width="299" height="120" decoding="async" / loading="lazy">
</button>
<div id="gch-ready-avatars-mg2" class="gch-ready-avatars layer" aria-hidden="true"></div>
</div>
@@ -5248,18 +5248,18 @@
</div>
<div id="gauntlet-crown-countdown" class="is-hidden" role="alert" aria-live="assertive" aria-atomic="true">
<div class="gcc-panel">
<img id="gauntlet-crown-countdown-num" class="gcc-num gcc-num--img" src="/Game/img/gauntlet-assets/3.png" width="240" height="240" alt="" decoding="async" />
<img id="gauntlet-crown-countdown-num" class="gcc-num gcc-num--img" src="/Game/img/gauntlet-assets/3.png" width="240" height="240" alt="" decoding="async" / loading="lazy">
<span id="gauntlet-crown-countdown-text" class="gcc-num gcc-num--text is-hidden" aria-hidden="true">3</span>
</div>
</div>
<div id="stack-tower-result-flash" class="is-hidden" role="dialog" aria-modal="true" aria-hidden="true" aria-label="ผลภารกิจ Tower">
<div class="stack-tower-result-flash-backdrop" aria-hidden="true"></div>
<img id="stack-tower-result-flash-img" src="" alt="" width="920" height="520" decoding="async" />
<img id="stack-tower-result-flash-img" src="" alt="" width="920" height="520" decoding="async" / loading="lazy">
</div>
<div id="gauntlet-crown-mission-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="gcm-heading">
<div class="gcm-backdrop" aria-hidden="true"></div>
<div class="gcm-shell">
<img class="gcm-bg" src="/Game/img/gauntlet-assets/popup-result.png" alt="" width="920" height="560" decoding="async" />
<img class="gcm-bg" src="/Game/img/gauntlet-assets/popup-result.png" alt="" width="920" height="560" decoding="async" / loading="lazy">
<div class="gcm-inner">
<div id="gcm-heading" class="gcm-header-tab">สรุปผลภารกิจ · Mission summary</div>
<div id="gcm-rank-row" class="gcm-rank-row"></div>
@@ -5270,7 +5270,7 @@
<div id="gcm-grade" class="gcm-grade sm-grade" aria-live="polite">A</div>
</div>
<div id="gcm-vrule" class="gcm-vrule sm-vrule is-hidden" aria-hidden="true">
<img class="gcm-line-2 sm-line-2" src="/Game/img/gauntlet-assets/popup-result-Line2.png" alt="" decoding="async" />
<img class="gcm-line-2 sm-line-2" src="/Game/img/gauntlet-assets/popup-result-Line2.png" alt="" decoding="async" / loading="lazy">
</div>
<div class="gcm-col-bonus">
<div class="gcm-h head">โบนัสพิเศษ</div>
@@ -5428,7 +5428,7 @@
<div class="sq-ov-panel">
<div id="sq-ov-quiz-body">
<div class="sq-ov-head">
<img id="sq-ov-icon" class="sq-ov-icon" src="/Game/img/special-quiz/icon-lawyer.png" alt="" />
<img id="sq-ov-icon" class="sq-ov-icon" src="/Game/img/special-quiz/icon-lawyer.png" alt="" / loading="lazy">
<div class="sq-ov-head-text">
<div id="sq-overlay-title" class="sq-ov-kicker">คำถามพิเศษ · Special Quiz</div>
<div id="sq-ov-progress" class="sq-ov-progress">ข้อ 1 / 1</div>
@@ -5445,10 +5445,10 @@
</div>
<div id="sq-ov-award" class="sq-ov-award is-hidden">
<div class="sq-ov-award-stage">
<img id="sq-ov-award-effect-img" class="sq-ov-award-effect" src="/Game/img/special-quiz/reward/card-effect.png" alt="" />
<img id="sq-ov-award-img" class="sq-ov-award-img" alt="" />
<img id="sq-ov-award-effect-img" class="sq-ov-award-effect" src="/Game/img/special-quiz/reward/card-effect.png" alt="" / loading="lazy">
<img id="sq-ov-award-img" class="sq-ov-award-img" alt="" / loading="lazy">
</div>
<img id="sq-ov-award-txt" class="sq-ov-award-txt" alt="" />
<img id="sq-ov-award-txt" class="sq-ov-award-txt" alt="" / loading="lazy">
<div id="sq-ov-award-msg" class="sq-ov-award-msg"></div>
<button type="button" id="sq-ov-award-continue" class="sq-ov-award-btn" aria-label="เล่นเกมต่อ">เล่นเกมต่อ</button>
</div>
@@ -5458,7 +5458,7 @@
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/sound.js?v=2"></script>
<script src="js/play.js?v=0.006301183"></script>
<script src="js/play.js?v=0.006301196"></script>
<div class="version-tag">v —</div>
<style id="qc-howto-mg2-fix">
/* HOW TO PLAY ของ quiz_carry (Minigame-4) -> ให้เหมือน Minigame-2 (gch-mg2-mock)
@@ -5665,7 +5665,23 @@
if (!isMg) return; /* เฉพาะตอนเข้ามินิเกม (detective) เท่านั้น — ฉาก detective ปกติไม่เล่น */
var ov = document.getElementById('jd-mg-transition'), vid = document.getElementById('jd-mg-vid');
if (!ov || !vid) return;
function done() { try { vid.pause(); } catch (e) {} ov.style.display = 'none'; } /* จบวิดีโอ → เอา overlay ออก (เกม/loading ที่โหลดข้างล่างโผล่มาเอง) */
/* [2026-07-23] จบวิดีโอ → เอา overlay ออก **และปล่อยวิดีโอทิ้งด้วย**
เดิมแค่ pause+ซ่อน → บัฟเฟอร์ ~3.1 MB ค้างทุกครั้งที่เข้ามินิเกม (เข้าหลายรอบ = สะสมไปเรื่อยๆ)
บนมือถือแรมไม่พอแล้วโดนเบราว์เซอร์ฆ่าแท็บ */
var released = false;
function done() {
if (released) return;
released = true;
try { vid.pause(); } catch (e) { /* ignore */ }
try {
vid.onended = null; vid.onerror = null;
vid.removeAttribute('src');
while (vid.firstChild) vid.removeChild(vid.firstChild);
vid.load();
} catch (e) { /* ignore */ }
ov.style.display = 'none';
try { if (ov.parentNode) ov.parentNode.removeChild(ov); } catch (e) { /* ignore */ }
}
vid.src = '/Game/video/minigame-transition.mp4';
vid.onended = done; vid.onerror = done; /* วิดีโอหาย/พัง → ไม่บล็อกเกม */
ov.style.display = 'flex'; /* ปิดจอระหว่างโหลด — ข้ามไม่ได้ (ไม่มีปุ่ม skip) */
+137 -14
View File
@@ -97,6 +97,7 @@
<button class="tool active" data-tool="path"><span class="ic">🛣️</span>เส้นทาง</button>
<button class="tool" data-tool="dome"><span class="ic"></span>จุดคำถาม</button>
<button class="tool" data-tool="spawn"><span class="ic">🚩</span>จุดเกิด</button>
<button class="tool" data-tool="fence"><span class="ic">🚧</span>รั้วกั้น</button>
<button class="tool" data-tool="erase"><span class="ic">🧽</span>ยางลบ</button>
</div>
</div>
@@ -105,12 +106,22 @@
<select id="brush"><option value="1">1×1</option><option value="2" selected>2×2</option><option value="3">3×3</option></select>
</div>
<div class="field" id="fence-field" style="display:none"><span>แบบรั้ว — คลิกวาง · ลากย้าย · คลิกขวาลบ</span>
<select id="fence-kind">
<option value="h1" selected>▬ แนวนอน สั้น (1 ช่อง)</option>
<option value="h2">▬▬ แนวนอน ยาว (2 ช่อง)</option>
<option value="v1">▮ แนวตั้ง สั้น (1 ช่อง)</option>
<option value="v2">▮▮ แนวตั้ง ยาว (2 ช่อง)</option>
</select>
</div>
<div class="stat grow" id="stat"></div>
<div class="row">
<button class="btn sec" id="btn-clear-path">ล้างเส้นทาง</button>
<button class="btn sec" id="btn-clear-dome">ล้างจุดคำถาม</button>
</div>
<button class="btn sec grow" id="btn-clear-fence">ล้างรั้วกั้น</button>
<button class="btn grow" id="btn-save">💾 บันทึก</button>
<button class="btn sec grow" id="btn-test">▶ ทดสอบเล่น</button>
<div class="toast" id="toast"></div>
@@ -128,6 +139,14 @@
var curId = qs.get('id') || 'qbroom1';
var map=null, W=24, H=49, path=[], dome=[], spawn={x:11,y:2};
/* รั้วกั้น — [{x,y,w,h}] หน่วยเป็น "ช่อง" ทศนิยมได้ วางอิสระไม่ยึดกริด
กริด 20x40 หยาบเกินจะกั้นตรงรั้วที่อาร์ตวาดกลางแถบได้ (รั้วอยู่กลางช่อง ไม่ใช่ขอบช่อง)
เกมเช็คด้วยกล่องชนของสไปรต์ซ้อนกล่องรั้ว (server.js quizBattleFenceBlocksServer + play.js ตัวเดียวกัน)
ในเกมมองไม่เห็น — อาร์ตวาดรั้วไว้ให้แล้ว ตรงนี้เป็นแค่ตัวกั้นการเดิน */
var fences=[], dragFence=null;
/* คลังคำถามของ Quiz Battle มี 20 ข้อต่อหัวข้อ (1 หัวข้อ = 1 ฉาก) → ฉากหนึ่งควรมีจุดคำถาม 20 ข้อพอดี
ถ้าวางเกิน เลขข้อจะวนซ้ำ (quizBattleQuestionIndexForCompPlay ใช้ (idx % poolLen)) */
var QUESTIONS_PER_MAP = 20;
var bgImg=new Image(), tool='path', brush=2, painting=false, paintVal=1;
var cv=document.getElementById('cv'), ctx=cv.getContext('2d');
@@ -171,6 +190,9 @@
document.querySelectorAll('.tool').forEach(function(x){x.classList.remove('active');});
b.classList.add('active'); tool=b.getAttribute('data-tool');
brushLabel.textContent = b.querySelector('.ic').textContent+' '+b.textContent.trim();
/* ตัวเลือกแบบรั้วโผล่เฉพาะตอนใช้เครื่องมือรั้ว · ขนาดพู่กันไม่เกี่ยวกับรั้ว (รั้วไม่ยึดกริด) */
var ff=document.getElementById('fence-field');
if(ff) ff.style.display=(tool==='fence')?'':'none';
});
});
document.getElementById('brush').addEventListener('change',function(e){ brush=parseInt(e.target.value,10)||1; });
@@ -182,6 +204,9 @@
.then(function(m){
map=m; W=m.width||24; H=m.height||49;
path=norm(m.quizBattlePathArea,W,H); dome=norm(m.quizBattleDomeArea,W,H);
fences=(Array.isArray(m.quizBattleFences)?m.quizBattleFences:[])
.map(function(f){ return f&&isFinite(f.x)&&isFinite(f.y)&&f.w>0&&f.h>0
? { x:+f.x, y:+f.y, w:+f.w, h:+f.h } : null; }).filter(Boolean);
spawn=(m.spawn&&typeof m.spawn.x==='number')?{x:m.spawn.x,y:m.spawn.y}:{x:11,y:2};
if(m.backgroundImage){ bgImg=new Image(); bgImg.onload=draw; bgImg.onerror=draw; bgImg.src=m.backgroundImage+(m.backgroundImage.indexOf('?')<0?('?_='+Date.now()):''); }
toast('โหลด "'+(m.name||curId)+'" แล้ว',true); layout();
@@ -205,29 +230,98 @@
for(var x=0;x<=W;x++){ ctx.beginPath(); ctx.moveTo(x*cell+.5,0); ctx.lineTo(x*cell+.5,h); ctx.stroke(); }
for(var y=0;y<=H;y++){ ctx.beginPath(); ctx.moveTo(0,y*cell+.5); ctx.lineTo(w,y*cell+.5); ctx.stroke(); }
ctx.fillStyle='rgba(122,162,247,.42)';
for(var py=0;py<H;py++) for(var px=0;px<W;px++) if(path[py][px]) ctx.fillRect(px*cell,py*cell,cell,cell);
var num=0;
for(var dy2=0;dy2<H;dy2++) for(var dx2=0;dx2<W;dx2++) if(dome[dy2][dx2]){
num++; var cx=dx2*cell+cell/2, cy=dy2*cell+cell/2, r=Math.max(8,cell*0.62);
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.fillStyle='rgba(255,209,102,.92)'; ctx.fill();
ctx.lineWidth=2; ctx.strokeStyle='#7a5a10'; ctx.stroke();
/* [2026-07-22] กัน TypeError ตอนเปิดหน้าครั้งแรก — layout() เรียก draw() ตั้งแต่ยังไม่ได้โหลดแผนที่
ตอนนั้น path/dome ยังเป็น [] แต่ H=49 → path[py] undefined แล้ว throw ทั้ง draw() (กริด/ฉากไม่ขึ้นเลย) */
for(var py=0;py<H;py++) for(var px=0;px<W;px++) if(path[py]&&path[py][px]) ctx.fillRect(px*cell,py*cell,cell,cell);
/* [2026-07-30] "1 คำถาม" = "โดม 1 กลุ่มที่ติดกัน" ไม่ใช่ "1 ช่อง"
server รวมช่องที่ติดกันเป็นกลุ่มเดียว (normalizeQuizBattleDomeCompOnMap) แล้วให้เลขข้อ 1 เลขต่อกลุ่ม
ตั้งแต่ 07-30 โดมถูกวางเป็น "ทั้งคอลัมน์ของเลน" (2-3 ช่อง) เพราะเลนหนา 3 แถวแต่กล่องชนสูง 1.35 ช่อง
ถ้าโดมเป็นช่องเดียวผู้เล่นจะมุดใต้ได้แล้วไปตันที่ประตูถัดไปแบบแก้ไม่ได้
ของเดิมนับ/ใส่เลขทีละช่อง → แมป 20 ข้อโชว์เป็น 52-56 (user: "จริง ๆ ควรมีแค่ 20 คำถามต่อ map")
ตรงนี้จึงจัดกลุ่มแบบเดียวกับ server แล้ววาดเลขครั้งเดียวต่อกลุ่ม (ที่ช่องบน-ซ้ายสุด เหมือน play.js) */
var comp=[], nGroup=0;
for(var gy=0;gy<H;gy++){ comp[gy]=[]; for(var gx=0;gx<W;gx++) comp[gy][gx]=0; }
for(var qy=0;qy<H;qy++) for(var qx=0;qx<W;qx++){
if(!(dome[qy]&&dome[qy][qx])||comp[qy][qx]) continue;
nGroup++; var st=[[qx,qy]]; comp[qy][qx]=nGroup;
while(st.length){
var c=st.pop();
var nb=[[c[0]+1,c[1]],[c[0]-1,c[1]],[c[0],c[1]+1],[c[0],c[1]-1]];
for(var k=0;k<4;k++){
var nx=nb[k][0], ny=nb[k][1];
if(nx<0||ny<0||nx>=W||ny>=H) continue;
if(!(dome[ny]&&dome[ny][nx])||comp[ny][nx]) continue;
comp[ny][nx]=nGroup; st.push([nx,ny]);
}
}
}
for(var dy2=0;dy2<H;dy2++) for(var dx2=0;dx2<W;dx2++) if(dome[dy2]&&dome[dy2][dx2]){
var id=comp[dy2][dx2];
var cx=dx2*cell+cell/2, cy=dy2*cell+cell/2, r=Math.max(8,cell*0.62);
/* ช่องอื่นในกลุ่มเดียวกันวาดจาง ๆ ไม่ใส่เลข — ให้เห็นว่า "ก้อนนี้คือข้อเดียวกัน" */
var head=!((comp[dy2-1]&&comp[dy2-1][dx2]===id)||(comp[dy2][dx2-1]===id));
ctx.beginPath(); ctx.arc(cx,cy,head?r:r*0.5,0,Math.PI*2);
ctx.fillStyle=head?'rgba(255,209,102,.92)':'rgba(255,209,102,.34)'; ctx.fill();
ctx.lineWidth=head?2:1; ctx.strokeStyle=head?'#7a5a10':'rgba(122,90,16,.5)'; ctx.stroke();
if(!head) continue;
ctx.fillStyle='#3a2c00'; ctx.font='bold '+Math.max(10,cell*0.7)+'px Kanit,sans-serif';
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText(String(num),cx,cy+1);
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText(String(id),cx,cy+1);
}
/* รั้วกั้น — วาดตามพิกัดทศนิยมจริง (ไม่ snap เข้าช่อง) จะได้เห็นว่ามันอยู่ตรงไหนของภาพจริง ๆ */
for(var fi=0;fi<fences.length;fi++){
var fr=fences[fi];
var rx=fr.x*cell, ry=fr.y*cell, rw=Math.max(2,fr.w*cell), rh=Math.max(2,fr.h*cell);
ctx.fillStyle='rgba(255,99,132,.55)'; ctx.fillRect(rx,ry,rw,rh);
ctx.lineWidth=2; ctx.strokeStyle='#ff4d6d'; ctx.strokeRect(rx+.5,ry+.5,rw-1,rh-1);
/* ขีดทแยงให้ดูเป็น "รั้ว" ไม่ใช่แค่แถบสี */
ctx.save(); ctx.beginPath(); ctx.rect(rx,ry,rw,rh); ctx.clip();
ctx.strokeStyle='rgba(255,255,255,.5)'; ctx.lineWidth=1;
for(var hx=rx-rh;hx<rx+rw;hx+=6){ ctx.beginPath(); ctx.moveTo(hx,ry+rh); ctx.lineTo(hx+rh,ry); ctx.stroke(); }
ctx.restore();
}
var sx=spawn.x*cell, sy=spawn.y*cell;
ctx.fillStyle='rgba(52,211,153,.9)'; ctx.fillRect(sx,sy,cell,cell);
ctx.fillStyle='#04241a'; ctx.font='bold '+Math.max(10,cell*0.6)+'px Kanit,sans-serif';
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('🚩',sx+cell/2,sy+cell/2);
var np=0,nd=0; for(var a=0;a<H;a++) for(var b=0;b<W;b++){ if(path[a][b])np++; if(dome[a][b])nd++; }
statEl.innerHTML='ฉาก: <b>'+(map&&map.name||curId)+'</b><br>เส้นทาง <b>'+np+'</b> ช่อง · จุดคำถาม <b>'+nd+'</b> · เกิด ('+spawn.x+','+spawn.y+')';
/* กันเคสเดียวกับด้านบน — นับก่อนโหลดแผนที่ path[a] ยัง undefined */
var np=0,nd=0; for(var a=0;a<H;a++) for(var b=0;b<W;b++){ if(path[a]&&path[a][b])np++; if(dome[a]&&dome[a][b])nd++; }
/* จำนวนคำถามจริง = จำนวน "กลุ่ม" (ตรงกับที่เกมถาม) · จำนวนช่องบอกไว้ในวงเล็บเผื่อเช็คว่าโดมกั้นเลนครบไหม */
var qWarn = (nGroup===QUESTIONS_PER_MAP) ? '' :
' <span style="color:#ff9f6e">— ควรมี '+QUESTIONS_PER_MAP+' ข้อต่อฉาก</span>';
statEl.innerHTML='ฉาก: <b>'+(map&&map.name||curId)+'</b><br>เส้นทาง <b>'+np+'</b> ช่อง · จุดคำถาม <b>'+nGroup+'</b> ข้อ <span style="opacity:.6">('+nd+' ช่อง)</span>'+qWarn
+' · รั้ว <b>'+fences.length+'</b> · เกิด ('+spawn.x+','+spawn.y+')';
}
function tileAt(ev){
var p=pointAt(ev); return { x:Math.floor(p.x), y:Math.floor(p.y) };
}
/** พิกัดแบบทศนิยม (หน่วยช่อง) — รั้วใช้ตัวนี้ เพราะวางอิสระไม่ยึดกริด */
function pointAt(ev){
var rect=cv.getBoundingClientRect();
var px=(ev.clientX!=null?ev.clientX:(ev.touches&&ev.touches[0].clientX))-rect.left;
var py=(ev.clientY!=null?ev.clientY:(ev.touches&&ev.touches[0].clientY))-rect.top;
var cell=cv._cell; return { x:Math.floor(px/cell), y:Math.floor(py/cell) };
var cell=cv._cell; return { x:px/cell, y:py/cell };
}
function fenceSize(){
var k=(document.getElementById('fence-kind')||{}).value||'h1';
if(k==='h2') return { w:2.0, h:0.22 };
if(k==='v1') return { w:0.22, h:1.0 };
if(k==='v2') return { w:0.22, h:2.0 };
return { w:1.0, h:0.22 };
}
function fenceAtPoint(p){
for(var i=fences.length-1;i>=0;i--){
var f=fences[i];
if(p.x>=f.x&&p.x<=f.x+f.w&&p.y>=f.y&&p.y<=f.y+f.h) return i;
}
/* เผื่อรั้วบางมาก (0.22 ช่อง) จิ้มโดนยาก — ขยายพื้นที่จับอีก 0.18 ช่องรอบตัว */
for(var j=fences.length-1;j>=0;j--){
var g=fences[j], m=0.18;
if(p.x>=g.x-m&&p.x<=g.x+g.w+m&&p.y>=g.y-m&&p.y<=g.y+g.h+m) return j;
}
return -1;
}
function round2(v){ return Math.round(v*100)/100; }
function applyAt(tx,ty){
if(tx<0||ty<0||tx>=W||ty>=H) return;
if(tool==='spawn'){ spawn={x:tx,y:ty}; return; }
@@ -240,14 +334,42 @@
}
}
function onDown(ev){
ev.preventDefault(); var t=tileAt(ev);
ev.preventDefault();
/* รั้ว: ทำงานด้วยพิกัดทศนิยมล้วน ไม่แตะกริดเลย — คลิกที่ว่าง = วางใหม่ (จุดที่คลิกคือ "กลาง" รั้ว)
คลิกบนรั้วเดิม = เริ่มลากย้าย · ยางลบคลิกโดนรั้วก็ลบรั้วนั้น */
var p=pointAt(ev);
if(tool==='fence'||tool==='erase'){
var hit=fenceAtPoint(p);
if(tool==='erase'&&hit>=0){ fences.splice(hit,1); draw(); return; }
if(tool==='fence'){
if(hit>=0){ dragFence={ i:hit, dx:p.x-fences[hit].x, dy:p.y-fences[hit].y }; return; }
var s=fenceSize();
fences.push({ x:round2(p.x-s.w/2), y:round2(p.y-s.h/2), w:s.w, h:s.h });
dragFence={ i:fences.length-1, dx:s.w/2, dy:s.h/2 };
draw(); return;
}
}
var t=tileAt(ev);
if(tool==='path') paintVal=(path[t.y]&&path[t.y][t.x])?0:1;
else if(tool==='dome') paintVal=(dome[t.y]&&dome[t.y][t.x])?0:1;
else paintVal=1;
painting=true; applyAt(t.x,t.y); draw();
}
function onMove(ev){ if(!painting) return; ev.preventDefault(); var t=tileAt(ev); applyAt(t.x,t.y); draw(); }
function onUp(){ painting=false; }
function onMove(ev){
if(dragFence){
ev.preventDefault(); var p=pointAt(ev), f=fences[dragFence.i];
if(f){ f.x=round2(p.x-dragFence.dx); f.y=round2(p.y-dragFence.dy); draw(); }
return;
}
if(!painting) return; ev.preventDefault(); var t=tileAt(ev); applyAt(t.x,t.y); draw();
}
function onUp(){ painting=false; dragFence=null; }
/* คลิกขวาบนรั้ว = ลบ (ไม่ต้องสลับไปยางลบ) */
cv.addEventListener('contextmenu',function(ev){
var hit=fenceAtPoint(pointAt(ev));
if(hit<0) return;
ev.preventDefault(); fences.splice(hit,1); draw();
});
cv.addEventListener('mousedown',onDown);
window.addEventListener('mousemove',onMove);
window.addEventListener('mouseup',onUp);
@@ -258,11 +380,12 @@
document.getElementById('btn-clear-path').addEventListener('click',function(){ path=emptyGrid(W,H); draw(); toast('ล้างเส้นทางแล้ว',true); });
document.getElementById('btn-clear-dome').addEventListener('click',function(){ dome=emptyGrid(W,H); draw(); toast('ล้างจุดคำถามแล้ว',true); });
document.getElementById('btn-clear-fence').addEventListener('click',function(){ fences=[]; draw(); toast('ล้างรั้วกั้นแล้ว',true); });
document.getElementById('btn-save').addEventListener('click',function(){
var btn=this; btn.disabled=true; toast('กำลังบันทึก…');
fetch(BASE+'/api/maps/'+encodeURIComponent(curId),{method:'PUT',headers:{'Content-Type':'application/json'},
body:JSON.stringify({ quizBattlePathArea:path, quizBattleDomeArea:dome, spawn:spawn, width:W, height:H })})
body:JSON.stringify({ quizBattlePathArea:path, quizBattleDomeArea:dome, quizBattleFences:fences, spawn:spawn, width:W, height:H })})
.then(function(r){return r.json();}).then(function(res){ btn.disabled=false;
if(res&&res.ok) toast('บันทึก "'+curId+'" สำเร็จ ✓',true); else toast((res&&res.error)||'บันทึกไม่สำเร็จ',false);
}).catch(function(e){ btn.disabled=false; toast('บันทึกไม่สำเร็จ: '+e.message,false); });
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
+14242 -13842
View File
File diff suppressed because it is too large Load Diff