minigame 2 just begin1.5

This commit is contained in:
2026-04-30 19:46:28 +00:00
parent f06130deb8
commit 53ee199a6d
5 changed files with 959 additions and 40 deletions
+258 -14
View File
@@ -14,6 +14,8 @@
const forceDefaultCharacter = params.get('defaultChar') === '1';
const editorEmbedReturn = params.get('editorEmbed') === '1';
const playMapIdFromQuery = (params.get('map') || '').trim();
/** mapId จาก join / game-start — ใช้จับฉาก crown เมื่อ URL ไม่มี ?map= */
let playSessionMapId = playMapIdFromQuery;
/** สรุปภารกิจแบบ mock (popup-result ฯลฯ) — ใช้เฉพาะฉากนี้จาก editor (id ใน URL เช่น editor.html?id=mnorwqx1) */
const quizCarryMissionSummaryMapId = 'mnorwqx1';
const quizCarryUseMissionSummaryOverlay = playMapIdFromQuery === quizCarryMissionSummaryMapId;
@@ -99,7 +101,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, 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 };
const others = new Map();
/** Stack preview HUD: เทอร์มินัลล็อก (cyber UI) */
const STACK_PREVIEW_HUD_LOG_MAX = 8;
@@ -2026,7 +2028,10 @@
if (o.botAnswerWander == null) o.botAnswerWander = false;
if (o.gauntletJumpTicks == null) o.gauntletJumpTicks = 0;
if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks;
if (o.gauntletScore == null) o.gauntletScore = 0;
if (o.gauntletScore == null) {
o.gauntletScore = (mapData && mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) ? 100 : 0;
}
if (o.gauntletEliminated == null) o.gauntletEliminated = false;
if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) {
const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)];
o.botWanderDx = wd[0];
@@ -2058,6 +2063,7 @@
const tierRoll = Math.random();
const botTier = tierRoll < 0.28 ? 'sharp' : tierRoll < 0.62 ? 'avg' : 'weak';
const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)];
const crownStart = mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay();
others.set(id, {
x, y, tx: x, ty: y,
direction: ['down', 'up', 'left', 'right'][Math.floor(Math.random() * 4)],
@@ -2067,7 +2073,8 @@
botTier,
gauntletJumpTicks: 0,
gauntletJumpVis: 0,
gauntletScore: 0,
gauntletScore: crownStart ? 100 : 0,
gauntletEliminated: false,
quizCannotTrue: false,
quizCannotFalse: false,
quizCarryHeld: null,
@@ -2441,7 +2448,15 @@
function isFrogger() { return mapData && mapData.gameType === 'frogger'; }
function isGauntlet() { return mapData && mapData.gameType === 'gauntlet'; }
function isGauntletFaceRightMapMno9kb07() {
return !!(isGauntlet() && playMapIdFromQuery === GAUNTLET_FACE_RIGHT_MAP_ID);
return !!(isGauntlet() && playSessionMapId === GAUNTLET_FACE_RIGHT_MAP_ID);
}
/** Crown heist rules + UI (เดียวกับหันขวา — ฉาก mno9kb07) */
function isGauntletCrownHeistMapPlay() {
return isGauntletFaceRightMapMno9kb07();
}
let gauntletCrownHowtoVisible = false;
function isGauntletCrownHowtoBlocking() {
return isGauntletCrownHeistMapPlay() && gauntletCrownHowtoVisible;
}
function isStack() { return mapData && mapData.gameType === 'stack'; }
function isJumpSurvive() { return mapData && mapData.gameType === 'jump_survive'; }
@@ -5068,6 +5083,7 @@
* ตอบสนองสงกดขวางทชนเซลลเดยวก + กระโดดลวงหนาเม threat อยคอลมนดไป
*/
function maybePreviewBotGauntletJump(o, obstacles, w, h) {
if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return;
let px = Math.floor(Number(o.x)) || 0;
let py = Math.floor(Number(o.y)) || 0;
px = Math.max(0, Math.min(w - 1, px));
@@ -5101,12 +5117,14 @@
/** หนึ่ง tick เดียวกับ runGauntletTick ฝั่งเซิร์ฟเวอร์ (เฉพาะ collision + เลื่อน x) */
function applyGauntletPhysicsToPreviewBot(o, obstacles, w, h) {
if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return;
let px = Math.floor(Number(o.x)) || 0;
let py = Math.floor(Number(o.y)) || 0;
px = Math.max(0, Math.min(w - 1, px));
py = Math.max(0, Math.min(h - 1, py));
const air = (o.gauntletJumpTicks || 0) > 0;
const { ch: gauntletCh2 } = getCharacterFootprintWH(mapData);
const crown = isGauntletCrownHeistMapPlay();
let advanceX = false;
let hitBack = false;
for (let i = 0; i < obstacles.length; i++) {
@@ -5129,9 +5147,21 @@
if (advanceX) {
px = Math.min(w - 2, px + 1);
o.gauntletJumpTicks = 0;
o.gauntletScore = (o.gauntletScore || 0) + 1;
if (!crown) {
o.gauntletScore = (o.gauntletScore || 0) + 1;
}
} else if (hitBack) {
px = Math.max(0, px - 1);
if (crown) {
if (px <= 0) {
o.gauntletEliminated = true;
o.gauntletScore = 0;
} else {
o.gauntletScore = Math.max(0, (o.gauntletScore || 0) - 10);
px = Math.max(0, px - 1);
}
} else {
px = Math.max(0, px - 1);
}
}
if ((o.gauntletJumpTicks || 0) > 0) o.gauntletJumpTicks--;
o.tx = px;
@@ -5165,6 +5195,163 @@
socket.emit('gauntlet-preview-rows', { ys: [...ysSet] });
}
/** ซ่อน overlay โหมดอื่นที่ค้างจาก embed / session ก่อน — กัน UI ซ้อนไม่ตรง mock */
function hideConflictingOverlaysForGauntletCrown() {
hideQuizCarryPregameOverlay();
quizCarryPregameActive = false;
const hideIds = [
'quiz-game-overlay',
'quiz-carry-mission-overlay',
'gauntlet-ended-overlay',
'quiz-battle-mcq-overlay',
'quiz-carry-embed-countdown',
'quiz-carry-embed-q-strip',
'play-quiz-scoreboard',
'play-quiz-feedback',
];
for (let i = 0; i < hideIds.length; i++) {
const el = document.getElementById(hideIds[i]);
if (el) el.classList.add('is-hidden');
}
const gcm = document.getElementById('gauntlet-crown-mission-overlay');
if (gcm) gcm.classList.add('is-hidden');
}
function gcmRankBadgeUrl(rank) {
const r = Math.floor(Number(rank)) || 99;
if (r === 1) return BASE + '/img/gauntlet-assets/result-1st.png';
if (r === 2) return BASE + '/img/gauntlet-assets/result-2nd.png';
if (r === 3) return BASE + '/img/gauntlet-assets/result-3rd.png';
return BASE + '/img/gauntlet-assets/result-txt-2.png';
}
function showGauntletCrownHowtoOverlay() {
if (!isGauntletCrownHeistMapPlay()) return;
hideConflictingOverlaysForGauntletCrown();
const ov = document.getElementById('gauntlet-crown-howto-overlay');
if (!ov) return;
gauntletCrownHowtoVisible = true;
ov.classList.remove('is-hidden');
const btn = document.getElementById('btn-gch-ready');
if (btn) {
btn.onclick = function () {
ov.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
};
}
}
function showGauntletCrownMissionOverlay(mission) {
const ov = document.getElementById('gauntlet-crown-mission-overlay');
const rowEl = document.getElementById('gcm-rank-row');
const totalEl = document.getElementById('gcm-total');
const gradeEl = document.getElementById('gcm-grade');
const bonusEl = document.getElementById('gcm-bonus');
const btn = document.getElementById('btn-gcm-done');
if (!ov || !rowEl || !mission || !Array.isArray(mission.ranked)) return;
rowEl.innerHTML = '';
mission.ranked.forEach((r) => {
const cell = document.createElement('div');
cell.className = 'gcm-cell';
const tag = document.createElement('div');
tag.className = 'gcm-rank-tag';
const rk = Math.floor(Number(r.rank)) || 0;
if (rk >= 1 && rk <= 3) {
const tagImg = document.createElement('img');
tagImg.src = gcmRankBadgeUrl(rk);
tagImg.alt = '[' + String(r.rankLabel || rk) + ']';
tagImg.style.maxWidth = '100%';
tagImg.style.height = 'auto';
tag.appendChild(tagImg);
} else {
tag.textContent = '[' + String(r.rankLabel || rk) + ']';
}
const frame = document.createElement('div');
frame.className = 'gcm-frame';
const img = document.createElement('img');
img.className = 'gcm-av';
img.alt = '';
const cid = r.characterId ? String(r.characterId) : '';
if (cid) {
const im = getCharacterImg(cid, 'down');
if (im && im.src) img.src = im.src;
} else {
img.src = defaultAvatarImg.src;
}
img.onerror = function () {
this.onerror = null;
this.src = defaultAvatarImg.src;
};
frame.appendChild(img);
if (r.eliminated) {
const st = document.createElement('img');
st.className = 'gcm-stamp';
st.src = BASE + '/img/gauntlet-assets/result-Lose_stamp.png';
st.alt = 'เสียชีวิต';
frame.appendChild(st);
}
const nick = document.createElement('div');
nick.className = 'gcm-nick';
nick.textContent = r.nickname || '—';
const sc = document.createElement('div');
sc.className = 'gcm-sc';
sc.textContent = String(r.baseScore || 0) + '(+' + String(r.rankBonus || 0) + ')';
cell.appendChild(tag);
cell.appendChild(frame);
cell.appendChild(nick);
cell.appendChild(sc);
rowEl.appendChild(cell);
});
if (totalEl) {
const parts = (mission.totalParts || []).map((n) => String(Math.max(0, Number(n) || 0)));
const sum = Math.max(0, Number(mission.totalSum) || 0);
const avg = Math.max(0, Math.floor(Number(mission.averageScore) || 0));
totalEl.innerHTML = 'คะแนนรวม <span class="gcm-total-nums">(' + parts.join('+') + ') = ' + sum + '</span> · เฉลี่ย ' + avg + ' → เกรด';
}
if (gradeEl) {
const g = String(mission.grade || 'C').toUpperCase().charAt(0);
gradeEl.textContent = g;
gradeEl.className = 'gcm-grade gcm-grade--' + g.toLowerCase();
}
if (bonusEl) {
bonusEl.innerHTML = '';
const row = document.createElement('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '8px';
const chk = document.createElement('img');
chk.src = BASE + '/img/gauntlet-assets/result-check.png';
chk.alt = '';
chk.width = 26;
chk.height = 26;
const sp = document.createElement('span');
sp.style.color = '#9ece6a';
sp.style.fontWeight = '700';
sp.textContent = 'ภารกิจสำเร็จ';
row.appendChild(chk);
row.appendChild(sp);
bonusEl.appendChild(row);
const rc = mission.rewardCard;
if (rc && rc.th) {
const pl = document.createElement('div');
pl.className = 'gcm-plaque';
pl.innerHTML = '<strong>' + String(rc.th) + '</strong><span>' + String(rc.en || '') + '</span>';
bonusEl.appendChild(pl);
}
}
ov.classList.remove('is-hidden');
function goLobby() {
ov.classList.add('is-hidden');
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
}
if (btn) {
btn.onclick = function () {
if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden');
else goLobby();
};
}
}
function isLobby() { return mapData && mapData.gameType === 'lobby'; }
function isQuiz() { return mapData && mapData.gameType === 'quiz'; }
function getLane(y) {
@@ -7351,6 +7538,9 @@
const prevWasSpaceShooter = mapData && mapData.gameType === 'space_shooter';
const prevWasBalloonBoss = mapData && mapData.gameType === 'balloon_boss';
mapData = gameMapData;
if (res && res.mapId != null && String(res.mapId).trim() !== '') {
playSessionMapId = String(res.mapId).trim();
}
if (res && res.hostId != null) playHostId = res.hostId;
if (mapData.gameType !== 'space_shooter') {
spaceShooterGameEnded = false;
@@ -7426,6 +7616,7 @@
{
const sc0 = Number(myPeer && myPeer.gauntletScore);
me.gauntletScore = Number.isFinite(sc0) ? Math.max(0, sc0) : 0;
me.gauntletEliminated = !!(myPeer && myPeer.gauntletEliminated);
}
{
const sh0 = Number(myPeer && myPeer.spaceShooterScore);
@@ -7465,6 +7656,7 @@
gauntletJumpTicks: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0,
gauntletJumpVis: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0,
gauntletScore: (() => { const s = Number(p.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
gauntletEliminated: !!p.gauntletEliminated,
spaceShooterScore: (() => { const s = Number(p.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
balloonBossScore: (() => { const s = Number(p.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
balloonBossBalloons: (() => {
@@ -7558,6 +7750,16 @@
else if (isQuizCarry()) setupPlayQuizCarryUi();
else if (isQuizBattle()) setupPlayQuizBattleUi();
else teardownPlayQuizUi();
if (mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) {
hideConflictingOverlaysForGauntletCrown();
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50);
} else {
const hov = document.getElementById('gauntlet-crown-howto-overlay');
if (hov) {
hov.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
}
}
resizeCanvas(); draw(); tick();
}
@@ -7862,8 +8064,10 @@
if (p.direction) me.direction = p.direction;
const jt = Number(p.gauntletJumpTicks);
meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0;
meGauntletJumpVis = meGauntletJumpTicks;
const sc = Number(p.gauntletScore);
me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0;
me.gauntletEliminated = !!p.gauntletEliminated;
} else {
const o = others.get(p.id) ?? others.get(pid);
if (o) {
@@ -7877,6 +8081,7 @@
if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks;
const sc = Number(p.gauntletScore);
o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0;
o.gauntletEliminated = !!p.gauntletEliminated;
}
}
});
@@ -7887,13 +8092,20 @@
});
socket.on('gauntlet-ended', (data) => {
gauntletEndsAtMs = null;
const hto = document.getElementById('gauntlet-crown-howto-overlay');
if (hto) hto.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
if (data && data.crownMission) {
showGauntletCrownMissionOverlay(data.crownMission);
return;
}
const ov = document.getElementById('gauntlet-ended-overlay');
const msgEl = document.getElementById('gauntlet-ended-message');
const titleEl = document.getElementById('gauntlet-ended-title');
const listEl = document.getElementById('gauntlet-ended-rankings');
const btn = document.getElementById('btn-gauntlet-ended-lobby');
if (!ov || !msgEl || !listEl) return;
gauntletEndsAtMs = null;
if (titleEl) {
titleEl.textContent = data && data.reason === 'time' ? 'หมดเวลา · Time up' : 'เกมจบ · Game over';
}
@@ -7923,6 +8135,9 @@
if (!ev || !ev.mapId) return;
const applySnap = (md) => {
mapData = md;
if (ev.mapId != null && String(ev.mapId).trim() !== '') {
playSessionMapId = String(ev.mapId).trim();
}
if (mapData.gameType !== 'space_shooter') {
spaceShooterGameEnded = false;
}
@@ -7955,6 +8170,7 @@
meGauntletJumpVis = meGauntletJumpTicks;
const sc = Number(p.gauntletScore);
me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0;
me.gauntletEliminated = !!p.gauntletEliminated;
} else {
const o = others.get(p.id) ?? others.get(String(p.id));
if (o) {
@@ -7968,6 +8184,7 @@
o.gauntletJumpVis = o.gauntletJumpTicks;
const sc = Number(p.gauntletScore);
o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0;
o.gauntletEliminated = !!p.gauntletEliminated;
}
}
});
@@ -7975,10 +8192,12 @@
meGauntletJumpTicks = 0;
meGauntletJumpVis = 0;
me.gauntletScore = 0;
me.gauntletEliminated = false;
others.forEach((o) => {
o.gauntletJumpTicks = 0;
o.gauntletJumpVis = 0;
o.gauntletScore = 0;
o.gauntletEliminated = false;
});
}
if (mapData.gameType === 'gauntlet') {
@@ -7995,8 +8214,18 @@
if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz;
})
.catch(() => {});
if (String(ev.mapId || '') === GAUNTLET_FACE_RIGHT_MAP_ID) {
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60);
} else {
const hto2 = document.getElementById('gauntlet-crown-howto-overlay');
if (hto2) hto2.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
}
} else {
gauntletEndsAtMs = null;
const hto3 = document.getElementById('gauntlet-crown-howto-overlay');
if (hto3) hto3.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
}
if (mapData.gameType !== 'gauntlet') {
me.tx = null;
@@ -8004,6 +8233,8 @@
}
const gauntletOv = document.getElementById('gauntlet-ended-overlay');
if (gauntletOv) gauntletOv.classList.add('is-hidden');
const gcmOv = document.getElementById('gauntlet-crown-mission-overlay');
if (gcmOv) gcmOv.classList.add('is-hidden');
if (mapData.gameType === 'stack') {
if (!mapData.stackReleaseArea) mapData.stackReleaseArea = [];
if (!mapData.stackLandArea) mapData.stackLandArea = [];
@@ -9192,6 +9423,8 @@
: (me.balloonBossEliminated
? 'คุณตกรอบแล้ว — ดูเพื่อนเล่น · You are out — spectate'
: 'ยานมีแรงเฉื่อย — A D / arrows / W S เร่งทิศทาง · ปล่อยปุ่มแล้วยังไหล (แรงหน่วง) · Space = ยิง (ดีเลย์) · ลูกโป้งหมด = ตกรอบ');
} else if (isGauntletCrownHeistMapPlay()) {
hintEl.textContent = 'Space / W / ↑ jump · Start 100 pts · Hit -10 · Fall off left edge = out · Rank bonus at end';
} else {
hintEl.textContent = 'Space / W / ↑ jump · Lanes + lasers · Clear = right + score';
}
@@ -9221,7 +9454,8 @@
: (isSpaceShooter() ? (me.spaceShooterScore || 0)
: (isBalloonBoss() ? (me.balloonBossScore || 0) : 0))),
y: safeYv(me.y),
eliminated: !!(isJumpSurvive() && jumpSurviveEliminated) || !!(isBalloonBoss() && me.balloonBossEliminated),
eliminated: !!(isJumpSurvive() && jumpSurviveEliminated) || !!(isBalloonBoss() && me.balloonBossEliminated)
|| !!(isGauntletCrownHeistMapPlay() && me.gauntletEliminated),
isMe: true,
});
others.forEach((o, id) => {
@@ -9235,7 +9469,8 @@
: (isBalloonBoss() ? (o.balloonBossScore || 0) : 0))),
y: safeYv(o.y),
eliminated: !!(isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated)
|| !!(isBalloonBoss() && o.balloonBossEliminated),
|| !!(isBalloonBoss() && o.balloonBossEliminated)
|| !!(isGauntletCrownHeistMapPlay() && o.gauntletEliminated),
isMe: false,
});
});
@@ -9296,7 +9531,7 @@
st.textContent = isQuizCarry()
? ''
: (row.eliminated
? (isBalloonBoss() ? 'OUT' : 'OFFLINE')
? (isBalloonBoss() ? 'OUT' : (isGauntletCrownHeistMapPlay() ? 'เสียชีวิต' : 'OFFLINE'))
: (isBalloonBoss()
? ('♥' + Math.max(0, row.isMe ? (me.balloonBossBalloons | 0) : ((() => {
const ox = others.get(row.id);
@@ -9987,10 +10222,17 @@
ctx.fillStyle = '#f7768e';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('พรมแดงสุดท้าย | คะแนน: ' + (me.gauntletScore || 0) + ' (ข้าม 1 ชิ้น +1) · สูงสุด 6 คน', 10, 24);
ctx.font = '12px sans-serif';
ctx.fillStyle = '#c0caf5';
ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · ข้ามสำเร็จ → ขวา 1 + คะแนน · ชน → ซ้าย 1', 10, 42);
if (isGauntletCrownHeistMapPlay()) {
ctx.fillText('พรมแดงสุดท้าย | คะแนน ' + (me.gauntletScore || 0) + ' · ชน -10 · ขอบซ้ายตาย · สูงสุด 6 คน', 10, 24);
ctx.font = '12px sans-serif';
ctx.fillStyle = '#c0caf5';
ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · จบเกมมีโบนัสอันดับ + เกรดทีม', 10, 42);
} else {
ctx.fillText('พรมแดงสุดท้าย | คะแนน: ' + (me.gauntletScore || 0) + ' (ข้าม 1 ชิ้น +1) · สูงสุด 6 คน', 10, 24);
ctx.font = '12px sans-serif';
ctx.fillStyle = '#c0caf5';
ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · ข้ามสำเร็จ → ขวา 1 + คะแนน · ชน → ซ้าย 1', 10, 42);
}
ctx.fillStyle = '#e0af68';
if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) {
const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000));
@@ -10082,6 +10324,8 @@
if (isGauntlet() && mapData && !isChatFocused()) {
if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
e.preventDefault();
if (isGauntletCrownHowtoBlocking()) return;
if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return;
const now = Date.now();
if (now - lastGauntletJumpKey < 200) return;
lastGauntletJumpKey = now;
+1 -1
View File
@@ -1,6 +1,6 @@
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
window.APP_VERSION = '0.0169';
window.APP_VERSION = '0.0170';
document.addEventListener('DOMContentLoaded', function () {
var t = document.querySelector('.version-tag');
if (t) t.textContent = 'v ' + window.APP_VERSION;
+317 -2
View File
@@ -12,6 +12,17 @@
<style>
html, body { height: 100%; margin: 0; }
body { min-height: 100dvh; }
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.play-canvas-stack {
position: relative;
flex: 1 1 auto;
@@ -891,6 +902,258 @@
white-space: nowrap !important;
}
/* —— Gauntlet crown heist (mno9kb07) — mock overlays — assets /Game/img/gauntlet-assets —— */
/* สูงกว่า #quiz-carry-pregame-overlay (10078) และ embed strip — กัน mock ถูกบังใน editor preview */
#gauntlet-crown-howto-overlay,
#gauntlet-crown-mission-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: max(10px, env(safe-area-inset-top)) max(10px, env(safe-area-inset-right)) max(10px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left));
box-sizing: border-box;
z-index: 10140;
}
#gauntlet-crown-mission-overlay { z-index: 10150; }
#gauntlet-crown-howto-overlay.is-hidden,
#gauntlet-crown-mission-overlay.is-hidden { display: none !important; }
.gch-backdrop,
.gcm-backdrop {
position: absolute;
inset: 0;
background: rgba(6, 8, 18, 0.78);
backdrop-filter: blur(5px);
}
.gch-shell,
.gcm-shell {
position: relative;
width: min(96vw, 920px);
max-height: min(92vh, 640px);
display: flex;
align-items: center;
justify-content: center;
padding: clamp(4px, 1.2vw, 10px);
border-radius: 14px;
border: 2px solid rgba(0, 255, 240, 0.75);
box-shadow:
0 0 0 1px rgba(255, 0, 200, 0.55),
0 0 28px rgba(0, 255, 240, 0.22),
0 0 40px rgba(255, 0, 200, 0.12);
background: rgba(6, 8, 18, 0.35);
pointer-events: auto;
}
.gch-bg,
.gcm-bg {
width: 100%;
height: auto;
max-height: min(88vh, 600px);
object-fit: contain;
display: block;
pointer-events: none;
filter: drop-shadow(0 0 18px rgba(0, 255, 240, 0.25)) drop-shadow(0 0 22px rgba(255, 0, 200, 0.2));
}
.gch-inner,
.gcm-inner {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: min(88%, 780px);
max-height: 82%;
overflow: auto;
padding: clamp(10px, 2.2vw, 18px);
box-sizing: border-box;
color: #e2e8f0;
font-family: ui-sans-serif, system-ui, 'Segoe UI', 'Kanit', sans-serif;
background: rgba(8, 10, 22, 0.72);
border-radius: 12px;
border: 1px solid rgba(0, 255, 240, 0.35);
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.35);
}
.gch-title {
margin: 0 0 4px;
font-size: clamp(1rem, 3.2vw, 1.35rem);
font-weight: 900;
letter-spacing: 0.06em;
color: #fff;
text-align: center;
text-shadow: 0 0 12px rgba(0, 255, 240, 0.45);
}
.gch-sub {
margin: 0 0 10px;
text-align: center;
font-size: clamp(0.78rem, 2.1vw, 0.95rem);
color: #7af8ff;
font-weight: 700;
}
.gch-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: clamp(6px, 1.5vw, 12px);
margin-bottom: 10px;
}
@media (max-width: 640px) {
.gch-grid { grid-template-columns: 1fr; }
}
.gch-col {
background: rgba(10, 14, 28, 0.72);
border: 1px solid rgba(0, 255, 240, 0.35);
border-radius: 10px;
padding: 8px 10px;
font-size: clamp(0.65rem, 1.65vw, 0.78rem);
line-height: 1.45;
}
.gch-col h3 {
margin: 0 0 6px;
font-size: clamp(0.72rem, 1.9vw, 0.85rem);
color: #f6e08a;
}
.gch-col p { margin: 0 0 6px; color: #c0caf5; }
.gch-col .warn { color: #f6e08a; font-weight: 600; }
.gch-foot {
text-align: center;
font-size: clamp(0.68rem, 1.8vw, 0.8rem);
color: #f7768e;
margin: 0 0 10px;
}
.btn-gch-ready {
display: block;
margin: 0 auto;
width: min(240px, 70%);
height: clamp(44px, 12vw, 56px);
border: none;
cursor: pointer;
background: url('/Game/img/gauntlet-assets/btn-ready.png') center / contain no-repeat;
background-color: transparent;
}
.btn-gch-ready:focus-visible { outline: 2px solid #7af8ff; outline-offset: 3px; }
.gcm-header-tab {
text-align: center;
font-weight: 800;
font-size: clamp(0.85rem, 2.4vw, 1rem);
color: #7af8ff;
margin-bottom: 8px;
letter-spacing: 0.04em;
}
.gcm-rank-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: clamp(6px, 1.5vw, 10px);
margin-bottom: 8px;
}
.gcm-cell {
flex: 0 0 auto;
width: clamp(72px, 18vw, 100px);
text-align: center;
}
.gcm-rank-tag {
font-size: clamp(0.55rem, 1.4vw, 0.65rem);
font-weight: 800;
color: #f6e08a;
margin-bottom: 4px;
}
.gcm-frame {
position: relative;
width: 100%;
aspect-ratio: 1;
border-radius: 8px;
border: 2px solid rgba(0, 255, 240, 0.5);
box-shadow: 0 0 12px rgba(0, 255, 240, 0.2);
overflow: hidden;
background: rgba(8, 10, 20, 0.9);
}
.gcm-frame img.gcm-av {
width: 100%;
height: 100%;
object-fit: contain;
image-rendering: pixelated;
}
.gcm-frame img.gcm-stamp {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
}
.gcm-nick {
font-size: clamp(0.52rem, 1.35vw, 0.65rem);
font-weight: 700;
color: #e2e8f0;
margin-top: 4px;
word-break: break-word;
}
.gcm-sc {
font-size: clamp(0.58rem, 1.5vw, 0.72rem);
font-weight: 800;
color: #f6e08a;
margin-top: 2px;
}
.gcm-total {
text-align: center;
font-size: clamp(0.65rem, 1.8vw, 0.8rem);
font-weight: 700;
color: #f1f5f9;
margin: 0 0 8px;
}
.gcm-total-nums { color: #f6e08a; }
.gcm-bottom {
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 10px;
align-items: start;
margin-bottom: 8px;
}
@media (max-width: 520px) { .gcm-bottom { grid-template-columns: 1fr; } }
.gcm-h {
font-size: clamp(0.62rem, 1.7vw, 0.75rem);
font-weight: 700;
color: #cbd5e1;
text-align: center;
margin-bottom: 4px;
}
.gcm-grade {
text-align: center;
font-size: clamp(2rem, 9vw, 3.2rem);
font-weight: 900;
line-height: 1;
color: #f8fafc;
text-shadow: 0 0 20px rgba(255, 255, 255, 0.35);
}
.gcm-grade--a { color: #9ece6a; text-shadow: 0 0 20px rgba(158, 206, 106, 0.5); }
.gcm-grade--b { color: #7aa2f7; }
.gcm-grade--c { color: #e0af68; }
.gcm-bonus {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: clamp(0.65rem, 1.7vw, 0.78rem);
color: #c0caf5;
}
.gcm-plaque {
border: 1px solid rgba(0, 255, 240, 0.55);
border-radius: 10px;
padding: 8px 10px;
background: linear-gradient(165deg, rgba(14, 18, 36, 0.96), rgba(8, 10, 22, 0.98));
text-align: center;
max-width: 200px;
}
.gcm-plaque strong { color: #f1f5f9; display: block; }
.gcm-plaque span { color: #7af8ff; font-size: 0.92em; }
.btn-gcm-done {
display: block;
margin: 4px auto 0;
width: min(92%, 300px);
height: clamp(44px, 11vw, 54px);
border: none;
cursor: pointer;
background: url('/Game/img/gauntlet-assets/btn-done.png') center / contain no-repeat;
background-color: transparent;
}
#gauntlet-ended-overlay {
position: fixed;
inset: 0;
@@ -1423,6 +1686,58 @@
<button type="button" id="quiz-battle-mcq-close">ปิด · Close</button>
</div>
</div>
<div id="gauntlet-crown-howto-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="gch-title">
<div class="gch-backdrop" aria-hidden="true"></div>
<div class="gch-shell">
<img class="gch-bg" src="/Game/img/gauntlet-assets/popup-Howto.png" alt="" width="920" height="520" decoding="async" />
<div class="gch-inner">
<h2 id="gch-title" class="gch-title">HOW TO PLAY</h2>
<p class="gch-sub">วิ่งหลบสิ่งกีดขวางให้รอด! · ระงับเหตุโจรกรรม ปกป้องมงกุฎทองคำ!</p>
<div class="gch-grid">
<section class="gch-col">
<h3>วิ่งหลบสิ่งกีดขวาง</h3>
<p>หลบสิ่งกีดขวางระหว่างทาง กด <strong>JUMP</strong> (Space / W / ↑)</p>
<p class="warn">ห้ามวิ่งตกขอบจอ — ตกขอบจอ = ตายทันที</p>
<p>คะแนนเริ่มที่ <strong>100</strong> · ชนสิ่งกีดขวาง = <strong>-10</strong> คะแนน</p>
</section>
<section class="gch-col">
<h3>โบนัสอันดับ</h3>
<p>อันดับ 1 = +100 · อันดับ 2 = +80 · อันดับ 3 = +60</p>
<p class="warn">คะแนนเท่ากัน = อันดับเท่ากัน</p>
</section>
<section class="gch-col">
<h3>ระดับความสำเร็จ</h3>
<p>นำคะแนนสุดท้ายของทุกคน (รวมโบนัส) มาเฉลี่ย → เกรด A / B / C และสุ่มการ์ดรางวัล</p>
<p>A: เฉลี่ย 80100 · B: 6079 · C: 059</p>
</section>
</div>
<p class="gch-foot">กด READY เพื่อเริ่มภารกิจ · Press READY</p>
<button type="button" class="btn-gch-ready" id="btn-gch-ready" title="READY"><span class="sr-only">READY</span></button>
</div>
</div>
</div>
<div id="gauntlet-crown-mission-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="gcm-heading">
<div class="gcm-backdrop" aria-hidden="true"></div>
<div class="gcm-shell">
<img class="gcm-bg" src="/Game/img/gauntlet-assets/popup-result.png" alt="" width="920" height="560" decoding="async" />
<div class="gcm-inner">
<div id="gcm-heading" class="gcm-header-tab">สรุปผลภารกิจ · Mission summary</div>
<div id="gcm-rank-row" class="gcm-rank-row"></div>
<p id="gcm-total" class="gcm-total"></p>
<div class="gcm-bottom">
<div>
<div class="gcm-h">ระดับความสำเร็จ</div>
<div id="gcm-grade" class="gcm-grade" aria-live="polite">A</div>
</div>
<div>
<div class="gcm-h">โบนัสพิเศษ</div>
<div id="gcm-bonus" class="gcm-bonus"></div>
</div>
</div>
<button type="button" class="btn-gcm-done" id="btn-gcm-done" title="รับหลักฐาน"><span class="sr-only">รับหลักฐาน · Receive evidence</span></button>
</div>
</div>
</div>
<div id="gauntlet-ended-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="gauntlet-ended-title">
<div class="gauntlet-ended-backdrop" aria-hidden="true"></div>
<div class="gauntlet-ended-card">
@@ -1461,8 +1776,8 @@
</div>
</div>
<script src="/Game/socket.io/socket.io.js"></script>
<script src="js/version.js?v=0.0166"></script>
<script src="js/play.js?v=0.192"></script>
<script src="js/version.js?v=0.0170"></script>
<script src="js/play.js?v=0.194"></script>
<div class="version-tag">v —</div>
</body>
</html>
+258 -14
View File
@@ -14,6 +14,8 @@
const forceDefaultCharacter = params.get('defaultChar') === '1';
const editorEmbedReturn = params.get('editorEmbed') === '1';
const playMapIdFromQuery = (params.get('map') || '').trim();
/** mapId จาก join / game-start — ใช้จับฉาก crown เมื่อ URL ไม่มี ?map= */
let playSessionMapId = playMapIdFromQuery;
/** สรุปภารกิจแบบ mock (popup-result ฯลฯ) — ใช้เฉพาะฉากนี้จาก editor (id ใน URL เช่น editor.html?id=mnorwqx1) */
const quizCarryMissionSummaryMapId = 'mnorwqx1';
const quizCarryUseMissionSummaryOverlay = playMapIdFromQuery === quizCarryMissionSummaryMapId;
@@ -99,7 +101,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, 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 };
const others = new Map();
/** Stack preview HUD: เทอร์มินัลล็อก (cyber UI) */
const STACK_PREVIEW_HUD_LOG_MAX = 8;
@@ -2026,7 +2028,10 @@
if (o.botAnswerWander == null) o.botAnswerWander = false;
if (o.gauntletJumpTicks == null) o.gauntletJumpTicks = 0;
if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks;
if (o.gauntletScore == null) o.gauntletScore = 0;
if (o.gauntletScore == null) {
o.gauntletScore = (mapData && mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) ? 100 : 0;
}
if (o.gauntletEliminated == null) o.gauntletEliminated = false;
if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) {
const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)];
o.botWanderDx = wd[0];
@@ -2058,6 +2063,7 @@
const tierRoll = Math.random();
const botTier = tierRoll < 0.28 ? 'sharp' : tierRoll < 0.62 ? 'avg' : 'weak';
const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)];
const crownStart = mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay();
others.set(id, {
x, y, tx: x, ty: y,
direction: ['down', 'up', 'left', 'right'][Math.floor(Math.random() * 4)],
@@ -2067,7 +2073,8 @@
botTier,
gauntletJumpTicks: 0,
gauntletJumpVis: 0,
gauntletScore: 0,
gauntletScore: crownStart ? 100 : 0,
gauntletEliminated: false,
quizCannotTrue: false,
quizCannotFalse: false,
quizCarryHeld: null,
@@ -2441,7 +2448,15 @@
function isFrogger() { return mapData && mapData.gameType === 'frogger'; }
function isGauntlet() { return mapData && mapData.gameType === 'gauntlet'; }
function isGauntletFaceRightMapMno9kb07() {
return !!(isGauntlet() && playMapIdFromQuery === GAUNTLET_FACE_RIGHT_MAP_ID);
return !!(isGauntlet() && playSessionMapId === GAUNTLET_FACE_RIGHT_MAP_ID);
}
/** Crown heist rules + UI (เดียวกับหันขวา — ฉาก mno9kb07) */
function isGauntletCrownHeistMapPlay() {
return isGauntletFaceRightMapMno9kb07();
}
let gauntletCrownHowtoVisible = false;
function isGauntletCrownHowtoBlocking() {
return isGauntletCrownHeistMapPlay() && gauntletCrownHowtoVisible;
}
function isStack() { return mapData && mapData.gameType === 'stack'; }
function isJumpSurvive() { return mapData && mapData.gameType === 'jump_survive'; }
@@ -5068,6 +5083,7 @@
* ตอบสนองสงกดขวางทชนเซลลเดยวก + กระโดดลวงหนาเม threat อยคอลมนดไป
*/
function maybePreviewBotGauntletJump(o, obstacles, w, h) {
if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return;
let px = Math.floor(Number(o.x)) || 0;
let py = Math.floor(Number(o.y)) || 0;
px = Math.max(0, Math.min(w - 1, px));
@@ -5101,12 +5117,14 @@
/** หนึ่ง tick เดียวกับ runGauntletTick ฝั่งเซิร์ฟเวอร์ (เฉพาะ collision + เลื่อน x) */
function applyGauntletPhysicsToPreviewBot(o, obstacles, w, h) {
if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return;
let px = Math.floor(Number(o.x)) || 0;
let py = Math.floor(Number(o.y)) || 0;
px = Math.max(0, Math.min(w - 1, px));
py = Math.max(0, Math.min(h - 1, py));
const air = (o.gauntletJumpTicks || 0) > 0;
const { ch: gauntletCh2 } = getCharacterFootprintWH(mapData);
const crown = isGauntletCrownHeistMapPlay();
let advanceX = false;
let hitBack = false;
for (let i = 0; i < obstacles.length; i++) {
@@ -5129,9 +5147,21 @@
if (advanceX) {
px = Math.min(w - 2, px + 1);
o.gauntletJumpTicks = 0;
o.gauntletScore = (o.gauntletScore || 0) + 1;
if (!crown) {
o.gauntletScore = (o.gauntletScore || 0) + 1;
}
} else if (hitBack) {
px = Math.max(0, px - 1);
if (crown) {
if (px <= 0) {
o.gauntletEliminated = true;
o.gauntletScore = 0;
} else {
o.gauntletScore = Math.max(0, (o.gauntletScore || 0) - 10);
px = Math.max(0, px - 1);
}
} else {
px = Math.max(0, px - 1);
}
}
if ((o.gauntletJumpTicks || 0) > 0) o.gauntletJumpTicks--;
o.tx = px;
@@ -5165,6 +5195,163 @@
socket.emit('gauntlet-preview-rows', { ys: [...ysSet] });
}
/** ซ่อน overlay โหมดอื่นที่ค้างจาก embed / session ก่อน — กัน UI ซ้อนไม่ตรง mock */
function hideConflictingOverlaysForGauntletCrown() {
hideQuizCarryPregameOverlay();
quizCarryPregameActive = false;
const hideIds = [
'quiz-game-overlay',
'quiz-carry-mission-overlay',
'gauntlet-ended-overlay',
'quiz-battle-mcq-overlay',
'quiz-carry-embed-countdown',
'quiz-carry-embed-q-strip',
'play-quiz-scoreboard',
'play-quiz-feedback',
];
for (let i = 0; i < hideIds.length; i++) {
const el = document.getElementById(hideIds[i]);
if (el) el.classList.add('is-hidden');
}
const gcm = document.getElementById('gauntlet-crown-mission-overlay');
if (gcm) gcm.classList.add('is-hidden');
}
function gcmRankBadgeUrl(rank) {
const r = Math.floor(Number(rank)) || 99;
if (r === 1) return BASE + '/img/gauntlet-assets/result-1st.png';
if (r === 2) return BASE + '/img/gauntlet-assets/result-2nd.png';
if (r === 3) return BASE + '/img/gauntlet-assets/result-3rd.png';
return BASE + '/img/gauntlet-assets/result-txt-2.png';
}
function showGauntletCrownHowtoOverlay() {
if (!isGauntletCrownHeistMapPlay()) return;
hideConflictingOverlaysForGauntletCrown();
const ov = document.getElementById('gauntlet-crown-howto-overlay');
if (!ov) return;
gauntletCrownHowtoVisible = true;
ov.classList.remove('is-hidden');
const btn = document.getElementById('btn-gch-ready');
if (btn) {
btn.onclick = function () {
ov.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
};
}
}
function showGauntletCrownMissionOverlay(mission) {
const ov = document.getElementById('gauntlet-crown-mission-overlay');
const rowEl = document.getElementById('gcm-rank-row');
const totalEl = document.getElementById('gcm-total');
const gradeEl = document.getElementById('gcm-grade');
const bonusEl = document.getElementById('gcm-bonus');
const btn = document.getElementById('btn-gcm-done');
if (!ov || !rowEl || !mission || !Array.isArray(mission.ranked)) return;
rowEl.innerHTML = '';
mission.ranked.forEach((r) => {
const cell = document.createElement('div');
cell.className = 'gcm-cell';
const tag = document.createElement('div');
tag.className = 'gcm-rank-tag';
const rk = Math.floor(Number(r.rank)) || 0;
if (rk >= 1 && rk <= 3) {
const tagImg = document.createElement('img');
tagImg.src = gcmRankBadgeUrl(rk);
tagImg.alt = '[' + String(r.rankLabel || rk) + ']';
tagImg.style.maxWidth = '100%';
tagImg.style.height = 'auto';
tag.appendChild(tagImg);
} else {
tag.textContent = '[' + String(r.rankLabel || rk) + ']';
}
const frame = document.createElement('div');
frame.className = 'gcm-frame';
const img = document.createElement('img');
img.className = 'gcm-av';
img.alt = '';
const cid = r.characterId ? String(r.characterId) : '';
if (cid) {
const im = getCharacterImg(cid, 'down');
if (im && im.src) img.src = im.src;
} else {
img.src = defaultAvatarImg.src;
}
img.onerror = function () {
this.onerror = null;
this.src = defaultAvatarImg.src;
};
frame.appendChild(img);
if (r.eliminated) {
const st = document.createElement('img');
st.className = 'gcm-stamp';
st.src = BASE + '/img/gauntlet-assets/result-Lose_stamp.png';
st.alt = 'เสียชีวิต';
frame.appendChild(st);
}
const nick = document.createElement('div');
nick.className = 'gcm-nick';
nick.textContent = r.nickname || '—';
const sc = document.createElement('div');
sc.className = 'gcm-sc';
sc.textContent = String(r.baseScore || 0) + '(+' + String(r.rankBonus || 0) + ')';
cell.appendChild(tag);
cell.appendChild(frame);
cell.appendChild(nick);
cell.appendChild(sc);
rowEl.appendChild(cell);
});
if (totalEl) {
const parts = (mission.totalParts || []).map((n) => String(Math.max(0, Number(n) || 0)));
const sum = Math.max(0, Number(mission.totalSum) || 0);
const avg = Math.max(0, Math.floor(Number(mission.averageScore) || 0));
totalEl.innerHTML = 'คะแนนรวม <span class="gcm-total-nums">(' + parts.join('+') + ') = ' + sum + '</span> · เฉลี่ย ' + avg + ' → เกรด';
}
if (gradeEl) {
const g = String(mission.grade || 'C').toUpperCase().charAt(0);
gradeEl.textContent = g;
gradeEl.className = 'gcm-grade gcm-grade--' + g.toLowerCase();
}
if (bonusEl) {
bonusEl.innerHTML = '';
const row = document.createElement('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '8px';
const chk = document.createElement('img');
chk.src = BASE + '/img/gauntlet-assets/result-check.png';
chk.alt = '';
chk.width = 26;
chk.height = 26;
const sp = document.createElement('span');
sp.style.color = '#9ece6a';
sp.style.fontWeight = '700';
sp.textContent = 'ภารกิจสำเร็จ';
row.appendChild(chk);
row.appendChild(sp);
bonusEl.appendChild(row);
const rc = mission.rewardCard;
if (rc && rc.th) {
const pl = document.createElement('div');
pl.className = 'gcm-plaque';
pl.innerHTML = '<strong>' + String(rc.th) + '</strong><span>' + String(rc.en || '') + '</span>';
bonusEl.appendChild(pl);
}
}
ov.classList.remove('is-hidden');
function goLobby() {
ov.classList.add('is-hidden');
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
}
if (btn) {
btn.onclick = function () {
if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden');
else goLobby();
};
}
}
function isLobby() { return mapData && mapData.gameType === 'lobby'; }
function isQuiz() { return mapData && mapData.gameType === 'quiz'; }
function getLane(y) {
@@ -7351,6 +7538,9 @@
const prevWasSpaceShooter = mapData && mapData.gameType === 'space_shooter';
const prevWasBalloonBoss = mapData && mapData.gameType === 'balloon_boss';
mapData = gameMapData;
if (res && res.mapId != null && String(res.mapId).trim() !== '') {
playSessionMapId = String(res.mapId).trim();
}
if (res && res.hostId != null) playHostId = res.hostId;
if (mapData.gameType !== 'space_shooter') {
spaceShooterGameEnded = false;
@@ -7426,6 +7616,7 @@
{
const sc0 = Number(myPeer && myPeer.gauntletScore);
me.gauntletScore = Number.isFinite(sc0) ? Math.max(0, sc0) : 0;
me.gauntletEliminated = !!(myPeer && myPeer.gauntletEliminated);
}
{
const sh0 = Number(myPeer && myPeer.spaceShooterScore);
@@ -7465,6 +7656,7 @@
gauntletJumpTicks: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0,
gauntletJumpVis: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0,
gauntletScore: (() => { const s = Number(p.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
gauntletEliminated: !!p.gauntletEliminated,
spaceShooterScore: (() => { const s = Number(p.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
balloonBossScore: (() => { const s = Number(p.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
balloonBossBalloons: (() => {
@@ -7558,6 +7750,16 @@
else if (isQuizCarry()) setupPlayQuizCarryUi();
else if (isQuizBattle()) setupPlayQuizBattleUi();
else teardownPlayQuizUi();
if (mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) {
hideConflictingOverlaysForGauntletCrown();
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50);
} else {
const hov = document.getElementById('gauntlet-crown-howto-overlay');
if (hov) {
hov.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
}
}
resizeCanvas(); draw(); tick();
}
@@ -7862,8 +8064,10 @@
if (p.direction) me.direction = p.direction;
const jt = Number(p.gauntletJumpTicks);
meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0;
meGauntletJumpVis = meGauntletJumpTicks;
const sc = Number(p.gauntletScore);
me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0;
me.gauntletEliminated = !!p.gauntletEliminated;
} else {
const o = others.get(p.id) ?? others.get(pid);
if (o) {
@@ -7877,6 +8081,7 @@
if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks;
const sc = Number(p.gauntletScore);
o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0;
o.gauntletEliminated = !!p.gauntletEliminated;
}
}
});
@@ -7887,13 +8092,20 @@
});
socket.on('gauntlet-ended', (data) => {
gauntletEndsAtMs = null;
const hto = document.getElementById('gauntlet-crown-howto-overlay');
if (hto) hto.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
if (data && data.crownMission) {
showGauntletCrownMissionOverlay(data.crownMission);
return;
}
const ov = document.getElementById('gauntlet-ended-overlay');
const msgEl = document.getElementById('gauntlet-ended-message');
const titleEl = document.getElementById('gauntlet-ended-title');
const listEl = document.getElementById('gauntlet-ended-rankings');
const btn = document.getElementById('btn-gauntlet-ended-lobby');
if (!ov || !msgEl || !listEl) return;
gauntletEndsAtMs = null;
if (titleEl) {
titleEl.textContent = data && data.reason === 'time' ? 'หมดเวลา · Time up' : 'เกมจบ · Game over';
}
@@ -7923,6 +8135,9 @@
if (!ev || !ev.mapId) return;
const applySnap = (md) => {
mapData = md;
if (ev.mapId != null && String(ev.mapId).trim() !== '') {
playSessionMapId = String(ev.mapId).trim();
}
if (mapData.gameType !== 'space_shooter') {
spaceShooterGameEnded = false;
}
@@ -7955,6 +8170,7 @@
meGauntletJumpVis = meGauntletJumpTicks;
const sc = Number(p.gauntletScore);
me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0;
me.gauntletEliminated = !!p.gauntletEliminated;
} else {
const o = others.get(p.id) ?? others.get(String(p.id));
if (o) {
@@ -7968,6 +8184,7 @@
o.gauntletJumpVis = o.gauntletJumpTicks;
const sc = Number(p.gauntletScore);
o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0;
o.gauntletEliminated = !!p.gauntletEliminated;
}
}
});
@@ -7975,10 +8192,12 @@
meGauntletJumpTicks = 0;
meGauntletJumpVis = 0;
me.gauntletScore = 0;
me.gauntletEliminated = false;
others.forEach((o) => {
o.gauntletJumpTicks = 0;
o.gauntletJumpVis = 0;
o.gauntletScore = 0;
o.gauntletEliminated = false;
});
}
if (mapData.gameType === 'gauntlet') {
@@ -7995,8 +8214,18 @@
if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz;
})
.catch(() => {});
if (String(ev.mapId || '') === GAUNTLET_FACE_RIGHT_MAP_ID) {
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60);
} else {
const hto2 = document.getElementById('gauntlet-crown-howto-overlay');
if (hto2) hto2.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
}
} else {
gauntletEndsAtMs = null;
const hto3 = document.getElementById('gauntlet-crown-howto-overlay');
if (hto3) hto3.classList.add('is-hidden');
gauntletCrownHowtoVisible = false;
}
if (mapData.gameType !== 'gauntlet') {
me.tx = null;
@@ -8004,6 +8233,8 @@
}
const gauntletOv = document.getElementById('gauntlet-ended-overlay');
if (gauntletOv) gauntletOv.classList.add('is-hidden');
const gcmOv = document.getElementById('gauntlet-crown-mission-overlay');
if (gcmOv) gcmOv.classList.add('is-hidden');
if (mapData.gameType === 'stack') {
if (!mapData.stackReleaseArea) mapData.stackReleaseArea = [];
if (!mapData.stackLandArea) mapData.stackLandArea = [];
@@ -9192,6 +9423,8 @@
: (me.balloonBossEliminated
? 'คุณตกรอบแล้ว — ดูเพื่อนเล่น · You are out — spectate'
: 'ยานมีแรงเฉื่อย — A D / arrows / W S เร่งทิศทาง · ปล่อยปุ่มแล้วยังไหล (แรงหน่วง) · Space = ยิง (ดีเลย์) · ลูกโป้งหมด = ตกรอบ');
} else if (isGauntletCrownHeistMapPlay()) {
hintEl.textContent = 'Space / W / ↑ jump · Start 100 pts · Hit -10 · Fall off left edge = out · Rank bonus at end';
} else {
hintEl.textContent = 'Space / W / ↑ jump · Lanes + lasers · Clear = right + score';
}
@@ -9221,7 +9454,8 @@
: (isSpaceShooter() ? (me.spaceShooterScore || 0)
: (isBalloonBoss() ? (me.balloonBossScore || 0) : 0))),
y: safeYv(me.y),
eliminated: !!(isJumpSurvive() && jumpSurviveEliminated) || !!(isBalloonBoss() && me.balloonBossEliminated),
eliminated: !!(isJumpSurvive() && jumpSurviveEliminated) || !!(isBalloonBoss() && me.balloonBossEliminated)
|| !!(isGauntletCrownHeistMapPlay() && me.gauntletEliminated),
isMe: true,
});
others.forEach((o, id) => {
@@ -9235,7 +9469,8 @@
: (isBalloonBoss() ? (o.balloonBossScore || 0) : 0))),
y: safeYv(o.y),
eliminated: !!(isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated)
|| !!(isBalloonBoss() && o.balloonBossEliminated),
|| !!(isBalloonBoss() && o.balloonBossEliminated)
|| !!(isGauntletCrownHeistMapPlay() && o.gauntletEliminated),
isMe: false,
});
});
@@ -9296,7 +9531,7 @@
st.textContent = isQuizCarry()
? ''
: (row.eliminated
? (isBalloonBoss() ? 'OUT' : 'OFFLINE')
? (isBalloonBoss() ? 'OUT' : (isGauntletCrownHeistMapPlay() ? 'เสียชีวิต' : 'OFFLINE'))
: (isBalloonBoss()
? ('♥' + Math.max(0, row.isMe ? (me.balloonBossBalloons | 0) : ((() => {
const ox = others.get(row.id);
@@ -9987,10 +10222,17 @@
ctx.fillStyle = '#f7768e';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('พรมแดงสุดท้าย | คะแนน: ' + (me.gauntletScore || 0) + ' (ข้าม 1 ชิ้น +1) · สูงสุด 6 คน', 10, 24);
ctx.font = '12px sans-serif';
ctx.fillStyle = '#c0caf5';
ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · ข้ามสำเร็จ → ขวา 1 + คะแนน · ชน → ซ้าย 1', 10, 42);
if (isGauntletCrownHeistMapPlay()) {
ctx.fillText('พรมแดงสุดท้าย | คะแนน ' + (me.gauntletScore || 0) + ' · ชน -10 · ขอบซ้ายตาย · สูงสุด 6 คน', 10, 24);
ctx.font = '12px sans-serif';
ctx.fillStyle = '#c0caf5';
ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · จบเกมมีโบนัสอันดับ + เกรดทีม', 10, 42);
} else {
ctx.fillText('พรมแดงสุดท้าย | คะแนน: ' + (me.gauntletScore || 0) + ' (ข้าม 1 ชิ้น +1) · สูงสุด 6 คน', 10, 24);
ctx.font = '12px sans-serif';
ctx.fillStyle = '#c0caf5';
ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · ข้ามสำเร็จ → ขวา 1 + คะแนน · ชน → ซ้าย 1', 10, 42);
}
ctx.fillStyle = '#e0af68';
if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) {
const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000));
@@ -10082,6 +10324,8 @@
if (isGauntlet() && mapData && !isChatFocused()) {
if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
e.preventDefault();
if (isGauntletCrownHowtoBlocking()) return;
if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return;
const now = Date.now();
if (now - lastGauntletJumpKey < 200) return;
lastGauntletJumpKey = now;
+125 -9
View File
@@ -2455,6 +2455,87 @@ const GAUNTLET_MAX_PLAYERS = 6;
const GAUNTLET_LASER_SPAWN_CHANCE = 0.14;
/** เฉพาะแถวที่มีผู้เล่น — แถวละโอกาสเกิด lane obstacle */
const GAUNTLET_LANE_ROW_SPAWN_CHANCE = 0.34;
/** Last Light / museum gauntlet — scoring, mission UI, how-to flow */
const GAUNTLET_CROWN_HEIST_MAP_ID = 'mno9kb07';
function isGauntletCrownHeistMapBySpace(space) {
return !!(space && space.mapId && String(space.mapId) === GAUNTLET_CROWN_HEIST_MAP_ID);
}
function rollGauntletCrownRewardCard(grade) {
const r = Math.random();
if (grade === 'A') {
if (r < 0.3) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' };
if (r < 0.8) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' };
return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' };
}
if (grade === 'B') {
if (r < 0.2) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' };
if (r < 0.5) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' };
return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' };
}
if (r < 0.2) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' };
return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' };
}
function gauntletCrownRankBonus(rankOrdinal) {
if (rankOrdinal === 1) return 100;
if (rankOrdinal === 2) return 80;
if (rankOrdinal === 3) return 60;
return 0;
}
function buildGauntletCrownMissionPayload(space) {
const peersArr = [...space.peers.values()];
const rows = peersArr.map((p) => ({
id: p.id,
nickname: (p.nickname || '').trim() || 'ผู้เล่น',
characterId: p.characterId ?? null,
baseScore: p.gauntletEliminated ? 0 : Math.max(0, p.gauntletScore | 0),
eliminated: !!p.gauntletEliminated,
}));
rows.sort((a, b) => {
if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
return String(a.nickname).localeCompare(String(b.nickname), 'th');
});
let lastScore = null;
let rankOrdinal = 0;
const ranked = [];
for (let i = 0; i < rows.length; i++) {
const pos = i + 1;
if (lastScore !== rows[i].baseScore) {
rankOrdinal = pos;
lastScore = rows[i].baseScore;
}
const bonus = gauntletCrownRankBonus(rankOrdinal);
const finalScore = rows[i].baseScore + bonus;
const rankLabel = rankOrdinal === 1 ? '1st' : rankOrdinal === 2 ? '2nd' : rankOrdinal === 3 ? '3rd' : String(rankOrdinal);
ranked.push({
id: rows[i].id,
nickname: rows[i].nickname,
characterId: rows[i].characterId,
baseScore: rows[i].baseScore,
eliminated: rows[i].eliminated,
rank: rankOrdinal,
rankLabel,
rankBonus: bonus,
finalScore,
});
}
const totalSum = ranked.reduce((s, r) => s + r.finalScore, 0);
const n = ranked.length || 1;
const averageScore = Math.floor(totalSum / n);
const grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C';
const rewardCard = rollGauntletCrownRewardCard(grade);
return {
ranked,
totalSum,
averageScore,
grade,
rewardCard,
totalParts: ranked.map((r) => r.finalScore),
};
}
function gauntletSpawnYs(md, playerCount) {
const h = md.height || 15;
@@ -2569,13 +2650,15 @@ function startGauntletTicker(sid, space) {
function initGauntletPeersAll(space, md) {
const list = [...space.peers.values()];
const pos = gauntletSpawnPositions(md, list.length);
const crown = isGauntletCrownHeistMapBySpace(space);
list.forEach((p, i) => {
const pt = pos[i] || pos[pos.length - 1];
p.x = pt.x;
p.y = pt.y;
p.gauntletJumpTicks = 0;
p.gauntletScore = 0;
p.gauntletScore = crown ? 100 : 0;
p.gauntletJumpPending = false;
p.gauntletEliminated = false;
});
space.gauntletRun = { obstacles: [], nextObsId: 1, spawnAcc: 0, nextSpawnIn: 3 };
space.gauntletPreviewRowsBySocket = new Map();
@@ -2593,11 +2676,19 @@ function endGauntletGame(sid, space, reason) {
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'gauntlet') return;
stopGauntletTicker(space);
const rankings = [...space.peers.values()].map((p) => ({
id: p.id,
nickname: (p.nickname || '').trim() || 'ผู้เล่น',
score: Math.max(0, p.gauntletScore | 0),
})).sort((a, b) => b.score - a.score);
const wasCrown = isGauntletCrownHeistMapBySpace(space);
const crownMission = wasCrown ? buildGauntletCrownMissionPayload(space) : null;
const rankings = crownMission
? crownMission.ranked.map((r) => ({
id: r.id,
nickname: r.nickname,
score: r.finalScore,
}))
: [...space.peers.values()].map((p) => ({
id: p.id,
nickname: (p.nickname || '').trim() || 'ผู้เล่น',
score: Math.max(0, p.gauntletScore | 0),
})).sort((a, b) => b.score - a.score);
let retId = space.gauntletReturnMapId;
let retMap = retId && maps.has(retId) ? maps.get(retId) : null;
@@ -2619,6 +2710,7 @@ function endGauntletGame(sid, space, reason) {
p.gauntletJumpTicks = 0;
p.gauntletScore = 0;
p.gauntletJumpPending = false;
p.gauntletEliminated = false;
});
} else {
console.warn('endGauntletGame: no lobby map to return to', sid);
@@ -2631,6 +2723,7 @@ function endGauntletGame(sid, space, reason) {
reason: reason || 'time',
message: msg,
rankings,
crownMission: crownMission || undefined,
});
}
@@ -2733,8 +2826,12 @@ function runGauntletTick(sid, space) {
}
/* กระโดดข้ามสำเร็จ → เลื่อนผู้เล่นไปขวา แต่ไม่ลบ obstacle (ยังไหลต่อให้คนอื่น/รอบถัดไป) */
const crownHeist = isGauntletCrownHeistMapBySpace(space);
const { ch: gauntletCh } = getCharacterFootprintWHForMove(md);
space.peers.forEach((p) => {
if (crownHeist && p.gauntletEliminated) {
return;
}
let px = Math.floor(Number(p.x)) || 0;
let py = Math.floor(Number(p.y)) || 0;
px = Math.max(0, Math.min(w - 1, px));
@@ -2764,9 +2861,21 @@ function runGauntletTick(sid, space) {
if (advanceX) {
px = Math.min(w - 2, px + 1);
p.gauntletJumpTicks = 0;
p.gauntletScore = (p.gauntletScore || 0) + 1;
if (!crownHeist) {
p.gauntletScore = (p.gauntletScore || 0) + 1;
}
} else if (hitBack) {
px = Math.max(0, px - 1);
if (crownHeist) {
if (px <= 0) {
p.gauntletEliminated = true;
p.gauntletScore = 0;
} else {
p.gauntletScore = Math.max(0, (p.gauntletScore || 0) - 10);
px = Math.max(0, px - 1);
}
} else {
px = Math.max(0, px - 1);
}
}
if ((p.gauntletJumpTicks || 0) > 0) p.gauntletJumpTicks--;
p.x = px;
@@ -2789,6 +2898,7 @@ function runGauntletTick(sid, space) {
characterId: p.characterId ?? null,
gauntletJumpTicks: p.gauntletJumpTicks || 0,
gauntletScore: p.gauntletScore || 0,
gauntletEliminated: !!p.gauntletEliminated,
}));
io.to(sid).emit('gauntlet-sync', {
obstacles: obsOut,
@@ -3055,7 +3165,7 @@ io.on('connection', (socket) => {
const bbStartBalloons = Math.max(1, Math.min(12, Math.floor(Number(mdJoin.balloonBossBalloonsPerPlayer)) || 5));
const peer = {
id: socket.id, x: +spawnPt.x, y: +spawnPt.y, direction: 'down', nickname: nickname || 'ผู้เล่น', ready: false, characterId: characterId || null, voiceMicOn: true,
gauntletJumpTicks: 0, gauntletScore: 0, gauntletJumpPending: false, spaceShooterScore: 0,
gauntletJumpTicks: 0, gauntletScore: 0, gauntletJumpPending: false, gauntletEliminated: false, spaceShooterScore: 0,
balloonBossScore: 0, balloonBossBalloons: mdJoin.gameType === 'balloon_boss' ? bbStartBalloons : 5, balloonBossEliminated: false,
};
if (mdJoin.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(mdJoin)) {
@@ -3076,6 +3186,10 @@ io.on('connection', (socket) => {
peer.x = pt.x;
peer.y = pt.y;
peer.gauntletJumpTicks = 0;
if (isGauntletCrownHeistMapBySpace(space)) {
peer.gauntletScore = 100;
peer.gauntletEliminated = false;
}
startGauntletTicker(spaceId, space);
}
const peersList = [...space.peers.values()];
@@ -3375,6 +3489,7 @@ io.on('connection', (socket) => {
characterId: p.characterId ?? null,
gauntletJumpTicks: p.gauntletJumpTicks || 0,
gauntletScore: p.gauntletScore || 0,
gauntletEliminated: !!p.gauntletEliminated,
}));
}
}
@@ -3437,6 +3552,7 @@ io.on('connection', (socket) => {
const p = space.peers.get(socket.id);
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
if (!md || md.gameType !== 'gauntlet') return;
if (isGauntletCrownHeistMapBySpace(space) && p.gauntletEliminated) return;
if ((p.gauntletJumpTicks || 0) > 0) return;
p.gauntletJumpPending = true;
});