push image to editor
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -265,6 +265,91 @@ button { background: #7aa2f7; color: #1a1b26; cursor: pointer; border: none; }
|
||||
align-items: center;
|
||||
gap: 0.35rem 0.75rem;
|
||||
}
|
||||
.editor-grid-image-tools {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
max-width: 100%;
|
||||
width: min(100%, 520px);
|
||||
}
|
||||
.editor-grid-image-size-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
.editor-grid-image-size-row input[type="number"] {
|
||||
width: 3.2rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
.editor-grid-image-gallery-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.editor-grid-image-gallery-label {
|
||||
font-size: 0.72rem;
|
||||
color: #a9b1d6;
|
||||
font-weight: 600;
|
||||
}
|
||||
.grid-cell-image-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
|
||||
gap: 0.4rem;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 0.35rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(65, 72, 104, 0.85);
|
||||
background: rgba(15, 16, 24, 0.6);
|
||||
}
|
||||
.grid-cell-image-gallery-empty {
|
||||
font-size: 0.72rem;
|
||||
color: #787c99;
|
||||
padding: 0.35rem;
|
||||
}
|
||||
.grid-cell-image-gallery-item {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: #24283b;
|
||||
line-height: 0;
|
||||
}
|
||||
.grid-cell-image-gallery-item.is-selected {
|
||||
border-color: #9ece6a;
|
||||
box-shadow: 0 0 0 1px rgba(158, 206, 106, 0.35);
|
||||
}
|
||||
.grid-cell-image-gallery-item img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.grid-cell-image-gallery-cap {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
text-align: center;
|
||||
padding: 0.15rem 0;
|
||||
color: #c0caf5;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.editor-grid-image-upload-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
body.editor-embed-admin .grid-cell-image-gallery {
|
||||
max-height: 160px;
|
||||
}
|
||||
body.editor-embed-admin .grid-cell-image-gallery-item img {
|
||||
height: 48px;
|
||||
}
|
||||
.editor-btn-primary {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<title>Editor ฉาก — Game</title>
|
||||
<link rel="stylesheet" href="css/style.css?v=0.0148">
|
||||
<link rel="stylesheet" href="css/style.css?v=20260427-gallery">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container editor-page">
|
||||
@@ -56,12 +58,14 @@
|
||||
|
||||
<section class="editor-card editor-card--wide" aria-labelledby="editor-card-draw-label">
|
||||
<h2 class="editor-card__title" id="editor-card-draw-label">เครื่องมือวาด</h2>
|
||||
<p class="editor-draw-build-hint" style="margin:0 0 0.4rem;font-size:0.74rem;color:#9ece6a;line-height:1.35;">มีโหมด <strong>รูปบนกริด</strong> ในเมนู «โหมดวาด» — ถ้าไม่เห็นข้อความนี้หรือไม่มีเมนูนั้น = หน้าเว็บยังเป็นไฟล์เก่า (ลอง <strong>Ctrl+F5</strong> หรืออัปโหลด <code>editor.html</code> / <code>js/editor.js</code> ขึ้นเซิร์ฟใหม่)</p>
|
||||
<div class="editor-card__row editor-card__row--wrap">
|
||||
<label class="editor-field-drawmode">โหมดวาด
|
||||
<select id="draw-mode" title="จัดกลุ่มตามประเภท — ตัวเลือกที่ไม่ใช้กับโหมดเกมจะถูกซ่อน">
|
||||
<optgroup label="พื้นฐาน / ทุกโหมด">
|
||||
<option value="wall">กำแพง (เดินไม่ได้)</option>
|
||||
<option value="noOverlap">โซนห้ามซ้อนผู้เล่น</option>
|
||||
<option value="cellImage">รูปบนกริด (อัปโหลด / เลือกจากคลัง)</option>
|
||||
<option value="interactive">โซน interactive (จุดกด/ใช้ได้)</option>
|
||||
<option value="spawnArea" id="draw-mode-option-spawn-area">พื้นที่สุ่มจุดเกิด (ฟ้า — ผู้เล่นใหม่สุ่มในช่องนี้)</option>
|
||||
<option value="startGame" id="draw-mode-option-start-game">พื้นที่เริ่มเกม (host ยืนแล้วกดเริ่มได้)</option>
|
||||
@@ -118,7 +122,21 @@
|
||||
<label>สี <input type="color" id="cell-color" value="#24283b" title="สี"></label>
|
||||
<label>Alpha <input type="range" id="cell-alpha" min="0" max="100" value="100" title="ความโปร่งใส 0=ใส 100=ทึบ"> <span id="cell-alpha-value">100</span>%</label>
|
||||
</span>
|
||||
<label class="editor-toggle"><input type="checkbox" id="show-map-in-game" checked> โชว์กริดในเกม</label>
|
||||
<div id="editor-grid-image-tools" class="editor-grid-image-tools" style="display:none;">
|
||||
<div class="editor-grid-image-size-row">
|
||||
<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 class="editor-grid-image-gallery-wrap">
|
||||
<span class="editor-grid-image-gallery-label">คลังรูป (คลิกเลือก)</span>
|
||||
<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>
|
||||
<button type="button" id="btn-grid-image-remove-from-lib" title="ลบรูปที่เลือกในคลัง (สไปรต์ที่ใช้รูปนี้จะหาย)">ลบจากคลัง</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="editor-toggle" title="ปิด = ไม่วาดกริด/ช่องฉาก แต่ยังแสดงรูปที่วาดบนกริด · Off = hide tile grid, grid images still show"><input type="checkbox" id="show-map-in-game" checked> โชว์กริดในเกม</label>
|
||||
<button type="button" id="btn-clear-gauntlet-spawns" hidden title="ล้างรายการจุดเกิดพรมแดงที่กำหนดเอง">ล้างลำดับ 1–6</button>
|
||||
<label id="shooter-slot-paint-wrap" class="editor-field-grow" style="display:none;" title="ช่องผู้เล่นลำดับที่ 1–6 ตรงกับคนในเกม (คุณ = ลำดับแรกจาก join)">ช่องเกิดยาน P
|
||||
<select id="shooter-slot-paint">
|
||||
@@ -178,6 +196,7 @@
|
||||
<li><strong>กระโดดให้รอด</strong>: โหมด <strong>แพลตฟอร์ม</strong> (ฟ้าอมเขียว) · กำแพง = ขอบซ้ายขวา/บน · Space / W = กระโดด</li>
|
||||
<li><strong>ลูกโป้งยิงบอส</strong>: จุดเกิดผู้เล่น P1–P6 + <strong>จุดเกิดบอส</strong> 1 ช่อง · ในเกมเดินช้า ยิงมีดีเลย์ · ลูกโป้งหมด = ตกรอบ</li>
|
||||
<li><strong>สีช่อง</strong>: เลือกโหมดสี + Alpha แล้วคลิกบนกริด</li>
|
||||
<li><strong>รูปบนกริด</strong>: โหมด «รูปบนกริด» — ตั้งกว้าง×สูง (ช่อง) · เลือกรูปจากแกลเลอรี · คลิกซ้ายวาง (ทับสไปรต์ที่ทับกัน) · คลิกขาวางลบทั้งแผ่น · บันทึกฉากเก็บเป็น <code>gridImageSprites</code> (ไฟล์ใหญ่ = JSON ใหญ่)</li>
|
||||
<li>ปิด «โชว์กริดในเกม» = ซ่อนกริด/กำแพงในเกม (รูปพื้นหลังยังแสดง)</li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -187,8 +206,8 @@
|
||||
<p class="legend" id="editor-status">บันทึกแล้วจะได้รหัสฉาก ใช้รหัสนี้ตอนสร้างห้องในล็อบบี้</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0104"></script>
|
||||
<script src="js/editor.js?v=0.040"></script>
|
||||
<script src="js/version.js?v=0.0169"></script>
|
||||
<script src="js/editor.js?v=20260428-zones-over-grid"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -86,6 +86,12 @@
|
||||
let showMapInGame = true;
|
||||
let backgroundImage = null;
|
||||
let backgroundImageImg = null;
|
||||
/** คลังรูป + สไปรต์ { i, x, y, w, h } — บันทึก gridImageSprites + gridImageCells (สำหรับรุ่นเก่า) */
|
||||
let gridImageLibrary = [];
|
||||
let gridImageSprites = [];
|
||||
let gridImageBrushIndex = 0;
|
||||
const editorGridImageElByIndex = {};
|
||||
const GRID_IMG_MAX_WH = 24;
|
||||
let isSpawnMode = false;
|
||||
let editorFroggerAnimationId = null;
|
||||
|
||||
@@ -358,6 +364,184 @@
|
||||
}
|
||||
}
|
||||
|
||||
function syncGridImageBrushInputCaps() {
|
||||
const wEl = document.getElementById('grid-image-brush-w');
|
||||
const hEl = document.getElementById('grid-image-brush-h');
|
||||
const cap = Math.min(GRID_IMG_MAX_WH, width, height);
|
||||
if (wEl) {
|
||||
wEl.max = String(Math.max(1, cap));
|
||||
wEl.min = '1';
|
||||
const vw = parseInt(wEl.value, 10);
|
||||
if (!Number.isFinite(vw) || vw < 1) wEl.value = '1';
|
||||
else if (vw > cap) wEl.value = String(cap);
|
||||
}
|
||||
if (hEl) {
|
||||
hEl.max = String(Math.max(1, cap));
|
||||
hEl.min = '1';
|
||||
const vh = parseInt(hEl.value, 10);
|
||||
if (!Number.isFinite(vh) || vh < 1) hEl.value = '1';
|
||||
else if (vh > cap) hEl.value = String(cap);
|
||||
}
|
||||
}
|
||||
|
||||
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
|
||||
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
||||
}
|
||||
|
||||
function deriveGridImageCellsFromSprites() {
|
||||
const cells = Array(height).fill(0).map(() => Array(width).fill(null));
|
||||
gridImageSprites.forEach((s) => {
|
||||
const i = s.i;
|
||||
if (!Number.isFinite(i) || i < 0 || i >= gridImageLibrary.length) return;
|
||||
for (let yy = s.y; yy < s.y + s.h; yy++) {
|
||||
for (let xx = s.x; xx < s.x + s.w; xx++) {
|
||||
if (yy >= 0 && yy < height && xx >= 0 && xx < width) cells[yy][xx] = i;
|
||||
}
|
||||
}
|
||||
});
|
||||
return cells;
|
||||
}
|
||||
|
||||
function migrateSpritesFromMapJson(m, mapW, mapH) {
|
||||
if (Array.isArray(m.gridImageSprites) && m.gridImageSprites.length) {
|
||||
return m.gridImageSprites.map((raw) => {
|
||||
const i = Math.max(0, Math.floor(Number(raw.i)));
|
||||
const x = Math.floor(Number(raw.x));
|
||||
const y = Math.floor(Number(raw.y));
|
||||
const 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 };
|
||||
}).filter((s) => Number.isFinite(s.x) && Number.isFinite(s.y));
|
||||
}
|
||||
const libLen = (Array.isArray(m.gridImageLibrary) && m.gridImageLibrary.length) || 0;
|
||||
const gic = m.gridImageCells;
|
||||
const out = [];
|
||||
if (!Array.isArray(gic) || !libLen) return out;
|
||||
const mh = Number.isFinite(mapH) && mapH > 0 ? Math.floor(mapH) : gic.length;
|
||||
const mw = Number.isFinite(mapW) && mapW > 0 ? Math.floor(mapW) : (gic[0] && gic[0].length) || 0;
|
||||
for (let yy = 0; yy < Math.min(gic.length, mh); yy++) {
|
||||
const row = gic[yy];
|
||||
if (!row) continue;
|
||||
for (let xx = 0; xx < Math.min(row.length, mw || row.length); xx++) {
|
||||
if (row[xx] == null) continue;
|
||||
const iv = parseInt(row[xx], 10);
|
||||
if (!Number.isFinite(iv) || iv < 0 || iv >= libLen) continue;
|
||||
out.push({ i: iv, x: xx, y: yy, w: 1, h: 1 });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function clampSpriteToMap(s) {
|
||||
let { i, x, y, w, h } = 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 };
|
||||
}
|
||||
|
||||
function sanitizeSpritesInPlace() {
|
||||
gridImageSprites = gridImageSprites.map(clampSpriteToMap).filter((sp) => {
|
||||
if (!Number.isFinite(sp.i) || sp.i < 0 || sp.i >= gridImageLibrary.length) return false;
|
||||
if (sp.w < 1 || sp.h < 1) return false;
|
||||
return sp.x < width && sp.y < height && sp.x + sp.w > 0 && sp.y + sp.h > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function removeSpritesOverlappingRect(x0, y0, w0, h0) {
|
||||
gridImageSprites = gridImageSprites.filter((sp) => !rectsOverlap(x0, y0, w0, h0, sp.x, sp.y, sp.w, sp.h));
|
||||
}
|
||||
|
||||
function findSpriteIndexAtCell(px, py) {
|
||||
for (let k = gridImageSprites.length - 1; k >= 0; k--) {
|
||||
const s = gridImageSprites[k];
|
||||
if (px >= s.x && py >= s.y && px < s.x + s.w && py < s.y + s.h) return k;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function canPlaceSpriteAt(anchorX, anchorY, sw, sh, libIdx) {
|
||||
if (!Number.isFinite(libIdx) || libIdx < 0 || libIdx >= gridImageLibrary.length) return null;
|
||||
const ax = Math.max(0, Math.min(Math.floor(anchorX), width - 1));
|
||||
const ay = Math.max(0, Math.min(Math.floor(anchorY), height - 1));
|
||||
const cap = Math.min(GRID_IMG_MAX_WH, width, height);
|
||||
let w = Math.max(1, Math.min(Math.floor(sw) || 1, width - ax, cap));
|
||||
let h = Math.max(1, Math.min(Math.floor(sh) || 1, height - ay, cap));
|
||||
for (let yy = ay; yy < ay + h; yy++) {
|
||||
for (let xx = ax; xx < ax + w; xx++) {
|
||||
if (objects[yy][xx] === 1) return null;
|
||||
}
|
||||
}
|
||||
return { i: libIdx, x: ax, y: ay, w, h };
|
||||
}
|
||||
|
||||
function rebuildGridImageGallery() {
|
||||
const gal = document.getElementById('grid-cell-image-gallery');
|
||||
if (!gal) return;
|
||||
gal.innerHTML = '';
|
||||
if (!gridImageLibrary.length) {
|
||||
gal.innerHTML = '<span class="grid-cell-image-gallery-empty">ยังไม่มีรูป — อัปโหลดด้านล่าง</span>';
|
||||
return;
|
||||
}
|
||||
if (gridImageBrushIndex >= gridImageLibrary.length) gridImageBrushIndex = gridImageLibrary.length - 1;
|
||||
gridImageLibrary.forEach((url, idx) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'grid-cell-image-gallery-item' + (idx === gridImageBrushIndex ? ' is-selected' : '');
|
||||
btn.title = 'รูป #' + (idx + 1) + ' — คลิกเลือก';
|
||||
btn.dataset.libIndex = String(idx);
|
||||
const thumb = document.createElement('img');
|
||||
thumb.alt = '';
|
||||
thumb.src = url;
|
||||
thumb.loading = 'lazy';
|
||||
btn.appendChild(thumb);
|
||||
const cap = document.createElement('span');
|
||||
cap.className = 'grid-cell-image-gallery-cap';
|
||||
cap.textContent = '#' + (idx + 1);
|
||||
btn.appendChild(cap);
|
||||
btn.addEventListener('click', () => {
|
||||
gridImageBrushIndex = idx;
|
||||
Array.from(gal.querySelectorAll('.grid-cell-image-gallery-item')).forEach((el) => {
|
||||
el.classList.toggle('is-selected', el.dataset.libIndex === String(idx));
|
||||
});
|
||||
});
|
||||
gal.appendChild(btn);
|
||||
primeEditorGridImage(idx);
|
||||
});
|
||||
}
|
||||
|
||||
function primeEditorGridImage(idx) {
|
||||
if (editorGridImageElByIndex[idx] || !gridImageLibrary[idx]) return;
|
||||
const im = new Image();
|
||||
im.onload = () => { draw(); };
|
||||
im.onerror = () => { delete editorGridImageElByIndex[idx]; };
|
||||
im.src = gridImageLibrary[idx];
|
||||
editorGridImageElByIndex[idx] = im;
|
||||
}
|
||||
|
||||
function removeGridImageFromLibraryAt(index) {
|
||||
const i = parseInt(index, 10);
|
||||
if (!Number.isFinite(i) || i < 0 || i >= gridImageLibrary.length) return;
|
||||
gridImageLibrary.splice(i, 1);
|
||||
gridImageSprites = gridImageSprites
|
||||
.map((s) => {
|
||||
if (s.i === i) return null;
|
||||
if (s.i > i) return { ...s, i: s.i - 1 };
|
||||
return s;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (gridImageBrushIndex >= gridImageLibrary.length) gridImageBrushIndex = Math.max(0, gridImageLibrary.length - 1);
|
||||
Object.keys(editorGridImageElByIndex).forEach((k) => { delete editorGridImageElByIndex[k]; });
|
||||
sanitizeSpritesInPlace();
|
||||
rebuildGridImageGallery();
|
||||
syncGridImageBrushInputCaps();
|
||||
draw();
|
||||
if (statusEl) statusEl.textContent = 'ลบรูปออกจากคลังแล้ว';
|
||||
}
|
||||
|
||||
function buildDefaultLanes() {
|
||||
const arr = [];
|
||||
for (let y = 0; y < height; y++) {
|
||||
@@ -706,6 +890,12 @@
|
||||
balloonBossPlayerSlots = Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
balloonBossBossSpawn = null;
|
||||
cellColors = Array(height).fill(0).map(() => Array(width).fill(null));
|
||||
gridImageLibrary = [];
|
||||
gridImageSprites = [];
|
||||
gridImageBrushIndex = 0;
|
||||
Object.keys(editorGridImageElByIndex).forEach((k) => { delete editorGridImageElByIndex[k]; });
|
||||
rebuildGridImageGallery();
|
||||
syncGridImageBrushInputCaps();
|
||||
for (let i = 0; i < width; i++) { objects[0][i] = 1; objects[height - 1][i] = 1; }
|
||||
for (let i = 0; i < height; i++) { objects[i][0] = 1; objects[i][width - 1] = 1; }
|
||||
spawn = { x: 1, y: 1 };
|
||||
@@ -726,6 +916,8 @@
|
||||
sanitizeGauntletPlayerSpawns();
|
||||
ensureShooterSpawnSlots();
|
||||
ensureBalloonBossPlayerSlots();
|
||||
sanitizeSpritesInPlace();
|
||||
syncGridImageBrushInputCaps();
|
||||
draw();
|
||||
}
|
||||
|
||||
@@ -805,6 +997,50 @@
|
||||
ctx.fillRect(tx, ty, tileSize, tileSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gridImageSprites.forEach((sp) => {
|
||||
primeEditorGridImage(sp.i);
|
||||
const gim = editorGridImageElByIndex[sp.i];
|
||||
if (!gim || !gim.complete || !gim.naturalWidth) return;
|
||||
const tx = sp.x * tileSize;
|
||||
const ty = sp.y * tileSize;
|
||||
const tw = sp.w * tileSize;
|
||||
const th = sp.h * tileSize;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(tx, ty, tw, th);
|
||||
ctx.clip();
|
||||
ctx.drawImage(gim, tx, ty, tw, th);
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.22)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(tx + 0.5, ty + 0.5, tw - 1, th - 1);
|
||||
});
|
||||
for (let y = 0; y < height; y++) {
|
||||
var rowFill = null;
|
||||
if (usesLanes && lanes[y]) {
|
||||
if (lanes[y].type === 'goal') rowFill = 'rgba(158,206,106,0.35)';
|
||||
else if (lanes[y].type === 'spawn') rowFill = 'rgba(187,154,247,0.35)';
|
||||
else if (lanes[y].type === 'road') rowFill = 'rgba(120,100,80,0.5)';
|
||||
else if (lanes[y].type === 'water') rowFill = 'rgba(125,207,255,0.4)';
|
||||
} else if (isGauntlet) {
|
||||
rowFill = (y % 2 === 0) ? 'rgba(247,118,190,0.1)' : 'rgba(160,80,120,0.08)';
|
||||
} else if (isStack) {
|
||||
rowFill = (y % 2 === 0) ? 'rgba(122, 162, 247, 0.09)' : 'rgba(187, 154, 247, 0.07)';
|
||||
} else if (isQuizCarry) {
|
||||
rowFill = (y % 2 === 0) ? 'rgba(100, 180, 160, 0.06)' : 'rgba(80, 140, 200, 0.05)';
|
||||
} else if (isQuizBattle) {
|
||||
rowFill = (y % 2 === 0) ? 'rgba(120, 190, 255, 0.07)' : 'rgba(255, 120, 150, 0.06)';
|
||||
} else if (isJumpSurvive) {
|
||||
rowFill = (y % 2 === 0) ? 'rgba(90, 200, 255, 0.07)' : 'rgba(60, 140, 200, 0.06)';
|
||||
} else if (isSpaceShooter) {
|
||||
rowFill = (y % 2 === 0) ? 'rgba(25, 40, 95, 0.42)' : 'rgba(18, 28, 72, 0.48)';
|
||||
} else if (isBalloonBoss) {
|
||||
rowFill = (y % 2 === 0) ? 'rgba(35, 22, 68, 0.45)' : 'rgba(22, 14, 52, 0.5)';
|
||||
}
|
||||
for (let x = 0; x < width; x++) {
|
||||
const tx = x * tileSize, ty = y * tileSize;
|
||||
if (blockPlayer[y] && blockPlayer[y][x] === 1) {
|
||||
ctx.fillStyle = 'rgba(255,180,80,0.5)';
|
||||
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
|
||||
@@ -1148,6 +1384,44 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (drawModeEl.value === 'cellImage') {
|
||||
if (objects[y][x] === 1) {
|
||||
if (statusEl) statusEl.textContent = 'ช่องกำแพง — วางรูปบนกริดไม่ได้';
|
||||
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);
|
||||
const bh = Math.max(1, parseInt(hEl && hEl.value, 10) || 1);
|
||||
if (left) {
|
||||
if (!gridImageLibrary.length) {
|
||||
if (statusEl) statusEl.textContent = 'อัปโหลดรูปเข้าคลังก่อน (หรือโหลดฉากที่มีคลังรูป)';
|
||||
return;
|
||||
}
|
||||
const bi = gridImageBrushIndex;
|
||||
if (bi < 0 || bi >= gridImageLibrary.length) {
|
||||
if (statusEl) statusEl.textContent = 'คลิกเลือกรูปในแกลเลอรีก่อนวาง';
|
||||
return;
|
||||
}
|
||||
const placed = canPlaceSpriteAt(x, y, bw, bh, bi);
|
||||
if (!placed) {
|
||||
if (statusEl) statusEl.textContent = 'วางไม่ได้ — มีกำแพงในพื้นที่ หรือลดกว้าง/สูง';
|
||||
return;
|
||||
}
|
||||
removeSpritesOverlappingRect(placed.x, placed.y, placed.w, placed.h);
|
||||
gridImageSprites.push(placed);
|
||||
draw();
|
||||
if (statusEl) statusEl.textContent = 'วางรูป ' + placed.w + '×' + placed.h + ' ช่องที่ (' + placed.x + ',' + placed.y + ')';
|
||||
} else {
|
||||
const hit = findSpriteIndexAtCell(x, y);
|
||||
if (hit >= 0) {
|
||||
gridImageSprites.splice(hit, 1);
|
||||
draw();
|
||||
if (statusEl) statusEl.textContent = 'ลบสไปรต์รูปแล้ว';
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (drawModeEl.value === 'wall') {
|
||||
objects[y][x] = left ? 1 : 0;
|
||||
} else if (drawModeEl.value === 'interactive') {
|
||||
@@ -1369,6 +1643,7 @@
|
||||
if (drawModeEl && drawModeEl.value === 'balloonBossBossPaint' && gameType !== 'balloon_boss') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && (drawModeEl.value === 'quizBattleDome' || drawModeEl.value === 'quizBattlePath') && gameType !== 'quiz_battle') drawModeEl.value = 'wall';
|
||||
toggleFroggerUI();
|
||||
syncGridImageBrushInputCaps();
|
||||
});
|
||||
|
||||
const btnClearGauntletSpawns = document.getElementById('btn-clear-gauntlet-spawns');
|
||||
@@ -1487,6 +1762,9 @@
|
||||
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 })),
|
||||
gridImageCells: deriveGridImageCellsFromSprites(),
|
||||
};
|
||||
if (backgroundImage) body.backgroundImage = backgroundImage;
|
||||
body.lanes = gameType === 'frogger' ? lanes : [];
|
||||
@@ -1501,7 +1779,7 @@
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
const id = data.mapId || mapId;
|
||||
statusEl.textContent = 'บันทึกแล้ว รหัสฉาก: ' + id + (showMapInGame ? '' : ' (ไม่โชว์ฉาก) — ออกจากห้องแล้วเข้าใหม่ หรือกด F5 ในหน้าเล่น ถึงจะเห็นผล');
|
||||
statusEl.textContent = 'บันทึกแล้ว รหัสฉาก: ' + id + (showMapInGame ? '' : ' (ซ่อนกริดในเกม — รูปบนกริดยังแสดง) — ออกจากห้องแล้วเข้าใหม่ หรือกด F5 ในหน้าเล่น ถึงจะเห็นผล');
|
||||
if (!mapId) window.history.replaceState({}, '', 'editor.html' + buildEditorSearch(id));
|
||||
} else statusEl.textContent = data.error || 'บันทึกไม่ได้';
|
||||
})
|
||||
@@ -1509,12 +1787,57 @@
|
||||
});
|
||||
|
||||
const cellColorWrap = document.getElementById('cell-color-wrap');
|
||||
const gridImageToolsWrap = document.getElementById('editor-grid-image-tools');
|
||||
const cellAlphaEl = document.getElementById('cell-alpha');
|
||||
const cellAlphaValue = document.getElementById('cell-alpha-value');
|
||||
if (drawModeEl) drawModeEl.addEventListener('change', () => {
|
||||
function syncDrawModeAuxUi() {
|
||||
if (!drawModeEl) return;
|
||||
if (cellColorWrap) cellColorWrap.style.display = drawModeEl.value === 'color' ? 'inline' : 'none';
|
||||
});
|
||||
if (cellColorWrap && drawModeEl) cellColorWrap.style.display = drawModeEl.value === 'color' ? 'inline' : 'none';
|
||||
if (gridImageToolsWrap) gridImageToolsWrap.style.display = drawModeEl.value === 'cellImage' ? 'flex' : 'none';
|
||||
}
|
||||
if (drawModeEl) drawModeEl.addEventListener('change', () => { syncDrawModeAuxUi(); });
|
||||
syncDrawModeAuxUi();
|
||||
const gridCellUpload = document.getElementById('grid-cell-image-upload');
|
||||
if (gridCellUpload) {
|
||||
gridCellUpload.addEventListener('change', (e) => {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (!file) return;
|
||||
if (file.size > 6 * 1024 * 1024) {
|
||||
if (statusEl) statusEl.textContent = 'ไฟล์ใหญ่เกิน 6MB — ลดขนาดก่อนอัปโหลด';
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
const r = new FileReader();
|
||||
r.onload = () => {
|
||||
const dataUrl = r.result;
|
||||
if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:')) {
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
gridImageLibrary.push(dataUrl);
|
||||
gridImageBrushIndex = gridImageLibrary.length - 1;
|
||||
delete editorGridImageElByIndex[gridImageBrushIndex];
|
||||
primeEditorGridImage(gridImageBrushIndex);
|
||||
rebuildGridImageGallery();
|
||||
syncGridImageBrushInputCaps();
|
||||
draw();
|
||||
if (statusEl) statusEl.textContent = 'เพิ่มรูปเข้าคลังแล้ว — คลิกซ้ายบนกริดเพื่อวาง (ตั้งกว้าง×สูงก่อนได้)';
|
||||
};
|
||||
r.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
});
|
||||
}
|
||||
const btnRmGridLib = document.getElementById('btn-grid-image-remove-from-lib');
|
||||
if (btnRmGridLib) {
|
||||
btnRmGridLib.addEventListener('click', () => {
|
||||
if (!gridImageLibrary.length) return;
|
||||
removeGridImageFromLibraryAt(gridImageBrushIndex);
|
||||
});
|
||||
}
|
||||
const brushWEl0 = document.getElementById('grid-image-brush-w');
|
||||
const brushHEl0 = document.getElementById('grid-image-brush-h');
|
||||
if (brushWEl0) brushWEl0.addEventListener('input', () => { syncGridImageBrushInputCaps(); draw(); });
|
||||
if (brushHEl0) brushHEl0.addEventListener('input', () => { syncGridImageBrushInputCaps(); draw(); });
|
||||
if (cellAlphaEl && cellAlphaValue) cellAlphaEl.addEventListener('input', () => { cellAlphaValue.textContent = cellAlphaEl.value; draw(); });
|
||||
|
||||
mapW.addEventListener('change', resize);
|
||||
@@ -1569,6 +1892,15 @@
|
||||
stackReleaseArea = m.stackReleaseArea && m.stackReleaseArea.length ? m.stackReleaseArea.map(r => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
|
||||
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)
|
||||
: [];
|
||||
gridImageSprites = migrateSpritesFromMapJson(m, width, height);
|
||||
sanitizeSpritesInPlace();
|
||||
gridImageBrushIndex = Math.min(gridImageBrushIndex, Math.max(0, gridImageLibrary.length - 1));
|
||||
Object.keys(editorGridImageElByIndex).forEach((k) => { delete editorGridImageElByIndex[k]; });
|
||||
rebuildGridImageGallery();
|
||||
syncGridImageBrushInputCaps();
|
||||
showMapInGame = m.showMapInGame !== false;
|
||||
const showMapEl = document.getElementById('show-map-in-game');
|
||||
if (showMapEl) showMapEl.checked = showMapInGame;
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
let stackFall = null;
|
||||
let lastStackTickMs = performance.now();
|
||||
let mapBackgroundImg = null;
|
||||
/** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */
|
||||
let mapGridImageImgs = [];
|
||||
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) */
|
||||
@@ -5299,6 +5301,73 @@
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const libLen = md.gridImageLibrary.length;
|
||||
let placements = [];
|
||||
if (Array.isArray(md.gridImageSprites) && md.gridImageSprites.length) {
|
||||
placements = md.gridImageSprites.map((raw) => {
|
||||
const i = Math.max(0, Math.floor(Number(raw.i)));
|
||||
const x = Math.floor(Number(raw.x));
|
||||
const y = Math.floor(Number(raw.y));
|
||||
const ww = Math.max(1, Math.floor(Number(raw.w)) || 1);
|
||||
const hh = Math.max(1, Math.floor(Number(raw.h)) || 1);
|
||||
return { i, x, y, w: ww, h: hh };
|
||||
}).filter((s) => Number.isFinite(s.i) && s.i >= 0 && s.i < libLen &&
|
||||
Number.isFinite(s.x) && Number.isFinite(s.y) && s.w >= 1 && s.h >= 1 &&
|
||||
s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0);
|
||||
placements = placements.map((s) => {
|
||||
let x = s.x, y = s.y, ww = s.w, hh = s.h;
|
||||
if (x < 0) { ww += x; x = 0; }
|
||||
if (y < 0) { hh += y; y = 0; }
|
||||
if (x + ww > w) ww = w - x;
|
||||
if (y + hh > h) hh = h - y;
|
||||
return { i: s.i, x, y, w: Math.max(1, ww), h: Math.max(1, hh) };
|
||||
}).filter((s) => s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0);
|
||||
} else {
|
||||
const cells = md.gridImageCells;
|
||||
if (Array.isArray(cells) && cells.length === h && libLen > 0) {
|
||||
for (let yy = 0; yy < h; yy++) {
|
||||
const row = cells[yy];
|
||||
if (!row) continue;
|
||||
for (let xx = 0; xx < w; xx++) {
|
||||
if (row[xx] == null) continue;
|
||||
const iv = parseInt(row[xx], 10);
|
||||
if (!Number.isFinite(iv) || iv < 0 || iv >= libLen) continue;
|
||||
placements.push({ i: iv, x: xx, y: yy, w: 1, h: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
md.gridImagePlacements = placements;
|
||||
const outCells = Array(h).fill(0).map(() => Array(w).fill(null));
|
||||
placements.forEach((sp) => {
|
||||
for (let yy = sp.y; yy < sp.y + sp.h; yy++) {
|
||||
for (let xx = sp.x; xx < sp.x + sp.w; xx++) {
|
||||
if (yy >= 0 && yy < h && xx >= 0 && xx < w) outCells[yy][xx] = sp.i;
|
||||
}
|
||||
}
|
||||
});
|
||||
md.gridImageCells = outCells;
|
||||
}
|
||||
|
||||
function loadMapGridImages() {
|
||||
mapGridImageImgs = [];
|
||||
if (!mapData || !Array.isArray(mapData.gridImageLibrary) || !mapData.gridImageLibrary.length) return;
|
||||
mapData.gridImageLibrary.forEach((src, i) => {
|
||||
mapGridImageImgs[i] = null;
|
||||
if (!src || typeof src !== 'string') return;
|
||||
const im = new Image();
|
||||
im.onload = () => { try { draw(); } catch (e) {} };
|
||||
im.onerror = () => {};
|
||||
im.src = src;
|
||||
mapGridImageImgs[i] = im;
|
||||
});
|
||||
}
|
||||
|
||||
function applyMapAndStart(gameMapData, res) {
|
||||
const prevWasSpaceShooter = mapData && mapData.gameType === 'space_shooter';
|
||||
const prevWasBalloonBoss = mapData && mapData.gameType === 'balloon_boss';
|
||||
@@ -5348,6 +5417,8 @@
|
||||
mapBackgroundImg = new Image();
|
||||
mapBackgroundImg.src = mapData.backgroundImage;
|
||||
}
|
||||
normalizeGridImageCellsOnMap(mapData);
|
||||
loadMapGridImages();
|
||||
var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน'
|
||||
: (isGauntlet() ? ' | พรมแดงสุดท้าย (Last Light)'
|
||||
: (isLobby() ? ' | โถงรอ' : (isQuiz() ? ' | ตอบคำถาม' : (isQuizCarry() ? ' | หยิบคำตอบมาวางกลาง' : (isQuizBattle() ? ' | Quiz Battle · โดม E' : (isStack() ? ' | Stack ซ้อนตึก' : (isJumpSurvive() ? ' | กระโดดให้รอด' : (isSpaceShooter() ? ' | ยิงยานอวกาศ' : (isBalloonBoss() ? ' | ลูกโป้งยิงบอส' : '')))))))));
|
||||
@@ -5871,6 +5942,8 @@
|
||||
mapBackgroundImg = new Image();
|
||||
mapBackgroundImg.src = mapData.backgroundImage;
|
||||
}
|
||||
normalizeGridImageCellsOnMap(mapData);
|
||||
loadMapGridImages();
|
||||
if (mapData.gameType === 'gauntlet' && ev.peersSnap && Array.isArray(ev.peersSnap)) {
|
||||
ev.peersSnap.forEach((p) => {
|
||||
if (p.id != null && myId != null && String(p.id) === String(myId)) {
|
||||
@@ -7185,11 +7258,38 @@
|
||||
return [sx, sy];
|
||||
}
|
||||
|
||||
function drawGridImagePlacementsLayer() {
|
||||
if (!mapData.gridImagePlacements || !mapData.gridImagePlacements.length || !mapData.gridImageLibrary || !mapData.gridImageLibrary.length) return;
|
||||
for (let gi = 0; gi < mapData.gridImagePlacements.length; gi++) {
|
||||
const sp = mapData.gridImagePlacements[gi];
|
||||
const wx0 = sp.x * tileSize;
|
||||
const wy0 = sp.y * tileSize;
|
||||
const ww = sp.w * tileSize;
|
||||
const wh = sp.h * tileSize;
|
||||
if (wx0 + ww <= worldMinX || wx0 >= worldMaxX || wy0 + wh <= worldMinY || wy0 >= worldMaxY) continue;
|
||||
const gim = mapGridImageImgs[sp.i];
|
||||
if (!gim || !gim.complete || !gim.naturalWidth) continue;
|
||||
const sx0 = (wx0 - camX) * zDraw + canvas.width / 2;
|
||||
const sy0 = (wy0 - camY) * zDraw + canvas.height / 2;
|
||||
const sw = ww * zDraw;
|
||||
const sh = wh * zDraw;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(sx0, sy0, sw, sh);
|
||||
ctx.clip();
|
||||
ctx.drawImage(gim, sx0, sy0, sw, sh);
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.18)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(sx0 + 0.5, sy0 + 0.5, sw - 1, sh - 1);
|
||||
}
|
||||
}
|
||||
|
||||
const startTileX = Math.max(0, Math.floor(worldMinX / tileSize));
|
||||
const endTileX = Math.min(w - 1, Math.ceil(worldMaxX / tileSize));
|
||||
const startTileY = Math.max(0, Math.floor(worldMinY / tileSize));
|
||||
const endTileY = Math.min(h - 1, Math.ceil(worldMaxY / tileSize));
|
||||
if (showGrid) {
|
||||
const startTileX = Math.max(0, Math.floor(worldMinX / tileSize));
|
||||
const endTileX = Math.min(w - 1, Math.ceil(worldMaxX / tileSize));
|
||||
const startTileY = Math.max(0, Math.floor(worldMinY / tileSize));
|
||||
const endTileY = Math.min(h - 1, Math.ceil(worldMaxY / tileSize));
|
||||
for (let y = startTileY; y <= endTileY; y++) {
|
||||
const lane = isFrogger() ? getLane(y) : null;
|
||||
let rowFill = null;
|
||||
@@ -7230,6 +7330,16 @@
|
||||
ctx.fillRect(sx, sy, size, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drawGridImagePlacementsLayer();
|
||||
if (showGrid) {
|
||||
for (let y = startTileY; y <= endTileY; y++) {
|
||||
for (let x = startTileX; x <= endTileX; x++) {
|
||||
const wx = x * tileSize, wy = y * tileSize;
|
||||
const [sx, sy] = worldToScreen(wx, wy);
|
||||
const size = tileSize * zDraw;
|
||||
const isInter = mapData.interactive && mapData.interactive[y] && mapData.interactive[y][x] === 1;
|
||||
if (isInter) {
|
||||
ctx.fillStyle = 'rgba(158,206,106,0.35)';
|
||||
|
||||
@@ -137,7 +137,78 @@
|
||||
/** ตรงกับเซิร์ฟ — เฟสเลือกผู้ต้องสงสัยยังไม่จบ (ปิด overlay แล้วยังเปิดได้จากปุ่มโถง) */
|
||||
let serverSuspectPhaseActive = false;
|
||||
let mapBackgroundImg = null;
|
||||
/** รูปต่อเซลล์ (gridImageLibrary / gridImageCells) — โหลดคู่กับ mapData */
|
||||
let mapLobbyGridImgs = [];
|
||||
let hostIconImg = null;
|
||||
|
||||
function normalizeLobbyGridImageCells(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);
|
||||
const libLen = md.gridImageLibrary.length;
|
||||
let placements = [];
|
||||
if (Array.isArray(md.gridImageSprites) && md.gridImageSprites.length) {
|
||||
placements = md.gridImageSprites.map((raw) => {
|
||||
const i = Math.max(0, Math.floor(Number(raw.i)));
|
||||
const x = Math.floor(Number(raw.x));
|
||||
const y = Math.floor(Number(raw.y));
|
||||
const ww = Math.max(1, Math.floor(Number(raw.w)) || 1);
|
||||
const hh = Math.max(1, Math.floor(Number(raw.h)) || 1);
|
||||
return { i, x, y, w: ww, h: hh };
|
||||
}).filter((s) => Number.isFinite(s.i) && s.i >= 0 && s.i < libLen &&
|
||||
Number.isFinite(s.x) && Number.isFinite(s.y) && s.w >= 1 && s.h >= 1 &&
|
||||
s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0);
|
||||
placements = placements.map((s) => {
|
||||
let x = s.x, y = s.y, ww = s.w, hh = s.h;
|
||||
if (x < 0) { ww += x; x = 0; }
|
||||
if (y < 0) { hh += y; y = 0; }
|
||||
if (x + ww > w) ww = w - x;
|
||||
if (y + hh > h) hh = h - y;
|
||||
return { i: s.i, x, y, w: Math.max(1, ww), h: Math.max(1, hh) };
|
||||
}).filter((s) => s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0);
|
||||
} else {
|
||||
const cells = md.gridImageCells;
|
||||
if (Array.isArray(cells) && cells.length === h && libLen > 0) {
|
||||
for (let yy = 0; yy < h; yy++) {
|
||||
const row = cells[yy];
|
||||
if (!row) continue;
|
||||
for (let xx = 0; xx < w; xx++) {
|
||||
if (row[xx] == null) continue;
|
||||
const iv = parseInt(row[xx], 10);
|
||||
if (!Number.isFinite(iv) || iv < 0 || iv >= libLen) continue;
|
||||
placements.push({ i: iv, x: xx, y: yy, w: 1, h: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
md.gridImagePlacements = placements;
|
||||
const outCells = Array(h).fill(0).map(() => Array(w).fill(null));
|
||||
placements.forEach((sp) => {
|
||||
for (let yy = sp.y; yy < sp.y + sp.h; yy++) {
|
||||
for (let xx = sp.x; xx < sp.x + sp.w; xx++) {
|
||||
if (yy >= 0 && yy < h && xx >= 0 && xx < w) outCells[yy][xx] = sp.i;
|
||||
}
|
||||
}
|
||||
});
|
||||
md.gridImageCells = outCells;
|
||||
}
|
||||
|
||||
function refreshLobbyGridImages() {
|
||||
mapLobbyGridImgs = [];
|
||||
if (!mapData) return;
|
||||
normalizeLobbyGridImageCells(mapData);
|
||||
if (!mapData.gridImageLibrary || !mapData.gridImageLibrary.length) return;
|
||||
mapData.gridImageLibrary.forEach((src, i) => {
|
||||
mapLobbyGridImgs[i] = null;
|
||||
if (!src || typeof src !== 'string') return;
|
||||
const im = new Image();
|
||||
im.onload = function () { try { resizeAndDraw(); } catch (e) {} };
|
||||
im.onerror = function () {};
|
||||
im.src = src;
|
||||
mapLobbyGridImgs[i] = im;
|
||||
});
|
||||
}
|
||||
const peers = new Map();
|
||||
/** รองรับ x/y string จาก Socket — ไม่งั้นตำแหน่งจากสุ่มจุดเกิดจะถูกทิ้ง */
|
||||
function normalizeLobbyPeerFromServer(p, mapRef) {
|
||||
@@ -410,6 +481,26 @@
|
||||
|
||||
const showGrid = mapData.showMapInGame !== false && mapData.showMapInGame !== 'false';
|
||||
const timeMs = Date.now();
|
||||
function drawLobbyGridImagePlacementsLayer() {
|
||||
if (!mapData.gridImagePlacements || !mapData.gridImagePlacements.length || !mapData.gridImageLibrary || !mapData.gridImageLibrary.length) return;
|
||||
for (let gi = 0; gi < mapData.gridImagePlacements.length; gi++) {
|
||||
const sp = mapData.gridImagePlacements[gi];
|
||||
const sx0 = sp.x * tileSize;
|
||||
const sy0 = sp.y * tileSize;
|
||||
const sw0 = sp.w * tileSize;
|
||||
const sh0 = sp.h * tileSize;
|
||||
const gim = mapLobbyGridImgs[sp.i];
|
||||
if (!gim || !gim.complete || !gim.naturalWidth) continue;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(sx0, sy0, sw0, sh0);
|
||||
ctx.clip();
|
||||
ctx.drawImage(gim, sx0, sy0, sw0, sh0);
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.16)';
|
||||
ctx.strokeRect(sx0 + 0.5, sy0 + 0.5, sw0 - 1, sh0 - 1);
|
||||
}
|
||||
}
|
||||
if (showGrid) {
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
@@ -429,6 +520,15 @@
|
||||
ctx.fillStyle = (x + y) % 2 === 0 ? '#24283b' : '#1f2335';
|
||||
ctx.fillRect(sx, sy, tileSize, tileSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drawLobbyGridImagePlacementsLayer();
|
||||
if (showGrid) {
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const sx = x * tileSize;
|
||||
const sy = y * tileSize;
|
||||
const isInter = mapData.interactive && mapData.interactive[y] && mapData.interactive[y][x] === 1;
|
||||
if (isInter) {
|
||||
ctx.fillStyle = 'rgba(158,206,106,0.35)';
|
||||
@@ -1227,6 +1327,7 @@
|
||||
mapBackgroundImg.src = mapData.backgroundImage;
|
||||
mapBackgroundImg.onload = () => { resizeAndDraw(); };
|
||||
}
|
||||
refreshLobbyGridImages();
|
||||
if (!mapData.interactive) mapData.interactive = [];
|
||||
if (!mapData.startGameArea) mapData.startGameArea = [];
|
||||
if (!mapData.quizTrueArea) mapData.quizTrueArea = [];
|
||||
@@ -2711,6 +2812,7 @@
|
||||
} else {
|
||||
mapBackgroundImg = null;
|
||||
}
|
||||
refreshLobbyGridImages();
|
||||
lobbyPath = [];
|
||||
try {
|
||||
window.__detectiveLobbyMeta = {
|
||||
@@ -2812,6 +2914,7 @@
|
||||
} else {
|
||||
mapBackgroundImg = null;
|
||||
}
|
||||
refreshLobbyGridImages();
|
||||
syncLobbyBUiChrome();
|
||||
try {
|
||||
var u = new URL(window.location.href);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
|
||||
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
|
||||
window.APP_VERSION = '0.0168';
|
||||
window.APP_VERSION = '0.0169';
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var t = document.querySelector('.version-tag');
|
||||
if (t) t.textContent = 'v ' + window.APP_VERSION;
|
||||
|
||||
@@ -902,7 +902,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.100"></script>
|
||||
<script src="js/play.js?v=0.104"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1015,7 +1015,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0122"></script>
|
||||
<script src="js/room-lobby.js?v=0.0176"></script>
|
||||
<script src="js/room-lobby.js?v=0.0180"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -62,6 +62,9 @@ const defaultMap = () => ({
|
||||
width: 20, height: 15, tileSize: 32, gameType: 'zep',
|
||||
characterCells: 1, characterCellsW: 1, characterCellsH: 1,
|
||||
ground: [], objects: [], blockPlayer: [], interactive: [], startGameArea: [], spawnArea: [], spawn: { x: 1, y: 1 },
|
||||
gridImageLibrary: [],
|
||||
gridImageSprites: [],
|
||||
gridImageCells: [],
|
||||
quizTrueArea: [], quizFalseArea: [], quizQuestionArea: [], quizQuestions: [],
|
||||
quizCarryHubArea: [], quizCarryOptionArea: [],
|
||||
/** Quiz Battle (MCQ โดม) — ช่องที่ 1 = จุดกด E เปิดคำถามจาก battleQuizMcq */
|
||||
|
||||
Reference in New Issue
Block a user