minigame 2 just begin1.5
This commit is contained in:
+258
-14
@@ -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,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;
|
||||
|
||||
@@ -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: เฉลี่ย 80–100 · B: 60–79 · C: 0–59</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
@@ -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
@@ -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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user