minigame 5 add more design 1.4

This commit is contained in:
2026-05-05 18:51:46 +00:00
parent 5aa7ab802f
commit ba58cd1b74
7 changed files with 140 additions and 9 deletions
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -123,6 +123,7 @@
</optgroup>
<optgroup label="กระโดดให้รอด">
<option value="jumpSurvivePlatform" id="draw-mode-option-jump-platform" hidden>แพลตฟอร์ม (ยืนได้ — ลอยขึ้นตามกล้องในเกม)</option>
<option value="jumpSurviveHazard" id="draw-mode-option-jump-hazard" hidden>โซนตาย (แดง — ตัวละครโดนกล่องชนแล้วตายทันที)</option>
</optgroup>
<optgroup label="ยิงยานอวกาศ">
<option value="shooterSpawnPaint" id="draw-mode-option-shooter-spawn" hidden>จุดเกิดยาน ผู้เล่น 1–6 (เลือกช่อง P ด้านขวา)</option>
@@ -345,7 +346,7 @@
<li><strong>ถามตอบ</strong>: โซนถูก/ผิด + พื้นที่คำถาม (ทอง)</li>
<li><strong>หยิบมาวาง</strong>: ม่วง = โซนกลาง (กำแพง) · <strong>ทอง</strong> = พื้นที่โชว์ข้อความคำถามบนแผนที่ (ถ้าไม่วาดทอง ข้อความไปที่โซนม่วง) · <strong>interactive เขียว</strong> = ยืนแล้วกด F ส่งคำตอบ · โหมดวาด <strong>ตัวเลือก 116</strong> = ช่องบนกริดตรงกับลำดับ <code>choices</code> · พรีวิวนับ <strong>3-2-1</strong> ไฮไลต์โซนข้อถูก (หรือใส่ <code>countdownHighlightSlot</code> 116 ต่อข้อใน <code>quizQuestions</code>) · <code>correctIndex</code> / <code>answerTrue</code></li>
<li><strong>Stack</strong>: โหมดวาดจุดปล่อย (ฟ้า) · จุดซ้อนตึก (ชมพู) · ถ้าอัปโหลดรูปทาง FTP ใช้โฟลเดอร์ <code>Game/public/img/TowerBlock</code> (ไม่มีช่องว่าง) — ชื่อ <code>Tower block</code> มี space มักได้ FTP <strong>550</strong></li>
<li><strong>กระโดดให้รอด</strong>: โหมด <strong>แพลตฟอร์ม</strong> (ฟ้าอมเขียว) · กำแพง = ขอบซ้ายขวา/บน · Space / W = กระโดด</li>
<li><strong>กระโดดให้รอด</strong>: โหมด <strong>แพลตฟอร์ม</strong> (ฟ้าอมเขียว) · โหมด <strong>โซนตาย</strong> (แดง — โดนแล้วตายทันที คงที่ในโลก) · กำแพง = ขอบซ้ายขวา/บน · Space / W = กระโดด</li>
<li><strong>ลูกโป้งยิงบอส (Mega Virus)</strong>: จุดเกิดผู้เล่น P1–P6 + <strong>จุดเกิดบอส</strong> 1 ช่อง · ในเกมเดินช้า ยิงมีดีเลย์ · ลูกโป้งหมด = ตกรอบ · อัปโหลดรูปทาง FTP ใช้ <code>Game/public/img/MegaVirus</code> (<strong>ไม่มีช่องว่าง</strong>) — โฟลเดอร์ <code>Mega Virus</code> มี space มักได้ FTP <strong>550</strong></li>
<li><strong>สีช่อง</strong>: เลือกโหมดสี + Alpha แล้วคลิกบนกริด</li>
<li><strong>รูปบนกริด</strong>: โหมด «รูปบนกริด» — ตั้งกว้าง×สูง (ช่อง) · เลือกรูปจากแกลเลอรี · คลิกซ้ายวาง (ทับสไปรต์ที่ทับกัน) · คลิกขาวางลบทั้งแผ่น · บันทึกฉากเก็บเป็น <code>gridImageSprites</code> (ไฟล์ใหญ่ = JSON ใหญ่)</li>
@@ -359,7 +360,7 @@
</div>
</div>
<script src="js/version.js?v=0.0258"></script>
<script src="js/editor.js?v=0.0311"></script>
<script src="js/editor.js?v=0.0316"></script>
<div class="version-tag">v —</div>
</body>
</html>
+51 -1
View File
@@ -106,6 +106,8 @@
/** jump_survive: แพลตฟอร์ม { x, y, w, h } tile — ใช้เต็มที่เมื่อมีมุมข้าง */
let jumpSurvivePlatforms = [];
let jumpSurvivePlatformArea = [];
/** jump_survive: กริด 0/1 — ช่องที่ 1 = โซนตาย (ชน hitbox แล้วตายทันทีในเกม) */
let jumpSurviveHazardArea = [];
let cellColors = [];
let gauntletPlayerSpawns = [];
/** gauntlet: แถว y บน–ล่างของแถบเลเซอร์ (null = เต็มความสูงแมป) */
@@ -1258,6 +1260,23 @@
}
}
function ensureJumpSurviveHazardArea() {
if (!jumpSurviveHazardArea.length || jumpSurviveHazardArea.length !== height) {
const existing = jumpSurviveHazardArea.slice().map((r) => r && r.slice());
jumpSurviveHazardArea = [];
for (let y = 0; y < height; y++) {
const row = existing[y] && existing[y].length === width ? existing[y].slice() : Array(width).fill(0);
jumpSurviveHazardArea.push(row);
}
} else {
for (let y = 0; y < height; y++) {
if (!jumpSurviveHazardArea[y] || jumpSurviveHazardArea[y].length !== width) {
jumpSurviveHazardArea[y] = Array(width).fill(0);
}
}
}
}
function sanitizeGauntletPlayerSpawns() {
gauntletPlayerSpawns = gauntletPlayerSpawns
.filter((s) => s && Number.isFinite(s.x) && Number.isFinite(s.y) &&
@@ -1812,6 +1831,7 @@
if (hint) {
hint.innerHTML = '<p class="editor-hint-lead"><strong>กระโดดให้รอด</strong> — กำแพง + แพลตฟอร์มเลื่อนขึ้นในกรอบจอ</p>' + editorHintBulletList([
'<strong>กำแพง</strong> = ขอบซ้ายขวา/บน (ชนแล้วตายหรือออกนอกจอ) · <strong>โหมดวาดแพลตฟอร์ม</strong> (ฟ้าอมเขียว) = ท่อนยืนได้ — ตอนเล่นแพลตฟอร์มจะเลื่อนขึ้นในกรอด ไม่ลากทั้งฉาก',
'<strong>โซนตาย (แดง)</strong> — วาดช่องที่ตัวละคร<strong>แตะแล้วตายทันที</strong> (ลาวา/glitch) · อยู่คงที่ในโลก ไม่เลื่อนไปกับแพลตฟอร์ม',
'ในเกม: กระโดดสูงขึ้น · แพลตฟอร์มเลื่อนขึ้นแล้ว<strong>วนกลับมาด้านล่าง</strong> (คาบ = ความสูงแมป) · กล้องตามตัว — หลุดขอบบน/ล่างจอ = กลับจุดเกิด',
'พื้นที่สุ่มเกิด (ฟ้า) + ปุ่มตั้งจุดเกิด — แนะนำสูงหลายแถวเพื่อให้มีที่กระโดด',
]);
@@ -1947,10 +1967,15 @@
drawModeEl.value = 'wall';
}
const jPlatOpt = document.getElementById('draw-mode-option-jump-platform');
const jHazOpt = document.getElementById('draw-mode-option-jump-hazard');
if (jPlatOpt) {
jPlatOpt.hidden = gt !== 'jump_survive';
if (gt !== 'jump_survive' && drawModeEl && drawModeEl.value === 'jumpSurvivePlatform') drawModeEl.value = 'wall';
}
if (jHazOpt) {
jHazOpt.hidden = gt !== 'jump_survive';
if (gt !== 'jump_survive' && drawModeEl && drawModeEl.value === 'jumpSurviveHazard') 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) {
@@ -1982,6 +2007,7 @@
stackReleaseArea = Array(height).fill(0).map(() => Array(width).fill(0));
stackLandArea = Array(height).fill(0).map(() => Array(width).fill(0));
jumpSurvivePlatformArea = Array(height).fill(0).map(() => Array(width).fill(0));
jumpSurviveHazardArea = Array(height).fill(0).map(() => Array(width).fill(0));
shooterSpawnSlots = Array(height).fill(0).map(() => Array(width).fill(0));
balloonBossPlayerSlots = Array(height).fill(0).map(() => Array(width).fill(0));
balloonBossBossSpawn = null;
@@ -2053,7 +2079,10 @@
ensureQuizBattleDomeArea();
}
if (gtDraw === 'stack') ensureStackAreas();
if (gtDraw === 'jump_survive') ensureJumpSurvivePlatformArea();
if (gtDraw === 'jump_survive') {
ensureJumpSurvivePlatformArea();
ensureJumpSurviveHazardArea();
}
if (gtDraw === 'space_shooter') ensureShooterSpawnSlots();
if (gtDraw === 'balloon_boss') ensureBalloonBossPlayerSlots();
if (isEditorScrollBgDrawing()) {
@@ -2220,6 +2249,19 @@
ctx.strokeRect(tx + 3, ty + 3, tileSize - 6, tileSize - 6);
ctx.lineWidth = 1;
}
if (gtDraw === 'jump_survive' && jumpSurviveHazardArea[y] && jumpSurviveHazardArea[y][x] === 1) {
ctx.fillStyle = 'rgba(220, 40, 60, 0.52)';
ctx.fillRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.strokeStyle = 'rgba(255, 100, 120, 0.95)';
ctx.lineWidth = 2;
ctx.strokeRect(tx + 2, ty + 2, tileSize - 4, tileSize - 4);
ctx.lineWidth = 1;
ctx.fillStyle = 'rgba(40, 0, 8, 0.88)';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('!', tx + tileSize / 2, ty + tileSize / 2 + 4);
ctx.textAlign = 'left';
}
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;
@@ -2738,6 +2780,10 @@
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'jump_survive') return;
ensureJumpSurvivePlatformArea();
jumpSurvivePlatformArea[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'jumpSurviveHazard') {
if ((gameTypeEl ? gameTypeEl.value : gameType) !== 'jump_survive') return;
ensureJumpSurviveHazardArea();
jumpSurviveHazardArea[y][x] = left ? 1 : 0;
} else if (drawModeEl.value === 'shooterSpawnPaint') {
const gtSh = (gameTypeEl ? gameTypeEl.value : gameType);
if (gtSh !== 'space_shooter' && gtSh !== 'jump_survive') return;
@@ -3325,6 +3371,7 @@
ensureQuizBattleDomeArea();
ensureQuizBattlePathArea();
ensureJumpSurvivePlatformArea();
ensureJumpSurviveHazardArea();
ensureShooterSpawnSlots();
ensureBalloonBossPlayerSlots();
ensureStackAreas();
@@ -3382,6 +3429,7 @@
stackLandArea: gameType === 'stack' ? stackLandArea.map(r => r.slice()) : [],
jumpSurvivePlatforms: gameType === 'jump_survive' ? jumpSurvivePlatforms.slice() : [],
jumpSurvivePlatformArea: gameType === 'jump_survive' ? jumpSurvivePlatformArea.map((r) => r.slice()) : [],
jumpSurviveHazardArea: gameType === 'jump_survive' ? jumpSurviveHazardArea.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)
@@ -3669,6 +3717,8 @@
jumpSurvivePlatforms = Array.isArray(m.jumpSurvivePlatforms) ? m.jumpSurvivePlatforms.map((p) => (p && typeof p === 'object' ? { ...p } : null)).filter(Boolean) : [];
jumpSurvivePlatformArea = m.jumpSurvivePlatformArea && m.jumpSurvivePlatformArea.length ? m.jumpSurvivePlatformArea.map((r) => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
ensureJumpSurvivePlatformArea();
jumpSurviveHazardArea = m.jumpSurviveHazardArea && m.jumpSurviveHazardArea.length ? m.jumpSurviveHazardArea.map((r) => r && r.slice()) : Array(height).fill(0).map(() => Array(width).fill(0));
ensureJumpSurviveHazardArea();
shooterSpawnSlots = m.shooterSpawnSlots && m.shooterSpawnSlots.length
? m.shooterSpawnSlots.map((r) => r && r.slice())
: Array(height).fill(0).map(() => Array(width).fill(0));
+63 -3
View File
@@ -10527,6 +10527,38 @@
md.jumpSurvivePlatformArea = rows;
}
function normalizeJumpSurviveHazardAreaInPlay(md) {
if (!md || md.gameType !== 'jump_survive') return;
const w = md.width || 20, h = md.height || 15;
const src = md.jumpSurviveHazardArea || [];
const rows = [];
for (let y = 0; y < h; y++) {
const r = src[y];
const row = [];
for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0);
rows.push(row);
}
md.jumpSurviveHazardArea = rows;
}
/** โซนตาย jump_survive — กริดคงที่ในโลก (ไม่เลื่อนกับแพลตฟอร์ม) */
function jumpSurviveOverlapsHazardArea(md, left, top, right, bottom) {
const ha = md && md.jumpSurviveHazardArea;
if (!ha) return false;
const ts = tileSize;
const w = md.width || 20, h = md.height || 15;
const x0 = Math.max(0, Math.floor(left / ts));
const x1 = Math.min(w - 1, Math.floor((right - 1e-6) / ts));
const y0 = Math.max(0, Math.floor(top / ts));
const y1 = Math.min(h - 1, Math.floor((bottom - 1e-6) / ts));
for (let ty = y0; ty <= y1; ty++) {
for (let tx = x0; tx <= x1; tx++) {
if (ha[ty] && ha[ty][tx] === 1) return true;
}
}
return false;
}
function normalizeShooterSpawnSlotsInPlay(md) {
if (!md || (md.gameType !== 'space_shooter' && md.gameType !== 'jump_survive')) return;
const w = md.width || 20, h = md.height || 15;
@@ -12159,6 +12191,11 @@
return { pxc, feetY, vy, onGround, died: true };
}
headTop = feetY - bh;
if (jumpSurviveOverlapsHazardArea(md, pxc - bw, headTop, pxc + bw, feetY)) {
return { pxc, feetY, vy, onGround, died: true };
}
headTop = feetY - bh;
const topKill = camCenterY - halfViewH - ts * 0.35;
const botKill = camCenterY + halfViewH + ts * 0.85;
@@ -12274,7 +12311,7 @@
return { minTy, maxTy };
}
/** ชนแพลตฟอร์มแถวเพดานหรือแถวพื้น (รวมเลื่อนแบบคาบ) = ตายทันที */
/** ชนแพลตฟอร์มแถวเพดานเท่านั้น (รวมเลื่อนแบบคาบ) = ตายทันที — ไม่ฆ่าที่แถวพื้นล่าง */
function jumpSurviveOverlapsEdgeKillPlatforms(md, left, top, right, bottom, scrollPx) {
const edges = jumpSurvivePlatformEdgeKillRows(md);
if (!edges) return false;
@@ -12299,18 +12336,18 @@
kMin = kGuess;
kMax = kGuess;
}
const zSlack = ts * 0.08;
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;
if (platBot > top - zSlack && platTop < bottom + zSlack && right > tileLeft && left < tileRight) return true;
}
}
return false;
};
if (overlapsRowTy(edges.minTy)) return true;
if (overlapsRowTy(edges.maxTy)) return true;
return false;
}
@@ -13017,7 +13054,9 @@
if (mapData.gameType === 'jump_survive') {
if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = [];
if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = [];
if (!mapData.jumpSurviveHazardArea) mapData.jumpSurviveHazardArea = [];
normalizeJumpSurvivePlatformAreaInPlay(mapData);
normalizeJumpSurviveHazardAreaInPlay(mapData);
normalizeShooterSpawnSlotsInPlay(mapData);
jumpSurviveEliminated = false;
jumpSurviveGameEnded = false;
@@ -13849,7 +13888,9 @@
if (mapData.gameType === 'jump_survive') {
if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = [];
if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = [];
if (!mapData.jumpSurviveHazardArea) mapData.jumpSurviveHazardArea = [];
normalizeJumpSurvivePlatformAreaInPlay(mapData);
normalizeJumpSurviveHazardAreaInPlay(mapData);
normalizeShooterSpawnSlotsInPlay(mapData);
jumpSurviveEliminated = false;
jumpSurviveGameEnded = false;
@@ -16035,6 +16076,25 @@
}
}
}
}
if (isJumpSurvive() && mapData.jumpSurviveHazardArea) {
const ha = mapData.jumpSurviveHazardArea;
const tsz = tileSize;
for (let py = startTileY; py <= endTileY; py++) {
for (let px = startTileX; px <= endTileX; px++) {
if (!ha[py] || ha[py][px] !== 1) continue;
const [psx, psy] = worldToScreen(px * tsz, py * tsz);
const psz = tsz * zDraw;
ctx.fillStyle = 'rgba(200, 28, 55, 0.38)';
ctx.fillRect(psx + 1, psy + 1, psz - 2, psz - 2);
ctx.strokeStyle = 'rgba(255, 90, 110, 0.82)';
ctx.lineWidth = 2;
ctx.strokeRect(psx + 1, psy + 1, psz - 2, psz - 2);
ctx.lineWidth = 1;
}
}
}
if (showGrid) {
if (isFrogger() && mapData.lanes) {
for (let y = startTileY; y <= endTileY; y++) {
const lane = getLane(y);
+1 -1
View File
@@ -1,6 +1,6 @@
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
window.APP_VERSION = '0.0312';
window.APP_VERSION = '0.0316';
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.0312"></script>
<script src="js/play.js?v=0.0316"></script>
<div class="version-tag">v —</div>
</body>
</html>
+20
View File
@@ -167,6 +167,7 @@ const defaultMap = () => ({
/** กระโดดให้รอด — แพลตฟอร์มในระบบ tile (ร่างสำหรับ side-view ภายหลัง) */
jumpSurvivePlatforms: [],
jumpSurvivePlatformArea: [],
jumpSurviveHazardArea: [],
jumpSurviveRisePxPerSec: 42,
jumpSurviveGravity: 3200,
/** 0 = ให้ไคลเอนต์ใช้ค่าเริ่มต้นกระโดดสูง; ตั้งเป็นพิกเซล/วิ (เช่น 980) เพื่อกำหนดเอง */
@@ -1864,6 +1865,20 @@ function normalizeJumpSurvivePlatformAreaOnMap(m) {
m.jumpSurvivePlatformArea = rows;
}
function normalizeJumpSurviveHazardAreaOnMap(m) {
if (!m || m.gameType !== 'jump_survive') return;
const w = m.width || 20, h = m.height || 15;
const src = m.jumpSurviveHazardArea || [];
const rows = [];
for (let y = 0; y < h; y++) {
const r = src[y];
const row = [];
for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0);
rows.push(row);
}
m.jumpSurviveHazardArea = rows;
}
function loadMaps() {
try {
if (!fs.existsSync(path.join(__dirname, 'data'))) fs.mkdirSync(path.join(__dirname, 'data'), { recursive: true });
@@ -1878,6 +1893,7 @@ function loadMaps() {
const data = JSON.parse(fs.readFileSync(path.join(MAPS_DIR, f), 'utf8'));
normalizeQuizCarryLayersOnMap(data);
normalizeJumpSurvivePlatformAreaOnMap(data);
normalizeJumpSurviveHazardAreaOnMap(data);
normalizeQuizBattleDomeAreaOnMap(data);
normalizeQuizBattlePathAreaOnMap(data);
maps.set(id, data);
@@ -1958,8 +1974,10 @@ const server = http.createServer((req, res) => {
if (!m.stackLandArea) m.stackLandArea = [];
if (!Array.isArray(m.jumpSurvivePlatforms)) m.jumpSurvivePlatforms = [];
if (!m.jumpSurvivePlatformArea) m.jumpSurvivePlatformArea = [];
if (!m.jumpSurviveHazardArea) m.jumpSurviveHazardArea = [];
normalizeQuizCarryLayersOnMap(m);
normalizeJumpSurvivePlatformAreaOnMap(m);
normalizeJumpSurviveHazardAreaOnMap(m);
normalizeQuizBattleDomeAreaOnMap(m);
normalizeQuizBattlePathAreaOnMap(m);
maps.set(id, m);
@@ -1998,8 +2016,10 @@ const server = http.createServer((req, res) => {
if (!m.stackLandArea) m.stackLandArea = [];
if (!Array.isArray(m.jumpSurvivePlatforms)) m.jumpSurvivePlatforms = [];
if (!m.jumpSurvivePlatformArea) m.jumpSurvivePlatformArea = [];
if (!m.jumpSurviveHazardArea) m.jumpSurviveHazardArea = [];
normalizeQuizCarryLayersOnMap(m);
normalizeJumpSurvivePlatformAreaOnMap(m);
normalizeJumpSurviveHazardAreaOnMap(m);
normalizeQuizBattleDomeAreaOnMap(m);
normalizeQuizBattlePathAreaOnMap(m);
maps.set(id, m);