diff --git a/scripts/deploy-www.sh b/scripts/deploy-www.sh
index 1a3c2bb..0a8859d 100755
--- a/scripts/deploy-www.sh
+++ b/scripts/deploy-www.sh
@@ -33,6 +33,11 @@ GA="$DST/Game/public/img/gauntlet-assets"
mkdir -p "$GA"
chown www-data:www-data "$GA"
chmod 2775 "$GA"
+# Jump Survival (Jumper) — โฟลเดอร์รูป; FTP มัก MKD ไม่ได้ถ้ายังไม่มีบนเซิร์ฟ
+JUMPER="$DST/Game/public/img/Jumper"
+mkdir -p "$JUMPER"
+chown www-data:www-data "$JUMPER"
+chmod 2775 "$JUMPER"
GAME_SRV_HASH_AFTER="$(hash_game_server_js "$GAME_SERVER_JS")"
if [[ -n "$GAME_SRV_HASH_AFTER" && "$GAME_SRV_HASH_BEFORE" != "$GAME_SRV_HASH_AFTER" ]]; then
diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html
index 5075d87..3f8d841 100644
--- a/www/html/Admin/index.html
+++ b/www/html/Admin/index.html
@@ -71,7 +71,7 @@
-
+
diff --git a/www/html/Game/public/img/Jumper/1.png b/www/html/Game/public/img/Jumper/1.png
new file mode 100644
index 0000000..2646cbd
Binary files /dev/null and b/www/html/Game/public/img/Jumper/1.png differ
diff --git a/www/html/Game/public/img/Jumper/2.png b/www/html/Game/public/img/Jumper/2.png
new file mode 100644
index 0000000..58c47cc
Binary files /dev/null and b/www/html/Game/public/img/Jumper/2.png differ
diff --git a/www/html/Game/public/img/Jumper/3.png b/www/html/Game/public/img/Jumper/3.png
new file mode 100644
index 0000000..b479ba8
Binary files /dev/null and b/www/html/Game/public/img/Jumper/3.png differ
diff --git a/www/html/Game/public/img/Jumper/Avartar-mark.png b/www/html/Game/public/img/Jumper/Avartar-mark.png
new file mode 100644
index 0000000..45a6644
Binary files /dev/null and b/www/html/Game/public/img/Jumper/Avartar-mark.png differ
diff --git a/www/html/Game/public/img/Jumper/Avartar.png b/www/html/Game/public/img/Jumper/Avartar.png
new file mode 100644
index 0000000..dabf3ad
Binary files /dev/null and b/www/html/Game/public/img/Jumper/Avartar.png differ
diff --git a/www/html/Game/public/img/Jumper/BG.png b/www/html/Game/public/img/Jumper/BG.png
new file mode 100644
index 0000000..6c4686f
Binary files /dev/null and b/www/html/Game/public/img/Jumper/BG.png differ
diff --git a/www/html/Game/public/img/Jumper/README.txt b/www/html/Game/public/img/Jumper/README.txt
new file mode 100644
index 0000000..2aa0355
--- /dev/null
+++ b/www/html/Game/public/img/Jumper/README.txt
@@ -0,0 +1,9 @@
+Assets for Jump Survival / Jumper mode — sprites, tiles, UI.
+Upload files here; path in browser: /Game/img/Jumper/ (public URL)
+
+ฉาก editor id **mnptfts2** — ไฟล์ UI ที่ play.js อ้างอิง (ถ้าไม่มีจะ fallback ไป gauntlet-assets):
+- **popup-Howto.png** — พื้นหลัง HOW TO ก่อนเริ่ม (ข้อความวิธีเล่นใส่ในรูปนี้ — หน้าเกมไม่ซ้อนข้อความ HTML)
+- **popup-result.png** — พื้นหลังสรุปภารกิจตอนจบ
+- **stamp-sacrifice.png** — ตราประทับ “ผู้เสียสละ” บนผู้เล่นที่ตกรอบ (optional; ถ้าโหลดไม่ได้ใช้ result-Lose_stamp)
+
+โฟลเดอร์นี้สร้างจาก repo + deploy — ถ้า FTP ยังอัปโหลดไม่ได้ ให้เช็คสิทธิ์ parent
diff --git a/www/html/Game/public/img/Jumper/btn-done.png b/www/html/Game/public/img/Jumper/btn-done.png
new file mode 100644
index 0000000..6ea7618
Binary files /dev/null and b/www/html/Game/public/img/Jumper/btn-done.png differ
diff --git a/www/html/Game/public/img/Jumper/btn-joystick-1.png b/www/html/Game/public/img/Jumper/btn-joystick-1.png
new file mode 100644
index 0000000..a521d5a
Binary files /dev/null and b/www/html/Game/public/img/Jumper/btn-joystick-1.png differ
diff --git a/www/html/Game/public/img/Jumper/btn-joystick-2.png b/www/html/Game/public/img/Jumper/btn-joystick-2.png
new file mode 100644
index 0000000..21b348f
Binary files /dev/null and b/www/html/Game/public/img/Jumper/btn-joystick-2.png differ
diff --git a/www/html/Game/public/img/Jumper/btn-jump.png b/www/html/Game/public/img/Jumper/btn-jump.png
new file mode 100644
index 0000000..6fd2381
Binary files /dev/null and b/www/html/Game/public/img/Jumper/btn-jump.png differ
diff --git a/www/html/Game/public/img/Jumper/btn-mic_off.png b/www/html/Game/public/img/Jumper/btn-mic_off.png
new file mode 100644
index 0000000..426093d
Binary files /dev/null and b/www/html/Game/public/img/Jumper/btn-mic_off.png differ
diff --git a/www/html/Game/public/img/Jumper/btn-mic_on.png b/www/html/Game/public/img/Jumper/btn-mic_on.png
new file mode 100644
index 0000000..755bb05
Binary files /dev/null and b/www/html/Game/public/img/Jumper/btn-mic_on.png differ
diff --git a/www/html/Game/public/img/Jumper/btn-ready.png b/www/html/Game/public/img/Jumper/btn-ready.png
new file mode 100644
index 0000000..6b50bfc
Binary files /dev/null and b/www/html/Game/public/img/Jumper/btn-ready.png differ
diff --git a/www/html/Game/public/img/Jumper/btn-start.png b/www/html/Game/public/img/Jumper/btn-start.png
new file mode 100644
index 0000000..cdc7776
Binary files /dev/null and b/www/html/Game/public/img/Jumper/btn-start.png differ
diff --git a/www/html/Game/public/img/Jumper/platfrom-1.png b/www/html/Game/public/img/Jumper/platfrom-1.png
new file mode 100644
index 0000000..3fe97d7
Binary files /dev/null and b/www/html/Game/public/img/Jumper/platfrom-1.png differ
diff --git a/www/html/Game/public/img/Jumper/platfrom-2.png b/www/html/Game/public/img/Jumper/platfrom-2.png
new file mode 100644
index 0000000..df93824
Binary files /dev/null and b/www/html/Game/public/img/Jumper/platfrom-2.png differ
diff --git a/www/html/Game/public/img/Jumper/platfrom-3.png b/www/html/Game/public/img/Jumper/platfrom-3.png
new file mode 100644
index 0000000..8603a58
Binary files /dev/null and b/www/html/Game/public/img/Jumper/platfrom-3.png differ
diff --git a/www/html/Game/public/img/Jumper/popup-Howto.png b/www/html/Game/public/img/Jumper/popup-Howto.png
new file mode 100644
index 0000000..d5fb874
Binary files /dev/null and b/www/html/Game/public/img/Jumper/popup-Howto.png differ
diff --git a/www/html/Game/public/img/Jumper/popup-result-Line1.png b/www/html/Game/public/img/Jumper/popup-result-Line1.png
new file mode 100644
index 0000000..07cbd6c
Binary files /dev/null and b/www/html/Game/public/img/Jumper/popup-result-Line1.png differ
diff --git a/www/html/Game/public/img/Jumper/popup-result-Line2.png b/www/html/Game/public/img/Jumper/popup-result-Line2.png
new file mode 100644
index 0000000..69f13d8
Binary files /dev/null and b/www/html/Game/public/img/Jumper/popup-result-Line2.png differ
diff --git a/www/html/Game/public/img/Jumper/popup-result.png b/www/html/Game/public/img/Jumper/popup-result.png
new file mode 100644
index 0000000..340a388
Binary files /dev/null and b/www/html/Game/public/img/Jumper/popup-result.png differ
diff --git a/www/html/Game/public/img/Jumper/result-Lose_stamp.png b/www/html/Game/public/img/Jumper/result-Lose_stamp.png
new file mode 100644
index 0000000..562b3fe
Binary files /dev/null and b/www/html/Game/public/img/Jumper/result-Lose_stamp.png differ
diff --git a/www/html/Game/public/img/Jumper/result-complete.png b/www/html/Game/public/img/Jumper/result-complete.png
new file mode 100644
index 0000000..4b5f062
Binary files /dev/null and b/www/html/Game/public/img/Jumper/result-complete.png differ
diff --git a/www/html/Game/public/img/Jumper/result-gameover.png b/www/html/Game/public/img/Jumper/result-gameover.png
new file mode 100644
index 0000000..610c6ec
Binary files /dev/null and b/www/html/Game/public/img/Jumper/result-gameover.png differ
diff --git a/www/html/Game/public/img/Jumper/result-timeup.png b/www/html/Game/public/img/Jumper/result-timeup.png
new file mode 100644
index 0000000..c467df7
Binary files /dev/null and b/www/html/Game/public/img/Jumper/result-timeup.png differ
diff --git a/www/html/Game/public/img/Jumper/time.png b/www/html/Game/public/img/Jumper/time.png
new file mode 100644
index 0000000..cc5acde
Binary files /dev/null and b/www/html/Game/public/img/Jumper/time.png differ
diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js
index 3ef44c3..cc1e5b2 100644
--- a/www/html/Game/public/js/play.js
+++ b/www/html/Game/public/js/play.js
@@ -21,6 +21,8 @@
const quizCarryUseMissionSummaryOverlay = playMapIdFromQuery === quizCarryMissionSummaryMapId;
/** Gauntlet พรมแดง — ฉากนี้จาก editor (?map=mno9kb07): วาดตัวหันขวา + ใช้รูป lane/laser จาก game-timing */
const GAUNTLET_FACE_RIGHT_MAP_ID = 'mno9kb07';
+ /** Jump Survival ฉาก Jumper — UI สรุปภารกิจแบบ crown + รูปใน /img/Jumper/ (editor ?map=mnptfts2) */
+ const JUMP_SURVIVE_MISSION_MAP_ID = 'mnptfts2';
/** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */
const LEGACY_PLACEHOLDER_CHARACTER_ID = 'Chatest';
let firstCharacterDefaultResolved = null;
@@ -818,6 +820,10 @@
let jumpSurviveJumpQueued = false;
let jumpSurviveOnGround = false;
let jumpSurviveLastEmitT = 0;
+ /** mnptfts2: howto → countdown → live → ended (คะแนน 100 ผู้รอดสุดท้าย) */
+ let jumpSurviveMissionPhase = null;
+ let jumpSurviveGameEnded = false;
+ let jumperMissionCountdownTimer = null;
/** space_shooter — ยิงหิน (จุดเกิดจากกริด P1–P6) */
let spaceShooterBullets = [];
@@ -2471,6 +2477,358 @@
}
function isStack() { return mapData && mapData.gameType === 'stack'; }
function isJumpSurvive() { return mapData && mapData.gameType === 'jump_survive'; }
+
+ function currentPlayMapId() {
+ const q = (playSessionMapId || '').trim();
+ if (q) return q;
+ if (mapData && mapData.id != null && String(mapData.id).trim() !== '') return String(mapData.id).trim();
+ return (playMapIdFromQuery || '').trim();
+ }
+
+ function isJumpSurviveMissionUiMapPlay() {
+ return isJumpSurvive() && currentPlayMapId() === JUMP_SURVIVE_MISSION_MAP_ID;
+ }
+
+ function isJumpSurviveMissionPregameBlockingPlay() {
+ if (!isJumpSurviveMissionUiMapPlay()) return false;
+ return jumpSurviveMissionPhase != null && jumpSurviveMissionPhase !== 'live' && jumpSurviveMissionPhase !== 'ended';
+ }
+
+ function jumperAssetUrl(file) {
+ return BASE + '/img/Jumper/' + file;
+ }
+
+ function hideConflictingOverlaysForJumpSurviveMission() {
+ hideConflictingOverlaysForGauntletCrown();
+ }
+
+ function jumpSurviveTimeLimitSecForMission() {
+ const m = Number(mapData && mapData.jumpSurviveTimeSec);
+ if (Number.isFinite(m) && m > 0 && m < 7200) return Math.floor(m);
+ const g = Number(gauntletRuntimeTimeLimitSec);
+ if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g);
+ return 60;
+ }
+
+ function jumpSurviveRemainingSecMission() {
+ if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveSessionStartMs <= 0) return null;
+ const lim = jumpSurviveTimeLimitSecForMission();
+ if (!(lim > 0)) return null;
+ const elapsed = (performance.now() - jumpSurviveSessionStartMs) / 1000;
+ return Math.max(0, Math.ceil(lim - elapsed));
+ }
+
+ function applyJumpSurviveJumperPanelImages() {
+ const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg');
+ if (howtoBg) {
+ howtoBg.src = jumperAssetUrl('popup-Howto.png');
+ howtoBg.onerror = function () {
+ this.onerror = null;
+ this.src = BASE + '/img/gauntlet-assets/popup-Howto.png';
+ };
+ }
+ const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg');
+ if (resBg) {
+ resBg.src = jumperAssetUrl('popup-result.png');
+ resBg.onerror = function () {
+ this.onerror = null;
+ this.src = BASE + '/img/gauntlet-assets/popup-result.png';
+ };
+ }
+ }
+
+ function showJumpSurviveMissionHowtoOverlay() {
+ if (!isJumpSurviveMissionUiMapPlay()) return;
+ hideConflictingOverlaysForJumpSurviveMission();
+ applyJumpSurviveJumperPanelImages();
+ if (jumperMissionCountdownTimer) {
+ clearTimeout(jumperMissionCountdownTimer);
+ jumperMissionCountdownTimer = null;
+ }
+ const cd = document.getElementById('gauntlet-crown-countdown');
+ if (cd) cd.classList.add('is-hidden');
+ jumpSurviveMissionPhase = 'howto';
+ const ov = document.getElementById('gauntlet-crown-howto-overlay');
+ if (!ov) return;
+ gauntletCrownHowtoVisible = true;
+ ov.classList.remove('is-hidden');
+ const st = document.getElementById('gauntlet-crown-howto-status');
+ if (st) {
+ st.textContent = '';
+ st.classList.remove('gch-status--jumper');
+ st.classList.add('is-hidden');
+ }
+ const btn = document.getElementById('btn-gch-ready');
+ if (btn) {
+ const humans = quizCarryPregameHumanIds();
+ const soleHuman = humans.length === 1;
+ const canGo = soleHuman || isMePlayHost();
+ btn.classList.remove('is-start-phase');
+ btn.classList.toggle('is-read-only', !canGo);
+ btn.disabled = !canGo;
+ btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host';
+ btn.setAttribute('aria-pressed', 'false');
+ }
+ }
+
+ function beginJumpSurviveMissionCountdownThenRun() {
+ if (!isJumpSurviveMissionUiMapPlay()) return;
+ if (jumperMissionCountdownTimer) {
+ clearTimeout(jumperMissionCountdownTimer);
+ jumperMissionCountdownTimer = null;
+ }
+ jumpSurviveMissionPhase = 'countdown';
+ const howto = document.getElementById('gauntlet-crown-howto-overlay');
+ if (howto) howto.classList.add('is-hidden');
+ gauntletCrownHowtoVisible = false;
+ const st = document.getElementById('gauntlet-crown-howto-status');
+ if (st) {
+ st.classList.add('is-hidden');
+ st.classList.remove('gch-status--jumper');
+ }
+ const cd = document.getElementById('gauntlet-crown-countdown');
+ const numEl = document.getElementById('gauntlet-crown-countdown-num');
+ const runFinish = function () {
+ if (cd) cd.classList.add('is-hidden');
+ if (jumperMissionCountdownTimer) {
+ clearTimeout(jumperMissionCountdownTimer);
+ jumperMissionCountdownTimer = null;
+ }
+ jumpSurviveMissionPhase = 'live';
+ jumpSurviveGameEnded = false;
+ jumpSurviveEliminated = false;
+ jumpSurviveInitRuntime();
+ };
+ if (!cd || !numEl) {
+ runFinish();
+ return;
+ }
+ cd.classList.remove('is-hidden');
+ let n = 3;
+ numEl.textContent = String(n);
+ const step = function () {
+ n--;
+ if (n > 0) {
+ numEl.textContent = String(n);
+ jumperMissionCountdownTimer = setTimeout(step, 1000);
+ } else {
+ jumperMissionCountdownTimer = null;
+ runFinish();
+ }
+ };
+ jumperMissionCountdownTimer = setTimeout(step, 1000);
+ }
+
+ /** Jumper ภารกิจ mnptfts2: เกรดจากจำนวนผู้รอด — 6=A … 3=D; น้อยกว่า 2 = F; พอดี 2 คน = E */
+ function jumpSurviveGradeFromSurvivorCount(survivors) {
+ const s = Math.max(0, Math.floor(Number(survivors) || 0));
+ if (s >= 6) return 'A';
+ if (s === 5) return 'B';
+ if (s === 4) return 'C';
+ if (s === 3) return 'D';
+ if (s === 2) return 'E';
+ return 'F';
+ }
+
+ function jumpSurviveRollRewardCardForGrade(grade) {
+ if (grade === 'F') return null;
+ const r = Math.random();
+ if (grade === 'A' || grade === 'B') {
+ if (r < 0.35) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' };
+ if (r < 0.7) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' };
+ return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' };
+ }
+ if (grade === 'C' || grade === 'D') {
+ if (r < 0.25) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' };
+ if (r < 0.45) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' };
+ return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' };
+ }
+ if (grade === 'E') {
+ if (r < 0.12) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' };
+ return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' };
+ }
+ if (r < 0.25) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' };
+ if (r < 0.45) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' };
+ return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' };
+ }
+
+ function jumpSurviveMissionTotalParticipants() {
+ let n = myId != null ? 1 : 0;
+ if (others && typeof others.size === 'number') n += others.size;
+ return n;
+ }
+
+ function jumpSurviveMissionCountAlive() {
+ let n = 0;
+ if (myId != null && !jumpSurviveEliminated) n++;
+ if (others && typeof others.forEach === 'function') {
+ others.forEach(function (o) {
+ if (o && !o.jumpSurviveEliminated) n++;
+ });
+ }
+ return n;
+ }
+
+ /** จบทันทีเมื่อไม่มีผู้รอด หรือเหลือคนเดียว (หลายคนในรอบ) — โหมด solo 1 คนยังเล่นจนหมดเวลา */
+ function jumpSurviveMissionMaybeEarlyFinish() {
+ if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) return;
+ const total = jumpSurviveMissionTotalParticipants();
+ const alive = jumpSurviveMissionCountAlive();
+ if (alive === 0) {
+ endJumpSurviveMissionRound();
+ return;
+ }
+ if (total >= 2 && alive === 1) {
+ endJumpSurviveMissionRound();
+ }
+ }
+
+ function jumpSurviveBuildMissionPayload() {
+ const rows = [];
+ const pushEnt = function (id, nickname, characterId, eliminated) {
+ const alive = !eliminated;
+ const baseScore = alive ? 100 : 0;
+ rows.push({
+ id: id,
+ nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id),
+ characterId: characterId ?? null,
+ baseScore: baseScore,
+ eliminated: !!eliminated,
+ rankBonus: 0,
+ finalScore: baseScore,
+ });
+ };
+ if (myId != null) {
+ pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), jumpSurviveEliminated);
+ }
+ others.forEach(function (o, id) {
+ if (!o) return;
+ pushEnt(id, o.nickname, o.characterId, !!o.jumpSurviveEliminated);
+ });
+ rows.sort(function (a, b) {
+ if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
+ return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
+ });
+ let survivorCount = 0;
+ for (let si = 0; si < rows.length; si++) {
+ if (rows[si] && !rows[si].eliminated) survivorCount++;
+ }
+ const participantCount = rows.length;
+ const grade = jumpSurviveGradeFromSurvivorCount(survivorCount);
+ const top = rows.slice(0, 5);
+ const ranked = top.map(function (row, idx) {
+ const pos = idx + 1;
+ return {
+ id: row.id,
+ nickname: row.nickname,
+ characterId: row.characterId,
+ baseScore: row.baseScore,
+ eliminated: row.eliminated,
+ rank: pos,
+ rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
+ rankBonus: 0,
+ finalScore: row.baseScore,
+ };
+ });
+ const totalParts = ranked.map(function (r) { return r.baseScore; });
+ const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0);
+ const n = ranked.length || 1;
+ const averageScore = Math.floor(totalSum / n);
+ const rewardCard = jumpSurviveRollRewardCardForGrade(grade);
+ return {
+ ranked: ranked,
+ totalSum: totalSum,
+ averageScore: averageScore,
+ grade: grade,
+ rewardCard: rewardCard,
+ totalParts: totalParts,
+ uiSkin: 'jumper',
+ survivorCount: survivorCount,
+ participantCount: participantCount,
+ };
+ }
+
+ function jumpSurviveMergePreviewBotsMission(mission) {
+ if (!mission || mission.uiSkin !== 'jumper' || !previewFillBots || !others || typeof others.forEach !== 'function') return mission;
+ const seen = new Set();
+ (mission.ranked || []).forEach(function (r) {
+ if (r && r.id != null) seen.add(String(r.id));
+ });
+ const baseRows = (mission.ranked || []).map(function (r) {
+ return {
+ id: r.id,
+ nickname: r.nickname,
+ characterId: r.characterId,
+ baseScore: Math.max(0, Number(r.baseScore) || 0),
+ eliminated: !!r.eliminated,
+ rankBonus: 0,
+ };
+ });
+ others.forEach(function (o, id) {
+ if (!o || !isPreviewBotId(id)) return;
+ const sid = String(id);
+ if (seen.has(sid)) return;
+ seen.add(sid);
+ baseRows.push({
+ id: id,
+ nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid,
+ characterId: o.characterId ?? null,
+ baseScore: o.jumpSurviveEliminated ? 0 : 100,
+ eliminated: !!o.jumpSurviveEliminated,
+ rankBonus: 0,
+ });
+ });
+ baseRows.sort(function (a, b) {
+ if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore;
+ return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id));
+ });
+ let survivorCount = 0;
+ for (let si = 0; si < baseRows.length; si++) {
+ if (baseRows[si] && !baseRows[si].eliminated) survivorCount++;
+ }
+ const participantCount = baseRows.length;
+ const grade = jumpSurviveGradeFromSurvivorCount(survivorCount);
+ const top = baseRows.slice(0, 5);
+ const ranked = top.map(function (row, idx) {
+ const pos = idx + 1;
+ const bs = Math.max(0, Number(row.baseScore) || 0);
+ return {
+ id: row.id,
+ nickname: row.nickname,
+ characterId: row.characterId,
+ baseScore: bs,
+ eliminated: !!row.eliminated,
+ rank: pos,
+ rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
+ rankBonus: 0,
+ finalScore: bs,
+ };
+ });
+ const totalParts = ranked.map(function (r) { return r.baseScore; });
+ const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0);
+ const n = ranked.length || 1;
+ const averageScore = Math.floor(totalSum / n);
+ return {
+ ranked: ranked,
+ totalSum: totalSum,
+ averageScore: averageScore,
+ grade: grade,
+ rewardCard: jumpSurviveRollRewardCardForGrade(grade),
+ totalParts: totalParts,
+ uiSkin: 'jumper',
+ survivorCount: survivorCount,
+ participantCount: participantCount,
+ };
+ }
+
+ function endJumpSurviveMissionRound() {
+ if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveGameEnded) return;
+ jumpSurviveGameEnded = true;
+ jumpSurviveMissionPhase = 'ended';
+ applyJumpSurviveJumperPanelImages();
+ const mission = jumpSurviveBuildMissionPayload();
+ showGauntletCrownMissionOverlay(mission);
+ }
function isSpaceShooter() { return mapData && mapData.gameType === 'space_shooter'; }
function isBalloonBoss() { return mapData && mapData.gameType === 'balloon_boss'; }
function isQuizCarry() { return mapData && mapData.gameType === 'quiz_carry'; }
@@ -5375,9 +5733,13 @@
updateGauntletCrownHowtoHud();
}
- /** Editor embed: เกรด F = ภาพจบแบบ gameover — เหมือน logic สรุป quiz_carry mission */
+ /** Editor embed: quiz_carry / crown ใช้เกรด F = gameover — Jumper: ภาพจบใช้เฉพาะเมื่อไม่มีผู้รอด (0 คน) */
function gauntletCrownEmbedMissionAnyOk(mission) {
if (!mission) return false;
+ if (mission.uiSkin === 'jumper') {
+ const sc = Number(mission.survivorCount);
+ return Number.isFinite(sc) ? sc > 0 : (String(mission.grade || '').trim().toUpperCase().charAt(0) !== 'F');
+ }
const g = String(mission.grade || '').trim().toUpperCase().charAt(0);
return g !== 'F';
}
@@ -5411,6 +5773,7 @@
*/
function gauntletCrownBuildDisplayMission(mission) {
if (!mission || !Array.isArray(mission.ranked)) return mission;
+ if (mission.uiSkin === 'jumper') return mission;
if (!(previewFillBots && isGauntletCrownHeistMapPlay() && others && typeof others.forEach === 'function')) return mission;
const seen = new Set();
@@ -5490,7 +5853,10 @@
const bonusEl = document.getElementById('gcm-bonus');
const btn = document.getElementById('btn-gcm-done');
if (!ov || !rowEl || !mission || !Array.isArray(mission.ranked)) return;
- const disp = gauntletCrownBuildDisplayMission(mission);
+ let disp = gauntletCrownBuildDisplayMission(mission);
+ if (mission.uiSkin === 'jumper') {
+ disp = jumpSurviveMergePreviewBotsMission(disp) || disp;
+ }
if (!disp || !Array.isArray(disp.ranked)) return;
rowEl.innerHTML = '';
disp.ranked.forEach((r) => {
@@ -5529,8 +5895,14 @@
if (r.eliminated) {
const st = document.createElement('img');
st.className = 'gcm-stamp';
- st.src = BASE + '/img/gauntlet-assets/result-Lose_stamp.png';
- st.alt = 'เสียชีวิต';
+ const jSkin = mission && mission.uiSkin === 'jumper';
+ st.src = jSkin ? jumperAssetUrl('stamp-sacrifice.png') : (BASE + '/img/gauntlet-assets/result-Lose_stamp.png');
+ st.alt = jSkin ? 'ผู้เสียสละ' : 'เสียชีวิต';
+ st.onerror = function () {
+ this.onerror = null;
+ this.src = BASE + '/img/gauntlet-assets/result-Lose_stamp.png';
+ this.alt = 'เสียชีวิต';
+ };
frame.appendChild(st);
}
const nick = document.createElement('div');
@@ -5538,8 +5910,9 @@
nick.textContent = r.nickname || '—';
const sc = document.createElement('div');
sc.className = 'gcm-sc';
+ const jumpPlain = mission && mission.uiSkin === 'jumper';
const botScorePlain = previewFillBots && isGauntletCrownHeistMapPlay() && isPreviewBotId(r.id);
- sc.textContent = botScorePlain
+ sc.textContent = (jumpPlain || botScorePlain)
? String(Math.max(0, Number(r.baseScore) || 0))
: String(r.baseScore || 0) + '(+' + String(r.rankBonus || 0) + ')';
cell.appendChild(tag);
@@ -5552,7 +5925,14 @@
const parts = (disp.totalParts || []).map((n) => String(Math.max(0, Number(n) || 0)));
const sum = Math.max(0, Number(disp.totalSum) || 0);
const avg = Math.max(0, Math.floor(Number(disp.averageScore) || 0));
- totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · เฉลี่ย ' + avg + ' → เกรด';
+ const jumperSkin = mission && mission.uiSkin === 'jumper';
+ if (jumperSkin) {
+ const sc = Math.max(0, Math.floor(Number(disp.survivorCount) || 0));
+ const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0));
+ totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้รอด ' + sc + '/' + pc + ' → เกรด';
+ } else {
+ totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · เฉลี่ย ' + avg + ' → เกรด';
+ }
}
if (gradeEl) {
const g = String(disp.grade || 'C').toUpperCase().charAt(0);
@@ -5561,28 +5941,39 @@
}
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 gLetter = String(disp.grade || 'C').toUpperCase().charAt(0);
+ const missionOk = gLetter !== 'F';
+ if (missionOk) {
+ 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 = disp.rewardCard;
- if (rc && rc.th) {
+ if (missionOk && rc && rc.th) {
const pl = document.createElement('div');
pl.className = 'gcm-plaque';
pl.innerHTML = '' + String(rc.th) + '' + String(rc.en || '') + '';
bonusEl.appendChild(pl);
+ } else if (!missionOk) {
+ const pl = document.createElement('div');
+ pl.className = 'gcm-plaque';
+ pl.style.borderColor = 'rgba(148, 153, 168, 0.45)';
+ pl.style.opacity = '0.95';
+ pl.innerHTML = 'ไม่ได้รับการ์ดเกรด ' + gLetter + ' · ภารกิจไม่ผ่านเกณฑ์';
+ bonusEl.appendChild(pl);
}
}
ov.classList.remove('is-hidden');
@@ -7342,8 +7733,14 @@
jumpSurvivePlatformScrollPx = 0;
const { cw, ch } = getCharacterFootprintWH(mapData);
const ts = tileSize;
- jumpSurviveCamCenterX = (me.x + cw * 0.5) * ts;
- jumpSurviveCamCenterY = (me.y + ch * 0.5) * ts;
+ if (isJumpSurviveMissionUiMapPlay()) {
+ const w = mapData.width || 20, h = mapData.height || 15;
+ jumpSurviveCamCenterX = (w * ts) * 0.5;
+ jumpSurviveCamCenterY = (h * ts) * 0.5;
+ } else {
+ jumpSurviveCamCenterX = (me.x + cw * 0.5) * ts;
+ jumpSurviveCamCenterY = (me.y + ch * 0.5) * ts;
+ }
}
function jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom) {
@@ -7420,8 +7817,12 @@
}
function jumpSurviveCameraTargetPx(md) {
- const { cw, ch } = getCharacterFootprintWH(md);
const ts = tileSize;
+ const w = md.width || 20, h = md.height || 15;
+ if (isJumpSurviveMissionUiMapPlay()) {
+ return { px: (w * ts) * 0.5, py: (h * ts) * 0.5 };
+ }
+ const { cw, ch } = getCharacterFootprintWH(md);
const cx = (e) => (e.x + cw * 0.5) * ts;
const cy = (e) => (e.y + ch * 0.5) * ts;
if (!jumpSurviveEliminated) return { px: cx(me), py: cy(me) };
@@ -7527,6 +7928,14 @@
function jumpSurviveTickFrame() {
if (!mapData || !isJumpSurvive()) return;
+ if (isJumpSurviveMissionUiMapPlay()) {
+ if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) return;
+ const remEnd = jumpSurviveRemainingSecMission();
+ if (remEnd !== null && remEnd <= 0) {
+ endJumpSurviveMissionRound();
+ return;
+ }
+ }
const md = mapData;
const ts = tileSize;
const now = performance.now();
@@ -7544,6 +7953,7 @@
if (jumpSurviveEliminated) {
stepJumpSurvivePreviewBots(dt, halfViewH);
+ jumpSurviveMissionMaybeEarlyFinish();
return;
}
@@ -7592,6 +8002,8 @@
socket.emit('move', { x: me.x, y: me.y, direction: me.direction });
}
}
+
+ jumpSurviveMissionMaybeEarlyFinish();
}
function gridImageLibIdleUrlPlay(entry) {
@@ -7907,6 +8319,7 @@
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,
+ jumpSurviveEliminated: false,
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: (() => {
@@ -7969,7 +8382,24 @@
if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = [];
normalizeJumpSurvivePlatformAreaInPlay(mapData);
jumpSurviveEliminated = false;
- jumpSurviveInitRuntime();
+ jumpSurviveGameEnded = false;
+ if (jumperMissionCountdownTimer) {
+ clearTimeout(jumperMissionCountdownTimer);
+ jumperMissionCountdownTimer = null;
+ }
+ if (isJumpSurviveMissionUiMapPlay()) {
+ jumpSurviveMissionPhase = 'howto';
+ hideConflictingOverlaysForJumpSurviveMission();
+ applyJumpSurviveJumperPanelImages();
+ fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function (t) { if (t) applyGauntletTimingFromServer(t); })
+ .catch(function () {});
+ setTimeout(function () { showJumpSurviveMissionHowtoOverlay(); }, 50);
+ } else {
+ jumpSurviveMissionPhase = null;
+ jumpSurviveInitRuntime();
+ }
}
if (mapData.gameType === 'space_shooter') {
spaceShooterGameEnded = false;
@@ -8011,6 +8441,15 @@
const gccR = document.getElementById('gauntlet-crown-countdown');
if (gccR) gccR.classList.add('is-hidden');
setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50);
+ } else if (mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay()) {
+ if (gauntletCrownCountdownTimer) {
+ clearTimeout(gauntletCrownCountdownTimer);
+ gauntletCrownCountdownTimer = null;
+ }
+ const gccRj = document.getElementById('gauntlet-crown-countdown');
+ if (gccRj) gccRj.classList.add('is-hidden');
+ gauntletCrownPregamePhase = null;
+ gauntletCrownLobbyReadyMap = {};
} else {
const hov = document.getElementById('gauntlet-crown-howto-overlay');
if (hov) {
@@ -8105,6 +8544,7 @@
gauntletJumpTicks: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0,
gauntletJumpVis: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0,
gauntletScore: (() => { const s = Number(data.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
+ jumpSurviveEliminated: false,
spaceShooterScore: (() => { const s = Number(data.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
balloonBossScore: (() => { const s = Number(data.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(),
balloonBossBalloons: (() => {
@@ -8527,9 +8967,12 @@
}
} else {
gauntletEndsAtMs = null;
- const hto3 = document.getElementById('gauntlet-crown-howto-overlay');
- if (hto3) hto3.classList.add('is-hidden');
- gauntletCrownHowtoVisible = false;
+ const skipHideCrownShell = mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay();
+ if (!skipHideCrownShell) {
+ const hto3 = document.getElementById('gauntlet-crown-howto-overlay');
+ if (hto3) hto3.classList.add('is-hidden');
+ gauntletCrownHowtoVisible = false;
+ }
gauntletCrownPregamePhase = null;
gauntletCrownLobbyReadyMap = {};
if (gauntletCrownCountdownTimer) {
@@ -8571,7 +9014,24 @@
if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = [];
normalizeJumpSurvivePlatformAreaInPlay(mapData);
jumpSurviveEliminated = false;
- jumpSurviveInitRuntime();
+ jumpSurviveGameEnded = false;
+ if (jumperMissionCountdownTimer) {
+ clearTimeout(jumperMissionCountdownTimer);
+ jumperMissionCountdownTimer = null;
+ }
+ if (isJumpSurviveMissionUiMapPlay()) {
+ jumpSurviveMissionPhase = 'howto';
+ hideConflictingOverlaysForJumpSurviveMission();
+ applyJumpSurviveJumperPanelImages();
+ fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((t) => { if (t) applyGauntletTimingFromServer(t); })
+ .catch(() => {});
+ setTimeout(function () { showJumpSurviveMissionHowtoOverlay(); }, 60);
+ } else {
+ jumpSurviveMissionPhase = null;
+ jumpSurviveInitRuntime();
+ }
}
if (mapData.gameType === 'space_shooter') {
spaceShooterGameEnded = false;
@@ -9630,7 +10090,8 @@
? 'QUIZ CARRY · COURT — หยิบป้ายถูกแล้วส่งที่ฮับ (F / Grab)'
: (isGauntlet() ? 'GAUNTLET · SURVIVAL RUN'
: (isSpaceShooter() ? 'SPACE SHOOTER · ARCADE'
- : (isBalloonBoss() ? 'BALLOON BOSS · MEGA VIRUS' : 'JUMP SURVIVE · NODE UPLINK')));
+ : (isBalloonBoss() ? 'BALLOON BOSS · MEGA VIRUS'
+ : (isJumpSurviveMissionUiMapPlay() ? 'JUMPER · SURVIVE — เหลือเวลา (วินาที) · TIME (sec)' : 'JUMP SURVIVE · NODE UPLINK'))));
}
if (timeVal) {
if (isQuizCarry()) {
@@ -9677,6 +10138,13 @@
const rem = balloonBossRemainingSecPlay();
timeVal.textContent = rem != null ? String(rem) : '∞';
}
+ } else if (isJumpSurviveMissionUiMapPlay()) {
+ if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) {
+ timeVal.textContent = jumpSurviveMissionPhase === 'howto' ? '···' : (jumpSurviveMissionPhase === 'countdown' ? '···' : '0');
+ } else {
+ const rem = jumpSurviveRemainingSecMission();
+ timeVal.textContent = rem != null ? String(rem) : '∞';
+ }
} else {
const t = jumpSurviveSessionStartMs > 0
? Math.max(0, Math.floor((performance.now() - jumpSurviveSessionStartMs) / 1000))
@@ -9724,9 +10192,15 @@
? 'ชุดคำถามจบแล้ว · Session ended — ดูคะแนนด้าน SCORE'
: ('ยืนโซนตัวเลือกแล้วกด F/Grab หยิบ · ถือป้ายถูกแล้วไปฮับส่ง — ถูก +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' แต้ม (correct +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' on hub)');
} else if (isJumpSurvive()) {
- hintEl.textContent = jumpSurviveEliminated
- ? 'Spectate remaining nodes · Exit room to reset session'
- : 'Space / W / ↑ jump · A D / arrows move · Hit ceiling/floor = out (watch others)';
+ if (isJumpSurviveMissionUiMapPlay()) {
+ hintEl.textContent = jumpSurviveEliminated
+ ? 'คุณตกน้ำแล้ว — ดูเพื่อนต่อ — รอจบเพื่อรับคะแนน (ผู้รอดได้ 100 คะแนน)'
+ : 'กระโดดตามแท่น — หลีกหนาม/กับดักด้านล่าง · หมดเวลาแล้วสรุปคะแนน (ผู้รอด 100) · Space / W / ↑ กระโดด · A D / ← → เดิน';
+ } else {
+ hintEl.textContent = jumpSurviveEliminated
+ ? 'Spectate remaining nodes · Exit room to reset session'
+ : 'Space / W / ↑ jump · A D / arrows move · Hit ceiling/floor = out (watch others)';
+ }
} else if (isSpaceShooter()) {
hintEl.textContent = spaceShooterGameEnded
? 'จบแล้ว · Session ended — ดูอันดับบน overlay / see rankings on overlay'
@@ -9758,6 +10232,7 @@
const ul = document.getElementById('play-cyber-score-list');
if (!ul) return;
const safeYv = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1);
+ const jumpMissionHud = isJumpSurvive() && isJumpSurviveMissionUiMapPlay();
const rows = [];
rows.push({
id: myId,
@@ -9766,7 +10241,8 @@
score: isQuizCarry() ? cyberHudQuizCarryPeerScore(myId)
: (isGauntlet() ? (me.gauntletScore || 0)
: (isSpaceShooter() ? (me.spaceShooterScore || 0)
- : (isBalloonBoss() ? (me.balloonBossScore || 0) : 0))),
+ : (isBalloonBoss() ? (me.balloonBossScore || 0)
+ : (jumpMissionHud ? (jumpSurviveEliminated ? 0 : 100) : 0)))),
y: safeYv(me.y),
eliminated: !!(isJumpSurvive() && jumpSurviveEliminated) || !!(isBalloonBoss() && me.balloonBossEliminated)
|| !!(isGauntletCrownHeistMapPlay() && me.gauntletEliminated),
@@ -9780,7 +10256,8 @@
score: isQuizCarry() ? cyberHudQuizCarryPeerScore(id)
: (isGauntlet() ? (o.gauntletScore || 0)
: (isSpaceShooter() ? (o.spaceShooterScore || 0)
- : (isBalloonBoss() ? (o.balloonBossScore || 0) : 0))),
+ : (isBalloonBoss() ? (o.balloonBossScore || 0)
+ : (jumpMissionHud ? (o.jumpSurviveEliminated ? 0 : 100) : 0)))),
y: safeYv(o.y),
eliminated: !!(isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated)
|| !!(isBalloonBoss() && o.balloonBossEliminated)
@@ -9789,7 +10266,7 @@
});
});
let leaderId = null;
- if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry()) {
+ if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud) {
let mx = -1;
for (let i = 0; i < rows.length; i++) {
if (rows[i].score > mx) {
@@ -9807,7 +10284,11 @@
}
}
rows.sort((a, b) => {
- if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry()) return b.score - a.score;
+ if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud) {
+ if (b.score !== a.score) return b.score - a.score;
+ if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1;
+ return String(a.nickname || '').localeCompare(String(b.nickname || ''), 'th');
+ }
if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1;
return a.y - b.y;
});
@@ -9856,7 +10337,7 @@
mid.appendChild(st);
const sc = document.createElement('span');
sc.className = 'play-cyber-score-val';
- if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry()) {
+ if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud) {
sc.textContent = String(row.score);
} else {
sc.textContent = row.eliminated ? '—' : String(r + 1);
@@ -10652,6 +11133,10 @@
}
}
if (isJumpSurvive() && mapData && !isChatFocused()) {
+ if (isJumpSurviveMissionPregameBlockingPlay()) {
+ if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) e.preventDefault();
+ return;
+ }
if (jumpSurviveEliminated) {
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) e.preventDefault();
return;
@@ -10720,6 +11205,12 @@
return;
}
if (isJumpSurvive()) {
+ if (isJumpSurviveMissionPregameBlockingPlay()) {
+ me.isWalking = false;
+ draw();
+ requestAnimationFrame(tick);
+ return;
+ }
jumpSurviveTickFrame();
draw();
requestAnimationFrame(tick);
@@ -10935,6 +11426,13 @@
const btn = document.getElementById('btn-gch-ready');
if (!btn) return;
btn.addEventListener('click', () => {
+ if (isJumpSurviveMissionUiMapPlay()) {
+ if (jumpSurviveMissionPhase !== 'howto') return;
+ const humans = quizCarryPregameHumanIds();
+ if (!(humans.length === 1 || isMePlayHost())) return;
+ beginJumpSurviveMissionCountdownThenRun();
+ return;
+ }
if (!isGauntletCrownHeistMapPlay()) return;
if (gauntletCrownPregamePhase !== 'howto') return;
if (myId == null || !isMePlayHost()) return;
diff --git a/www/html/Game/public/js/version.js b/www/html/Game/public/js/version.js
index 2917ad9..dbe50aa 100644
--- a/www/html/Game/public/js/version.js
+++ b/www/html/Game/public/js/version.js
@@ -1,6 +1,6 @@
// ทุกครั้งที่มีการเพิ่มหรือเปลี่ยน ให้เพิ่ม v +0.0001
// หลังแก้ค่าแล้วต้อง copy ไป path ที่ Nginx ชี้ (หรือรัน copy-frogger-files-only.sh) ถึงจะเห็นบนเว็บ
-window.APP_VERSION = '0.0175';
+window.APP_VERSION = '0.0180';
document.addEventListener('DOMContentLoaded', function () {
var t = document.querySelector('.version-tag');
if (t) t.textContent = 'v ' + window.APP_VERSION;
diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html
index 2fb1d91..d583ab9 100644
--- a/www/html/Game/public/play.html
+++ b/www/html/Game/public/play.html
@@ -572,15 +572,21 @@
overflow: auto;
}
.play-quiz-scoreboard-list li {
- display: flex;
- justify-content: space-between;
- gap: 0.4rem;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(2.25rem, auto);
+ align-items: baseline;
+ column-gap: 0.5rem;
font-size: 0.74rem;
color: #c0caf5;
padding: 0.15rem 0;
}
.play-quiz-scoreboard-me { color: #9ece6a !important; font-weight: 700; }
- .play-quiz-scoreboard-val { font-variant-numeric: tabular-nums; font-weight: 800; }
+ .play-quiz-scoreboard-val {
+ font-variant-numeric: tabular-nums;
+ font-weight: 800;
+ text-align: right;
+ justify-self: end;
+ }
.play-quiz-scoreboard-me .play-quiz-scoreboard-val { color: #9ece6a; }
#play-quiz-feedback {
position: fixed;
@@ -686,7 +692,7 @@
width: clamp(68px, 16vw, 104px);
display: flex;
flex-direction: column;
- align-items: center;
+ align-items: stretch;
gap: 4px;
text-align: center;
color: #e2e8f0;
@@ -731,20 +737,26 @@
pointer-events: none;
}
.qc-mission-rank-nick {
+ flex: 1 1 auto;
+ min-height: 2.5em;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
font-size: clamp(0.52rem, 1.45vw, 0.68rem);
font-weight: 700;
color: #e2e8f0;
line-height: 1.15;
width: 100%;
max-width: 100%;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
overflow: hidden;
word-break: break-word;
hyphens: auto;
+ text-align: center;
}
.qc-mission-rank-score {
+ flex: 0 0 auto;
+ margin-top: auto;
+ font-variant-numeric: tabular-nums;
font-size: clamp(0.62rem, 1.85vw, 0.78rem);
font-weight: 800;
color: #f6e08a;
@@ -799,6 +811,8 @@
.qc-mission-grade--a { color: #9ece6a; text-shadow: 0 0 24px rgba(158, 206, 106, 0.55); }
.qc-mission-grade--b { color: #7aa2f7; text-shadow: 0 0 24px rgba(122, 162, 247, 0.5); }
.qc-mission-grade--c { color: #e0af68; text-shadow: 0 0 22px rgba(224, 175, 104, 0.45); }
+ .qc-mission-grade--d { color: #bb9af7; text-shadow: 0 0 20px rgba(187, 154, 247, 0.4); }
+ .qc-mission-grade--e { color: #7dcfff; text-shadow: 0 0 20px rgba(125, 207, 255, 0.35); }
.qc-mission-grade--f { color: #f7768e; text-shadow: 0 0 22px rgba(247, 118, 140, 0.45); }
.qc-mission-bonus-row {
display: flex;
@@ -1134,6 +1148,7 @@
display: flex;
flex-wrap: wrap;
justify-content: center;
+ align-items: stretch;
gap: clamp(6px, 1.5vw, 10px);
margin-bottom: 8px;
}
@@ -1141,8 +1156,16 @@
flex: 0 0 auto;
width: clamp(72px, 18vw, 100px);
text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
}
.gcm-rank-tag {
+ flex: 0 0 auto;
+ min-height: 1.15em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
font-size: clamp(0.55rem, 1.4vw, 0.65rem);
font-weight: 800;
color: #f6e08a;
@@ -1150,6 +1173,7 @@
}
.gcm-frame {
position: relative;
+ flex: 0 0 auto;
width: 100%;
aspect-ratio: 1;
border-radius: 8px;
@@ -1173,17 +1197,28 @@
pointer-events: none;
}
.gcm-nick {
+ flex: 1 1 auto;
+ min-height: 2.55em;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
font-size: clamp(0.52rem, 1.35vw, 0.65rem);
font-weight: 700;
color: #e2e8f0;
margin-top: 4px;
word-break: break-word;
+ line-height: 1.15;
}
.gcm-sc {
+ flex: 0 0 auto;
+ width: 100%;
+ text-align: center;
+ font-variant-numeric: tabular-nums;
font-size: clamp(0.58rem, 1.5vw, 0.72rem);
font-weight: 800;
color: #f6e08a;
- margin-top: 2px;
+ margin-top: auto;
+ padding-top: 3px;
}
.gcm-total {
text-align: center;
@@ -1219,6 +1254,9 @@
.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-grade--d { color: #bb9af7; text-shadow: 0 0 16px rgba(187, 154, 247, 0.4); }
+ .gcm-grade--e { color: #7dcfff; text-shadow: 0 0 16px rgba(125, 207, 255, 0.35); }
+ .gcm-grade--f { color: #f7768e; text-shadow: 0 0 18px rgba(247, 118, 140, 0.45); }
.gcm-bonus {
display: flex;
flex-direction: column;
@@ -1373,7 +1411,7 @@
}
.play-cyber-score-row {
display: grid;
- grid-template-columns: 36px 1fr auto;
+ grid-template-columns: 36px minmax(0, 1fr) minmax(2.75rem, auto);
align-items: center;
gap: 0.35rem;
padding: 0.28rem 0;
@@ -1418,6 +1456,9 @@
color: #7af8ff;
text-shadow: 0 0 10px rgba(0, 255, 240, 0.35);
font-variant-numeric: tabular-nums;
+ text-align: right;
+ justify-self: end;
+ min-width: 2.5rem;
}
.play-cyber-lead-crown {
display: inline-block;
@@ -1858,8 +1899,8 @@
-
-
+
+
v —