minigame 5 add more design 1.3

This commit is contained in:
2026-05-05 18:38:44 +00:00
parent 9375b8b0ae
commit 5aa7ab802f
7 changed files with 234 additions and 31 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -359,7 +359,7 @@
</div>
</div>
<script src="js/version.js?v=0.0258"></script>
<script src="js/editor.js?v=0.0258"></script>
<script src="js/editor.js?v=0.0311"></script>
<div class="version-tag">v —</div>
</body>
</html>
+13 -11
View File
@@ -1907,12 +1907,13 @@
const shOpt = document.getElementById('draw-mode-option-shooter-spawn');
const shWrap = document.getElementById('shooter-slot-paint-wrap');
const btnClrSh = document.getElementById('btn-clear-shooter-spawns');
const shJump = gt === 'space_shooter' || gt === 'jump_survive';
if (shOpt) {
shOpt.hidden = gt !== 'space_shooter';
if (gt !== 'space_shooter' && drawModeEl && drawModeEl.value === 'shooterSpawnPaint') drawModeEl.value = 'wall';
shOpt.hidden = !shJump;
if (!shJump && drawModeEl && drawModeEl.value === 'shooterSpawnPaint') drawModeEl.value = 'wall';
}
if (shWrap) shWrap.style.display = gt === 'space_shooter' ? '' : 'none';
if (btnClrSh) btnClrSh.hidden = gt !== 'space_shooter';
if (shWrap) shWrap.style.display = shJump ? '' : 'none';
if (btnClrSh) btnClrSh.hidden = !shJump;
const bbPlayerOpt = document.getElementById('draw-mode-option-balloon-boss-player');
const bbBossOpt = document.getElementById('draw-mode-option-balloon-boss-boss');
const bbWrap = document.getElementById('balloon-boss-slot-paint-wrap');
@@ -1950,7 +1951,7 @@
jPlatOpt.hidden = gt !== 'jump_survive';
if (gt !== 'jump_survive' && drawModeEl && drawModeEl.value === 'jumpSurvivePlatform') drawModeEl.value = 'wall';
}
if (drawModeEl && drawModeEl.value === 'shooterSpawnPaint' && gt !== 'space_shooter') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'shooterSpawnPaint' && gt !== 'space_shooter' && gt !== 'jump_survive') drawModeEl.value = 'wall';
const lobbySpawnOpt = document.getElementById('draw-mode-option-lobby-spawn');
if (lobbySpawnOpt) {
const useLobby = supportsLobbySpawnPaint(gt);
@@ -2219,7 +2220,7 @@
ctx.strokeRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.lineWidth = 1;
}
if (gtDraw === 'space_shooter' && shooterSpawnSlots[y] && shooterSpawnSlots[y][x] >= 1 && shooterSpawnSlots[y][x] <= 6) {
if ((gtDraw === 'space_shooter' || gtDraw === 'jump_survive') && shooterSpawnSlots[y] && shooterSpawnSlots[y][x] >= 1 && shooterSpawnSlots[y][x] <= 6) {
const sn = shooterSpawnSlots[y][x];
const hue = (sn * 52 + 180) % 360;
ctx.fillStyle = 'hsla(' + hue + ', 70%, 55%, 0.38)';
@@ -2738,7 +2739,8 @@
ensureJumpSurvivePlatformArea();
jumpSurvivePlatformArea[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'shooterSpawnPaint') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'space_shooter') return;
const gtSh = (gameTypeEl ? gameTypeEl.value : gameType);
if (gtSh !== 'space_shooter' && gtSh !== 'jump_survive') return;
ensureShooterSpawnSlots();
if (objects[y][x] === 1) {
if (statusEl) statusEl.textContent = 'ช่องกำแพง — ใช้เป็นจุดเกิดยานไม่ได้';
@@ -2753,10 +2755,10 @@
}
}
shooterSpawnSlots[y][x] = slot;
if (statusEl) statusEl.textContent = 'ยาน P' + slot + ' → ช่อง (' + x + ',' + y + ')';
if (statusEl) statusEl.textContent = (gtSh === 'jump_survive' ? 'Jump P' : 'ยาน P') + slot + ' → ช่อง (' + x + ',' + y + ')';
} else {
shooterSpawnSlots[y][x] = 0;
if (statusEl) statusEl.textContent = 'ลบจุดเกิดยานที่ช่อง (' + x + ',' + y + ')';
if (statusEl) statusEl.textContent = (gtSh === 'jump_survive' ? 'ลบ Jump P' : 'ลบจุดเกิดยาน') + ' ที่ช่อง (' + x + ',' + y + ')';
}
return;
} else if (drawModeEl.value === 'balloonBossPlayerPaint') {
@@ -3193,7 +3195,7 @@
if (drawModeEl && drawModeEl.value === 'gauntletPlayerSpawn' && gameType !== 'gauntlet') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'gauntletLaserStart' && gameType !== 'gauntlet') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'gauntletLaserEnd' && gameType !== 'gauntlet') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'shooterSpawnPaint' && gameType !== 'space_shooter') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'shooterSpawnPaint' && gameType !== 'space_shooter' && gameType !== 'jump_survive') drawModeEl.value = 'wall';
if (drawModeEl && drawModeEl.value === 'balloonBossPlayerPaint' && gameType !== 'balloon_boss') drawModeEl.value = 'wall';
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';
@@ -3380,7 +3382,7 @@
stackLandArea: gameType === 'stack' ? stackLandArea.map(r => r.slice()) : [],
jumpSurvivePlatforms: gameType === 'jump_survive' ? jumpSurvivePlatforms.slice() : [],
jumpSurvivePlatformArea: gameType === 'jump_survive' ? jumpSurvivePlatformArea.map((r) => r.slice()) : [],
shooterSpawnSlots: gameType === 'space_shooter' ? shooterSpawnSlots.map((r) => r.slice()) : [],
shooterSpawnSlots: (gameType === 'space_shooter' || gameType === 'jump_survive') ? shooterSpawnSlots.map((r) => r.slice()) : [],
balloonBossPlayerSlots: gameType === 'balloon_boss' ? balloonBossPlayerSlots.map((r) => r.slice()) : [],
balloonBossBossSpawn: gameType === 'balloon_boss' && balloonBossBossSpawn && Number.isFinite(balloonBossBossSpawn.x)
? { x: Math.floor(balloonBossBossSpawn.x), y: Math.floor(balloonBossBossSpawn.y) }
+192 -16
View File
@@ -3218,6 +3218,30 @@
return out;
}
/** jump_survive: ช่อง P1P6 ที่วาดแบบยาน (shooterSpawnSlots) เติมใน slots6 เมื่อ lobbyPlayerSpawns ว่าง */
function augmentLobbySlotsFromShooterPaintJumpSurvivePlay(md, slots6) {
if (!md || md.gameType !== 'jump_survive' || !Array.isArray(slots6)) return;
const g = md.shooterSpawnSlots;
if (!g) return;
const w = md.width || 20, h = md.height || 15;
for (let slot = 1; slot <= 6; slot++) {
const idx = slot - 1;
if (slots6[idx]) continue;
let found = false;
for (let yy = 0; yy < h && !found; yy++) {
const row = g[yy];
if (!row) continue;
for (let xx = 0; xx < w; xx++) {
if (row[xx] === slot) {
slots6[idx] = { x: xx, y: yy };
found = true;
break;
}
}
}
}
}
/** สอดคล้องกับ server pickSpawnForJoin — ใช้พรีวิวบอท / ทดสอบ */
function pickSpawnForJoinPlay(md, joinOrderIndex) {
if (!md) return { x: 1, y: 1 };
@@ -3237,6 +3261,7 @@
}
if (mode === 'slots6') {
const slots = parseLobbyPlayerSpawnsFromMapPlay(md);
augmentLobbySlotsFromShooterPaintJumpSurvivePlay(md, slots);
const pick = slots[j];
if (pick && spawnTileWalkablePlay(md, pick.x, pick.y)) return { x: pick.x, y: pick.y };
return pickRandomSpawnFromMapPlay(md);
@@ -3421,11 +3446,19 @@
while (botIds.length < wantBots) {
const id = PREVIEW_BOT_PREFIX + (++previewBotSeq);
const joinIdx = countPlayHumans() + botIds.length;
const sp = pickSpawnForJoinPlay(mapData, joinIdx);
const jx = (Math.random() - 0.5) * 0.4;
const jy = (Math.random() - 0.5) * 0.4;
let x = sp.x + 0.5 + jx;
let y = sp.y + 0.5 + jy;
let x;
let y;
if (mapData.gameType === 'jump_survive') {
const pos = jumpSurviveSpawnWorldFromJoinOrderPlay(mapData, joinIdx);
x = pos.x;
y = pos.y;
} else {
const sp = pickSpawnForJoinPlay(mapData, joinIdx);
const jx = (Math.random() - 0.5) * 0.4;
const jy = (Math.random() - 0.5) * 0.4;
x = sp.x + 0.5 + jx;
y = sp.y + 0.5 + jy;
}
if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) {
const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(x, y);
x = pathSnap.x;
@@ -5109,7 +5142,7 @@
jumpSurviveMissionPhase = 'live';
jumpSurviveGameEnded = false;
jumpSurviveEliminated = false;
jumpSurviveInitRuntime();
applyJumpSurvivePreviewSpawnLayout(false);
};
if (!cd || !numEl) {
runFinish();
@@ -10495,7 +10528,7 @@
}
function normalizeShooterSpawnSlotsInPlay(md) {
if (!md || md.gameType !== 'space_shooter') return;
if (!md || (md.gameType !== 'space_shooter' && md.gameType !== 'jump_survive')) return;
const w = md.width || 20, h = md.height || 15;
const src = md.shooterSpawnSlots || [];
const rows = [];
@@ -11908,17 +11941,53 @@
return jumpSurviveGridPosStandingOnPlatform(md, Number(sp.x) || 1, Number(sp.y) || 1, sc);
}
function jumpSurviveTileHasPlatformCellPlay(md, tx, ty) {
const pa = md && md.jumpSurvivePlatformArea;
const h = md.height || (pa && pa.length) || 15;
const w = md.width || 20;
if (!pa || ty < 0 || ty >= h) return false;
const row = pa[ty];
if (!row || tx < 0 || tx >= w) return false;
return row[tx] === 1;
}
/** หาแพลตฟอร์มในคอลัมน์ tx ใกล้ ty — กันจุดเกิดช่องว่างแล้วลอย */
function jumpSurviveFindPlatformColumnPlay(md, tx, tyHint) {
const w = md.width || 20, h = md.height || 15;
const tcx = Math.max(0, Math.min(w - 1, Math.floor(tx)));
let ty = Math.max(0, Math.min(h - 1, Math.floor(tyHint)));
if (jumpSurviveTileHasPlatformCellPlay(md, tcx, ty)) return { x: tcx, y: ty };
for (let d = 1; d < h; d++) {
const yb = ty + d;
if (yb < h && jumpSurviveTileHasPlatformCellPlay(md, tcx, yb)) return { x: tcx, y: yb };
const ya = ty - d;
if (ya >= 0 && jumpSurviveTileHasPlatformCellPlay(md, tcx, ya)) return { x: tcx, y: ya };
}
return { x: tcx, y: ty };
}
/** จุดเกิดตามลำดับ join / P1–P6 → world ยืนบนแพลตฟอร์ม (ไม่ทับสุ่ม pool) */
function jumpSurviveSpawnWorldFromJoinOrderPlay(md, joinOrder) {
const sp = pickSpawnForJoinPlay(md, joinOrder | 0);
let tx = Math.floor(Number(sp.x));
let ty = Math.floor(Number(sp.y));
if (!Number.isFinite(tx) || !Number.isFinite(ty)) {
const fb = md.spawn || { x: 1, y: 1 };
tx = Math.floor(Number(fb.x)) || 1;
ty = Math.floor(Number(fb.y)) || 1;
}
const cell = jumpSurviveFindPlatformColumnPlay(md, tx, ty);
const sc = jumpSurvivePlatformScrollPx;
const p = jumpSurviveGridPosStandingOnPlatform(md, cell.x, cell.y, sc);
return {
x: p.x + (Math.random() - 0.5) * 0.05,
y: p.y + (Math.random() - 0.5) * 0.05,
};
}
/** ทดสอบจากเอดิเตอร์: วางคน+บอทบนแพลตฟอร์ม (ไม่ให้เกิดบน spawnArea บนฟ้าแล้วลอย) */
function applyJumpSurvivePreviewSpawnLayout(onlyBots) {
if (!mapData || mapData.gameType !== 'jump_survive') return;
const cells = jumpSurviveCollectPlatformCells(mapData);
cells.sort((a, b) => b.y - a.y || a.x - b.x);
const sc = jumpSurvivePlatformScrollPx;
const pool = cells.length
? cells.map((c) => jumpSurviveGridPosStandingOnPlatform(mapData, c.x, c.y, sc))
: [jumpSurviveGridPosStandingOnPlatform(mapData, Number(mapData.spawn && mapData.spawn.x) || 1, Number(mapData.spawn && mapData.spawn.y) || 1, sc)];
const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort();
const botIds = [...others.keys()].filter(isPreviewBotId).sort();
function stampJumpBotState(o) {
if (!o) return;
@@ -11929,6 +11998,36 @@
o.ty = o.y;
}
if (isJumpSurviveMissionUiMapPlay()) {
function snapEnt(ent) {
if (!ent) return;
const ordRaw = Number(ent.spawnJoinOrder);
const jo = Number.isFinite(ordRaw) ? Math.max(0, Math.floor(ordRaw)) : 0;
const pos = jumpSurviveSpawnWorldFromJoinOrderPlay(mapData, jo);
ent.x = pos.x;
ent.y = pos.y;
stampJumpBotState(ent);
}
if (!onlyBots) {
snapEnt(me);
const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort();
realIds.forEach((rid) => snapEnt(others.get(rid)));
}
const botIds = [...others.keys()].filter(isPreviewBotId).sort();
botIds.forEach((bid) => snapEnt(others.get(bid)));
jumpSurviveInitRuntime();
return;
}
const cells = jumpSurviveCollectPlatformCells(mapData);
cells.sort((a, b) => b.y - a.y || a.x - b.x);
const sc = jumpSurvivePlatformScrollPx;
const pool = cells.length
? cells.map((c) => jumpSurviveGridPosStandingOnPlatform(mapData, c.x, c.y, sc))
: [jumpSurviveGridPosStandingOnPlatform(mapData, Number(mapData.spawn && mapData.spawn.x) || 1, Number(mapData.spawn && mapData.spawn.y) || 1, sc)];
const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort();
const botIds = [...others.keys()].filter(isPreviewBotId).sort();
if (onlyBots) {
let idx = 1 + realIds.length;
botIds.forEach((bid) => {
@@ -12055,6 +12154,11 @@
return { pxc, feetY, vy, onGround, died: true };
}
headTop = feetY - bh;
if (jumpSurviveOverlapsEdgeKillPlatforms(md, pxc - bw, headTop, pxc + bw, feetY, jumpSurvivePlatformScrollPx)) {
return { pxc, feetY, vy, onGround, died: true };
}
headTop = feetY - bh;
const topKill = camCenterY - halfViewH - ts * 0.35;
const botKill = camCenterY + halfViewH + ts * 0.85;
@@ -12142,6 +12246,74 @@
return false;
}
/** แถวแพลตฟอร์มบนสุด/ล่างสุดของแมป (มีช่องอย่างน้อยหนึ่งช่อง) — ใช้เป็นเพดาน/พื้นตาย */
function jumpSurvivePlatformEdgeKillRows(md) {
const pa = md && md.jumpSurvivePlatformArea;
if (!pa) return null;
const h = Math.min(pa.length, md.height || pa.length || 15);
const wM = md.width || 20;
let minTy = h;
let maxTy = -1;
for (let ty = 0; ty < h; ty++) {
const row = pa[ty];
if (!row) continue;
const wlim = Math.min(row.length || 0, wM);
let rowHas = false;
for (let tx = 0; tx < wlim; tx++) {
if (row[tx] === 1) {
rowHas = true;
break;
}
}
if (!rowHas) continue;
if (ty < minTy) minTy = ty;
if (ty > maxTy) maxTy = ty;
}
if (minTy > maxTy) return null;
if (minTy === maxTy) return null;
return { minTy, maxTy };
}
/** ชนแพลตฟอร์มแถวเพดานหรือแถวพื้น (รวมเลื่อนแบบคาบ) = ตายทันที */
function jumpSurviveOverlapsEdgeKillPlatforms(md, left, top, right, bottom, scrollPx) {
const edges = jumpSurvivePlatformEdgeKillRows(md);
if (!edges) return false;
const ts = tileSize;
const w = md.width || 20, h = md.height || 15;
const pa = md.jumpSurvivePlatformArea;
if (!pa) return false;
const period = jumpSurvivePlatformPeriodPx(md);
if (period < ts) return false;
const x0 = Math.max(0, Math.floor(left / ts));
const x1 = Math.min(w - 1, Math.floor((right - 1e-6) / ts));
const vm = ts * 2;
const overlapsRowTy = function (ty) {
if (ty < 0 || ty >= h || !pa[ty]) return false;
for (let tx = x0; tx <= x1; tx++) {
if (pa[ty][tx] !== 1) continue;
const rawTop = ty * ts - scrollPx;
let kMin = Math.ceil((top - rawTop - ts - vm) / period);
let kMax = Math.floor((bottom - rawTop + vm) / period);
if (kMin > kMax) {
const kGuess = Math.round((bottom - rawTop - ts * 0.5) / period);
kMin = kGuess;
kMax = kGuess;
}
for (let k = kMin; k <= kMax; k++) {
const platTop = rawTop + k * period;
const platBot = platTop + ts;
const tileLeft = tx * ts;
const tileRight = (tx + 1) * ts;
if (platBot > top && platTop < bottom && right > tileLeft && left < tileRight) return true;
}
}
return false;
};
if (overlapsRowTy(edges.minTy)) return true;
if (overlapsRowTy(edges.maxTy)) return true;
return false;
}
function jumpSurviveOverlapsSolid(md, left, top, right, bottom) {
if (jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom)) return true;
return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, jumpSurvivePlatformScrollPx);
@@ -12349,7 +12521,9 @@
const dt = Math.min(0.05, Math.max(0, (now - jumpSurviveLastTickMs) / 1000));
jumpSurviveLastTickMs = now;
const halfViewH = canvas.height / (2 * zoom);
let zJumpSurviveWorld = zoom;
if (previewMode && editorEmbedReturn && mapData) zJumpSurviveWorld *= playEmbedUserZoomMul;
const halfViewH = canvas.height / (2 * zJumpSurviveWorld);
const rise = (Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42) * dt;
jumpSurvivePlatformScrollPx += rise;
@@ -12844,6 +13018,7 @@
if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = [];
if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = [];
normalizeJumpSurvivePlatformAreaInPlay(mapData);
normalizeShooterSpawnSlotsInPlay(mapData);
jumpSurviveEliminated = false;
jumpSurviveGameEnded = false;
if (jumperMissionCountdownTimer) {
@@ -13675,6 +13850,7 @@
if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = [];
if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = [];
normalizeJumpSurvivePlatformAreaInPlay(mapData);
normalizeShooterSpawnSlotsInPlay(mapData);
jumpSurviveEliminated = false;
jumpSurviveGameEnded = false;
if (jumperMissionCountdownTimer) {
+1 -1
View File
@@ -1,6 +1,6 @@
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
window.APP_VERSION = '0.0308';
window.APP_VERSION = '0.0312';
document.addEventListener('DOMContentLoaded', function () {
var t = document.querySelector('.version-tag');
if (t) t.textContent = 'v ' + window.APP_VERSION;
+1 -1
View File
@@ -3026,7 +3026,7 @@
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0306"></script>
<script src="js/play.js?v=0.0308"></script>
<script src="js/play.js?v=0.0312"></script>
<div class="version-tag">v —</div>
</body>
</html>
+25
View File
@@ -3554,6 +3554,30 @@ function parseLobbyPlayerSpawnsFromMap(md) {
return out;
}
/** jump_survive: ช่อง P บนกริด (shooterSpawnSlots) เติม slots6 เมื่อ lobbyPlayerSpawns ว่าง */
function augmentLobbySlotsFromShooterPaintJumpSurvive(md, slots6) {
if (!md || md.gameType !== 'jump_survive' || !Array.isArray(slots6)) return;
const g = md.shooterSpawnSlots;
if (!g) return;
const w = md.width || 20, h = md.height || 15;
for (let slot = 1; slot <= 6; slot++) {
const idx = slot - 1;
if (slots6[idx]) continue;
let found = false;
for (let yy = 0; yy < h && !found; yy++) {
const row = g[yy];
if (!row) continue;
for (let xx = 0; xx < w; xx++) {
if (row[xx] === slot) {
slots6[idx] = { x: xx, y: yy };
found = true;
break;
}
}
}
}
}
/**
* จุดเกิดตอน join — random = สุ่มใน spawnArea / fixed = ปุ่มตั้งจุดเกิด / slots6 = P ตามลำดับเข้า (0=คนแรก)
* โหมดพรมแดงยังถูกทับด้วย gauntletSpawnPositions หลัง join ตามเดิม
@@ -3576,6 +3600,7 @@ function pickSpawnForJoin(md, joinOrderIndex) {
}
if (mode === 'slots6') {
const slots = parseLobbyPlayerSpawnsFromMap(md);
augmentLobbySlotsFromShooterPaintJumpSurvive(md, slots);
const pick = slots[j];
if (pick && isMapTileWalkableForSpawn(md, pick.x, pick.y)) return { x: pick.x, y: pick.y };
return pickRandomSpawnFromMap(md);