minigame 2 add more design 1.5
This commit is contained in:
@@ -71,9 +71,9 @@ server {
|
||||
try_files $uri =404;
|
||||
access_log off;
|
||||
}
|
||||
# Game — API ต้องรับ body ใหญ่ (รูปพื้นหลัง base64) จึงตั้ง 20M
|
||||
# Game — API ต้องรับ body ใหญ่ (แมป + runway 5 ช่อง base64 / grid) — 20M มักไม่พอ
|
||||
location /Game/api/ {
|
||||
client_max_body_size 20M;
|
||||
client_max_body_size 80M;
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -309,6 +309,30 @@
|
||||
<button type="button" id="editor-bg-scroll-reset-assets">คืนรูปเริ่มจากเซิร์ฟ / Reset to bundled</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="editor-gauntlet-runway-bg-block" class="editor-bg-scroll-block" hidden>
|
||||
<p class="legend" style="margin:0.5rem 0 0.35rem;line-height:1.45"><strong>พื้นหลังรันเวย์แนวนอน</strong> (เฉพาะฉากรหัส <code>mno9kb07</code> + โหมดพรมแดง) — รูป 1 = จุดเริ่ม (START) แล้วเลื่อนไปวนรูป 2–4 ระหว่างเล่น · พอหมดเวลาหรือเหลือผู้เล่นคนสุดท้าย (หลายคน) จะซ่อนอุปสรรค์แล้วแสดงรูปที่ 5 (FINISH) · บันทึกเป็น <code>gauntletCrownRunwayBg</code> ในแมป · <em>English:</em> Horizontal strip: start → loop images 2–4 → finish; obstacles hidden on end.</p>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="align-items:center;gap:0.75rem 1rem">
|
||||
<label class="editor-bg-scroll-check"><input type="checkbox" id="editor-gauntlet-runway-bg-enabled" checked> เปิดเลื่อน / Enable runway BG</label>
|
||||
<label>ความเร็ว px/s <input type="number" id="editor-gauntlet-runway-bg-speed" min="8" max="400" step="4" value="48" style="width:5rem" title="ความเร็วเลื่อนแนวนอน (จอ)"></label>
|
||||
<label title="จุดเกิด (คอลัมน์ซ้ายสุด) เทียบตำแหน่งในแถบรูป START — เก็บเป็น runwaySpawnAlignFrac ในแมป">จุดเกิด vs START (0.02–0.95) <input type="number" id="editor-gauntlet-runway-bg-align-frac" min="0.02" max="0.95" step="0.01" value="0.32" style="width:4.2rem"></label>
|
||||
</div>
|
||||
<div class="editor-card__row editor-card__row--wrap" style="margin-top:0.35rem">
|
||||
<label>รูปเริ่ม (1) <input type="file" id="editor-gauntlet-runway-bg-start-file" accept="image/*"></label>
|
||||
<label>รูปลูป (2) <input type="file" id="editor-gauntlet-runway-bg-loop2-file" accept="image/*"></label>
|
||||
<label>รูปลูป (3) <input type="file" id="editor-gauntlet-runway-bg-loop3-file" accept="image/*"></label>
|
||||
<label>รูปลูป (4) <input type="file" id="editor-gauntlet-runway-bg-loop4-file" accept="image/*"></label>
|
||||
<label>รูปจบ (5) <input type="file" id="editor-gauntlet-runway-bg-finish-file" accept="image/*"></label>
|
||||
<button type="button" id="editor-gauntlet-runway-bg-reset-assets">คืนรูปเริ่มจากเซิร์ฟ / Reset to bundled</button>
|
||||
</div>
|
||||
<div id="editor-gauntlet-runway-bg-previews" class="legend" style="margin-top:0.45rem;display:flex;flex-wrap:wrap;gap:0.45rem 0.6rem;align-items:flex-end" aria-label="พรีวิวรูปรันเวย์ที่ใช้จริง (รวมค่าเริ่มจากเซิร์ฟถ้ายังไม่อัปโหลด)">
|
||||
<figure style="margin:0;text-align:center"><figcaption style="font-size:0.72rem;opacity:0.9">1 START</figcaption><img id="editor-gauntlet-runway-bg-prev-0" width="128" height="72" alt="" style="display:block;width:128px;height:72px;object-fit:cover;border:1px solid #3d3f52;border-radius:6px;background:#1a1b26"></figure>
|
||||
<figure style="margin:0;text-align:center"><figcaption style="font-size:0.72rem;opacity:0.9">2 LOOP</figcaption><img id="editor-gauntlet-runway-bg-prev-1" width="128" height="72" alt="" style="display:block;width:128px;height:72px;object-fit:cover;border:1px solid #3d3f52;border-radius:6px;background:#1a1b26"></figure>
|
||||
<figure style="margin:0;text-align:center"><figcaption style="font-size:0.72rem;opacity:0.9">3 LOOP</figcaption><img id="editor-gauntlet-runway-bg-prev-2" width="128" height="72" alt="" style="display:block;width:128px;height:72px;object-fit:cover;border:1px solid #3d3f52;border-radius:6px;background:#1a1b26"></figure>
|
||||
<figure style="margin:0;text-align:center"><figcaption style="font-size:0.72rem;opacity:0.9">4 LOOP</figcaption><img id="editor-gauntlet-runway-bg-prev-3" width="128" height="72" alt="" style="display:block;width:128px;height:72px;object-fit:cover;border:1px solid #3d3f52;border-radius:6px;background:#1a1b26"></figure>
|
||||
<figure style="margin:0;text-align:center"><figcaption style="font-size:0.72rem;opacity:0.9">5 FINISH</figcaption><img id="editor-gauntlet-runway-bg-prev-4" width="128" height="72" alt="" style="display:block;width:128px;height:72px;object-fit:cover;border:1px solid #3d3f52;border-radius:6px;background:#1a1b26"></figure>
|
||||
</div>
|
||||
<p id="editor-gauntlet-runway-bg-upload-summary" class="legend" style="margin:0.4rem 0 0;line-height:1.5;font-size:0.82rem" aria-live="polite"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="editor-card editor-card--help" aria-labelledby="editor-card-help-label">
|
||||
@@ -334,8 +358,8 @@
|
||||
<p class="legend" id="editor-status">บันทึกแล้วจะได้รหัสฉาก ใช้รหัสนี้ตอนสร้างห้องในล็อบบี้</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/version.js?v=0.0169"></script>
|
||||
<script src="js/editor.js?v=20260503-mega-virus-ftp-dir"></script>
|
||||
<script src="js/version.js?v=0.0249"></script>
|
||||
<script src="js/editor.js?v=0.0249"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Place start.png, loop2.png, loop3.png, loop4.png, finish.png here for defaults (or set images in editor).
|
||||
If files are missing, play.js draws colored placeholder strips so the runway still scrolls until you upload real assets.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 5.5 KiB |
@@ -485,6 +485,387 @@
|
||||
return { enabled: !!(en && en.checked), pxPerLayer, maxPx };
|
||||
}
|
||||
|
||||
/** พื้นหลังแนวนอน start + วน 2–4 + finish — เฉพาะแมป Last Light mno9kb07 (Editor + map JSON: gauntletCrownRunwayBg) */
|
||||
const EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID = 'mno9kb07';
|
||||
const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START = BASE + '/img/editor-bg-mno9kb07/start.png';
|
||||
const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP2 = BASE + '/img/editor-bg-mno9kb07/loop2.png';
|
||||
const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP3 = BASE + '/img/editor-bg-mno9kb07/loop3.png';
|
||||
const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP4 = BASE + '/img/editor-bg-mno9kb07/loop4.png';
|
||||
const EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_FINISH = BASE + '/img/editor-bg-mno9kb07/finish.png';
|
||||
let editorGauntletRunwayBgConfig = {
|
||||
enabled: true,
|
||||
speedPxPerSec: 48,
|
||||
runwaySpawnAlignFrac: 0.32,
|
||||
startImage: null,
|
||||
loopImage2: null,
|
||||
loopImage3: null,
|
||||
loopImage4: null,
|
||||
finishImage: null,
|
||||
};
|
||||
let editorGauntletRunwayBgImgs = [null, null, null, null, null];
|
||||
let editorGauntletRunwayBgScrollPx = 0;
|
||||
let editorGauntletRunwayBgRaf = null;
|
||||
let editorGauntletRunwayBgLastTs = 0;
|
||||
/** ชื่อไฟล์ล่าสุดที่ผู้ใช้เลือก (ช่อง input จะว่างหลังอ่านไฟล์ — ใช้โชว์สถานะแทน) */
|
||||
let editorGauntletRunwayBgFileNames = {
|
||||
startImage: '',
|
||||
loopImage2: '',
|
||||
loopImage3: '',
|
||||
loopImage4: '',
|
||||
finishImage: '',
|
||||
};
|
||||
|
||||
const RUNWAY_PICK_MAX_W = 2048;
|
||||
const RUNWAY_PICK_MAX_H = 1152;
|
||||
const RUNWAY_PICK_COMPRESS_MIN_LEN = 650000;
|
||||
|
||||
/** ลดขนาด data URL ก่อนเก็บใน JSON — กันบันทึกไม่ขึ้นเมื่อรวมแมปใหญ่เกิน client_max_body_size */
|
||||
function compressRunwayDataUrlIfLarge(dataUrl, done) {
|
||||
if (typeof dataUrl !== 'string' || dataUrl.indexOf('data:image') !== 0) {
|
||||
done(dataUrl);
|
||||
return;
|
||||
}
|
||||
const im = new Image();
|
||||
im.onload = () => {
|
||||
try {
|
||||
const nw = im.naturalWidth;
|
||||
const nh = im.naturalHeight;
|
||||
const needShrink = dataUrl.length > RUNWAY_PICK_COMPRESS_MIN_LEN
|
||||
|| nw > RUNWAY_PICK_MAX_W
|
||||
|| nh > RUNWAY_PICK_MAX_H;
|
||||
if (!needShrink || nw < 1 || nh < 1) {
|
||||
done(dataUrl);
|
||||
return;
|
||||
}
|
||||
let w = nw;
|
||||
let h = nh;
|
||||
const sc = Math.min(1, RUNWAY_PICK_MAX_W / w, RUNWAY_PICK_MAX_H / h);
|
||||
w = Math.max(1, Math.round(w * sc));
|
||||
h = Math.max(1, Math.round(h * sc));
|
||||
const c = document.createElement('canvas');
|
||||
c.width = w;
|
||||
c.height = h;
|
||||
const g = c.getContext('2d');
|
||||
if (!g) {
|
||||
done(dataUrl);
|
||||
return;
|
||||
}
|
||||
g.imageSmoothingEnabled = true;
|
||||
g.drawImage(im, 0, 0, w, h);
|
||||
let best = c.toDataURL('image/jpeg', 0.84);
|
||||
try {
|
||||
const wu = c.toDataURL('image/webp', 0.84);
|
||||
if (wu.length < best.length) best = wu;
|
||||
} catch (e1) { /* webp ไม่รองรับ */ }
|
||||
done(best.length < dataUrl.length ? best : dataUrl);
|
||||
} catch (e) {
|
||||
done(dataUrl);
|
||||
}
|
||||
};
|
||||
im.onerror = () => { done(dataUrl); };
|
||||
im.src = dataUrl;
|
||||
}
|
||||
|
||||
function isEditorGauntletRunwayBgMap() {
|
||||
const gt = gameTypeEl ? gameTypeEl.value : gameType;
|
||||
return mapId === EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID && gt === 'gauntlet';
|
||||
}
|
||||
|
||||
function stopEditorGauntletRunwayBgRaf() {
|
||||
if (editorGauntletRunwayBgRaf != null) {
|
||||
cancelAnimationFrame(editorGauntletRunwayBgRaf);
|
||||
editorGauntletRunwayBgRaf = null;
|
||||
}
|
||||
editorGauntletRunwayBgLastTs = 0;
|
||||
}
|
||||
|
||||
function syncEditorGauntletRunwayBgBlockVisibility() {
|
||||
const block = document.getElementById('editor-gauntlet-runway-bg-block');
|
||||
if (!block) return;
|
||||
/** โชว์ทันทีจากรหัสใน URL — ไม่ผูกกับค่า gameType ตอนก่อน fetch (เดิมเป็น zep ทำให้บล็อกถูกซ่อนจนเหมือนไม่มีฟีเจอร์) */
|
||||
const show = mapId === EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID;
|
||||
if (!show) stopEditorGauntletRunwayBgRaf();
|
||||
block.hidden = !show;
|
||||
}
|
||||
|
||||
function editorGauntletRunwayBgEligibleFromMap(m) {
|
||||
const gt = (m && m.gameType != null && m.gameType !== '') ? m.gameType : (gameTypeEl ? gameTypeEl.value : gameType);
|
||||
return mapId === EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID && gt === 'gauntlet';
|
||||
}
|
||||
|
||||
function editorGauntletRunwayBgStripWidthsAtCh(chR) {
|
||||
const ch0 = Math.max(1, Math.round(chR));
|
||||
const wOf = (im) => {
|
||||
if (!im || !im.complete || !im.naturalWidth || !im.naturalHeight) return 0;
|
||||
return Math.max(1, Math.round(im.naturalWidth * (ch0 / im.naturalHeight)));
|
||||
};
|
||||
return {
|
||||
w0: wOf(editorGauntletRunwayBgImgs[0]),
|
||||
w1: wOf(editorGauntletRunwayBgImgs[1]),
|
||||
w2: wOf(editorGauntletRunwayBgImgs[2]),
|
||||
w3: wOf(editorGauntletRunwayBgImgs[3]),
|
||||
};
|
||||
}
|
||||
|
||||
function isEditorGauntletRunwayBgDrawing() {
|
||||
if (!isEditorGauntletRunwayBgMap() || !editorGauntletRunwayBgConfig.enabled) return false;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const im = editorGauntletRunwayBgImgs[i];
|
||||
if (!im || !im.complete || !im.naturalWidth) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function drawRunwayHSliceEditor(ctx, img, worldX0, segW, scrollLeft, cwR, chR) {
|
||||
if (!img || !img.complete || !img.naturalWidth || !img.naturalHeight || segW <= 0) return;
|
||||
const natW = img.naturalWidth;
|
||||
const natH = img.naturalHeight;
|
||||
const v0 = scrollLeft;
|
||||
const v1 = scrollLeft + cwR;
|
||||
const o0 = Math.max(worldX0, v0);
|
||||
const o1 = Math.min(worldX0 + segW, v1);
|
||||
if (o1 <= o0) return;
|
||||
const u0 = ((o0 - worldX0) / segW) * natW;
|
||||
const u1 = ((o1 - worldX0) / segW) * natW;
|
||||
const dx = o0 - scrollLeft;
|
||||
const dw = o1 - o0;
|
||||
ctx.drawImage(img, u0, 0, u1 - u0, natH, dx, 0, dw, chR);
|
||||
}
|
||||
|
||||
function drawEditorGauntletRunwayScrollingBackground(cw, ch) {
|
||||
const start = editorGauntletRunwayBgImgs[0];
|
||||
const L2 = editorGauntletRunwayBgImgs[1];
|
||||
const L3 = editorGauntletRunwayBgImgs[2];
|
||||
const L4 = editorGauntletRunwayBgImgs[3];
|
||||
const cwR = Math.max(1, Math.round(cw));
|
||||
const chR = Math.max(1, Math.round(ch));
|
||||
const { w0, w1, w2, w3 } = editorGauntletRunwayBgStripWidthsAtCh(chR);
|
||||
const cycle = w1 + w2 + w3;
|
||||
const S = editorGauntletRunwayBgScrollPx;
|
||||
ctx.save();
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.fillStyle = '#1a1b26';
|
||||
ctx.fillRect(0, 0, cwR, chR);
|
||||
if (w0 > 0) drawRunwayHSliceEditor(ctx, start, 0, w0, S, cwR, chR);
|
||||
if (cycle <= 0) {
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
const ws = [w1, w2, w3];
|
||||
const ims = [L2, L3, L4];
|
||||
const uTotal = Math.max(0, S - w0);
|
||||
let cycN = Math.floor(uTotal / cycle);
|
||||
let uRem = uTotal - cycN * cycle;
|
||||
let si = 0;
|
||||
while (si < 3 && uRem >= ws[si]) {
|
||||
uRem -= ws[si];
|
||||
si++;
|
||||
}
|
||||
if (si >= 3) {
|
||||
cycN++;
|
||||
si = 0;
|
||||
uRem = 0;
|
||||
}
|
||||
let acc = 0;
|
||||
for (let k = 0; k < si; k++) acc += ws[k];
|
||||
let curLeft = w0 + cycN * cycle + acc;
|
||||
let guard = 0;
|
||||
let idx = si;
|
||||
while (curLeft < S + cwR && guard++ < 500) {
|
||||
drawRunwayHSliceEditor(ctx, ims[idx], curLeft, ws[idx], S, cwR, chR);
|
||||
curLeft += ws[idx];
|
||||
idx = (idx + 1) % 3;
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function editorGauntletRunwayBgTick(ts) {
|
||||
editorGauntletRunwayBgRaf = null;
|
||||
if (!isEditorGauntletRunwayBgDrawing()) return;
|
||||
if (!editorGauntletRunwayBgLastTs) editorGauntletRunwayBgLastTs = ts;
|
||||
const dt = Math.min(64, Math.max(0, ts - editorGauntletRunwayBgLastTs));
|
||||
editorGauntletRunwayBgLastTs = ts;
|
||||
editorGauntletRunwayBgScrollPx += (editorGauntletRunwayBgConfig.speedPxPerSec || 48) * (dt / 1000);
|
||||
draw();
|
||||
editorGauntletRunwayBgRaf = requestAnimationFrame(editorGauntletRunwayBgTick);
|
||||
}
|
||||
|
||||
function startEditorGauntletRunwayBgRaf() {
|
||||
if (!isEditorGauntletRunwayBgDrawing()) return;
|
||||
stopEditorGauntletRunwayBgRaf();
|
||||
editorGauntletRunwayBgLastTs = 0;
|
||||
editorGauntletRunwayBgRaf = requestAnimationFrame(editorGauntletRunwayBgTick);
|
||||
}
|
||||
|
||||
function loadEditorGauntletRunwayBgImages(s0, s1, s2, s3, s4, onDone) {
|
||||
stopEditorGauntletRunwayBgRaf();
|
||||
editorGauntletRunwayBgImgs = [null, null, null, null, null];
|
||||
let pending = 5;
|
||||
const doneOne = () => {
|
||||
pending--;
|
||||
if (pending <= 0 && typeof onDone === 'function') onDone();
|
||||
};
|
||||
const srcs = [s0, s1, s2, s3, s4];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const im = new Image();
|
||||
const idx = i;
|
||||
im.onload = () => {
|
||||
editorGauntletRunwayBgImgs[idx] = im;
|
||||
doneOne();
|
||||
};
|
||||
im.onerror = () => { doneOne(); };
|
||||
im.src = srcs[i];
|
||||
}
|
||||
}
|
||||
|
||||
function applyGauntletCrownRunwayBgFromMap(m) {
|
||||
syncEditorGauntletRunwayBgBlockVisibility();
|
||||
if (mapId !== EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) {
|
||||
editorGauntletRunwayBgConfig = {
|
||||
enabled: true,
|
||||
speedPxPerSec: 48,
|
||||
runwaySpawnAlignFrac: 0.32,
|
||||
startImage: null,
|
||||
loopImage2: null,
|
||||
loopImage3: null,
|
||||
loopImage4: null,
|
||||
finishImage: null,
|
||||
};
|
||||
editorGauntletRunwayBgFileNames = { startImage: '', loopImage2: '', loopImage3: '', loopImage4: '', finishImage: '' };
|
||||
stopEditorGauntletRunwayBgRaf();
|
||||
editorGauntletRunwayBgImgs = [null, null, null, null, null];
|
||||
return;
|
||||
}
|
||||
editorGauntletRunwayBgFileNames = { startImage: '', loopImage2: '', loopImage3: '', loopImage4: '', finishImage: '' };
|
||||
const raw = m && m.gauntletCrownRunwayBg && typeof m.gauntletCrownRunwayBg === 'object' ? m.gauntletCrownRunwayBg : {};
|
||||
const sp = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 48));
|
||||
const afRaw = Number(raw.runwaySpawnAlignFrac);
|
||||
const af = Number.isFinite(afRaw) ? Math.max(0.02, Math.min(0.95, afRaw)) : 0.32;
|
||||
editorGauntletRunwayBgConfig = {
|
||||
enabled: raw.enabled !== false,
|
||||
speedPxPerSec: sp,
|
||||
runwaySpawnAlignFrac: af,
|
||||
startImage: typeof raw.startImage === 'string' && raw.startImage.length ? raw.startImage : null,
|
||||
loopImage2: typeof raw.loopImage2 === 'string' && raw.loopImage2.length ? raw.loopImage2 : null,
|
||||
loopImage3: typeof raw.loopImage3 === 'string' && raw.loopImage3.length ? raw.loopImage3 : null,
|
||||
loopImage4: typeof raw.loopImage4 === 'string' && raw.loopImage4.length ? raw.loopImage4 : null,
|
||||
finishImage: typeof raw.finishImage === 'string' && raw.finishImage.length ? raw.finishImage : null,
|
||||
};
|
||||
const eligible = editorGauntletRunwayBgEligibleFromMap(m || {});
|
||||
if (!editorGauntletRunwayBgConfig.enabled) {
|
||||
stopEditorGauntletRunwayBgRaf();
|
||||
editorGauntletRunwayBgImgs = [null, null, null, null, null];
|
||||
syncEditorGauntletRunwayBgUiFromConfig();
|
||||
draw();
|
||||
return;
|
||||
}
|
||||
const s0 = editorGauntletRunwayBgConfig.startImage || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START;
|
||||
const s1 = editorGauntletRunwayBgConfig.loopImage2 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP2;
|
||||
const s2 = editorGauntletRunwayBgConfig.loopImage3 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP3;
|
||||
const s3 = editorGauntletRunwayBgConfig.loopImage4 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP4;
|
||||
const s4 = editorGauntletRunwayBgConfig.finishImage || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_FINISH;
|
||||
editorGauntletRunwayBgScrollPx = 0;
|
||||
loadEditorGauntletRunwayBgImages(s0, s1, s2, s3, s4, () => {
|
||||
syncEditorGauntletRunwayBgUiFromConfig();
|
||||
draw();
|
||||
stopEditorGauntletRunwayBgRaf();
|
||||
if (eligible && editorGauntletRunwayBgConfig.enabled) startEditorGauntletRunwayBgRaf();
|
||||
});
|
||||
}
|
||||
|
||||
function syncEditorGauntletRunwayBgUiFromConfig() {
|
||||
const en = document.getElementById('editor-gauntlet-runway-bg-enabled');
|
||||
const sp = document.getElementById('editor-gauntlet-runway-bg-speed');
|
||||
const af = document.getElementById('editor-gauntlet-runway-bg-align-frac');
|
||||
if (en) en.checked = !!editorGauntletRunwayBgConfig.enabled;
|
||||
if (sp) sp.value = String(editorGauntletRunwayBgConfig.speedPxPerSec || 48);
|
||||
if (af) {
|
||||
const v = editorGauntletRunwayBgConfig.runwaySpawnAlignFrac;
|
||||
af.value = String(Number.isFinite(v) ? Math.max(0.02, Math.min(0.95, v)) : 0.32);
|
||||
}
|
||||
syncEditorGauntletRunwayBgUploadSummary();
|
||||
syncEditorGauntletRunwayBgThumbnails();
|
||||
}
|
||||
|
||||
function syncEditorGauntletRunwayBgThumbnails() {
|
||||
if (mapId !== EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) return;
|
||||
const keys = ['startImage', 'loopImage2', 'loopImage3', 'loopImage4', 'finishImage'];
|
||||
const defaults = [
|
||||
EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START,
|
||||
EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP2,
|
||||
EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP3,
|
||||
EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP4,
|
||||
EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_FINISH,
|
||||
];
|
||||
const labels = ['START', 'LOOP', 'LOOP', 'LOOP', 'FINISH'];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const el = document.getElementById('editor-gauntlet-runway-bg-prev-' + i);
|
||||
if (!el) continue;
|
||||
const custom = editorGauntletRunwayBgConfig[keys[i]];
|
||||
const src = (typeof custom === 'string' && custom.length) ? custom : defaults[i];
|
||||
el.alt = 'Runway ' + labels[i] + ' — slot ' + (i + 1);
|
||||
el.src = src;
|
||||
}
|
||||
}
|
||||
|
||||
function describeRunwaySlotStoredValue(val, fileName) {
|
||||
if (!val || typeof val !== 'string' || !val.length) {
|
||||
return { th: 'ยังไม่อัปโหลด — เกมใช้พื้นสีชั่วคราว', en: 'Not set — game uses soft color strip' };
|
||||
}
|
||||
if (val.startsWith('data:image')) {
|
||||
const fn = (fileName && String(fileName).trim()) ? ` · ${fileName.trim()}` : '';
|
||||
return { th: `อัปโหลดแล้ว (เก็บในฉาก)${fn}`, en: `Saved in map (data URL)${fn}` };
|
||||
}
|
||||
const short = val.length > 56 ? (val.slice(0, 54) + '…') : val;
|
||||
return { th: `ตั้งจากแมป/URL: ${short}`, en: `From map/URL: ${short}` };
|
||||
}
|
||||
|
||||
function syncEditorGauntletRunwayBgUploadSummary() {
|
||||
const el = document.getElementById('editor-gauntlet-runway-bg-upload-summary');
|
||||
if (!el) return;
|
||||
if (mapId !== EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) {
|
||||
el.textContent = '';
|
||||
return;
|
||||
}
|
||||
const keys = ['startImage', 'loopImage2', 'loopImage3', 'loopImage4', 'finishImage'];
|
||||
const labels = ['1 เริ่ม', '2 ลูป', '3 ลูป', '4 ลูป', '5 จบ'];
|
||||
const parts = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const d = describeRunwaySlotStoredValue(editorGauntletRunwayBgConfig[keys[i]], editorGauntletRunwayBgFileNames[keys[i]]);
|
||||
parts.push(`${labels[i]}: ${d.th} / ${d.en}`);
|
||||
}
|
||||
const tail = '\n\nพรีวิวรูปแถวด้านบน = สิ่งที่เกมจะใช้ (รวมรูปเซิร์ฟถ้าช่องว่าง)\n'
|
||||
+ 'Thumbnails above = what the game uses (bundled defaults if slot empty).\n\n'
|
||||
+ 'หลังเลือกไฟล์ ช่องจะกลับเป็น «No file chosen» เป็นปกติของเบราว์เซอร์\n'
|
||||
+ 'After pick, the file box clears — use thumbnails + this text.\n\n'
|
||||
+ 'ต้องกด บันทึกฉาก ถึงจะไปโชว์ในเกมจริง · Click Save scene for play mode.';
|
||||
el.textContent = 'สถานะรูปรันเวย์ · Runway images\n' + parts.join('\n') + tail;
|
||||
}
|
||||
|
||||
function readGauntletCrownRunwayBgForSave() {
|
||||
/** บันทึกจากรหัสแมป — ไม่ผูกกับ dropdown โหมดอย่างเดียว (เดิมถ้าไม่ใช่ gauntlet จะไม่ส่ง → รีเฟรชแล้วหาย) · ส่งครบทุกคีย์เพราะ PUT แทนที่ทั้ง object */
|
||||
if (mapId !== EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) return undefined;
|
||||
const en = document.getElementById('editor-gauntlet-runway-bg-enabled');
|
||||
const spEl = document.getElementById('editor-gauntlet-runway-bg-speed');
|
||||
const sp = Math.max(8, Math.min(400, Math.floor(Number(spEl && spEl.value)) || 48));
|
||||
const s = (k) => {
|
||||
const v = editorGauntletRunwayBgConfig[k];
|
||||
return (typeof v === 'string' && v.length) ? v : null;
|
||||
};
|
||||
const afEl = document.getElementById('editor-gauntlet-runway-bg-align-frac');
|
||||
const afv = Math.max(0.02, Math.min(0.95, Number(afEl && afEl.value) || 0.32));
|
||||
return {
|
||||
enabled: !!(en && en.checked),
|
||||
speedPxPerSec: sp,
|
||||
runwaySpawnAlignFrac: afv,
|
||||
startImage: s('startImage'),
|
||||
loopImage2: s('loopImage2'),
|
||||
loopImage3: s('loopImage3'),
|
||||
loopImage4: s('loopImage4'),
|
||||
finishImage: s('finishImage'),
|
||||
};
|
||||
}
|
||||
|
||||
function readEditorBgScrollConfigForSave() {
|
||||
if (!isEditorScrollBgMap()) return null;
|
||||
const en = document.getElementById('editor-bg-scroll-enabled');
|
||||
@@ -1318,6 +1699,7 @@
|
||||
}
|
||||
|
||||
function toggleFroggerUI() {
|
||||
syncEditorGauntletRunwayBgBlockVisibility();
|
||||
syncStackTowerCamBlockVisibilityOnly();
|
||||
const froggerWrap = document.getElementById('frogger-lanes-wrap');
|
||||
const hint = document.getElementById('game-type-hint');
|
||||
@@ -1675,6 +2057,8 @@
|
||||
if (gtDraw === 'balloon_boss') ensureBalloonBossPlayerSlots();
|
||||
if (isEditorScrollBgDrawing()) {
|
||||
drawEditorScrollingBackground(canvas.width, canvas.height);
|
||||
} else if (isEditorGauntletRunwayBgDrawing()) {
|
||||
drawEditorGauntletRunwayScrollingBackground(canvas.width, canvas.height);
|
||||
} else if (isEditorStackTowerBgDrawing()) {
|
||||
drawStackTowerScrollingBackground(canvas.width, canvas.height);
|
||||
} else if (backgroundImageImg && backgroundImageImg.complete && backgroundImageImg.naturalWidth) {
|
||||
@@ -2599,7 +2983,110 @@
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
(function wireEditorGauntletRunwayBgUi() {
|
||||
const en = document.getElementById('editor-gauntlet-runway-bg-enabled');
|
||||
const sp = document.getElementById('editor-gauntlet-runway-bg-speed');
|
||||
if (en) {
|
||||
en.addEventListener('change', () => {
|
||||
editorGauntletRunwayBgConfig.enabled = !!en.checked;
|
||||
if (editorGauntletRunwayBgConfig.enabled) {
|
||||
applyGauntletCrownRunwayBgFromMap({
|
||||
gameType: 'gauntlet',
|
||||
gauntletCrownRunwayBg: {
|
||||
enabled: true,
|
||||
speedPxPerSec: editorGauntletRunwayBgConfig.speedPxPerSec,
|
||||
runwaySpawnAlignFrac: editorGauntletRunwayBgConfig.runwaySpawnAlignFrac,
|
||||
startImage: editorGauntletRunwayBgConfig.startImage,
|
||||
loopImage2: editorGauntletRunwayBgConfig.loopImage2,
|
||||
loopImage3: editorGauntletRunwayBgConfig.loopImage3,
|
||||
loopImage4: editorGauntletRunwayBgConfig.loopImage4,
|
||||
finishImage: editorGauntletRunwayBgConfig.finishImage,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
stopEditorGauntletRunwayBgRaf();
|
||||
editorGauntletRunwayBgImgs = [null, null, null, null, null];
|
||||
draw();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (sp) {
|
||||
sp.addEventListener('change', () => {
|
||||
editorGauntletRunwayBgConfig.speedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(sp.value)) || 48));
|
||||
});
|
||||
}
|
||||
const afIn = document.getElementById('editor-gauntlet-runway-bg-align-frac');
|
||||
if (afIn) {
|
||||
afIn.addEventListener('change', () => {
|
||||
editorGauntletRunwayBgConfig.runwaySpawnAlignFrac = Math.max(0.02, Math.min(0.95, Number(afIn.value) || 0.32));
|
||||
});
|
||||
}
|
||||
function wireFile(id, key) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.addEventListener('change', (e) => {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (!file) return;
|
||||
const pickedName = file.name || '';
|
||||
const r = new FileReader();
|
||||
r.onload = () => {
|
||||
compressRunwayDataUrlIfLarge(r.result, (finalUrl) => {
|
||||
editorGauntletRunwayBgConfig[key] = finalUrl;
|
||||
editorGauntletRunwayBgFileNames[key] = pickedName;
|
||||
syncEditorGauntletRunwayBgThumbnails();
|
||||
const s0 = editorGauntletRunwayBgConfig.startImage || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START;
|
||||
const s1 = editorGauntletRunwayBgConfig.loopImage2 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP2;
|
||||
const s2 = editorGauntletRunwayBgConfig.loopImage3 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP3;
|
||||
const s3 = editorGauntletRunwayBgConfig.loopImage4 || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP4;
|
||||
const s4 = editorGauntletRunwayBgConfig.finishImage || EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_FINISH;
|
||||
editorGauntletRunwayBgScrollPx = 0;
|
||||
loadEditorGauntletRunwayBgImages(s0, s1, s2, s3, s4, () => {
|
||||
syncEditorGauntletRunwayBgUploadSummary();
|
||||
syncEditorGauntletRunwayBgThumbnails();
|
||||
draw();
|
||||
if (editorGauntletRunwayBgConfig.enabled) startEditorGauntletRunwayBgRaf();
|
||||
});
|
||||
});
|
||||
};
|
||||
r.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
});
|
||||
}
|
||||
wireFile('editor-gauntlet-runway-bg-start-file', 'startImage');
|
||||
wireFile('editor-gauntlet-runway-bg-loop2-file', 'loopImage2');
|
||||
wireFile('editor-gauntlet-runway-bg-loop3-file', 'loopImage3');
|
||||
wireFile('editor-gauntlet-runway-bg-loop4-file', 'loopImage4');
|
||||
wireFile('editor-gauntlet-runway-bg-finish-file', 'finishImage');
|
||||
const btnR = document.getElementById('editor-gauntlet-runway-bg-reset-assets');
|
||||
if (btnR) {
|
||||
btnR.addEventListener('click', () => {
|
||||
editorGauntletRunwayBgConfig.startImage = null;
|
||||
editorGauntletRunwayBgConfig.loopImage2 = null;
|
||||
editorGauntletRunwayBgConfig.loopImage3 = null;
|
||||
editorGauntletRunwayBgConfig.loopImage4 = null;
|
||||
editorGauntletRunwayBgConfig.finishImage = null;
|
||||
editorGauntletRunwayBgFileNames = { startImage: '', loopImage2: '', loopImage3: '', loopImage4: '', finishImage: '' };
|
||||
editorGauntletRunwayBgScrollPx = 0;
|
||||
loadEditorGauntletRunwayBgImages(
|
||||
EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_START,
|
||||
EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP2,
|
||||
EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP3,
|
||||
EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_LOOP4,
|
||||
EDITOR_GAUNTLET_RUNWAY_BG_DEFAULT_FINISH,
|
||||
() => {
|
||||
syncEditorGauntletRunwayBgUiFromConfig();
|
||||
draw();
|
||||
if (editorGauntletRunwayBgConfig.enabled) startEditorGauntletRunwayBgRaf();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
syncEditorGauntletRunwayBgBlockVisibility();
|
||||
if (isEditorScrollBgMap()) applyEditorBgScrollFromMap({});
|
||||
if (isEditorGauntletRunwayBgMap()) applyGauntletCrownRunwayBgFromMap({});
|
||||
if (isEditorStackTowerMissionMap()) applyStackTowerCameraFollowFromMap({});
|
||||
else {
|
||||
stopStackTowerBgScrollRaf();
|
||||
@@ -2688,8 +3175,14 @@
|
||||
isSpawnMode = true;
|
||||
document.getElementById('btn-spawn').textContent = 'คลิกช่องที่ต้องการ';
|
||||
});
|
||||
if (gameTypeEl) gameTypeEl.addEventListener('change', () => {
|
||||
gameType = gameTypeEl.value;
|
||||
if (gameTypeEl) gameTypeEl.addEventListener('change', () => {
|
||||
gameType = gameTypeEl.value;
|
||||
syncEditorGauntletRunwayBgBlockVisibility();
|
||||
if (mapId === EDITOR_GAUNTLET_RUNWAY_BG_MAP_ID) {
|
||||
stopEditorGauntletRunwayBgRaf();
|
||||
if (isEditorGauntletRunwayBgMap() && editorGauntletRunwayBgConfig.enabled) startEditorGauntletRunwayBgRaf();
|
||||
draw();
|
||||
}
|
||||
if (drawModeEl && drawModeEl.value === 'startGame' && gameType !== 'lobby') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && drawModeEl.value === 'spawnArea' && gameType === 'frogger') drawModeEl.value = 'wall';
|
||||
if (drawModeEl && (drawModeEl.value === 'quizTrue' || drawModeEl.value === 'quizFalse') && gameType !== 'quiz') drawModeEl.value = 'wall';
|
||||
@@ -2907,6 +3400,8 @@
|
||||
if (stcSave !== undefined) body.stackTowerCameraFollow = stcSave;
|
||||
const stBgSave = readStackTowerBgScrollForSave();
|
||||
if (stBgSave !== undefined) body.stackTowerBgScroll = stBgSave;
|
||||
const gcrSave = readGauntletCrownRunwayBgForSave();
|
||||
if (gcrSave !== undefined) body.gauntletCrownRunwayBg = gcrSave;
|
||||
body.lanes = gameType === 'frogger' ? lanes : [];
|
||||
sanitizeGauntletPlayerSpawns();
|
||||
body.gauntletPlayerSpawns = gameType === 'gauntlet'
|
||||
@@ -2924,8 +3419,36 @@
|
||||
}
|
||||
const url = mapId ? BASE + '/api/maps/' + mapId : BASE + '/api/maps';
|
||||
const method = mapId ? 'PUT' : 'POST';
|
||||
fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||
.then(r => r.json())
|
||||
let payloadStr;
|
||||
try {
|
||||
payloadStr = JSON.stringify(body);
|
||||
} catch (err) {
|
||||
if (statusEl) statusEl.textContent = 'แปลงฉากเป็น JSON ไม่ได้ — รูป/ข้อมูลใหญ่เกิน · Cannot stringify map JSON';
|
||||
return;
|
||||
}
|
||||
const payloadBytes = (typeof TextEncoder !== 'undefined')
|
||||
? new TextEncoder().encode(payloadStr).length
|
||||
: new Blob([payloadStr]).size;
|
||||
const maxSaveBytes = 78 * 1024 * 1024;
|
||||
if (payloadBytes > maxSaveBytes) {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'ข้อมูลฉาก ~' + Math.round(payloadBytes / (1024 * 1024)) + 'MB ใหญ่เกิน ' + Math.round(maxSaveBytes / (1024 * 1024))
|
||||
+ 'MB — ลดรูปรันเวย์/คลังกริด หรือให้แอดมินเพิ่ม client_max_body_size · Map payload too large';
|
||||
}
|
||||
return;
|
||||
}
|
||||
fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: payloadStr })
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
return r.text().then((t) => {
|
||||
const hint = r.status === 413
|
||||
? ' (413 = body ใหญ่เกิน nginx — เพิ่ม client_max_body_size ใน /Game/api/ หรือลดรูป)'
|
||||
: '';
|
||||
throw new Error('HTTP ' + r.status + hint + (t ? ' ' + t.slice(0, 120) : ''));
|
||||
});
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
const id = data.mapId || mapId;
|
||||
@@ -2948,7 +3471,13 @@
|
||||
if (!mapId) window.history.replaceState({}, '', 'editor.html' + buildEditorSearch(id));
|
||||
} else statusEl.textContent = data.error || 'บันทึกไม่ได้';
|
||||
})
|
||||
.catch(() => { statusEl.textContent = 'บันทึกไม่ได้'; });
|
||||
.catch((err) => {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'บันทึกไม่ได้'
|
||||
+ (err && err.message ? ' — ' + err.message : '')
|
||||
+ ' · ถ้าเป็น 413 ให้ reload nginx หลังเพิ่ม client_max_body_size';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const cellColorWrap = document.getElementById('cell-color-wrap');
|
||||
@@ -3221,6 +3750,8 @@
|
||||
}
|
||||
gameType = m.gameType || 'zep';
|
||||
if (gameTypeEl) gameTypeEl.value = gameType;
|
||||
syncEditorGauntletRunwayBgBlockVisibility();
|
||||
applyGauntletCrownRunwayBgFromMap(m);
|
||||
applyStackTowerCameraFollowFromMap(m);
|
||||
lanes = (m.lanes && m.lanes.length) ? m.lanes.map(l => ({ ...l })) : [];
|
||||
if (gameType === 'gauntlet' || gameType === 'stack' || gameType === 'quiz_carry' || gameType === 'quiz_battle' || gameType === 'jump_survive' || gameType === 'space_shooter' || gameType === 'balloon_boss') lanes = [];
|
||||
|
||||
+589
-23
@@ -418,6 +418,374 @@
|
||||
playScrollBgPx += (playScrollBgSpeedPxPerSec || 56) * dtSec;
|
||||
}
|
||||
|
||||
/** Last Light (mno9kb07) — พื้นหลังแนวนอน: start → วน loop 2–4 → จบแล้ว finish + ซ่อนอุปสรรค (gauntletCrownRunwayBg ในแมป) */
|
||||
const GAUNTLET_CROWN_RUNWAY_BG_MAP_ID = 'mno9kb07';
|
||||
const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_START = BASE + '/img/editor-bg-mno9kb07/start.png';
|
||||
const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP2 = BASE + '/img/editor-bg-mno9kb07/loop2.png';
|
||||
const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP3 = BASE + '/img/editor-bg-mno9kb07/loop3.png';
|
||||
const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP4 = BASE + '/img/editor-bg-mno9kb07/loop4.png';
|
||||
const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_FINISH = BASE + '/img/editor-bg-mno9kb07/finish.png';
|
||||
let gauntletCrownRunwayBgImgs = [null, null, null, null, null];
|
||||
let gauntletCrownRunwayBgOff = false;
|
||||
let gauntletCrownRunwayBgSpeedPxPerSec = 48;
|
||||
let gauntletCrownRunwayBgScrollPx = 0;
|
||||
let gauntletCrownRunwayBgFinishLatched = false;
|
||||
let gauntletCrownRunwayBgInitialTotalPeers = null;
|
||||
/** พรมแดง mno9kb07 — จำกัดขอบขวาของตัว (ขอบขวา footprint ไม่เกินสัดส่วนนี้ของความกว้างแมปเป็นช่อง) */
|
||||
const GAUNTLET_CROWN_HEIST_MAX_X_WORLD_FRAC = 0.8;
|
||||
const GAUNTLET_CROWN_SCORE_POP_URL = BASE + '/img/gauntlet-assets/score-.png';
|
||||
let gauntletCrownScorePenaltyImg = null;
|
||||
function ensureGauntletCrownScorePenaltyImgPlay() {
|
||||
if (gauntletCrownScorePenaltyImg) return gauntletCrownScorePenaltyImg;
|
||||
const im = new Image();
|
||||
im.src = GAUNTLET_CROWN_SCORE_POP_URL;
|
||||
gauntletCrownScorePenaltyImg = im;
|
||||
return im;
|
||||
}
|
||||
let lastGauntletCrownRunwayBgTickMs = 0;
|
||||
/** จัด offset เลื่อนรันเวย์ให้จุดเกิดตรงกับงานศิลป์ START (รูป 1) — รีเซ็ตตอน howto / โหลดแมปใหม่ */
|
||||
let gauntletCrownRunwaySpawnScrollSnapped = false;
|
||||
|
||||
function gauntletCrownRunwayBgMapEligiblePlay() {
|
||||
return !!(mapData && mapData.gameType === 'gauntlet' && (currentPlayMapId() || '').trim() === GAUNTLET_CROWN_RUNWAY_BG_MAP_ID);
|
||||
}
|
||||
|
||||
function gauntletCrownRunwayBgNatDims(im) {
|
||||
if (!im) return null;
|
||||
if (im.tagName === 'CANVAS' && im.width > 0 && im.height > 0) return { nw: im.width, nh: im.height };
|
||||
if (im.complete && im.naturalWidth > 0 && im.naturalHeight > 0) return { nw: im.naturalWidth, nh: im.naturalHeight };
|
||||
return null;
|
||||
}
|
||||
|
||||
function gauntletCrownRunwayBgImageSlotReadyPlay(im) {
|
||||
return !!gauntletCrownRunwayBgNatDims(im);
|
||||
}
|
||||
|
||||
function gauntletCrownRunwayBgStripWidthsAtCh(chR) {
|
||||
const ch0 = Math.max(1, Math.round(chR));
|
||||
const wOf = (im) => {
|
||||
const d = gauntletCrownRunwayBgNatDims(im);
|
||||
if (!d) return 0;
|
||||
return Math.max(1, Math.round(d.nw * (ch0 / d.nh)));
|
||||
};
|
||||
return {
|
||||
w0: wOf(gauntletCrownRunwayBgImgs[0]),
|
||||
w1: wOf(gauntletCrownRunwayBgImgs[1]),
|
||||
w2: wOf(gauntletCrownRunwayBgImgs[2]),
|
||||
w3: wOf(gauntletCrownRunwayBgImgs[3]),
|
||||
w4: wOf(gauntletCrownRunwayBgImgs[4]),
|
||||
};
|
||||
}
|
||||
|
||||
function gauntletCrownRunwayBgImagesReadyPlay() {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!gauntletCrownRunwayBgImageSlotReadyPlay(gauntletCrownRunwayBgImgs[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** ถ้า URL บนเซิร์ฟยังไม่มีไฟล์ (404) ให้แผ่นเลื่อนได้ — พื้นผิวนุ่ม ไม่ใส่ข้อความใหญ่ (กันเหมือน debug) */
|
||||
function ensureGauntletCrownRunwayBgPlaceholdersPlay() {
|
||||
if (!gauntletCrownRunwayBgMapEligiblePlay() || gauntletCrownRunwayBgOff) return;
|
||||
const W = 960;
|
||||
const H = 540;
|
||||
const hues = [352, 210, 165, 225, 38];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (gauntletCrownRunwayBgImageSlotReadyPlay(gauntletCrownRunwayBgImgs[i])) continue;
|
||||
const c = document.createElement('canvas');
|
||||
c.width = W;
|
||||
c.height = H;
|
||||
const g = c.getContext('2d');
|
||||
const h0 = hues[i];
|
||||
const grd = g.createLinearGradient(0, 0, W, H * 0.9);
|
||||
grd.addColorStop(0, `hsl(${h0},26%,20%)`);
|
||||
grd.addColorStop(0.45, `hsl(${h0},18%,14%)`);
|
||||
grd.addColorStop(1, `hsl(${h0},12%,10%)`);
|
||||
g.fillStyle = grd;
|
||||
g.fillRect(0, 0, W, H);
|
||||
g.strokeStyle = 'rgba(255,255,255,0.06)';
|
||||
g.lineWidth = 1;
|
||||
for (let x = 0; x < W; x += 48) {
|
||||
g.beginPath();
|
||||
g.moveTo(x, 0);
|
||||
g.lineTo(x + H * 0.04, H);
|
||||
g.stroke();
|
||||
}
|
||||
const vg = g.createRadialGradient(W * 0.45, H * 0.35, 20, W * 0.5, H * 0.55, W * 0.85);
|
||||
vg.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
vg.addColorStop(1, 'rgba(0,0,0,0.38)');
|
||||
g.fillStyle = vg;
|
||||
g.fillRect(0, 0, W, H);
|
||||
g.fillStyle = 'rgba(255,255,255,0.1)';
|
||||
g.font = '10px system-ui,sans-serif';
|
||||
g.textAlign = 'right';
|
||||
g.fillText(String(i + 1), W - 10, H - 8);
|
||||
gauntletCrownRunwayBgImgs[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
function gauntletCrownRunwayBgDrawActivePlay() {
|
||||
if (!gauntletCrownRunwayBgMapEligiblePlay() || gauntletCrownRunwayBgOff) return false;
|
||||
return gauntletCrownRunwayBgImagesReadyPlay();
|
||||
}
|
||||
|
||||
function gauntletCrownRunwayBgHideObstaclesPlay() {
|
||||
return gauntletCrownRunwayBgDrawActivePlay() && gauntletCrownRunwayBgFinishLatched;
|
||||
}
|
||||
|
||||
function drawImageCoverCanvasPlay(ctx, img, cwR, chR) {
|
||||
const d = gauntletCrownRunwayBgNatDims(img);
|
||||
if (!d) return;
|
||||
const iw = d.nw;
|
||||
const ih = d.nh;
|
||||
if (!iw || !ih) return;
|
||||
const scale = Math.max(cwR / iw, chR / ih);
|
||||
const dw = iw * scale;
|
||||
const dh = ih * scale;
|
||||
const dx = (cwR - dw) / 2;
|
||||
const dy = (chR - dh) / 2;
|
||||
ctx.drawImage(img, 0, 0, iw, ih, dx, dy, dw, dh);
|
||||
}
|
||||
|
||||
function drawRunwayHSlicePlay(ctx, img, worldX0, segW, scrollLeft, cwR, chR) {
|
||||
const d0 = gauntletCrownRunwayBgNatDims(img);
|
||||
if (!d0 || segW <= 0) return;
|
||||
const natW = d0.nw;
|
||||
const natH = d0.nh;
|
||||
const v0 = scrollLeft;
|
||||
const v1 = scrollLeft + cwR;
|
||||
const o0 = Math.max(worldX0, v0);
|
||||
const o1 = Math.min(worldX0 + segW, v1);
|
||||
if (o1 <= o0) return;
|
||||
const u0 = ((o0 - worldX0) / segW) * natW;
|
||||
const u1 = ((o1 - worldX0) / segW) * natW;
|
||||
const dx = o0 - scrollLeft;
|
||||
const dw = o1 - o0;
|
||||
/** ทาบแนวตั้งเล็กน้อยกันช่องว่างย่อย 1px (seam) ระหว่างแถบลูป */
|
||||
ctx.drawImage(img, u0, 0, u1 - u0, natH, dx - 0.25, 0, dw + 0.55, chR);
|
||||
}
|
||||
|
||||
/** วาดแถบรันเวย์ในระบบพิกัด X = ความกว้างมองเห็น (cwR) × สูงแถบ chR · scrollView = จุดเริ่ม viewport ในโลกรันเวย์ */
|
||||
function drawGauntletCrownRunwayStripCorePlay(ctx, cwR, chR, scrollView) {
|
||||
const start = gauntletCrownRunwayBgImgs[0];
|
||||
const L2 = gauntletCrownRunwayBgImgs[1];
|
||||
const L3 = gauntletCrownRunwayBgImgs[2];
|
||||
const L4 = gauntletCrownRunwayBgImgs[3];
|
||||
const { w0, w1, w2, w3 } = gauntletCrownRunwayBgStripWidthsAtCh(chR);
|
||||
const cycle = w1 + w2 + w3;
|
||||
const S = scrollView;
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.fillStyle = '#1a1b26';
|
||||
ctx.fillRect(0, 0, cwR, chR);
|
||||
if (w0 > 0) drawRunwayHSlicePlay(ctx, start, 0, w0, S, cwR, chR);
|
||||
if (cycle <= 0) return;
|
||||
const ws = [w1, w2, w3];
|
||||
const ims = [L2, L3, L4];
|
||||
const uTotal = Math.max(0, S - w0);
|
||||
let cycN = Math.floor(uTotal / cycle);
|
||||
let uRem = uTotal - cycN * cycle;
|
||||
let si = 0;
|
||||
while (si < 3 && uRem >= ws[si]) {
|
||||
uRem -= ws[si];
|
||||
si++;
|
||||
}
|
||||
if (si >= 3) {
|
||||
cycN++;
|
||||
si = 0;
|
||||
uRem = 0;
|
||||
}
|
||||
let acc = 0;
|
||||
for (let k = 0; k < si; k++) acc += ws[k];
|
||||
let curLeft = w0 + cycN * cycle + acc;
|
||||
let guard = 0;
|
||||
let idx = si;
|
||||
while (curLeft < S + cwR && guard++ < 500) {
|
||||
drawRunwayHSlicePlay(ctx, ims[idx], curLeft, ws[idx], S, cwR, chR);
|
||||
curLeft += ws[idx];
|
||||
idx = (idx + 1) % 3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} canvasW
|
||||
* @param {number} canvasH
|
||||
* @param {null|{ worldMinX:number, worldMaxX:number, camX:number, camY:number, zDraw:number, mapWpx:number, mapHpx:number }} worldAlign — ถ้ามี จะคลิป+สเกลให้ตรงกริดแมป (ตัว/สิ่งกีดขวางตรงพื้นที่ตั้ง)
|
||||
*/
|
||||
function drawGauntletCrownRunwayBgFullCanvasPlay(canvasW, canvasH, worldAlign) {
|
||||
const finish = gauntletCrownRunwayBgImgs[4];
|
||||
const align = worldAlign && typeof worldAlign === 'object'
|
||||
&& Number.isFinite(worldAlign.worldMinX)
|
||||
&& Number.isFinite(worldAlign.worldMaxX)
|
||||
&& Number.isFinite(worldAlign.camX)
|
||||
&& Number.isFinite(worldAlign.camY)
|
||||
&& Number.isFinite(worldAlign.zDraw)
|
||||
&& Number.isFinite(worldAlign.mapWpx)
|
||||
&& Number.isFinite(worldAlign.mapHpx);
|
||||
if (gauntletCrownRunwayBgFinishLatched && gauntletCrownRunwayBgImageSlotReadyPlay(finish)) {
|
||||
if (align) {
|
||||
const { camX, camY, zDraw, mapWpx, mapHpx } = worldAlign;
|
||||
ctx.fillStyle = '#1a1b26';
|
||||
ctx.fillRect(0, 0, canvasW, canvasH);
|
||||
ctx.save();
|
||||
/* เมทริกซ์เดียวกับ worldToScreen + คลิปโลก [0,map] — รูปจบครอบทั้งแผนที่ที่ world origin */
|
||||
ctx.setTransform(zDraw, 0, 0, zDraw, canvasW / 2 - camX * zDraw, canvasH / 2 - camY * zDraw);
|
||||
ctx.beginPath();
|
||||
ctx.rect(0, 0, mapWpx, mapHpx);
|
||||
ctx.clip();
|
||||
drawImageCoverCanvasPlay(ctx, finish, Math.max(1, Math.round(mapWpx)), Math.max(1, Math.round(mapHpx)));
|
||||
ctx.restore();
|
||||
} else {
|
||||
drawImageCoverCanvasPlay(ctx, finish, Math.max(1, Math.round(canvasW)), Math.max(1, Math.round(canvasH)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* ใช้ความกว้างมองเห็นแบบ float ให้เท่ากับ canvas.width/zDraw — ห้าม Math.round ไม่งั้นซูมแล้วแถบ runway กว้างไม่ตรง worldToScreen (แผนหลุดจากตัว) */
|
||||
const cwR = align
|
||||
? Math.max(1e-6, worldAlign.worldMaxX - worldAlign.worldMinX)
|
||||
: Math.max(1, Math.round(canvasW));
|
||||
const chR = align
|
||||
? Math.max(1, Math.round(worldAlign.mapHpx))
|
||||
: Math.max(1, Math.round(canvasH));
|
||||
const scrollView = align
|
||||
? (gauntletCrownRunwayBgScrollPx + worldAlign.worldMinX)
|
||||
: gauntletCrownRunwayBgScrollPx;
|
||||
if (align) {
|
||||
const { camX, camY, zDraw, mapWpx, mapHpx, worldMinX } = worldAlign;
|
||||
ctx.fillStyle = '#1a1b26';
|
||||
ctx.fillRect(0, 0, canvasW, canvasH);
|
||||
ctx.save();
|
||||
ctx.setTransform(zDraw, 0, 0, zDraw, canvasW / 2 - camX * zDraw, canvasH / 2 - camY * zDraw);
|
||||
ctx.beginPath();
|
||||
ctx.rect(0, 0, mapWpx, mapHpx);
|
||||
ctx.clip();
|
||||
ctx.translate(worldMinX, 0);
|
||||
drawGauntletCrownRunwayStripCorePlay(ctx, cwR, chR, scrollView);
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
ctx.save();
|
||||
drawGauntletCrownRunwayStripCorePlay(ctx, cwR, chR, scrollView);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function gauntletCrownRunwayAliveCountPlay() {
|
||||
let n = 0;
|
||||
if (!me.gauntletEliminated) n++;
|
||||
others.forEach((o, id) => {
|
||||
if (isPreviewBotId(id)) return;
|
||||
if (!o.gauntletEliminated) n++;
|
||||
});
|
||||
return n;
|
||||
}
|
||||
|
||||
function ensureGauntletCrownRunwayInitialPeerSnapshotPlay() {
|
||||
if (gauntletCrownRunwayBgInitialTotalPeers != null) return;
|
||||
if (!gauntletCrownRunwayBgMapEligiblePlay()) return;
|
||||
if (gauntletCrownPregamePhase !== 'live') return;
|
||||
let n = 1;
|
||||
others.forEach((o, id) => {
|
||||
if (isPreviewBotId(id)) return;
|
||||
n++;
|
||||
});
|
||||
gauntletCrownRunwayBgInitialTotalPeers = n;
|
||||
}
|
||||
|
||||
function gauntletCrownRunwayShouldFinishPlay() {
|
||||
if (!gauntletCrownRunwayBgDrawActivePlay()) return false;
|
||||
if (!mapData || gauntletCrownPregamePhase !== 'live') return false;
|
||||
ensureGauntletCrownRunwayInitialPeerSnapshotPlay();
|
||||
if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs) && Date.now() >= gauntletEndsAtMs) return true;
|
||||
const tot = gauntletCrownRunwayBgInitialTotalPeers || 1;
|
||||
if (tot >= 2 && gauntletCrownRunwayAliveCountPlay() <= 1) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function resetGauntletCrownRunwaySpawnScrollSnap() {
|
||||
gauntletCrownRunwaySpawnScrollSnapped = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* จัด gauntletCrownRunwayBgScrollPx ครั้งแรกให้คอลัมน์เกิด (ซ้ายสุด) ตรง ~runwaySpawnAlignFrac ของความกว้างแถบ START (w0)
|
||||
* ในแมป: gauntletCrownRunwayBg.runwaySpawnAlignFrac 0.02–0.95 (ค่าเริ่ม 0.32)
|
||||
* Anchor: คอลัมน์ซ้ายสุดระหว่างช่องเกิดในแมปกับตำแหน่งจริงของทุกคนในรอบ (รวม me + บอทพรีวิว) — ถ้า me ถูก resolve ไปคอลัมน์ซ้ายกว่า min ของช่องวาด แต่ runway ยึดแค่ช่องวาด จะเห็นตัวลอยซ้ายของพรมแดง
|
||||
* Use min(editor spawn column, floor of every alive entity x) so runway START art stays tied to where characters actually stand.
|
||||
*/
|
||||
function trySnapGauntletCrownRunwayScrollToSpawnsPlay() {
|
||||
if (gauntletCrownRunwaySpawnScrollSnapped) return;
|
||||
if (!gauntletCrownRunwayBgMapEligiblePlay() || gauntletCrownRunwayBgOff || !mapData) return;
|
||||
if (!gauntletCrownRunwayBgImagesReadyPlay()) return;
|
||||
const h = mapData.height || 15;
|
||||
const ts = mapData.tileSize || 32;
|
||||
const mapHpx = Math.max(1, Math.round(h * ts));
|
||||
const { w0 } = gauntletCrownRunwayBgStripWidthsAtCh(mapHpx);
|
||||
if (!(w0 > 0)) return;
|
||||
const slots = collectGauntletSpawnSlotsPlay(mapData);
|
||||
let minTX = Infinity;
|
||||
if (slots.length) minTX = Math.min(...slots.map((s) => s.x));
|
||||
if (Number.isFinite(me.x)) minTX = Math.min(minTX, Math.floor(me.x));
|
||||
others.forEach((o, id) => {
|
||||
if (!o || o.gauntletEliminated) return;
|
||||
if (Number.isFinite(o.x)) minTX = Math.min(minTX, Math.floor(o.x));
|
||||
});
|
||||
if (!Number.isFinite(minTX) || minTX === Infinity) minTX = 1;
|
||||
const anchorWorldX = (Math.max(0, minTX) + 0.5) * ts;
|
||||
const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {};
|
||||
const frac = Math.max(0.02, Math.min(0.95, Number(raw.runwaySpawnAlignFrac) || 0.32));
|
||||
gauntletCrownRunwayBgScrollPx = frac * w0 - anchorWorldX;
|
||||
gauntletCrownRunwaySpawnScrollSnapped = true;
|
||||
}
|
||||
|
||||
function reloadGauntletCrownRunwayBgFromMap() {
|
||||
gauntletCrownRunwayBgImgs = [null, null, null, null, null];
|
||||
gauntletCrownRunwayBgScrollPx = 0;
|
||||
gauntletCrownRunwayBgFinishLatched = false;
|
||||
gauntletCrownRunwayBgInitialTotalPeers = null;
|
||||
lastGauntletCrownRunwayBgTickMs = 0;
|
||||
resetGauntletCrownRunwaySpawnScrollSnap();
|
||||
if (!gauntletCrownRunwayBgMapEligiblePlay()) return;
|
||||
const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {};
|
||||
gauntletCrownRunwayBgOff = raw.enabled === false;
|
||||
gauntletCrownRunwayBgSpeedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 48));
|
||||
if (gauntletCrownRunwayBgOff) return;
|
||||
const srcs = [
|
||||
(typeof raw.startImage === 'string' && raw.startImage.length) ? raw.startImage : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_START,
|
||||
(typeof raw.loopImage2 === 'string' && raw.loopImage2.length) ? raw.loopImage2 : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP2,
|
||||
(typeof raw.loopImage3 === 'string' && raw.loopImage3.length) ? raw.loopImage3 : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP3,
|
||||
(typeof raw.loopImage4 === 'string' && raw.loopImage4.length) ? raw.loopImage4 : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP4,
|
||||
(typeof raw.finishImage === 'string' && raw.finishImage.length) ? raw.finishImage : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_FINISH,
|
||||
];
|
||||
let pending = 5;
|
||||
const bump = () => {
|
||||
pending--;
|
||||
if (pending <= 0) {
|
||||
ensureGauntletCrownRunwayBgPlaceholdersPlay();
|
||||
try { draw(); } catch (e) { /* ignore */ }
|
||||
}
|
||||
};
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const im = new Image();
|
||||
const idx = i;
|
||||
im.onload = () => {
|
||||
gauntletCrownRunwayBgImgs[idx] = im;
|
||||
bump();
|
||||
};
|
||||
im.onerror = () => { bump(); };
|
||||
im.src = srcs[i];
|
||||
}
|
||||
}
|
||||
|
||||
function tickGauntletCrownRunwayBgPlay(dtSec) {
|
||||
if (!gauntletCrownRunwayBgDrawActivePlay()) return;
|
||||
/** พรีรัน (howto / countdown) — ห้ามเลื่อนก่อน GO · Mega Virus ใช้ shell เดียวกัน */
|
||||
if (usesCrownLobbyShellPlay() && gauntletCrownPregamePhase !== 'live') return;
|
||||
if (gauntletCrownRunwayBgFinishLatched) return;
|
||||
if (gauntletCrownRunwayShouldFinishPlay()) {
|
||||
gauntletCrownRunwayBgFinishLatched = true;
|
||||
return;
|
||||
}
|
||||
gauntletCrownRunwayBgScrollPx += (gauntletCrownRunwayBgSpeedPxPerSec || 48) * dtSec;
|
||||
}
|
||||
|
||||
/** stack + แมป mnn93hpi — พื้นหลัง intro+loop เลื่อนแนวตั้ง (stackTowerBgScroll ในแมป) */
|
||||
const STACK_TOWER_SCROLL_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnn93hpi/intro.png';
|
||||
const STACK_TOWER_SCROLL_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnn93hpi/loop.png';
|
||||
@@ -582,7 +950,7 @@
|
||||
/** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */
|
||||
let mapGridImageImgs = [];
|
||||
let mapGridImageHeldImgs = [];
|
||||
let me = { x: 1, y: 1, direction: 'down', nickname: nick, isWalking: false, playTint: null, gauntletScore: 0, gauntletEliminated: false, tx: null, ty: null, quizCarryHeld: null };
|
||||
let me = { x: 1, y: 1, direction: 'down', nickname: nick, isWalking: false, playTint: null, gauntletScore: 0, gauntletEliminated: false, tx: null, ty: null, quizCarryHeld: null, gauntletCrownPenaltyFxUntil: 0 };
|
||||
const others = new Map();
|
||||
/** Stack preview HUD: เทอร์มินัลล็อก (cyber UI) */
|
||||
const STACK_PREVIEW_HUD_LOG_MAX = 8;
|
||||
@@ -3394,6 +3762,12 @@
|
||||
const im = grabBtnTeardown.querySelector('img');
|
||||
if (im) im.src = '/Game/img/quiz-carry/btn-grab.png';
|
||||
}
|
||||
const gchJumpTeardown = document.getElementById('gauntlet-crown-jump-btn');
|
||||
if (gchJumpTeardown) {
|
||||
gchJumpTeardown.classList.add('is-hidden');
|
||||
gchJumpTeardown.setAttribute('aria-hidden', 'true');
|
||||
gchJumpTeardown.style.pointerEvents = '';
|
||||
}
|
||||
}
|
||||
|
||||
function setupPlayQuizUi() {
|
||||
@@ -3516,6 +3890,69 @@
|
||||
if (gauntletCrownPregamePhase != null && gauntletCrownPregamePhase !== 'live') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last Light (crown heist): center camera on the whole party bbox instead of only `me`
|
||||
* — reduces empty void on the side when you fall behind and keeps bots + you in frame together when possible.
|
||||
*/
|
||||
function getGauntletCrownHeistGroupCameraCenterPxPlay(tileSizeRef, canvasW, canvasH, zDrawVal) {
|
||||
if (!isGauntletCrownHeistMapPlay() || !mapData) return null;
|
||||
const md = mapData;
|
||||
const { cw, ch } = getCharacterFootprintWH(md);
|
||||
const ww = md.width || 20;
|
||||
const hh = md.height || 15;
|
||||
const mapWpx = ww * tileSizeRef;
|
||||
const mapHpx = hh * tileSizeRef;
|
||||
const halfW = canvasW / (2 * zDrawVal);
|
||||
const halfH = canvasH / (2 * zDrawVal);
|
||||
const ents = [];
|
||||
if (!me.gauntletEliminated && Number.isFinite(me.x) && Number.isFinite(me.y)) {
|
||||
ents.push({ cx: (me.x + cw * 0.5) * tileSizeRef, cy: (me.y + ch * 0.5) * tileSizeRef });
|
||||
}
|
||||
others.forEach((o, id) => {
|
||||
if (!o || o.gauntletEliminated) return;
|
||||
if (!previewFillBots && isPreviewBotId(id)) return;
|
||||
if (!Number.isFinite(o.x) || !Number.isFinite(o.y)) return;
|
||||
ents.push({ cx: (o.x + cw * 0.5) * tileSizeRef, cy: (o.y + ch * 0.5) * tileSizeRef });
|
||||
});
|
||||
if (!ents.length) return null;
|
||||
let minCx = Infinity;
|
||||
let maxCx = -Infinity;
|
||||
let sumCy = 0;
|
||||
for (let i = 0; i < ents.length; i++) {
|
||||
const e = ents[i];
|
||||
minCx = Math.min(minCx, e.cx);
|
||||
maxCx = Math.max(maxCx, e.cx);
|
||||
sumCy += e.cy;
|
||||
}
|
||||
let px = (minCx + maxCx) * 0.5;
|
||||
let py = sumCy / ents.length;
|
||||
const minCamX = halfW;
|
||||
const maxCamX = Math.max(minCamX, mapWpx - halfW);
|
||||
px = Math.max(minCamX, Math.min(maxCamX, px));
|
||||
const minCamY = halfH;
|
||||
const maxCamY = Math.max(minCamY, mapHpx - halfH);
|
||||
py = Math.max(minCamY, Math.min(maxCamY, py));
|
||||
return { px, py };
|
||||
}
|
||||
|
||||
/** Preview bots only — full tail-off loss needs server support for real humans */
|
||||
const GAUNTLET_CROWN_TAIL_OFFSCREEN_MARGIN_PX = 40;
|
||||
function maybeEliminateGauntletPreviewBotsTailOffCameraPlay(camX, camY, zDrawVal) {
|
||||
if (!previewFillBots || !isGauntletCrownHeistMapPlay() || !mapData) return;
|
||||
if (gauntletCrownPregamePhase !== 'live') return;
|
||||
const halfW = canvas.width / (2 * zDrawVal);
|
||||
const worldMinX = camX - halfW;
|
||||
const gate = worldMinX - GAUNTLET_CROWN_TAIL_OFFSCREEN_MARGIN_PX;
|
||||
const { cw } = getCharacterFootprintWH(mapData);
|
||||
const ts = mapData.tileSize || 32;
|
||||
others.forEach((o, id) => {
|
||||
if (!isPreviewBotId(id) || !o || o.gauntletEliminated) return;
|
||||
const rightWorld = (Number(o.x) + cw) * ts;
|
||||
if (rightWorld < gate) o.gauntletEliminated = true;
|
||||
});
|
||||
}
|
||||
|
||||
function isStack() { return mapData && mapData.gameType === 'stack'; }
|
||||
function isJumpSurvive() { return mapData && mapData.gameType === 'jump_survive'; }
|
||||
|
||||
@@ -6615,6 +7052,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** Gauntlet (รวมพรมแดง): กระโดด — ใช้ร่วมกับ Space/W/↑ และปุ่ม UI */
|
||||
function tryRequestGauntletJumpPlay() {
|
||||
if (!mapData || !isGauntlet() || isChatFocused()) return false;
|
||||
if (isGauntletCrownPregameBlockingPlay()) return false;
|
||||
if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return false;
|
||||
const now = Date.now();
|
||||
if (now - lastGauntletJumpKey < 200) return false;
|
||||
lastGauntletJumpKey = now;
|
||||
socket.emit('gauntlet-jump');
|
||||
return true;
|
||||
}
|
||||
|
||||
function syncGauntletCrownJumpButton() {
|
||||
const btn = document.getElementById('gauntlet-crown-jump-btn');
|
||||
if (!btn) return;
|
||||
const show = !!(mapData && isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live' && !me.gauntletEliminated);
|
||||
if (!show) {
|
||||
btn.classList.add('is-hidden');
|
||||
btn.setAttribute('aria-hidden', 'true');
|
||||
btn.style.pointerEvents = 'none';
|
||||
return;
|
||||
}
|
||||
btn.classList.remove('is-hidden');
|
||||
btn.setAttribute('aria-hidden', 'false');
|
||||
btn.style.pointerEvents = '';
|
||||
}
|
||||
|
||||
function resetQuizCarryPlayState() {
|
||||
quizCarryPregameActive = false;
|
||||
hideQuizCarryPregameOverlay();
|
||||
@@ -7720,6 +8184,7 @@
|
||||
gauntletCrownRunHeldRemote = !!payload.gauntletCrownRunHeld;
|
||||
if (!gauntletCrownRunHeldRemote && usesCrownLobbyShellPlay()) {
|
||||
gauntletCrownPregamePhase = 'live';
|
||||
trySnapGauntletCrownRunwayScrollToSpawnsPlay();
|
||||
}
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaneImageUrls') && Array.isArray(payload.gauntletLaneImageUrls)) {
|
||||
@@ -8110,12 +8575,20 @@
|
||||
o.gauntletScore = 0;
|
||||
} else {
|
||||
o.gauntletScore = Math.max(0, (o.gauntletScore || 0) - 10);
|
||||
o.gauntletCrownPenaltyFxUntil = Date.now() + 1100;
|
||||
px = Math.max(0, px - 1);
|
||||
}
|
||||
} else {
|
||||
px = Math.max(0, px - 1);
|
||||
}
|
||||
}
|
||||
if (crown && mapData) {
|
||||
const mx = getGauntletCrownHeistMaxEntityXPlay(mapData);
|
||||
if (mx != null && Number.isFinite(mx)) {
|
||||
const maxPx = Math.floor(mx + 1e-6);
|
||||
px = Math.min(px, maxPx);
|
||||
}
|
||||
}
|
||||
if ((o.gauntletJumpTicks || 0) > 0) o.gauntletJumpTicks--;
|
||||
o.tx = px;
|
||||
o.ty = py;
|
||||
@@ -8251,6 +8724,10 @@
|
||||
gauntletCrownCountdownTimer = null;
|
||||
}
|
||||
gauntletCrownPregamePhase = 'countdown';
|
||||
if (previewFillBots && mapData && mapData.gameType === 'gauntlet') {
|
||||
applyGauntletPreviewSpawnLayout(false);
|
||||
}
|
||||
trySnapGauntletCrownRunwayScrollToSpawnsPlay();
|
||||
const howto = document.getElementById('gauntlet-crown-howto-overlay');
|
||||
if (howto) howto.classList.add('is-hidden');
|
||||
gauntletCrownHowtoVisible = false;
|
||||
@@ -8284,11 +8761,13 @@
|
||||
socket.emit('gauntlet-crown-begin-run', (res) => {
|
||||
if (res && res.ok) {
|
||||
gauntletCrownPregamePhase = 'live';
|
||||
trySnapGauntletCrownRunwayScrollToSpawnsPlay();
|
||||
if (isMegaVirusMissionShellMapPlay()) applyMegaBalloonLiveStartPlay();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
gauntletCrownPregamePhase = 'live';
|
||||
trySnapGauntletCrownRunwayScrollToSpawnsPlay();
|
||||
if (isMegaVirusMissionShellMapPlay()) applyMegaBalloonLiveStartPlay();
|
||||
}
|
||||
};
|
||||
@@ -8323,6 +8802,7 @@
|
||||
gauntletCrownCountdownTimer = null;
|
||||
}
|
||||
gauntletCrownPregamePhase = 'howto';
|
||||
resetGauntletCrownRunwaySpawnScrollSnap();
|
||||
const ov = document.getElementById('gauntlet-crown-howto-overlay');
|
||||
if (!ov) return;
|
||||
gauntletCrownHowtoVisible = true;
|
||||
@@ -11308,6 +11788,7 @@
|
||||
}
|
||||
reloadPlayScrollBgFromMap();
|
||||
reloadStackTowerScrollBgFromMap();
|
||||
reloadGauntletCrownRunwayBgFromMap();
|
||||
normalizeGridImageCellsOnMap(mapData);
|
||||
loadMapGridImages();
|
||||
var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน'
|
||||
@@ -11970,6 +12451,7 @@
|
||||
const pid = String(p.id);
|
||||
const mid = String(myId);
|
||||
if (pid === mid) {
|
||||
const prevSc = Math.max(0, Number(me.gauntletScore) || 0);
|
||||
const px = Number(p.x);
|
||||
const py = Number(p.y);
|
||||
if (Number.isFinite(px)) me.tx = px;
|
||||
@@ -11979,11 +12461,17 @@
|
||||
meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0;
|
||||
meGauntletJumpVis = meGauntletJumpTicks;
|
||||
const sc = Number(p.gauntletScore);
|
||||
me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0;
|
||||
const newSc = Number.isFinite(sc) ? Math.max(0, sc) : 0;
|
||||
me.gauntletScore = newSc;
|
||||
if (isGauntletCrownHeistMapPlay() && prevSc - newSc >= 10) {
|
||||
me.gauntletCrownPenaltyFxUntil = Date.now() + 1100;
|
||||
}
|
||||
me.gauntletEliminated = !!p.gauntletEliminated;
|
||||
clampGauntletCrownHeistEntityXInPlacePlay(me);
|
||||
} else {
|
||||
const o = others.get(p.id) ?? others.get(pid);
|
||||
if (o) {
|
||||
const prevSc = Math.max(0, Number(o.gauntletScore) || 0);
|
||||
const px = Number(p.x);
|
||||
const py = Number(p.y);
|
||||
if (Number.isFinite(px)) o.tx = px;
|
||||
@@ -11993,8 +12481,13 @@
|
||||
o.gauntletJumpTicks = Number.isFinite(jt) ? jt : 0;
|
||||
if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks;
|
||||
const sc = Number(p.gauntletScore);
|
||||
o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0;
|
||||
const newSc = Number.isFinite(sc) ? Math.max(0, sc) : 0;
|
||||
o.gauntletScore = newSc;
|
||||
if (isGauntletCrownHeistMapPlay() && prevSc - newSc >= 10) {
|
||||
o.gauntletCrownPenaltyFxUntil = Date.now() + 1100;
|
||||
}
|
||||
o.gauntletEliminated = !!p.gauntletEliminated;
|
||||
clampGauntletCrownHeistEntityXInPlacePlay(o);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -12007,9 +12500,12 @@
|
||||
socket.on('gauntlet-ended', (data) => {
|
||||
cancelQuizCarryResultEndAfterTimeup();
|
||||
gauntletEndsAtMs = null;
|
||||
gauntletCrownRunwayBgFinishLatched = false;
|
||||
gauntletCrownRunwayBgInitialTotalPeers = null;
|
||||
gauntletCrownRunHeldRemote = false;
|
||||
gauntletCrownPregamePhase = null;
|
||||
gauntletCrownLobbyReadyMap = {};
|
||||
resetGauntletCrownRunwaySpawnScrollSnap();
|
||||
if (gauntletCrownCountdownTimer) {
|
||||
clearTimeout(gauntletCrownCountdownTimer);
|
||||
gauntletCrownCountdownTimer = null;
|
||||
@@ -12096,6 +12592,7 @@
|
||||
}
|
||||
reloadPlayScrollBgFromMap();
|
||||
reloadStackTowerScrollBgFromMap();
|
||||
reloadGauntletCrownRunwayBgFromMap();
|
||||
normalizeGridImageCellsOnMap(mapData);
|
||||
loadMapGridImages();
|
||||
if (mapData.gameType === 'gauntlet' && ev.peersSnap && Array.isArray(ev.peersSnap)) {
|
||||
@@ -12436,12 +12933,6 @@
|
||||
syncQuizCarryEmbedCountdownLayout();
|
||||
}
|
||||
|
||||
// ให้ scroll wheel ใช้ซูมเข้า/ออกแบบง่าย ๆ
|
||||
canvas.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
const factor = e.deltaY < 0 ? 1.1 : 0.9;
|
||||
zoom = Math.max(0.7, Math.min(2.5, zoom * factor));
|
||||
}, { passive: false });
|
||||
canvas.addEventListener('click', (e) => {
|
||||
if (!mapData || !isStack() || isChatFocused()) return;
|
||||
tryHumanStackDrop();
|
||||
@@ -12492,12 +12983,30 @@
|
||||
return { cw, ch, colW, colH };
|
||||
}
|
||||
|
||||
function getGauntletCrownHeistMaxEntityXPlay(md) {
|
||||
if (!md || !isGauntletCrownHeistMapPlay()) return null;
|
||||
const w = md.width || 20;
|
||||
const { cw } = getCharacterFootprintWH(md);
|
||||
const capRight = GAUNTLET_CROWN_HEIST_MAX_X_WORLD_FRAC * w - cw;
|
||||
const fullMapMax = Math.max(0, w - cw + 0.999);
|
||||
return Math.max(0, Math.min(fullMapMax, capRight));
|
||||
}
|
||||
function clampGauntletCrownHeistEntityXInPlacePlay(ent) {
|
||||
if (!ent || !mapData || !isGauntletCrownHeistMapPlay()) return;
|
||||
const mx = getGauntletCrownHeistMaxEntityXPlay(mapData);
|
||||
if (mx == null || !Number.isFinite(mx)) return;
|
||||
if (Number.isFinite(ent.x)) ent.x = Math.max(0, Math.min(mx, ent.x));
|
||||
if (ent.tx != null && Number.isFinite(ent.tx)) ent.tx = Math.max(0, Math.min(mx, ent.tx));
|
||||
}
|
||||
|
||||
/** กันตัวละคร footprint ล้นออกนอกแมป — เดิมใช้ w-0.01 ทำให้ cw/ch>1 ชนขอบแล้วตรรกะเดินค้าง */
|
||||
function clampPlayEntityFootprintToMap(oEnt, md) {
|
||||
if (!oEnt || !md) return;
|
||||
const w = md.width || 20, h = md.height || 15;
|
||||
const { cw, ch } = getCharacterFootprintWH(md);
|
||||
const maxX = Math.max(0, w - cw + 0.999);
|
||||
let maxX = Math.max(0, w - cw + 0.999);
|
||||
const crownMx = getGauntletCrownHeistMaxEntityXPlay(md);
|
||||
if (crownMx != null) maxX = Math.min(maxX, crownMx);
|
||||
const maxY = Math.max(0, h - ch + 0.999);
|
||||
if (!Number.isFinite(oEnt.x)) oEnt.x = 0.5;
|
||||
if (!Number.isFinite(oEnt.y)) oEnt.y = 0.5;
|
||||
@@ -13954,6 +14463,8 @@
|
||||
|
||||
function draw() {
|
||||
if (!mapData) return;
|
||||
if (isGauntletCrownHeistMapPlay()) ensureGauntletCrownScorePenaltyImgPlay();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
const w = mapData.width, h = mapData.height;
|
||||
|
||||
const stackCam = isStack() ? getStackCameraCentersPx() : null;
|
||||
@@ -13970,6 +14481,11 @@
|
||||
camY = mhPx * 0.5;
|
||||
zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96;
|
||||
}
|
||||
const gauntletGroupCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zDraw);
|
||||
if (gauntletGroupCam) {
|
||||
camX = gauntletGroupCam.px;
|
||||
camY = gauntletGroupCam.py;
|
||||
}
|
||||
const halfW = canvas.width / (2 * zDraw);
|
||||
const halfH = canvas.height / (2 * zDraw);
|
||||
|
||||
@@ -13988,6 +14504,18 @@
|
||||
ctx.fillStyle = '#0a0e22';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
drawPlayScrollBgFullCanvas(canvas.width, canvas.height);
|
||||
} else if (gauntletCrownRunwayBgDrawActivePlay()) {
|
||||
ctx.fillStyle = '#1a1b26';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
drawGauntletCrownRunwayBgFullCanvasPlay(canvas.width, canvas.height, {
|
||||
worldMinX,
|
||||
worldMaxX,
|
||||
camX,
|
||||
camY,
|
||||
zDraw: zDraw,
|
||||
mapWpx,
|
||||
mapHpx,
|
||||
});
|
||||
} else if (stackTowerScrollBgDrawActive()) {
|
||||
ctx.fillStyle = '#0a0e22';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
@@ -14343,7 +14871,9 @@
|
||||
drawStackMinigame(ctx, worldToScreen, zDraw);
|
||||
}
|
||||
|
||||
const gauntletObsDraw = isGauntlet() ? getGauntletObsDrawPositionsAt(performance.now()) : [];
|
||||
const gauntletObsDraw = (isGauntlet() && !gauntletCrownRunwayBgHideObstaclesPlay())
|
||||
? getGauntletObsDrawPositionsAt(performance.now())
|
||||
: [];
|
||||
if (isGauntlet() && gauntletObsDraw.length) {
|
||||
const stx = Math.max(0, Math.floor(worldMinX / tileSize));
|
||||
const enx = Math.min(w - 1, Math.ceil(worldMaxX / tileSize));
|
||||
@@ -14453,7 +14983,7 @@
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawAvatar(ax, ay, isMe, name, characterId, direction, isWalking, playTint, gauntletAirTicks, gauntletScoreLabel, quizCarrySignPayload) {
|
||||
function drawAvatar(ax, ay, isMe, name, characterId, direction, isWalking, playTint, gauntletAirTicks, gauntletScoreLabel, quizCarrySignPayload, crownPenaltyFxUntil) {
|
||||
const { cw, ch } = getCharacterFootprintWH(mapData);
|
||||
const air = Number(gauntletAirTicks) || 0;
|
||||
let liftWorldY = 0;
|
||||
@@ -14509,6 +15039,18 @@
|
||||
? (nameBase + ' · ' + gauntletScoreLabel)
|
||||
: nameBase;
|
||||
ctx.fillText(withScore, sx, sy + 10);
|
||||
if (isGauntletCrownHeistMapPlay()
|
||||
&& typeof crownPenaltyFxUntil === 'number'
|
||||
&& crownPenaltyFxUntil > timeMs) {
|
||||
const penImg = ensureGauntletCrownScorePenaltyImgPlay();
|
||||
if (penImg && penImg.complete && penImg.naturalWidth > 0) {
|
||||
const ref = Math.max(18, r * 2);
|
||||
const scale = Math.min(1.15, ref / Math.max(penImg.naturalWidth, penImg.naturalHeight, 1));
|
||||
const dw = penImg.naturalWidth * scale;
|
||||
const dh = penImg.naturalHeight * scale;
|
||||
ctx.drawImage(penImg, 0, 0, penImg.naturalWidth, penImg.naturalHeight, sx - dw * 0.5, spriteTopY - dh - 5, dw, dh);
|
||||
}
|
||||
}
|
||||
if (isQuizCarry() && quizCarrySignPayload) {
|
||||
drawQuizCarryHeldSignBoard(
|
||||
sx, sy, halfSpriteW, spriteTopY,
|
||||
@@ -14548,7 +15090,12 @@
|
||||
me.playTint = mt;
|
||||
}
|
||||
const meTag = isFrogger() ? (' (กบ) ' + froggerScore) : (isGauntlet() && meGauntletJumpTicks > 0 ? ' (กระโดด)' : (isJumpSurvive() && jumpSurviveEliminated ? ' (ตกรอบ)' : ' (คุณ)'));
|
||||
const meWalking = isFrogger() ? false : (isGauntlet() ? (meGauntletJumpTicks > 0 || meGauntletJumpVis > 0.08) : !!me.isWalking);
|
||||
const crownRunStripLive = isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live';
|
||||
const meWalking = isFrogger()
|
||||
? false
|
||||
: (isGauntlet()
|
||||
? ((crownRunStripLive && !me.gauntletEliminated) || meGauntletJumpTicks > 0 || meGauntletJumpVis > 0.08)
|
||||
: !!me.isWalking);
|
||||
const drawRows = [];
|
||||
others.forEach((o, id) => {
|
||||
if (!o) return;
|
||||
@@ -14572,7 +15119,7 @@
|
||||
const o = row.o;
|
||||
const off = isGauntlet() ? { ax: 0, ay: 0 } : peerVisualOffset(id);
|
||||
const otherWalk = isGauntlet()
|
||||
? ((o.gauntletJumpTicks || 0) > 0 || (o.gauntletJumpVis || 0) > 0.08)
|
||||
? ((crownRunStripLive && !o.gauntletEliminated) || (o.gauntletJumpTicks || 0) > 0 || (o.gauntletJumpVis || 0) > 0.08)
|
||||
: (isPreviewBotId(id)
|
||||
? !!o.botIsWalking
|
||||
: !!((o.tx != null && Math.abs((o.tx || o.x) - o.x) > 0.02) || (o.ty != null && Math.abs((o.ty || o.y) - o.y) > 0.02)));
|
||||
@@ -14582,13 +15129,13 @@
|
||||
const peerName = botOut ? (o.nickname + ' (ตกรอบ)') : o.nickname;
|
||||
if (botOut) ctx.save();
|
||||
if (botOut) ctx.globalAlpha = 0.4;
|
||||
drawAvatar(safeX(o.x) + off.ax, safeY(o.y) + off.ay, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o));
|
||||
drawAvatar(safeX(o.x) + off.ax, safeY(o.y) + off.ay, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o), isGauntletCrownHeistMapPlay() ? (o.gauntletCrownPenaltyFxUntil || 0) : 0);
|
||||
if (botOut) ctx.restore();
|
||||
} else {
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) ctx.save();
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) ctx.globalAlpha = 0.4;
|
||||
const faceDirMe = isGauntletFaceRightMapMno9kb07() ? 'right' : me.direction;
|
||||
drawAvatar(safeX(me.x), safeY(me.y), true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me));
|
||||
drawAvatar(safeX(me.x), safeY(me.y), true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me), isGauntletCrownHeistMapPlay() ? (me.gauntletCrownPenaltyFxUntil || 0) : 0);
|
||||
if (isJumpSurvive() && jumpSurviveEliminated) ctx.restore();
|
||||
}
|
||||
});
|
||||
@@ -14694,6 +15241,7 @@
|
||||
drawQuizTfScorePopupsLayer(ctx, worldToScreen, zDraw, timeMs);
|
||||
syncPlayCyberHud();
|
||||
syncQuizCarryGrabButton();
|
||||
syncGauntletCrownJumpButton();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
@@ -14741,12 +15289,7 @@
|
||||
if (isGauntlet() && mapData && !isChatFocused()) {
|
||||
if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
|
||||
e.preventDefault();
|
||||
if (isGauntletCrownPregameBlockingPlay()) return;
|
||||
if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return;
|
||||
const now = Date.now();
|
||||
if (now - lastGauntletJumpKey < 200) return;
|
||||
lastGauntletJumpKey = now;
|
||||
socket.emit('gauntlet-jump');
|
||||
tryRequestGauntletJumpPlay();
|
||||
return;
|
||||
}
|
||||
if (['ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) {
|
||||
@@ -14880,13 +15423,19 @@
|
||||
return;
|
||||
}
|
||||
if (isGauntlet()) {
|
||||
{
|
||||
const nowBg = performance.now();
|
||||
const dtBg = Math.min(0.06, lastGauntletCrownRunwayBgTickMs ? (nowBg - lastGauntletCrownRunwayBgTickMs) / 1000 : 0.016);
|
||||
lastGauntletCrownRunwayBgTickMs = nowBg;
|
||||
tickGauntletCrownRunwayBgPlay(dtBg);
|
||||
}
|
||||
if (isGauntletCrownPregameBlockingPlay()) {
|
||||
me.isWalking = false;
|
||||
draw();
|
||||
requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
me.isWalking = meGauntletJumpTicks > 0;
|
||||
me.isWalking = (isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live' && !me.gauntletEliminated) || meGauntletJumpTicks > 0;
|
||||
const stepGauntletJumpVis = (vis, tgt) => {
|
||||
const t = Number(tgt) || 0;
|
||||
let v = Number(vis) || 0;
|
||||
@@ -14905,6 +15454,13 @@
|
||||
if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks || 0;
|
||||
o.gauntletJumpVis = stepGauntletJumpVis(o.gauntletJumpVis, o.gauntletJumpTicks || 0);
|
||||
});
|
||||
clampPlayEntityFootprintToMap(me, mapData);
|
||||
others.forEach((o) => {
|
||||
if (o) clampPlayEntityFootprintToMap(o, mapData);
|
||||
});
|
||||
const zGa = zoom;
|
||||
const gCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zGa);
|
||||
if (gCam) maybeEliminateGauntletPreviewBotsTailOffCameraPlay(gCam.px, gCam.py, zGa);
|
||||
draw();
|
||||
requestAnimationFrame(tick);
|
||||
return;
|
||||
@@ -15066,6 +15622,16 @@
|
||||
});
|
||||
})();
|
||||
|
||||
(function wireGauntletCrownJumpButton() {
|
||||
const btn = document.getElementById('gauntlet-crown-jump-btn');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
if (!mapData || !isGauntlet() || isChatFocused()) return;
|
||||
tryRequestGauntletJumpPlay();
|
||||
});
|
||||
})();
|
||||
|
||||
(function wireGauntletCrownHowtoPrimary() {
|
||||
const btn = document.getElementById('btn-gch-ready');
|
||||
if (!btn) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
|
||||
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
|
||||
window.APP_VERSION = '0.0233';
|
||||
window.APP_VERSION = '0.0249';
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var t = document.querySelector('.version-tag');
|
||||
if (t) t.textContent = 'v ' + window.APP_VERSION;
|
||||
|
||||
@@ -153,6 +153,41 @@
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
/* Last Light (mno9kb07) — ปุ่มกระโดดมุมขวาล่าง · เทียบ Space / W / ↑ */
|
||||
#gauntlet-crown-jump-btn {
|
||||
position: absolute;
|
||||
right: max(10px, env(safe-area-inset-right, 0px));
|
||||
bottom: max(10px, env(safe-area-inset-bottom, 0px));
|
||||
z-index: 59;
|
||||
width: min(14vh, 16vw, 200px);
|
||||
height: min(14vh, 16vw, 200px);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
transition: transform 0.15s ease, filter 0.15s ease;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
box-shadow: none;
|
||||
filter: drop-shadow(0 0 12px rgba(94, 234, 255, 0.35));
|
||||
}
|
||||
#gauntlet-crown-jump-btn.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
#gauntlet-crown-jump-btn:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
#gauntlet-crown-jump-btn img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
#quiz-game-overlay {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
@@ -2472,6 +2507,9 @@
|
||||
<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>
|
||||
<button type="button" id="gauntlet-crown-jump-btn" class="is-hidden" aria-label="กระโดด (Jump)" title="กระโดด — เหมือน Space / W / ↑" aria-hidden="true">
|
||||
<img src="/Game/img/gauntlet-assets/btn-jump.png" alt="" width="256" height="256" decoding="async" />
|
||||
</button>
|
||||
<div id="quiz-map-question-panel" class="is-hidden" aria-hidden="true">
|
||||
<img id="quiz-map-q-feedback-icon" class="quiz-map-q-feedback-icon is-hidden" alt="" width="64" height="64" decoding="async" />
|
||||
<div id="quiz-map-question-kicker" class="quiz-map-question-kicker is-hidden" aria-hidden="true"></div>
|
||||
@@ -2675,8 +2713,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0233"></script>
|
||||
<script src="js/play.js?v=0.0233"></script>
|
||||
<script src="js/version.js?v=0.0249"></script>
|
||||
<script src="js/play.js?v=0.0249"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user