minigame 4 add more
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -283,6 +283,24 @@ button { background: #7aa2f7; color: #1a1b26; cursor: pointer; border: none; }
|
||||
width: 3.2rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
.editor-grid-image-carry-bind-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem 0.75rem;
|
||||
align-items: center;
|
||||
font-size: 0.72rem;
|
||||
color: #a9b1d6;
|
||||
}
|
||||
.editor-grid-image-carry-bind-row select {
|
||||
margin-left: 0.35rem;
|
||||
max-width: 12rem;
|
||||
}
|
||||
.editor-hint-inline {
|
||||
font-size: 0.68rem;
|
||||
color: #787c99;
|
||||
line-height: 1.35;
|
||||
flex: 1 1 140px;
|
||||
}
|
||||
.editor-grid-image-gallery-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -293,6 +311,13 @@ button { background: #7aa2f7; color: #1a1b26; cursor: pointer; border: none; }
|
||||
color: #a9b1d6;
|
||||
font-weight: 600;
|
||||
}
|
||||
.editor-grid-image-gallery-note {
|
||||
margin: 0.1rem 0 0.35rem;
|
||||
font-size: 0.65rem;
|
||||
line-height: 1.35;
|
||||
color: #787c99;
|
||||
max-width: 36rem;
|
||||
}
|
||||
.grid-cell-image-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
|
||||
@@ -330,6 +355,70 @@ button { background: #7aa2f7; color: #1a1b26; cursor: pointer; border: none; }
|
||||
height: 56px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.grid-cell-image-gallery-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
.grid-cell-image-gallery-same-hint {
|
||||
font-size: 0.62rem;
|
||||
line-height: 1.35;
|
||||
color: #f7768e;
|
||||
padding: 0.2rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(247, 118, 142, 0.12);
|
||||
border: 1px solid rgba(247, 118, 142, 0.35);
|
||||
}
|
||||
.grid-cell-image-gallery-held-side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
padding: 0.15rem 0.1rem 0.25rem;
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
.grid-cell-image-gallery-held-label {
|
||||
font-size: 0.58rem;
|
||||
font-weight: 700;
|
||||
color: #7dcfff;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.grid-cell-image-gallery-held-thumb {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 44px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background: #1a1b26;
|
||||
}
|
||||
.grid-cell-image-gallery-held-btn {
|
||||
font-size: 0.58rem;
|
||||
padding: 0.15rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(125, 207, 255, 0.45);
|
||||
background: rgba(26, 27, 38, 0.95);
|
||||
color: #c0caf5;
|
||||
cursor: pointer;
|
||||
}
|
||||
.grid-cell-image-gallery-held-btn:hover {
|
||||
border-color: rgba(158, 206, 106, 0.65);
|
||||
color: #9ece6a;
|
||||
}
|
||||
.grid-cell-image-gallery-clear-held {
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
padding: 0.05rem 0.25rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(247, 118, 142, 0.25);
|
||||
color: #f7768e;
|
||||
cursor: pointer;
|
||||
}
|
||||
.grid-cell-image-gallery-clear-held:hover {
|
||||
background: rgba(247, 118, 142, 0.45);
|
||||
}
|
||||
.grid-cell-image-gallery-cap {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
@@ -1721,6 +1810,34 @@ body.room-lobby--quiz-active .lobby-b-extra-row {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* play — quiz carry pregame (สอดคล้อง play.html) */
|
||||
.quiz-carry-pregame-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
max-width: min(94vw, 900px);
|
||||
width: 100%;
|
||||
max-height: min(94vh, 900px);
|
||||
overflow: hidden;
|
||||
background: rgba(10, 12, 26, 0.55);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.quiz-carry-pregame-footer {
|
||||
flex: 0 0 auto;
|
||||
padding: 0.85rem 1rem 1rem;
|
||||
background: linear-gradient(180deg, rgba(8, 10, 22, 0.2), rgba(8, 10, 22, 0.96));
|
||||
border-top: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
/* Editor — เกมตอบคำถาม */
|
||||
.quiz-editor-wrap {
|
||||
margin: 1rem 0;
|
||||
|
||||
@@ -145,12 +145,38 @@
|
||||
<label>กว้าง (ช่อง) <input type="number" id="grid-image-brush-w" value="1" min="1" max="24" title="จำนวนช่องกริดแนวนอน"></label>
|
||||
<label>สูง (ช่อง) <input type="number" id="grid-image-brush-h" value="1" min="1" max="24" title="จำนวนช่องกริดแนวตั้ง"></label>
|
||||
</div>
|
||||
<div id="grid-sprite-carry-bind-wrap" class="editor-grid-image-carry-bind-row" style="display:none;" title="quiz carry: สไปรต์ที่ไม่ทับช่องโซนตัวเลือก — ต้องผูกข้อเองถึงจะสลับรูปหยิบแล้ว">
|
||||
<label>ผูกสไปรต์กับข้อ (หยิบแล้ว)
|
||||
<select id="grid-sprite-carry-bind">
|
||||
<option value="auto">อัตโนมัติ (ช่องที่ทับโซนตัวเลือก)</option>
|
||||
<option value="1">ข้อ 1</option>
|
||||
<option value="2">ข้อ 2</option>
|
||||
<option value="3">ข้อ 3</option>
|
||||
<option value="4">ข้อ 4</option>
|
||||
<option value="5">ข้อ 5</option>
|
||||
<option value="6">ข้อ 6</option>
|
||||
<option value="7">ข้อ 7</option>
|
||||
<option value="8">ข้อ 8</option>
|
||||
<option value="9">ข้อ 9</option>
|
||||
<option value="10">ข้อ 10</option>
|
||||
<option value="11">ข้อ 11</option>
|
||||
<option value="12">ข้อ 12</option>
|
||||
<option value="13">ข้อ 13</option>
|
||||
<option value="14">ข้อ 14</option>
|
||||
<option value="15">ข้อ 15</option>
|
||||
<option value="16">ข้อ 16</option>
|
||||
</select>
|
||||
</label>
|
||||
<span class="editor-hint-inline">Alt+คลิกซ้ายบนสไปรต์ = ใส่/เปลี่ยนผูก · รายการ "อัตโนมัติ" = ล้างผูก</span>
|
||||
</div>
|
||||
<div class="editor-grid-image-gallery-wrap">
|
||||
<span class="editor-grid-image-gallery-label">คลังรูป (คลิกเลือก)</span>
|
||||
<p class="editor-grid-image-gallery-note">สองช่องคือรูปที่บันทึกในแมป (ก่อนหยิบ / หยิบแล้ว) — ไม่อัปเดตตามการหยิบในห้อง · ทดสอบ idle/held ให้เปิดหน้าเล่น หลังบันทึกแมป</p>
|
||||
<div id="grid-cell-image-gallery" class="grid-cell-image-gallery" role="listbox" aria-label="คลังรูปบนกริด"></div>
|
||||
</div>
|
||||
<div class="editor-grid-image-upload-row">
|
||||
<label>อัปโหลดเข้าคลัง <input type="file" id="grid-cell-image-upload" accept="image/*"></label>
|
||||
<input type="file" id="grid-cell-image-held-upload" accept="image/*" hidden tabindex="-1" aria-hidden="true" title="อัปโหลดรูปตอนถือ (quiz carry)">
|
||||
<button type="button" id="btn-grid-image-remove-from-lib" title="ลบรูปที่เลือกในคลัง (สไปรต์ที่ใช้รูปนี้จะหาย)">ลบจากคลัง</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,7 +271,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0169"></script>
|
||||
<script src="js/editor.js?v=20260427-lobby-spawn-mode"></script>
|
||||
<script src="js/editor.js?v=20260428-quiz-carry-toast-silent"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -94,8 +94,39 @@
|
||||
let gridImageLibrary = [];
|
||||
let gridImageSprites = [];
|
||||
let gridImageBrushIndex = 0;
|
||||
let pendingHeldLibIndex = -1;
|
||||
const editorGridImageElByIndex = {};
|
||||
const editorGridImageHeldElByIndex = {};
|
||||
const GRID_IMG_MAX_WH = 24;
|
||||
|
||||
function gridLibIdleUrl(entry) {
|
||||
if (entry == null) return '';
|
||||
if (typeof entry === 'string') return entry;
|
||||
if (typeof entry === 'object' && entry.idle) return String(entry.idle);
|
||||
return '';
|
||||
}
|
||||
function gridLibHeldUrl(entry) {
|
||||
if (entry == null || typeof entry === 'string') return '';
|
||||
if (typeof entry === 'object' && entry.held && String(entry.held).length) return String(entry.held);
|
||||
return '';
|
||||
}
|
||||
function normalizeGridLibEntry(raw) {
|
||||
if (raw == null) return null;
|
||||
if (typeof raw === 'string' && raw.length > 0) return { idle: raw, held: null };
|
||||
if (typeof raw === 'object' && typeof raw.idle === 'string' && raw.idle.length > 0) {
|
||||
let held = (typeof raw.held === 'string' && raw.held.length > 0) ? raw.held : null;
|
||||
if (held && held === raw.idle) held = null;
|
||||
return { idle: raw.idle, held };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function serializeGridLibEntry(entry) {
|
||||
const idle = gridLibIdleUrl(entry);
|
||||
if (!idle) return null;
|
||||
const held = gridLibHeldUrl(entry);
|
||||
if (held && held !== idle) return { idle, held };
|
||||
return idle;
|
||||
}
|
||||
let isSpawnMode = false;
|
||||
let editorFroggerAnimationId = null;
|
||||
|
||||
@@ -464,7 +495,10 @@
|
||||
const y = Math.floor(Number(raw.y));
|
||||
const w = Math.max(1, Math.floor(Number(raw.w)) || 1);
|
||||
const h = Math.max(1, Math.floor(Number(raw.h)) || 1);
|
||||
return { i, x, y, w, h };
|
||||
const b = parseSpriteBindCarryOption(raw.bindCarryOption);
|
||||
const o = { i, x, y, w, h };
|
||||
if (b != null) o.bindCarryOption = b;
|
||||
return o;
|
||||
}).filter((s) => Number.isFinite(s.x) && Number.isFinite(s.y));
|
||||
}
|
||||
const libLen = (Array.isArray(m.gridImageLibrary) && m.gridImageLibrary.length) || 0;
|
||||
@@ -486,15 +520,33 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseSpriteBindCarryOption(raw) {
|
||||
if (raw == null || raw === '') return null;
|
||||
const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10);
|
||||
if (!Number.isFinite(n) || n < 1 || n > QUIZ_CARRY_EDITOR_OPT_COUNT) return null;
|
||||
return n;
|
||||
}
|
||||
|
||||
function readSpriteCarryBindFromSelect() {
|
||||
const el = document.getElementById('grid-sprite-carry-bind');
|
||||
if (!el) return null;
|
||||
const v = String(el.value || '').trim();
|
||||
if (v === '' || v === 'auto') return null;
|
||||
return parseSpriteBindCarryOption(v);
|
||||
}
|
||||
|
||||
function clampSpriteToMap(s) {
|
||||
let { i, x, y, w, h } = s;
|
||||
let { i, x, y, w, h, bindCarryOption } = s;
|
||||
if (x < 0) { w += x; x = 0; }
|
||||
if (y < 0) { h += y; y = 0; }
|
||||
if (x + w > width) w = width - x;
|
||||
if (y + h > height) h = height - y;
|
||||
w = Math.max(1, w);
|
||||
h = Math.max(1, h);
|
||||
return { i, x, y, w, h };
|
||||
const out = { i, x, y, w, h };
|
||||
const b = parseSpriteBindCarryOption(bindCarryOption);
|
||||
if (b != null) out.bindCarryOption = b;
|
||||
return out;
|
||||
}
|
||||
|
||||
function sanitizeSpritesInPlace() {
|
||||
@@ -529,7 +581,10 @@
|
||||
if (objects[yy][xx] === 1) return null;
|
||||
}
|
||||
}
|
||||
return { i: libIdx, x: ax, y: ay, w, h };
|
||||
const out = { i: libIdx, x: ax, y: ay, w, h };
|
||||
const b = readSpriteCarryBindFromSelect();
|
||||
if (b != null) out.bindCarryOption = b;
|
||||
return out;
|
||||
}
|
||||
|
||||
function rebuildGridImageGallery() {
|
||||
@@ -541,15 +596,17 @@
|
||||
return;
|
||||
}
|
||||
if (gridImageBrushIndex >= gridImageLibrary.length) gridImageBrushIndex = gridImageLibrary.length - 1;
|
||||
gridImageLibrary.forEach((url, idx) => {
|
||||
gridImageLibrary.forEach((entry, idx) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'grid-cell-image-gallery-row';
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'grid-cell-image-gallery-item' + (idx === gridImageBrushIndex ? ' is-selected' : '');
|
||||
btn.title = 'รูป #' + (idx + 1) + ' — คลิกเลือก';
|
||||
btn.title = 'รูป #' + (idx + 1) + ' (ก่อนหยิบ) — คลิกเลือกแปรง';
|
||||
btn.dataset.libIndex = String(idx);
|
||||
const thumb = document.createElement('img');
|
||||
thumb.alt = '';
|
||||
thumb.src = url;
|
||||
thumb.src = gridLibIdleUrl(entry);
|
||||
thumb.loading = 'lazy';
|
||||
btn.appendChild(thumb);
|
||||
const cap = document.createElement('span');
|
||||
@@ -562,20 +619,92 @@
|
||||
el.classList.toggle('is-selected', el.dataset.libIndex === String(idx));
|
||||
});
|
||||
});
|
||||
gal.appendChild(btn);
|
||||
const heldSide = document.createElement('div');
|
||||
heldSide.className = 'grid-cell-image-gallery-held-side';
|
||||
const heldLabel = document.createElement('span');
|
||||
heldLabel.className = 'grid-cell-image-gallery-held-label';
|
||||
heldLabel.textContent = 'หยิบแล้ว';
|
||||
heldSide.appendChild(heldLabel);
|
||||
const heldUrl = gridLibHeldUrl(entry);
|
||||
if (heldUrl) {
|
||||
const him = document.createElement('img');
|
||||
him.className = 'grid-cell-image-gallery-held-thumb';
|
||||
him.alt = '';
|
||||
him.src = heldUrl;
|
||||
him.loading = 'lazy';
|
||||
heldSide.appendChild(him);
|
||||
}
|
||||
const btnHeld = document.createElement('button');
|
||||
btnHeld.type = 'button';
|
||||
btnHeld.className = 'grid-cell-image-gallery-held-btn';
|
||||
btnHeld.textContent = heldUrl ? 'เปลี่ยนรูปถือ' : 'ใส่รูปถือ';
|
||||
btnHeld.title = 'รูปบนแมปเมื่อมีคนหยิบตัวเลือกในช่องนี้ (quiz carry) จนกว่าจะปล่อย';
|
||||
btnHeld.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
pendingHeldLibIndex = idx;
|
||||
const inp = document.getElementById('grid-cell-image-held-upload');
|
||||
if (inp) inp.click();
|
||||
});
|
||||
heldSide.appendChild(btnHeld);
|
||||
if (heldUrl) {
|
||||
const clr = document.createElement('button');
|
||||
clr.type = 'button';
|
||||
clr.className = 'grid-cell-image-gallery-clear-held';
|
||||
clr.title = 'ลบรูปตอนถือ';
|
||||
clr.textContent = '×';
|
||||
clr.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
gridImageLibrary[idx] = { idle: gridLibIdleUrl(gridImageLibrary[idx]), held: null };
|
||||
delete editorGridImageHeldElByIndex[idx];
|
||||
rebuildGridImageGallery();
|
||||
draw();
|
||||
if (statusEl) statusEl.textContent = 'ลบรูปตอนถือของ #' + (idx + 1) + ' แล้ว';
|
||||
});
|
||||
heldSide.appendChild(clr);
|
||||
}
|
||||
row.appendChild(btn);
|
||||
row.appendChild(heldSide);
|
||||
if (heldUrl) {
|
||||
const idleStr = gridLibIdleUrl(entry);
|
||||
if (idleStr && heldUrl === idleStr) {
|
||||
const sameHint = document.createElement('div');
|
||||
sameHint.className = 'grid-cell-image-gallery-same-hint';
|
||||
sameHint.textContent = 'รูปก่อนหยิบกับหยิบแล้วเป็นข้อมูลเดียวกัน — อัปโหลดภาพคนละไฟล์สำหรับ «หยิบแล้ว» จึงจะเห็นต่างในเกม';
|
||||
row.appendChild(sameHint);
|
||||
}
|
||||
}
|
||||
gal.appendChild(row);
|
||||
primeEditorGridImage(idx);
|
||||
primeEditorGridImageHeld(idx);
|
||||
});
|
||||
}
|
||||
|
||||
function primeEditorGridImage(idx) {
|
||||
if (editorGridImageElByIndex[idx] || !gridImageLibrary[idx]) return;
|
||||
const idle = gridLibIdleUrl(gridImageLibrary[idx]);
|
||||
if (editorGridImageElByIndex[idx] || !idle) return;
|
||||
const im = new Image();
|
||||
im.onload = () => { draw(); };
|
||||
im.onerror = () => { delete editorGridImageElByIndex[idx]; };
|
||||
im.src = gridImageLibrary[idx];
|
||||
im.src = idle;
|
||||
editorGridImageElByIndex[idx] = im;
|
||||
}
|
||||
|
||||
function primeEditorGridImageHeld(idx) {
|
||||
const held = gridLibHeldUrl(gridImageLibrary[idx]);
|
||||
if (!held) {
|
||||
delete editorGridImageHeldElByIndex[idx];
|
||||
return;
|
||||
}
|
||||
if (editorGridImageHeldElByIndex[idx]) return;
|
||||
const im = new Image();
|
||||
im.onload = () => { draw(); };
|
||||
im.onerror = () => { delete editorGridImageHeldElByIndex[idx]; };
|
||||
im.src = held;
|
||||
editorGridImageHeldElByIndex[idx] = im;
|
||||
}
|
||||
|
||||
function removeGridImageFromLibraryAt(index) {
|
||||
const i = parseInt(index, 10);
|
||||
if (!Number.isFinite(i) || i < 0 || i >= gridImageLibrary.length) return;
|
||||
@@ -589,6 +718,7 @@
|
||||
.filter(Boolean);
|
||||
if (gridImageBrushIndex >= gridImageLibrary.length) gridImageBrushIndex = Math.max(0, gridImageLibrary.length - 1);
|
||||
Object.keys(editorGridImageElByIndex).forEach((k) => { delete editorGridImageElByIndex[k]; });
|
||||
Object.keys(editorGridImageHeldElByIndex).forEach((k) => { delete editorGridImageHeldElByIndex[k]; });
|
||||
sanitizeSpritesInPlace();
|
||||
rebuildGridImageGallery();
|
||||
syncGridImageBrushInputCaps();
|
||||
@@ -959,6 +1089,7 @@
|
||||
gridImageSprites = [];
|
||||
gridImageBrushIndex = 0;
|
||||
Object.keys(editorGridImageElByIndex).forEach((k) => { delete editorGridImageElByIndex[k]; });
|
||||
Object.keys(editorGridImageHeldElByIndex).forEach((k) => { delete editorGridImageHeldElByIndex[k]; });
|
||||
rebuildGridImageGallery();
|
||||
syncGridImageBrushInputCaps();
|
||||
for (let i = 0; i < width; i++) { objects[0][i] = 1; objects[height - 1][i] = 1; }
|
||||
@@ -1469,7 +1600,7 @@
|
||||
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
|
||||
}
|
||||
|
||||
function setCell(x, y, left) {
|
||||
function setCell(x, y, left, mouseEv) {
|
||||
if (drawModeEl.value === 'gauntletPlayerSpawn') {
|
||||
const gt = gameTypeEl ? gameTypeEl.value : gameType;
|
||||
if (gt !== 'gauntlet') return;
|
||||
@@ -1531,6 +1662,24 @@
|
||||
if (statusEl) statusEl.textContent = 'ช่องกำแพง — วางรูปบนกริดไม่ได้';
|
||||
return;
|
||||
}
|
||||
const gtCell = gameTypeEl ? gameTypeEl.value : gameType;
|
||||
if (left && mouseEv && mouseEv.altKey && gtCell === 'quiz_carry') {
|
||||
const hit = findSpriteIndexAtCell(x, y);
|
||||
if (hit >= 0) {
|
||||
const bind = readSpriteCarryBindFromSelect();
|
||||
if (bind == null) delete gridImageSprites[hit].bindCarryOption;
|
||||
else gridImageSprites[hit].bindCarryOption = bind;
|
||||
sanitizeSpritesInPlace();
|
||||
if (statusEl) {
|
||||
statusEl.textContent = bind == null
|
||||
? 'สไปรต์: ผูกแบบอัตโนมัติ (นับจากช่องโซนตัวเลือกที่ทับสไปรต์)'
|
||||
: ('ผูกสไปรต์กับตัวเลือกข้อ ' + bind + ' แล้ว (Alt+คลิกซ้ายอีกครั้งเปลี่ยนค่าในรายการ)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (statusEl) statusEl.textContent = 'ไม่มีสไปรต์ในช่องนี้ — Alt+คลิกบนพื้นที่ที่มีรูป';
|
||||
return;
|
||||
}
|
||||
const wEl = document.getElementById('grid-image-brush-w');
|
||||
const hEl = document.getElementById('grid-image-brush-h');
|
||||
const bw = Math.max(1, parseInt(wEl && wEl.value, 10) || 1);
|
||||
@@ -1727,14 +1876,14 @@
|
||||
document.getElementById('btn-spawn').textContent = 'ตั้งจุดเกิด';
|
||||
return;
|
||||
}
|
||||
setCell(x, y, e.button === 0);
|
||||
setCell(x, y, e.button === 0, e);
|
||||
draw();
|
||||
});
|
||||
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (e.buttons !== 1 && e.buttons !== 2) return;
|
||||
const { x, y } = getCell(e);
|
||||
setCell(x, y, e.buttons === 1);
|
||||
setCell(x, y, e.buttons === 1, e);
|
||||
draw();
|
||||
});
|
||||
|
||||
@@ -1802,6 +1951,7 @@
|
||||
if (drawModeEl && drawModeEl.value === 'lobbyPlayerSpawn' && !supportsLobbySpawnPaint(gameType)) drawModeEl.value = 'wall';
|
||||
toggleFroggerUI();
|
||||
syncGridImageBrushInputCaps();
|
||||
syncDrawModeAuxUi();
|
||||
});
|
||||
|
||||
const btnClearGauntletSpawns = document.getElementById('btn-clear-gauntlet-spawns');
|
||||
@@ -1972,8 +2122,12 @@
|
||||
balloonBossBossSpawn: gameType === 'balloon_boss' && balloonBossBossSpawn && Number.isFinite(balloonBossBossSpawn.x)
|
||||
? { x: Math.floor(balloonBossBossSpawn.x), y: Math.floor(balloonBossBossSpawn.y) }
|
||||
: null,
|
||||
gridImageLibrary: gridImageLibrary.slice(),
|
||||
gridImageSprites: gridImageSprites.map((s) => ({ i: s.i, x: s.x, y: s.y, w: s.w, h: s.h })),
|
||||
gridImageLibrary: gridImageLibrary.map(serializeGridLibEntry).filter((x) => x != null),
|
||||
gridImageSprites: gridImageSprites.map((s) => {
|
||||
const o = { i: s.i, x: s.x, y: s.y, w: s.w, h: s.h };
|
||||
if (s.bindCarryOption != null) o.bindCarryOption = s.bindCarryOption;
|
||||
return o;
|
||||
}),
|
||||
gridImageCells: deriveGridImageCellsFromSprites(),
|
||||
};
|
||||
if (backgroundImage) body.backgroundImage = backgroundImage;
|
||||
@@ -2004,6 +2158,11 @@
|
||||
if (!drawModeEl) return;
|
||||
if (cellColorWrap) cellColorWrap.style.display = drawModeEl.value === 'color' ? 'inline' : 'none';
|
||||
if (gridImageToolsWrap) gridImageToolsWrap.style.display = drawModeEl.value === 'cellImage' ? 'flex' : 'none';
|
||||
const bindWrap = document.getElementById('grid-sprite-carry-bind-wrap');
|
||||
if (bindWrap) {
|
||||
const gt = gameTypeEl ? gameTypeEl.value : gameType;
|
||||
bindWrap.style.display = (drawModeEl.value === 'cellImage' && gt === 'quiz_carry') ? 'flex' : 'none';
|
||||
}
|
||||
}
|
||||
if (drawModeEl) drawModeEl.addEventListener('change', () => { syncDrawModeAuxUi(); syncLobbySpawnAuxUi(); });
|
||||
syncDrawModeAuxUi();
|
||||
@@ -2025,7 +2184,7 @@
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
gridImageLibrary.push(dataUrl);
|
||||
gridImageLibrary.push({ idle: dataUrl, held: null });
|
||||
gridImageBrushIndex = gridImageLibrary.length - 1;
|
||||
delete editorGridImageElByIndex[gridImageBrushIndex];
|
||||
primeEditorGridImage(gridImageBrushIndex);
|
||||
@@ -2038,6 +2197,38 @@
|
||||
e.target.value = '';
|
||||
});
|
||||
}
|
||||
const gridHeldUpload = document.getElementById('grid-cell-image-held-upload');
|
||||
if (gridHeldUpload) {
|
||||
gridHeldUpload.addEventListener('change', (e) => {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
const idx = pendingHeldLibIndex;
|
||||
pendingHeldLibIndex = -1;
|
||||
e.target.value = '';
|
||||
if (!file || idx < 0 || idx >= gridImageLibrary.length) return;
|
||||
if (file.size > 6 * 1024 * 1024) {
|
||||
if (statusEl) statusEl.textContent = 'ไฟล์ใหญ่เกิน 6MB — ลดขนาดก่อนอัปโหลด';
|
||||
return;
|
||||
}
|
||||
const r = new FileReader();
|
||||
r.onload = () => {
|
||||
const dataUrl = r.result;
|
||||
if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:')) return;
|
||||
const idle = gridLibIdleUrl(gridImageLibrary[idx]);
|
||||
if (dataUrl === idle) {
|
||||
if (statusEl) statusEl.textContent = 'ไฟล์นี้เหมือนรูปก่อนหยิบทุกไบต์ — เลือกภาพอื่นสำหรับ «หยิบแล้ว» ถึงจะเห็นต่างในเกม';
|
||||
rebuildGridImageGallery();
|
||||
draw();
|
||||
return;
|
||||
}
|
||||
gridImageLibrary[idx] = { idle, held: dataUrl };
|
||||
delete editorGridImageHeldElByIndex[idx];
|
||||
rebuildGridImageGallery();
|
||||
draw();
|
||||
if (statusEl) statusEl.textContent = 'ตั้งรูปตอนถือสำหรับ #' + (idx + 1) + ' แล้ว (quiz carry — แสดงบนแมปจนกว่าจะปล่อย)';
|
||||
};
|
||||
r.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
const btnRmGridLib = document.getElementById('btn-grid-image-remove-from-lib');
|
||||
if (btnRmGridLib) {
|
||||
btnRmGridLib.addEventListener('click', () => {
|
||||
@@ -2107,12 +2298,13 @@
|
||||
stackLandArea = m.stackLandArea && m.stackLandArea.length ? m.stackLandArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
cellColors = (m.cellColors && m.cellColors.length === height) ? m.cellColors.map(r => r && r.slice ? r.slice() : Array(width).fill(null)) : Array(height).fill(0).map(() => Array(width).fill(null));
|
||||
gridImageLibrary = Array.isArray(m.gridImageLibrary)
|
||||
? m.gridImageLibrary.filter((s) => typeof s === 'string' && s.length > 0 && s.length < 25000000)
|
||||
? m.gridImageLibrary.map(normalizeGridLibEntry).filter((e) => e && gridLibIdleUrl(e) && gridLibIdleUrl(e).length < 25000000)
|
||||
: [];
|
||||
gridImageSprites = migrateSpritesFromMapJson(m, width, height);
|
||||
sanitizeSpritesInPlace();
|
||||
gridImageBrushIndex = Math.min(gridImageBrushIndex, Math.max(0, gridImageLibrary.length - 1));
|
||||
Object.keys(editorGridImageElByIndex).forEach((k) => { delete editorGridImageElByIndex[k]; });
|
||||
Object.keys(editorGridImageHeldElByIndex).forEach((k) => { delete editorGridImageHeldElByIndex[k]; });
|
||||
rebuildGridImageGallery();
|
||||
syncGridImageBrushInputCaps();
|
||||
showMapInGame = m.showMapInGame !== false;
|
||||
|
||||
+315
-65
@@ -85,6 +85,7 @@
|
||||
let mapBackgroundImg = null;
|
||||
/** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */
|
||||
let mapGridImageImgs = [];
|
||||
let mapGridImageHeldImgs = [];
|
||||
let me = { x: 1, y: 1, direction: 'down', nickname: nick, isWalking: false, playTint: null, gauntletScore: 0, tx: null, ty: null, quizCarryHeld: null };
|
||||
const others = new Map();
|
||||
/** Stack preview HUD: เทอร์มินัลล็อก (cyber UI) */
|
||||
@@ -161,6 +162,8 @@
|
||||
const playLayerAllDirsQueued = {};
|
||||
/** จาก GET /api/characters — hasLayerFiles สแกนจากชื่อไฟล์จริง (แม่นกว่า probe จาก <img> อย่างเดียว) */
|
||||
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;
|
||||
@@ -171,7 +174,18 @@
|
||||
const map = Object.create(null);
|
||||
if (Array.isArray(list)) {
|
||||
list.forEach((c) => {
|
||||
if (c && c.id) map[c.id] = !!c.hasLayerFiles;
|
||||
if (!c || !c.id) return;
|
||||
map[c.id] = !!c.hasLayerFiles;
|
||||
if (Array.isArray(c.layers) && c.layers.length) {
|
||||
const st = new Set();
|
||||
c.layers.forEach((n) => {
|
||||
if (typeof n === 'string' && n.length) st.add(n);
|
||||
});
|
||||
if (st.size) playCharDiskLayersById[c.id] = st;
|
||||
else delete playCharDiskLayersById[c.id];
|
||||
} else {
|
||||
delete playCharDiskLayersById[c.id];
|
||||
}
|
||||
});
|
||||
}
|
||||
playCharLayerApi = { status: 'done', map };
|
||||
@@ -190,6 +204,13 @@
|
||||
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() {
|
||||
@@ -369,8 +390,10 @@
|
||||
const cctx = c.getContext('2d');
|
||||
let anyTintColorLayer = false;
|
||||
let skippedShadowWhilePending = false;
|
||||
const diskLayers = playCharDiskLayersSet(id);
|
||||
for (let li = 0; li < PLAY_LAYER_ORDER.length; li++) {
|
||||
const layerName = PLAY_LAYER_ORDER[li];
|
||||
if (diskLayers && layerName !== 'shadow' && !diskLayers.has(layerName)) continue;
|
||||
const urls = layerName === 'shadow'
|
||||
? shadowUrlCandidates(id, dir, frameIndex)
|
||||
: layerUrlCandidates(id, dir, layerName, frameIndex);
|
||||
@@ -400,7 +423,8 @@
|
||||
if (playCharLayerDiscoveryPending(characterId)) return rawImg;
|
||||
|
||||
const apiFlag = playCharLayerFromApi(characterId);
|
||||
const charHasLayerFiles = apiFlag === true || playCharAnyDirectionLayered(characterId);
|
||||
/* API บอกชัดว่าไม่มี *_layer_* → ห้ามประกอบเลเยอร์ (กันยิง URL เช่น layer_face แล้ว 404) */
|
||||
const charHasLayerFiles = apiFlag === true || (apiFlag == null && playCharAnyDirectionLayered(characterId));
|
||||
const cacheKey = [characterId, dir, frameIndex, tint.head, tint.hair, tint.body].join('|');
|
||||
const hit = playLayerCompositeCache.get(cacheKey);
|
||||
if (hit) return hit;
|
||||
@@ -1937,6 +1961,15 @@
|
||||
quizCarryChoicePlaqueThemes = null;
|
||||
resetQuizCarryPlayState();
|
||||
resetQuizBattlePlayState();
|
||||
const grabBtnTeardown = document.getElementById('quiz-carry-grab-btn');
|
||||
if (grabBtnTeardown) {
|
||||
grabBtnTeardown.classList.add('is-hidden');
|
||||
grabBtnTeardown.classList.remove('quiz-carry-grab-btn--active');
|
||||
grabBtnTeardown.classList.remove('quiz-carry-grab-btn--place');
|
||||
grabBtnTeardown.style.pointerEvents = '';
|
||||
const im = grabBtnTeardown.querySelector('img');
|
||||
if (im) im.src = '/Game/img/quiz-carry/btn-grab.png';
|
||||
}
|
||||
}
|
||||
|
||||
function setupPlayQuizUi() {
|
||||
@@ -2141,7 +2174,10 @@
|
||||
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 img = imgUrl ? getQuizCarryChoiceImageCached(imgUrl) : null;
|
||||
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() {
|
||||
@@ -2162,7 +2198,7 @@
|
||||
ctx.fillStyle = fillCol;
|
||||
pathBoard();
|
||||
ctx.fill();
|
||||
if (img && img.complete && img.naturalWidth > 0) {
|
||||
if (drawPlaqueImg) {
|
||||
ctx.save();
|
||||
pathBoard();
|
||||
ctx.clip();
|
||||
@@ -2172,7 +2208,9 @@
|
||||
const iw = Math.max(0, w - padImg * 2);
|
||||
const ih = Math.max(0, h - padImg * 2);
|
||||
if (iw > 0 && ih > 0) {
|
||||
const ir = img.naturalWidth / img.naturalHeight;
|
||||
const nw = drawPlaqueImg.naturalWidth;
|
||||
const nh = drawPlaqueImg.naturalHeight;
|
||||
const ir = nw / nh;
|
||||
let dw;
|
||||
let dh;
|
||||
if (iw / ih > ir) {
|
||||
@@ -2184,7 +2222,7 @@
|
||||
}
|
||||
const dx = ix + (iw - dw) / 2;
|
||||
const dy = iy + (ih - dh) / 2;
|
||||
ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, dx, dy, dw, dh);
|
||||
ctx.drawImage(drawPlaqueImg, 0, 0, nw, nh, dx, dy, dw, dh);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -2329,12 +2367,15 @@
|
||||
if (!b) continue;
|
||||
const choiceText = String(choices[i] || '').trim();
|
||||
const imgUrl = getQuizCarryPlaqueImageUrlForIndex(quizCarryCurrent, i);
|
||||
if (!choiceText && !imgUrl) continue;
|
||||
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 && imgUrl) {
|
||||
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;
|
||||
@@ -2343,7 +2384,7 @@
|
||||
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, { imageUrl: imgUrl });
|
||||
drawQuizCarryNeonPlaque(ctx, sx - minW / 2, sy - minH / 2, minW, minH, [], lineH, padY, i, null, plaqueExtra);
|
||||
continue;
|
||||
}
|
||||
const line = choiceText;
|
||||
@@ -2373,7 +2414,7 @@
|
||||
const padY = 8;
|
||||
let w = Math.ceil(maxLineW + padX * 2);
|
||||
let h = Math.ceil(textLines.length * lh2 + padY * 2);
|
||||
if (imgUrl) {
|
||||
if (imgUrl || plaqueExtra.imageElement) {
|
||||
w = Math.max(w, minPlaqueW);
|
||||
h = Math.max(h, minPlaqueH);
|
||||
}
|
||||
@@ -2382,7 +2423,7 @@
|
||||
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, { imageUrl: imgUrl });
|
||||
drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, textLines, lh2, padY, i, null, plaqueExtra);
|
||||
continue;
|
||||
}
|
||||
for (let ti = 0; ti < textLines.length; ti++) {
|
||||
@@ -2400,7 +2441,7 @@
|
||||
const padY = 8;
|
||||
let w = Math.ceil(Math.max(maxLineW, 52) + padX * 2);
|
||||
let h = Math.ceil(textLines.length * lineH + padY * 2);
|
||||
if (imgUrl) {
|
||||
if (imgUrl || plaqueExtra.imageElement) {
|
||||
w = Math.max(w, minPlaqueW);
|
||||
h = Math.max(h, minPlaqueH);
|
||||
}
|
||||
@@ -2409,7 +2450,7 @@
|
||||
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, { imageUrl: imgUrl });
|
||||
drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, textLines, lineH, padY, i, null, plaqueExtra);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -2531,8 +2572,8 @@
|
||||
const phaseEl = document.getElementById('quiz-game-phase-label');
|
||||
if (!phaseEl || !mapData) return;
|
||||
phaseEl.textContent = quizCarryMapHasAnswerInteractive(mapData)
|
||||
? 'กด F หยิบบนโซนตัวเลือก · กด F ส่งที่โซน interactive (เขียว) — เล่นพร้อมกันได้'
|
||||
: 'กด F หยิบบนโซนตัวเลือก · กด F ส่งเมื่อชิดโซนกลาง (ม่วงเป็นกำแพง) — เล่นพร้อมกันได้';
|
||||
? 'กด F หรือปุ่ม GRAB หยิบบนโซนตัวเลือก · ส่งที่โซน interactive (เขียว) — เล่นพร้อมกันได้'
|
||||
: 'กด F หรือปุ่ม GRAB หยิบบนโซนตัวเลือก · ส่งเมื่อชิดโซนกลาง (ม่วงเป็นกำแพง) — เล่นพร้อมกันได้';
|
||||
}
|
||||
|
||||
function isQuizCarryEmbedCountdownBlockingMovement() {
|
||||
@@ -2965,12 +3006,16 @@
|
||||
playLiveQuizScores[actorId] = (playLiveQuizScores[actorId] || 0) + 1;
|
||||
const lim = previewMode && isQuizCarry() ? quizCarrySessionLength : 0;
|
||||
const willEnd = lim > 0 && (quizCarryRoundsCompleted + 1) >= lim;
|
||||
showQuizCarryToast(willEnd ? 'ถูกต้อง +1 แต้ม — ครบชุดคำถาม' : 'ถูกต้อง +1 แต้ม — สุ่มคำถามใหม่', true);
|
||||
if (!opts.silent) {
|
||||
showQuizCarryToast(willEnd ? 'ถูกต้อง +1 แต้ม — ครบชุดคำถาม' : 'ถูกต้อง +1 แต้ม — สุ่มคำถามใหม่', true);
|
||||
}
|
||||
me.quizCarryHeld = null;
|
||||
others.forEach((o) => { if (o) o.quizCarryHeld = null; });
|
||||
quizCarryAfterRoundResolved();
|
||||
} else {
|
||||
showQuizCarryToast('ผิด — คืนป้ายที่โซนตัวเลือกแล้ว กด F หยิบใหม่ได้', false);
|
||||
if (!opts.silent) {
|
||||
showQuizCarryToast('ผิด — คืนป้ายที่โซนตัวเลือกแล้ว กด F หยิบใหม่ได้', false);
|
||||
}
|
||||
}
|
||||
renderPlayQuizScoreboard(playLiveQuizScores);
|
||||
return true;
|
||||
@@ -2985,7 +3030,9 @@
|
||||
}
|
||||
ent.quizCarryHeld = pickupIdx;
|
||||
const label = (quizCarryCurrent.choices && quizCarryCurrent.choices[pickupIdx]) || String(pickupIdx);
|
||||
showQuizCarryToast('หยิบ: ' + label, true);
|
||||
if (!opts.silent) {
|
||||
showQuizCarryToast('หยิบ: ' + label, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (opts.fromKey && actorId === myId && !opts.silent) {
|
||||
@@ -3006,6 +3053,77 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
function quizCarryGrabCoreGatesOk() {
|
||||
if (!isQuizCarry() || !mapData || !quizCarryCurrent) return false;
|
||||
if (quizCarryPregameActive || quizCarrySessionEnded) return false;
|
||||
if (myId == null) return false;
|
||||
if (isQuizCarryEmbedCountdownBlockingMovement()) return false;
|
||||
if (!quizCarryOptionsPickableNow()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** ยืนบนช่องตัวเลือกและหยิบได้ */
|
||||
function quizCarryGrabPickupAvailable() {
|
||||
if (!quizCarryGrabCoreGatesOk()) return false;
|
||||
if (me.quizCarryHeld != null) return false;
|
||||
const md = mapData;
|
||||
const footprint = quizTilesFootprintPlay(me.x, me.y);
|
||||
let pickupIdx = null;
|
||||
for (const k of footprint) {
|
||||
const p = k.split(',');
|
||||
const tx = +p[0];
|
||||
const ty = +p[1];
|
||||
const opt = quizCarryOptionIndexAtPlay(md, tx, ty);
|
||||
if (opt != null) pickupIdx = pickupIdx === null ? opt : pickupIdx;
|
||||
}
|
||||
return pickupIdx != null && !quizCarryOptionHeldByAnyone(pickupIdx);
|
||||
}
|
||||
|
||||
/** ถือป้ายแล้วยืนโซนส่งได้ */
|
||||
function quizCarryGrabSubmitAvailable() {
|
||||
if (!quizCarryGrabCoreGatesOk()) return false;
|
||||
if (me.quizCarryHeld == null) return false;
|
||||
return quizCarryCanSubmitAnswerAt(mapData, me.x, me.y);
|
||||
}
|
||||
|
||||
/** true = กด F / ปุ่ม จะหยิบหรือส่งได้ทันที */
|
||||
function quizCarryGrabInteractionAvailable() {
|
||||
return quizCarryGrabPickupAvailable() || quizCarryGrabSubmitAvailable();
|
||||
}
|
||||
|
||||
function syncQuizCarryGrabButton() {
|
||||
const btn = document.getElementById('quiz-carry-grab-btn');
|
||||
if (!btn) return;
|
||||
if (!isQuizCarry() || !mapData) {
|
||||
btn.classList.add('is-hidden');
|
||||
btn.classList.remove('quiz-carry-grab-btn--active');
|
||||
btn.classList.remove('quiz-carry-grab-btn--place');
|
||||
btn.setAttribute('aria-disabled', 'true');
|
||||
return;
|
||||
}
|
||||
const grabSrc = BASE + '/img/quiz-carry/btn-grab.png';
|
||||
const placeSrc = BASE + '/img/quiz-carry/btn-drop.png';
|
||||
const img = btn.querySelector('img');
|
||||
btn.classList.remove('is-hidden');
|
||||
const active = quizCarryGrabInteractionAvailable();
|
||||
btn.classList.toggle('quiz-carry-grab-btn--active', active);
|
||||
btn.setAttribute('aria-disabled', active ? 'false' : 'true');
|
||||
btn.style.pointerEvents = active ? '' : 'none';
|
||||
if (img) {
|
||||
if (active && quizCarryGrabSubmitAvailable()) {
|
||||
img.src = placeSrc;
|
||||
btn.classList.add('quiz-carry-grab-btn--place');
|
||||
btn.title = 'ส่ง / วางคำตอบ — เหมือนกด F';
|
||||
btn.setAttribute('aria-label', 'ส่งหรือวางคำตอบ');
|
||||
} else {
|
||||
img.src = grabSrc;
|
||||
btn.classList.remove('quiz-carry-grab-btn--place');
|
||||
btn.title = 'หยิบตัวเลือก — เหมือนกด F';
|
||||
btn.setAttribute('aria-label', 'หยิบตัวเลือก (Grab)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetQuizCarryPlayState() {
|
||||
quizCarryPregameActive = false;
|
||||
hideQuizCarryPregameOverlay();
|
||||
@@ -3027,42 +3145,38 @@
|
||||
});
|
||||
}
|
||||
|
||||
/** รวม carry จากดิสก์ผ่าน Node GET /Game/api/quiz-carry-from-disk (อยู่ใต้ /Game/api/ เพื่อให้ nginx proxy ตรงกับเซิร์ฟอื่น) — ทาง .php ใต้ /Game/ มักโดน fastcgi SCRIPT_FILENAME ผิดจึง 404 */
|
||||
/** รวม carry จากดิสก์ — ใช้เฉพาะ GET ที่ Node รองรับ (path เดียวกับ server.js) ไม่เรียก .php เพื่อกัน 404 บน nginx/php-fpm */
|
||||
async function mergeQuizCarrySettingsFromDisk(s) {
|
||||
const out = s && typeof s === 'object' ? s : {};
|
||||
const urls = [
|
||||
BASE + '/api/quiz-carry-from-disk?_=' + Date.now(),
|
||||
BASE + '/api-quiz-carry-from-disk.php?_=' + (Date.now() + 1),
|
||||
];
|
||||
for (let ui = 0; ui < urls.length; ui++) {
|
||||
try {
|
||||
const rd = await fetch(urls[ui], { cache: 'no-store' });
|
||||
if (!rd.ok) continue;
|
||||
try {
|
||||
const rd = await fetch(BASE + '/api/quiz-carry-from-disk?_=' + Date.now(), { cache: 'no-store' });
|
||||
if (rd.ok) {
|
||||
const disk = await rd.json();
|
||||
if (!disk || typeof disk !== 'object') continue;
|
||||
if (disk.carryMapPanelTheme && typeof disk.carryMapPanelTheme === 'object') {
|
||||
out.carryMapPanelTheme = disk.carryMapPanelTheme;
|
||||
if (disk && typeof disk === 'object') {
|
||||
if (disk.carryMapPanelTheme && typeof disk.carryMapPanelTheme === 'object') {
|
||||
out.carryMapPanelTheme = disk.carryMapPanelTheme;
|
||||
}
|
||||
if (disk.carryEmbedCountdownTheme && typeof disk.carryEmbedCountdownTheme === 'object') {
|
||||
out.carryEmbedCountdownTheme = disk.carryEmbedCountdownTheme;
|
||||
}
|
||||
if (Array.isArray(disk.carryChoicePlaqueThemes) && disk.carryChoicePlaqueThemes.length) {
|
||||
out.carryChoicePlaqueThemes = disk.carryChoicePlaqueThemes;
|
||||
} else if (disk.carryChoicePlaqueTheme && typeof disk.carryChoicePlaqueTheme === 'object') {
|
||||
out.carryChoicePlaqueTheme = disk.carryChoicePlaqueTheme;
|
||||
}
|
||||
if (disk.carryReadMs != null) out.carryReadMs = disk.carryReadMs;
|
||||
if (disk.carryAnswerMs != null) out.carryAnswerMs = disk.carryAnswerMs;
|
||||
if (disk.carrySessionLength != null) out.carrySessionLength = disk.carrySessionLength;
|
||||
const diskPlaqueScale = Number(disk.carryChoicePlaqueMapScale);
|
||||
if (Number.isFinite(diskPlaqueScale)) {
|
||||
out.carryChoicePlaqueMapScale = Math.max(0.85, Math.min(2.5, diskPlaqueScale));
|
||||
}
|
||||
if (Array.isArray(disk.carryQuestions) && disk.carryQuestions.length > 0) {
|
||||
out.carryQuestions = disk.carryQuestions;
|
||||
}
|
||||
}
|
||||
if (disk.carryEmbedCountdownTheme && typeof disk.carryEmbedCountdownTheme === 'object') {
|
||||
out.carryEmbedCountdownTheme = disk.carryEmbedCountdownTheme;
|
||||
}
|
||||
if (Array.isArray(disk.carryChoicePlaqueThemes) && disk.carryChoicePlaqueThemes.length) {
|
||||
out.carryChoicePlaqueThemes = disk.carryChoicePlaqueThemes;
|
||||
} else if (disk.carryChoicePlaqueTheme && typeof disk.carryChoicePlaqueTheme === 'object') {
|
||||
out.carryChoicePlaqueTheme = disk.carryChoicePlaqueTheme;
|
||||
}
|
||||
if (disk.carryReadMs != null) out.carryReadMs = disk.carryReadMs;
|
||||
if (disk.carryAnswerMs != null) out.carryAnswerMs = disk.carryAnswerMs;
|
||||
if (disk.carrySessionLength != null) out.carrySessionLength = disk.carrySessionLength;
|
||||
const diskPlaqueScale = Number(disk.carryChoicePlaqueMapScale);
|
||||
if (Number.isFinite(diskPlaqueScale)) {
|
||||
out.carryChoicePlaqueMapScale = Math.max(0.85, Math.min(2.5, diskPlaqueScale));
|
||||
}
|
||||
if (Array.isArray(disk.carryQuestions) && disk.carryQuestions.length > 0) {
|
||||
out.carryQuestions = disk.carryQuestions;
|
||||
}
|
||||
} catch (e) { /* next url */ }
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -3186,8 +3300,8 @@
|
||||
const leg = document.getElementById('quiz-play-legend');
|
||||
if (leg) {
|
||||
leg.textContent = quizCarryMapHasAnswerInteractive(mapData)
|
||||
? 'แต่ละตัวเลือกมีคนถือได้คนเดียว — ส่งคำตอบที่โซน interactive (เขียว) · โซนกลางม่วงเป็นกำแพง'
|
||||
: 'แต่ละตัวเลือกมีคนถือได้คนเดียว — โซนกลางเป็นกำแพง · ยืนชิดขอบแล้วกด F ส่งเมื่อถือป้าย';
|
||||
? 'แต่ละตัวเลือกมีคนถือได้คนเดียว — ส่งคำตอบที่โซน interactive (เขียว) · โซนกลางม่วงเป็นกำแพง · กด F หรือปุ่ม GRAB มุมขวาล่าง'
|
||||
: 'แต่ละตัวเลือกมีคนถือได้คนเดียว — โซนกลางเป็นกำแพง · ยืนชิดขอบแล้วกด F หรือปุ่ม GRAB ส่งเมื่อถือป้าย';
|
||||
}
|
||||
resetQuizCarryPlayState();
|
||||
if (previewMode) {
|
||||
@@ -3197,6 +3311,7 @@
|
||||
loadQuizCarryPoolAndStart();
|
||||
}
|
||||
renderPlayQuizScoreboard(playLiveQuizScores);
|
||||
syncQuizCarryGrabButton();
|
||||
}
|
||||
|
||||
function pickRandomTileForQuizCarryOption(md, optionIndex, o) {
|
||||
@@ -6077,11 +6192,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
function gridImageLibIdleUrlPlay(entry) {
|
||||
if (entry == null) return '';
|
||||
if (typeof entry === 'string') return entry;
|
||||
if (typeof entry === 'object' && entry.idle) return String(entry.idle);
|
||||
return '';
|
||||
}
|
||||
function gridImageLibHeldUrlPlay(entry) {
|
||||
if (entry == null || typeof entry === 'string') return '';
|
||||
if (typeof entry === 'object' && entry.held && String(entry.held).length) return String(entry.held);
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeGridImageCellsOnMap(md) {
|
||||
if (!md) return;
|
||||
const w = md.width || 20, h = md.height || 15;
|
||||
if (!Array.isArray(md.gridImageLibrary)) md.gridImageLibrary = [];
|
||||
md.gridImageLibrary = md.gridImageLibrary.filter((s) => typeof s === 'string' && s.length > 0 && s.length < 30000000);
|
||||
md.gridImageLibrary = md.gridImageLibrary
|
||||
.map((raw) => {
|
||||
if (typeof raw === 'string' && raw.length > 0 && raw.length < 30000000) return { idle: raw, held: null };
|
||||
if (raw && typeof raw === 'object' && typeof raw.idle === 'string' && raw.idle.length > 0 && raw.idle.length < 30000000) {
|
||||
let held = typeof raw.held === 'string' && raw.held.length > 0 && raw.held.length < 30000000 ? raw.held : null;
|
||||
if (held && held === raw.idle) held = null;
|
||||
return { idle: raw.idle, held };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
const libLen = md.gridImageLibrary.length;
|
||||
let placements = [];
|
||||
if (Array.isArray(md.gridImageSprites) && md.gridImageSprites.length) {
|
||||
@@ -6091,7 +6228,11 @@
|
||||
const y = Math.floor(Number(raw.y));
|
||||
const ww = Math.max(1, Math.floor(Number(raw.w)) || 1);
|
||||
const hh = Math.max(1, Math.floor(Number(raw.h)) || 1);
|
||||
return { i, x, y, w: ww, h: hh };
|
||||
const rawB = raw.bindCarryOption;
|
||||
const bn = typeof rawB === 'number' ? rawB : parseInt(String(rawB), 10);
|
||||
const base = { i, x, y, w: ww, h: hh };
|
||||
if (Number.isFinite(bn) && bn >= 1 && bn <= QUIZ_CARRY_MAX_OPTION_SLOTS) base.bindCarryOption = bn;
|
||||
return base;
|
||||
}).filter((s) => Number.isFinite(s.i) && s.i >= 0 && s.i < libLen &&
|
||||
Number.isFinite(s.x) && Number.isFinite(s.y) && s.w >= 1 && s.h >= 1 &&
|
||||
s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0);
|
||||
@@ -6101,7 +6242,9 @@
|
||||
if (y < 0) { hh += y; y = 0; }
|
||||
if (x + ww > w) ww = w - x;
|
||||
if (y + hh > h) hh = h - y;
|
||||
return { i: s.i, x, y, w: Math.max(1, ww), h: Math.max(1, hh) };
|
||||
const out = { i: s.i, x, y, w: Math.max(1, ww), h: Math.max(1, hh) };
|
||||
if (s.bindCarryOption != null) out.bindCarryOption = s.bindCarryOption;
|
||||
return out;
|
||||
}).filter((s) => s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0);
|
||||
} else {
|
||||
const cells = md.gridImageCells;
|
||||
@@ -6132,18 +6275,102 @@
|
||||
|
||||
function loadMapGridImages() {
|
||||
mapGridImageImgs = [];
|
||||
mapGridImageHeldImgs = [];
|
||||
if (!mapData || !Array.isArray(mapData.gridImageLibrary) || !mapData.gridImageLibrary.length) return;
|
||||
mapData.gridImageLibrary.forEach((src, i) => {
|
||||
mapData.gridImageLibrary.forEach((entry, i) => {
|
||||
mapGridImageImgs[i] = null;
|
||||
if (!src || typeof src !== 'string') return;
|
||||
mapGridImageHeldImgs[i] = null;
|
||||
const idle = gridImageLibIdleUrlPlay(entry);
|
||||
if (!idle) return;
|
||||
const im = new Image();
|
||||
im.onload = () => { try { draw(); } catch (e) {} };
|
||||
im.onerror = () => {};
|
||||
im.src = src;
|
||||
im.src = idle;
|
||||
mapGridImageImgs[i] = im;
|
||||
const held = gridImageLibHeldUrlPlay(entry);
|
||||
if (held && held !== idle) {
|
||||
const hm = new Image();
|
||||
hm.onload = () => { try { draw(); } catch (e) {} };
|
||||
hm.onerror = () => {};
|
||||
hm.src = held;
|
||||
mapGridImageHeldImgs[i] = hm;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** โซนตัวเลือกที่ทับสไปรต์มากที่สุด — quiz_carry สลับรูป idle/held */
|
||||
function dominantQuizCarryOptionInSpriteRect(md, sp) {
|
||||
if (!md || md.gameType !== 'quiz_carry' || !md.quizCarryOptionArea) return null;
|
||||
const g = md.quizCarryOptionArea;
|
||||
const mw = md.width || 20, mh = md.height || 15;
|
||||
const x0 = sp.x | 0, y0 = sp.y | 0, ww = sp.w | 0, hh = sp.h | 0;
|
||||
const counts = new Map();
|
||||
for (let yy = y0; yy < y0 + hh; yy++) {
|
||||
if (yy < 0 || yy >= mh) continue;
|
||||
const row = g[yy];
|
||||
if (!row) continue;
|
||||
for (let xx = x0; xx < x0 + ww; xx++) {
|
||||
if (xx < 0 || xx >= mw) continue;
|
||||
const v = row[xx];
|
||||
const n = typeof v === 'number' ? v : parseInt(String(v), 10);
|
||||
if (!Number.isFinite(n) || n < 1 || n > QUIZ_CARRY_MAX_OPTION_SLOTS) continue;
|
||||
const idx = n - 1;
|
||||
counts.set(idx, (counts.get(idx) || 0) + 1);
|
||||
}
|
||||
}
|
||||
if (!counts.size) return null;
|
||||
let best = null;
|
||||
let bestN = -1;
|
||||
counts.forEach((n, k) => {
|
||||
if (best == null || n > bestN || (n === bestN && k < best)) {
|
||||
bestN = n;
|
||||
best = k;
|
||||
}
|
||||
});
|
||||
return best;
|
||||
}
|
||||
|
||||
/** 0-based choice index สำหรับสลับ idle/held — ใช้ bindCarryOption จากแมปก่อน แล้วค่อยนับทับโซน */
|
||||
function quizCarryOptionIndexForGridSprite(md, sp) {
|
||||
if (!sp) return null;
|
||||
const rawB = sp.bindCarryOption;
|
||||
if (rawB != null && rawB !== '') {
|
||||
const n = typeof rawB === 'number' ? rawB : parseInt(String(rawB), 10);
|
||||
if (Number.isFinite(n) && n >= 1 && n <= QUIZ_CARRY_MAX_OPTION_SLOTS) return n - 1;
|
||||
}
|
||||
return dominantQuizCarryOptionInSpriteRect(md, sp);
|
||||
}
|
||||
|
||||
/** รูป held จากคลังกริดสำหรับข้อนี้ — ใช้วาดป้ายติดตัว (ไม่ผ่าน sanitize URL) */
|
||||
function findQuizCarryGridHeldImgForChoiceIndex(choiceIdx) {
|
||||
if (choiceIdx == null || choiceIdx < 0 || !isQuizCarry() || !mapData) return null;
|
||||
const pl = mapData.gridImagePlacements;
|
||||
if (!Array.isArray(pl) || !pl.length) return null;
|
||||
for (let gi = 0; gi < pl.length; gi++) {
|
||||
const sp = pl[gi];
|
||||
const opt = quizCarryOptionIndexForGridSprite(mapData, sp);
|
||||
if (opt !== choiceIdx) continue;
|
||||
const gh = mapGridImageHeldImgs[sp.i];
|
||||
if (gh && gh.complete && gh.naturalWidth) return gh;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** รูป idle จากคลังกริดสำหรับข้อนี้ — วาดบนป้ายตัวเลือกบนพื้นเมื่อไม่มีรูปจากคำถาม/ธีม */
|
||||
function findQuizCarryGridIdleImgForChoiceIndex(choiceIdx) {
|
||||
if (choiceIdx == null || choiceIdx < 0 || !isQuizCarry() || !mapData) return null;
|
||||
const pl = mapData.gridImagePlacements;
|
||||
if (!Array.isArray(pl) || !pl.length) return null;
|
||||
for (let gi = 0; gi < pl.length; gi++) {
|
||||
const sp = pl[gi];
|
||||
const opt = quizCarryOptionIndexForGridSprite(mapData, sp);
|
||||
if (opt !== choiceIdx) continue;
|
||||
const gid = mapGridImageImgs[sp.i];
|
||||
if (gid && gid.complete && gid.naturalWidth) return gid;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function applyMapAndStart(gameMapData, res) {
|
||||
const prevWasSpaceShooter = mapData && mapData.gameType === 'space_shooter';
|
||||
const prevWasBalloonBoss = mapData && mapData.gameType === 'balloon_boss';
|
||||
@@ -8063,7 +8290,14 @@
|
||||
const ww = sp.w * tileSize;
|
||||
const wh = sp.h * tileSize;
|
||||
if (wx0 + ww <= worldMinX || wx0 >= worldMaxX || wy0 + wh <= worldMinY || wy0 >= worldMaxY) continue;
|
||||
const gim = mapGridImageImgs[sp.i];
|
||||
let gim = mapGridImageImgs[sp.i];
|
||||
if (isQuizCarry() && quizCarryCurrent) {
|
||||
const domOpt = quizCarryOptionIndexForGridSprite(mapData, sp);
|
||||
if (domOpt != null && quizCarryOptionHeldByAnyone(domOpt)) {
|
||||
const gh = mapGridImageHeldImgs[sp.i];
|
||||
if (gh && gh.complete && gh.naturalWidth) gim = gh;
|
||||
}
|
||||
}
|
||||
if (!gim || !gim.complete || !gim.naturalWidth) continue;
|
||||
const sx0 = (wx0 - camX) * zDraw + canvas.width / 2;
|
||||
const sy0 = (wy0 - camY) * zDraw + canvas.height / 2;
|
||||
@@ -8386,10 +8620,11 @@
|
||||
}
|
||||
|
||||
/** ป้ายติดตัว — กึ่งกลางลำตัว (ทับอก/เอว) แบบเดียวกับป้ายบนพื้น ไม่มีเส้นเชื่อม */
|
||||
function drawQuizCarryHeldSignBoard(sxFeet, feetSy, halfSpriteW, spriteTopY, signText, _faceDir, choiceIndex, imageUrl) {
|
||||
function drawQuizCarryHeldSignBoard(sxFeet, feetSy, halfSpriteW, spriteTopY, signText, _faceDir, choiceIndex, imageUrl, imageElementOpt) {
|
||||
const imgUrlSan = sanitizeQuizCarryImageUrlClient(imageUrl);
|
||||
const gridHeldDraw = imageElementOpt && imageElementOpt.complete && imageElementOpt.naturalWidth > 0 ? imageElementOpt : null;
|
||||
const raw = String(signText || '').trim();
|
||||
if (!raw && !imgUrlSan) return;
|
||||
if (!raw && !imgUrlSan && !gridHeldDraw) return;
|
||||
ctx.save();
|
||||
const heldPs = (() => {
|
||||
const sc = Number(quizCarryPlaqueMapScale);
|
||||
@@ -8430,7 +8665,7 @@
|
||||
const lineH = fontPx * 1.22;
|
||||
let w;
|
||||
let h;
|
||||
if (!lines.length && imgUrlSan) {
|
||||
if (!lines.length && (imgUrlSan || gridHeldDraw)) {
|
||||
w = Math.ceil(Math.min(maxPlaqueW, Math.max(104, maxInner + padX * 2)));
|
||||
h = Math.ceil(Math.max(56, fontPx * 2.8 + padY * 2));
|
||||
} else {
|
||||
@@ -8448,7 +8683,7 @@
|
||||
if (signY < 6) signY = 6;
|
||||
if (signY + h > feetSy + 8) signY = feetSy + 8 - h;
|
||||
|
||||
drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, null, { imageUrl: imgUrlSan });
|
||||
drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, null, { imageUrl: imgUrlSan, imageElement: gridHeldDraw });
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
@@ -8513,6 +8748,7 @@
|
||||
sx, sy, halfSpriteW, spriteTopY,
|
||||
quizCarrySignPayload.text, dir, quizCarrySignPayload.choiceIndex,
|
||||
quizCarrySignPayload.imageUrl,
|
||||
quizCarrySignPayload.imageElement,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8523,8 +8759,9 @@
|
||||
if (!Array.isArray(ch) || idx < 0 || idx >= ch.length) return null;
|
||||
const t = String(ch[idx] != null ? ch[idx] : '').trim();
|
||||
const imgUrl = getQuizCarryPlaqueImageUrlForIndex(quizCarryCurrent, idx);
|
||||
if (!t && !imgUrl) return null;
|
||||
return { text: t, choiceIndex: idx, imageUrl: imgUrl };
|
||||
const gridHeldEl = findQuizCarryGridHeldImgForChoiceIndex(idx);
|
||||
if (!t && !imgUrl && !gridHeldEl) return null;
|
||||
return { text: t, choiceIndex: idx, imageUrl: imgUrl, imageElement: gridHeldEl || undefined };
|
||||
}
|
||||
const safeX = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1);
|
||||
const safeY = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1);
|
||||
@@ -8654,6 +8891,7 @@
|
||||
syncQuizCarryEmbedCountdownLayout();
|
||||
}
|
||||
syncPlayCyberHud();
|
||||
syncQuizCarryGrabButton();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
@@ -8950,6 +9188,18 @@
|
||||
}
|
||||
});
|
||||
})();
|
||||
(function wireQuizCarryGrabButton() {
|
||||
const btn = document.getElementById('quiz-carry-grab-btn');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', (ev) => {
|
||||
if (!isQuizCarry() || !mapData || isChatFocused()) return;
|
||||
if (!btn.classList.contains('quiz-carry-grab-btn--active')) return;
|
||||
ev.preventDefault();
|
||||
if (myId == null) return;
|
||||
tryQuizCarryInteractionForPlayer(myId, me.x, me.y, { fromKey: true });
|
||||
});
|
||||
})();
|
||||
|
||||
(function wireQuizCarryPregameOverlay() {
|
||||
const primary = document.getElementById('quiz-carry-pregame-primary');
|
||||
if (!primary) return;
|
||||
|
||||
@@ -27,6 +27,47 @@
|
||||
display: block;
|
||||
touch-action: none;
|
||||
}
|
||||
/* Quiz carry — ปุ่ม GRAB มุมขวาล่าง (เทียบ F) · mockup ≈ 10–12% ความสูงจอ */
|
||||
#quiz-carry-grab-btn {
|
||||
position: absolute;
|
||||
right: max(10px, env(safe-area-inset-right, 0px));
|
||||
bottom: max(10px, env(safe-area-inset-bottom, 0px));
|
||||
z-index: 60;
|
||||
width: min(12vh, 13vw, 168px);
|
||||
height: min(12vh, 13vw, 168px);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
opacity: 0.38;
|
||||
transition: opacity 0.2s ease, transform 0.15s ease;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
#quiz-carry-grab-btn.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
#quiz-carry-grab-btn.quiz-carry-grab-btn--active {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
#quiz-carry-grab-btn.quiz-carry-grab-btn--active:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
#quiz-carry-grab-btn.quiz-carry-grab-btn--place.quiz-carry-grab-btn--active {
|
||||
filter: drop-shadow(0 0 10px rgba(94, 234, 255, 0.45));
|
||||
}
|
||||
#quiz-carry-grab-btn img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
#quiz-game-overlay {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
@@ -840,6 +881,9 @@
|
||||
</div>
|
||||
<div class="play-canvas-stack" id="play-canvas-stack">
|
||||
<canvas id="game-canvas"></canvas>
|
||||
<button type="button" id="quiz-carry-grab-btn" class="is-hidden" aria-label="หยิบหรือส่งคำตอบ (Grab)" title="หยิบ / ส่งคำตอบ — เหมือนกด F">
|
||||
<img src="/Game/img/quiz-carry/btn-grab.png" alt="" width="256" height="256" decoding="async" />
|
||||
</button>
|
||||
<div id="quiz-map-question-panel" class="is-hidden" aria-hidden="true">
|
||||
<p id="quiz-map-question-text"></p>
|
||||
</div>
|
||||
@@ -936,7 +980,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0166"></script>
|
||||
<script src="js/play.js?v=0.123"></script>
|
||||
<script src="js/play.js?v=0.133"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-1
@@ -1757,7 +1757,16 @@ const server = http.createServer((req, res) => {
|
||||
const list = idArr.map((id) => {
|
||||
const prefix = id + '_';
|
||||
const hasLayerFiles = files.some((f) => f.startsWith(prefix) && f.includes('_layer_') && ext.test(f));
|
||||
return { id, name: id, hasLayerFiles };
|
||||
const layerNameSet = new Set();
|
||||
if (hasLayerFiles) {
|
||||
files.forEach((f) => {
|
||||
if (!f.startsWith(prefix) || !ext.test(f)) return;
|
||||
const 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() : [];
|
||||
return { id, name: id, hasLayerFiles, layers };
|
||||
});
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
return res.end(JSON.stringify(list));
|
||||
|
||||
Reference in New Issue
Block a user