minigame 4 80% done
@@ -19,4 +19,8 @@ PLAQUE="$DST/Game/public/img/quiz-carry-plaque-assets"
|
||||
mkdir -p "$PLAQUE"
|
||||
chown www-data:www-data "$PLAQUE"
|
||||
chmod 2775 "$PLAQUE"
|
||||
GA="$DST/Game/public/img/gauntlet-assets"
|
||||
mkdir -p "$GA"
|
||||
chown www-data:www-data "$GA"
|
||||
chmod 2775 "$GA"
|
||||
echo "OK: deployed $SRC -> $DST (node_modules on server untouched)"
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<button type="button" class="tab" data-tab="quiz-carry" role="tab" id="tab-quiz-carry" aria-controls="tab-panel-quiz-carry"><span class="tab-label">Minigame-4-คำศัพท์ในกระบวนการยุติธรรม</span><span class="tab-desc" aria-hidden="true"></span></button>
|
||||
<button type="button" class="tab" data-tab="quiz-battle" role="tab" id="tab-quiz-battle" aria-controls="tab-panel-quiz-battle"><span class="tab-label">Quiz Battle</span><span class="tab-desc">โดม · A B C · หมวด</span></button>
|
||||
<button type="button" class="tab" data-tab="jump-survive" role="tab" id="tab-jump-survive" aria-controls="tab-panel-jump-survive"><span class="tab-label">กระโดดให้รอด</span><span class="tab-desc">ความสูงกระโดด · timing</span></button>
|
||||
<button type="button" class="tab" data-tab="game-timing" role="tab" id="tab-game-timing" aria-controls="tab-panel-game-timing"><span class="tab-label">เวลาเกม</span><span class="tab-desc">พรมแดง · tick & laser</span></button>
|
||||
<button type="button" class="tab" data-tab="game-timing" role="tab" id="tab-game-timing" aria-controls="tab-panel-game-timing"><span class="tab-label">Minigame-2-วิ่งหลบสิ่งกีดขวาง</span><span class="tab-desc" aria-hidden="true"></span></button>
|
||||
<button type="button" class="tab" data-tab="stack-game" role="tab" id="tab-stack-game" aria-controls="tab-panel-stack-game"><span class="tab-label">Stack</span><span class="tab-desc">สวิง · ความยาวแท่ง</span></button>
|
||||
<button type="button" class="tab" data-tab="characters" role="tab" id="tab-characters" aria-controls="tab-panel-characters"><span class="tab-label">ตัวละคร</span><span class="tab-desc">อัปโหลด · เลือกใช้ในเกม</span></button>
|
||||
<button type="button" class="tab" data-tab="accounts" role="tab" id="tab-accounts" aria-controls="tab-panel-accounts"><span class="tab-label">ผู้ใช้</span><span class="tab-desc">บัญชี & COINS</span></button>
|
||||
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -13,6 +13,10 @@
|
||||
const previewMode = params.get('preview') === '1';
|
||||
const forceDefaultCharacter = params.get('defaultChar') === '1';
|
||||
const editorEmbedReturn = params.get('editorEmbed') === '1';
|
||||
const playMapIdFromQuery = (params.get('map') || '').trim();
|
||||
/** สรุปภารกิจแบบ mock (popup-result ฯลฯ) — ใช้เฉพาะฉากนี้จาก editor (id ใน URL เช่น editor.html?id=mnorwqx1) */
|
||||
const quizCarryMissionSummaryMapId = 'mnorwqx1';
|
||||
const quizCarryUseMissionSummaryOverlay = playMapIdFromQuery === quizCarryMissionSummaryMapId;
|
||||
/** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */
|
||||
const LEGACY_PLACEHOLDER_CHARACTER_ID = 'Chatest';
|
||||
let firstCharacterDefaultResolved = null;
|
||||
@@ -159,8 +163,39 @@
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** ตัวละครอัปโหลดจากระบบมักเป็น char- + ตัวเลข (อาจมี suffix) — ไม่มี idle strip / idle layer ครบทุกทิศ */
|
||||
function isUploadedCharAssetId(id) {
|
||||
if (!id) return false;
|
||||
const s = String(id).trim();
|
||||
return /^char-\d/i.test(s);
|
||||
}
|
||||
|
||||
function useMultiFrameIdleSpriteSheets(id) {
|
||||
if (!id) return true;
|
||||
return !isUploadedCharAssetId(id);
|
||||
}
|
||||
|
||||
function ensureCharacterIdleAnim(id, dir) {
|
||||
const d = dir || 'down';
|
||||
/* char-* ไม่มี *_idle_0.png — ใช้ strip เดิน dir_0..3 เป็น idle ต่อทิศ (มีลูป) แทนรูปเดียวที่มักไม่มี */
|
||||
if (!useMultiFrameIdleSpriteSheets(id)) {
|
||||
const key = String(id) + '_' + d + '_idle';
|
||||
let anim = characterIdleAnimations[key];
|
||||
if (!anim) {
|
||||
anim = { frames: [], fallback: null };
|
||||
characterIdleAnimations[key] = anim;
|
||||
const enc = encodeURIComponent(id);
|
||||
for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) {
|
||||
const img = new Image();
|
||||
img.src = BASE + '/img/characters/' + enc + '_' + d + '_' + i + '.png';
|
||||
anim.frames.push(img);
|
||||
}
|
||||
const fb = new Image();
|
||||
fb.src = BASE + '/img/characters/' + enc + '_' + d + '_idle.png';
|
||||
anim.fallback = fb;
|
||||
}
|
||||
return anim;
|
||||
}
|
||||
const key = id + '_' + d + '_idle';
|
||||
let anim = characterIdleAnimations[key];
|
||||
if (!anim) {
|
||||
@@ -470,13 +505,27 @@
|
||||
let anyTintColorLayer = false;
|
||||
let skippedShadowWhilePending = false;
|
||||
const diskLayers = playCharDiskLayersSet(id);
|
||||
/* ยืน (opts.idle): ลอง compose จาก *_idle_*_layer_* เหมือนหน้า character upload — รวม char-* ที่อัปโหลด idle เลเยอร์; ถ้าไม่มีไฟล์จะ missing → ไม่มีเลเยอร์ย้อม → getPlayTintedAvatarSource fallback ไป walk compose */
|
||||
const idleLayerStripes = !!useIdle;
|
||||
const resDir = dir;
|
||||
const resFi = frameIndex;
|
||||
for (let li = 0; li < PLAY_LAYER_ORDER.length; li++) {
|
||||
const layerName = PLAY_LAYER_ORDER[li];
|
||||
if (diskLayers && layerName !== 'shadow' && !diskLayers.has(layerName)) continue;
|
||||
/* ห้าม fallback face จากทิศ down เมื่อเป็น up/left/right — จะได้ตาอยู่หลังหัวเพราะวางหน้าหน้าบนร่างหันหลัง */
|
||||
const urls = layerName === 'shadow'
|
||||
? (useIdle ? shadowUrlCandidatesIdle(id, dir, frameIndex) : shadowUrlCandidates(id, dir, frameIndex))
|
||||
: (useIdle ? layerUrlCandidatesIdle(id, dir, layerName, frameIndex) : layerUrlCandidates(id, dir, layerName, frameIndex));
|
||||
/* char-* หันหลัง (up): ไม่มี face ทิศนี้ — ข้ามกันตาหน้าซ้อนหลังหัว; ซ้าย/ขวา/หน้าใช้ face ตามทิศได้ */
|
||||
if (layerName === 'face' && isUploadedCharAssetId(id) && dir === 'up') {
|
||||
continue;
|
||||
}
|
||||
let urls;
|
||||
if (layerName === 'shadow') {
|
||||
urls = idleLayerStripes
|
||||
? shadowUrlCandidatesIdle(id, dir, frameIndex)
|
||||
: shadowUrlCandidates(id, resDir, resFi);
|
||||
} else {
|
||||
urls = idleLayerStripes
|
||||
? layerUrlCandidatesIdle(id, dir, layerName, frameIndex)
|
||||
: layerUrlCandidates(id, resDir, layerName, resFi);
|
||||
}
|
||||
const r = resolvePlayLayerImage(urls);
|
||||
/* เงาโหลดช้า/404 บ่อย — อย่าให้บล็อกทั้งคอมโพส (ไม่งั้นตกไป fallback แถบแนวตั้งแทนเลเยอร์จริง) */
|
||||
if (r.status === 'pending' && layerName === 'shadow') {
|
||||
@@ -504,24 +553,24 @@
|
||||
|
||||
if (playCharLayerDiscoveryPending(characterId)) return rawImg;
|
||||
|
||||
/* มี PNG idle รวม — อย่าทับด้วยคอมโพสเลเยอร์จากเฟรมเดิน (idle เป็นภาพรวมแล้ว) */
|
||||
if (!isWalking && characterIdleSpritesVisible(characterId, dirn)) {
|
||||
return rawImg;
|
||||
}
|
||||
|
||||
const apiFlag = playCharLayerFromApi(characterId);
|
||||
/* เหมือนเดิม: มีเลเยอร์แยกไฟล์เมื่อ API บอก hasLayerFiles หรือ probe เจอ — gen สีทำบน canvas จาก *_layer_* เท่านั้น */
|
||||
const charHasLayerFiles = apiFlag === true
|
||||
|| (apiFlag == null && playCharAnyDirectionLayered(characterId));
|
||||
/* ct3: ไม่ใช้ face จาก down บนทิศอื่น (แก้ตาหลังหัว) */
|
||||
|
||||
/* มี PNG idle รวม — ใช้เมื่อไม่มีระบบเลเยอร์สี (ถ้ามีเลเยอร์ต้อง compose ไม่ return raw ขาว) */
|
||||
if (!charHasLayerFiles && !isWalking && characterIdleSpritesVisible(characterId, dirn)) {
|
||||
return rawImg;
|
||||
}
|
||||
const cacheKeyWalk = [characterId, dirn, frameIndexWalk, tint.head, tint.hair, tint.body, 'ct3'].join('|');
|
||||
|
||||
if (charHasLayerFiles) {
|
||||
if (!isWalking) {
|
||||
const cacheKeyIdle = [characterId, dirn, idlePhase, tint.head, tint.hair, tint.body, 'ct3', 'idleL'].join('|');
|
||||
const composeFrame = isUploadedCharAssetId(characterId) ? 0 : idlePhase;
|
||||
const cacheKeyIdle = [characterId, dirn, composeFrame, tint.head, tint.hair, tint.body, 'ct3', 'idleL'].join('|');
|
||||
const hitI = playLayerCompositeCache.get(cacheKeyIdle);
|
||||
if (hitI) return hitI;
|
||||
const packI = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, idlePhase, tint, { idle: true });
|
||||
const packI = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, composeFrame, tint, { idle: true });
|
||||
if (packI && packI.canvas) {
|
||||
if (!packI.skipCache) playLayerCompositeCache.set(cacheKeyIdle, packI.canvas);
|
||||
return packI.canvas;
|
||||
@@ -559,7 +608,8 @@
|
||||
const t = typeof now === 'number' ? now : Date.now();
|
||||
if (!isWalking) {
|
||||
const idleAnim = ensureCharacterIdleAnim(id, dir);
|
||||
const idlePhase = idleAnimPhaseIndex(t);
|
||||
let idlePhase = idleAnimPhaseIndex(t);
|
||||
if (isUploadedCharAssetId(id)) idlePhase = 0;
|
||||
const idleFi = pickLoadedWalkFrameIndex(idleAnim, idlePhase);
|
||||
if (idleFi >= 0) return idleAnim.frames[idleFi];
|
||||
const idfb = idleAnim.fallback;
|
||||
@@ -707,6 +757,8 @@
|
||||
let quizCarrySessionLength = 0;
|
||||
let quizCarryRoundsCompleted = 0;
|
||||
let quizCarrySessionEnded = false;
|
||||
/** หลัง timeup บนโต๊ะ (embed): รอ 5s แล้วโชว์ result-complete / result-gameover */
|
||||
let quizCarryResultEndTimer = null;
|
||||
/** embed พรีวิว: รอ Ready / START ของโฮสต์ก่อนนับ 3–2–1 */
|
||||
let quizCarryPregameActive = false;
|
||||
let playHostId = null;
|
||||
@@ -1462,6 +1514,72 @@
|
||||
fitQuizMapQuestionFontToPanel(panel, textEl, startFontPx, mnFont);
|
||||
}
|
||||
|
||||
/** โซนเดียวกับแผงคำถามบนแผนที่ — ยึด timeup ให้ตรงกล่อง #quiz-map-question-panel (ห้ามคำนวณซ้ำแล้วคลาด) */
|
||||
function syncQuizCarryTimeupDeskLayerPosition() {
|
||||
const layer = document.getElementById('quiz-carry-timeup-desk-layer');
|
||||
if (!layer || layer.classList.contains('is-hidden')) return;
|
||||
const anchor = layer.querySelector('.qc-timeup-desk-anchor')
|
||||
|| layer.querySelector('.qc-timeup-desk-inner')
|
||||
|| (() => {
|
||||
const img = document.getElementById('quiz-carry-timeup-txt-img');
|
||||
return img && img.parentElement && img.parentElement !== layer ? img.parentElement : null;
|
||||
})();
|
||||
if (!anchor || !canvas) return;
|
||||
const clearToFullStack = () => {
|
||||
anchor.style.left = '0';
|
||||
anchor.style.top = '0';
|
||||
anchor.style.width = '100%';
|
||||
anchor.style.height = '100%';
|
||||
};
|
||||
const stack = document.getElementById('play-canvas-stack');
|
||||
const panel = document.getElementById('quiz-map-question-panel');
|
||||
if (stack && panel && !panel.classList.contains('is-hidden')) {
|
||||
try {
|
||||
const sr = stack.getBoundingClientRect();
|
||||
const pr = panel.getBoundingClientRect();
|
||||
const rw = pr.width;
|
||||
const rh = pr.height;
|
||||
if (rw >= 8 && rh >= 8 && sr.width > 0 && sr.height > 0) {
|
||||
anchor.style.left = Math.round(pr.left - sr.left) + 'px';
|
||||
anchor.style.top = Math.round(pr.top - sr.top) + 'px';
|
||||
anchor.style.width = Math.round(rw) + 'px';
|
||||
anchor.style.height = Math.round(rh) + 'px';
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* fallback ด้านล่าง */ }
|
||||
}
|
||||
const pl = panel && panel.style && String(panel.style.left || '').trim();
|
||||
const pt = panel && panel.style && String(panel.style.top || '').trim();
|
||||
const pw = panel && panel.style && String(panel.style.width || '').trim();
|
||||
const ph = panel && panel.style && String(panel.style.height || '').trim();
|
||||
if (pl && pt && pw && ph) {
|
||||
anchor.style.left = pl;
|
||||
anchor.style.top = pt;
|
||||
anchor.style.width = pw;
|
||||
anchor.style.height = ph;
|
||||
return;
|
||||
}
|
||||
if (!mapData || mapData.gameType !== 'quiz_carry') {
|
||||
clearToFullStack();
|
||||
return;
|
||||
}
|
||||
const bounds = getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData);
|
||||
if (!bounds) {
|
||||
clearToFullStack();
|
||||
return;
|
||||
}
|
||||
const camX = me.x * tileSize;
|
||||
const camY = me.y * tileSize;
|
||||
const left = (bounds.minX * tileSize - camX) * zoom + canvas.width / 2;
|
||||
const top = (bounds.minY * tileSize - camY) * zoom + canvas.height / 2;
|
||||
const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zoom;
|
||||
const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zoom;
|
||||
anchor.style.left = Math.round(left) + 'px';
|
||||
anchor.style.top = Math.round(top) + 'px';
|
||||
anchor.style.width = Math.round(Math.max(48, wPx)) + 'px';
|
||||
anchor.style.height = Math.round(Math.max(40, hPx)) + 'px';
|
||||
}
|
||||
|
||||
function carryEmbedCountdownAnchorResolved() {
|
||||
if (!mapData || !isQuizCarry()) return 'screen';
|
||||
const v = mapData.carryEmbedCountdownAnchor;
|
||||
@@ -3288,6 +3406,306 @@
|
||||
quizCarryPickNextQuestion();
|
||||
}
|
||||
|
||||
function playQuizCarryAvatarUrlForMission(characterId) {
|
||||
if (!characterId) return '';
|
||||
return BASE + '/img/characters/' + encodeURIComponent(String(characterId)) + '_down.png';
|
||||
}
|
||||
|
||||
/** อวาตาร์สรุปภารกิจ / DOM — ใช้สี gen เดียวกับในเกม (เลเยอร์ + tint) */
|
||||
function applyQuizCarryRankAvatarTint(imgEl, characterId, peerId) {
|
||||
if (!imgEl || !characterId) return;
|
||||
let attempts = 0;
|
||||
const fallbackUrl = playQuizCarryAvatarUrlForMission(characterId);
|
||||
const tick = () => {
|
||||
attempts++;
|
||||
const nowT = Date.now();
|
||||
const rawImg = getCharacterFrame(characterId, 'down', nowT, false) || getCharacterImg(characterId, 'down');
|
||||
if (!rawImg) {
|
||||
if (fallbackUrl) imgEl.src = fallbackUrl;
|
||||
return;
|
||||
}
|
||||
if ((!rawImg.complete || !rawImg.naturalWidth) && attempts < 28) {
|
||||
if (fallbackUrl) imgEl.src = fallbackUrl + '?p=' + String(attempts);
|
||||
setTimeout(tick, 90);
|
||||
return;
|
||||
}
|
||||
if (!rawImg.naturalWidth) {
|
||||
if (fallbackUrl) imgEl.src = fallbackUrl;
|
||||
return;
|
||||
}
|
||||
const tint = playTintFromPeerId(peerId != null ? String(peerId) : String(characterId));
|
||||
const out = getPlayTintedAvatarSource(rawImg, characterId, 'down', nowT, false, tint);
|
||||
if (out && out.tagName === 'CANVAS' && out.width > 0) {
|
||||
try {
|
||||
imgEl.src = out.toDataURL('image/png');
|
||||
return;
|
||||
} catch (e) { /* ต่อไปลอง walk หรือ URL */ }
|
||||
}
|
||||
if (out && out.src && out !== rawImg) {
|
||||
imgEl.src = out.src;
|
||||
return;
|
||||
}
|
||||
if (attempts < 28 && playCharLayerDiscoveryPending(characterId)) {
|
||||
setTimeout(tick, 120);
|
||||
return;
|
||||
}
|
||||
imgEl.src = rawImg.src || fallbackUrl;
|
||||
};
|
||||
tick();
|
||||
}
|
||||
|
||||
/** เกรดจากค่าเฉลี่ยคะแนนทีม (0–100 หลังปัด) — A 80–100, B 60–79, C 0–59 */
|
||||
function quizCarryGradeFromTeamAverage(avgRounded) {
|
||||
if (avgRounded >= 80) return 'A';
|
||||
if (avgRounded >= 60) return 'B';
|
||||
return 'C';
|
||||
}
|
||||
|
||||
function quizCarryRollCardRewardForGrade(grade) {
|
||||
const u = Math.random() * 100;
|
||||
if (grade === 'A') {
|
||||
if (u < 30) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' };
|
||||
if (u < 80) return { th: 'การ์ด Rare', en: 'Rare card' };
|
||||
return { th: 'การ์ดธรรมดา', en: 'Common card' };
|
||||
}
|
||||
if (grade === 'B') {
|
||||
if (u < 20) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' };
|
||||
if (u < 50) return { th: 'การ์ด Rare', en: 'Rare card' };
|
||||
return { th: 'การ์ดธรรมดา', en: 'Common card' };
|
||||
}
|
||||
if (grade === 'C') {
|
||||
if (u < 20) return { th: 'การ์ด Rare', en: 'Rare card' };
|
||||
return { th: 'การ์ดธรรมดา', en: 'Common card' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function quizCarryBuildMissionRankList() {
|
||||
const ranks = [];
|
||||
if (myId != null) {
|
||||
ranks.push({
|
||||
id: myId,
|
||||
nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ',
|
||||
score: Math.max(0, Number(playLiveQuizScores[myId]) || 0),
|
||||
characterId: me.characterId || getPlayCharacterId(),
|
||||
});
|
||||
}
|
||||
others.forEach((o, id) => {
|
||||
if (!o) return;
|
||||
ranks.push({
|
||||
id,
|
||||
nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id),
|
||||
score: Math.max(0, Number(playLiveQuizScores[id]) || 0),
|
||||
characterId: o.characterId || null,
|
||||
});
|
||||
});
|
||||
ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th'));
|
||||
return ranks.slice(0, 5);
|
||||
}
|
||||
|
||||
function cancelQuizCarryResultEndAfterTimeup() {
|
||||
if (quizCarryResultEndTimer != null) {
|
||||
clearTimeout(quizCarryResultEndTimer);
|
||||
quizCarryResultEndTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** มีผู้เล่นอย่างน้อยหนึ่งคนได้คะแนน > 0 (= ส่งคำตอบถูกที่ฮับอย่างน้อย 1 ครั้ง; คะแนนต่อข้อคงที่ QUIZ_CARRY_POINTS_PER_CORRECT) */
|
||||
function quizCarryAnyPlayerHasCorrectAnswer() {
|
||||
if (!playLiveQuizScores || typeof playLiveQuizScores !== 'object') return false;
|
||||
const keys = Object.keys(playLiveQuizScores);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (Math.max(0, Number(playLiveQuizScores[keys[i]]) || 0) > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hideQuizCarryResultEndLayer() {
|
||||
const el = document.getElementById('quiz-carry-result-end-layer');
|
||||
if (!el) return;
|
||||
el.classList.add('is-hidden');
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
function showQuizCarryResultEndLayer(useComplete) {
|
||||
const el = document.getElementById('quiz-carry-result-end-layer');
|
||||
const img = document.getElementById('quiz-carry-result-end-img');
|
||||
if (!el || !img) return;
|
||||
const fname = useComplete ? 'result-complete.png' : 'result-gameover.png';
|
||||
img.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now());
|
||||
el.classList.remove('is-hidden');
|
||||
el.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
/** หลังแสดง timeup บนโต๊ะ (embed): รอ 5 วินาที แล้วโชว์ mock จบตามว่ามีคนตอบถูกหรือไม่ */
|
||||
function scheduleQuizCarryResultEndAfterTimeup() {
|
||||
cancelQuizCarryResultEndAfterTimeup();
|
||||
hideQuizCarryResultEndLayer();
|
||||
quizCarryResultEndTimer = setTimeout(function () {
|
||||
quizCarryResultEndTimer = null;
|
||||
const anyOk = quizCarryAnyPlayerHasCorrectAnswer();
|
||||
hideQuizCarryTimeupOnDeskLayer();
|
||||
showQuizCarryResultEndLayer(anyOk);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function hideQuizCarryTimeupOnDeskLayer() {
|
||||
cancelQuizCarryResultEndAfterTimeup();
|
||||
const el = document.getElementById('quiz-carry-timeup-desk-layer');
|
||||
if (!el) return;
|
||||
const anchor = el.querySelector('.qc-timeup-desk-anchor') || el.querySelector('.qc-timeup-desk-inner');
|
||||
if (anchor) {
|
||||
anchor.style.left = '';
|
||||
anchor.style.top = '';
|
||||
anchor.style.width = '';
|
||||
anchor.style.height = '';
|
||||
}
|
||||
el.classList.add('is-hidden');
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
/** หลังกดรับหลักฐาน (embed preview) — แสดง timeup-txt กลางโต๊ะบนแคนวาส แยกจากป๊อปสรุปคะแนน */
|
||||
function showQuizCarryTimeupOnDeskLayer() {
|
||||
const el = document.getElementById('quiz-carry-timeup-desk-layer');
|
||||
if (!el) return;
|
||||
const img = document.getElementById('quiz-carry-timeup-txt-img');
|
||||
if (img) {
|
||||
img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now());
|
||||
}
|
||||
el.classList.remove('is-hidden');
|
||||
el.setAttribute('aria-hidden', 'false');
|
||||
try {
|
||||
syncQuizCarryTimeupDeskLayerPosition();
|
||||
} catch (e) { /* ignore */ }
|
||||
if (previewMode && editorEmbedReturn) {
|
||||
scheduleQuizCarryResultEndAfterTimeup();
|
||||
}
|
||||
}
|
||||
|
||||
function showQuizCarryMissionSummaryOverlay(opts) {
|
||||
opts = opts || {};
|
||||
const forceF = !!opts.forceGradeF;
|
||||
const overlay = document.getElementById('quiz-carry-mission-overlay');
|
||||
if (!overlay) return;
|
||||
hideQuizCarryTimeupOnDeskLayer();
|
||||
const ranks = quizCarryBuildMissionRankList();
|
||||
const parts = ranks.map((r) => Math.max(0, Number(r.score) || 0));
|
||||
const total = parts.reduce((a, b) => a + b, 0);
|
||||
const n = ranks.length;
|
||||
const avgForGrade = n ? Math.min(100, Math.max(0, Math.round(total / n))) : 0;
|
||||
const grade = forceF ? 'F' : quizCarryGradeFromTeamAverage(avgForGrade);
|
||||
const reward = grade === 'F' ? null : quizCarryRollCardRewardForGrade(grade);
|
||||
|
||||
const rowEl = document.getElementById('qc-mission-rank-row');
|
||||
const totalEl = document.getElementById('qc-mission-total-line');
|
||||
const gradeEl = document.getElementById('qc-mission-grade');
|
||||
const cardTextEl = document.getElementById('qc-mission-card-text');
|
||||
const checkEl = document.getElementById('qc-mission-check');
|
||||
const successTxt = document.getElementById('qc-mission-success-txt');
|
||||
const rankTagLabels = ['[1st]', '[2nd]', '[3rd]', '[4th]', '[5th]'];
|
||||
if (rowEl) {
|
||||
rowEl.innerHTML = '';
|
||||
ranks.forEach((r, idx) => {
|
||||
const rank = idx + 1;
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'qc-mission-rank-cell';
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'qc-mission-rank-tag';
|
||||
tag.textContent = rankTagLabels[rank - 1] || ('[' + rank + ']');
|
||||
const frame = document.createElement('div');
|
||||
frame.className = 'qc-mission-rank-frame';
|
||||
const img = document.createElement('img');
|
||||
img.className = 'qc-mission-rank-avatar';
|
||||
img.alt = '';
|
||||
applyQuizCarryRankAvatarTint(img, r.characterId, r.id);
|
||||
img.onerror = function () {
|
||||
this.onerror = null;
|
||||
this.src = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect fill="%2318223b" width="64" height="64"/><text x="32" y="36" fill="%237aa2f7" font-size="10" text-anchor="middle">?</text></svg>');
|
||||
};
|
||||
frame.appendChild(img);
|
||||
const nick = document.createElement('div');
|
||||
nick.className = 'qc-mission-rank-nick';
|
||||
nick.textContent = r.nickname;
|
||||
nick.title = r.nickname;
|
||||
const sc = document.createElement('div');
|
||||
sc.className = 'qc-mission-rank-score';
|
||||
sc.textContent = String(r.score);
|
||||
cell.appendChild(tag);
|
||||
cell.appendChild(frame);
|
||||
cell.appendChild(nick);
|
||||
cell.appendChild(sc);
|
||||
rowEl.appendChild(cell);
|
||||
});
|
||||
}
|
||||
if (totalEl) {
|
||||
totalEl.textContent = '';
|
||||
if (!n) {
|
||||
totalEl.textContent = '—';
|
||||
} else {
|
||||
const lab = document.createElement('span');
|
||||
lab.textContent = 'คะแนนรวม ';
|
||||
const nums = document.createElement('span');
|
||||
nums.className = 'qc-mission-total-nums';
|
||||
nums.textContent = '(' + parts.join('+') + ') = ' + total;
|
||||
const tail = document.createElement('span');
|
||||
tail.textContent = ' · เฉลี่ยทีม ' + avgForGrade + '/100';
|
||||
totalEl.appendChild(lab);
|
||||
totalEl.appendChild(nums);
|
||||
totalEl.appendChild(tail);
|
||||
}
|
||||
}
|
||||
if (gradeEl) {
|
||||
gradeEl.textContent = grade;
|
||||
gradeEl.className = 'qc-mission-grade qc-mission-grade--' + String(grade).toLowerCase();
|
||||
}
|
||||
const okMission = grade !== 'F';
|
||||
if (checkEl) checkEl.style.display = okMission ? '' : 'none';
|
||||
if (successTxt) {
|
||||
successTxt.style.display = okMission ? '' : 'none';
|
||||
if (okMission) successTxt.textContent = 'ภารกิจสำเร็จ';
|
||||
}
|
||||
if (cardTextEl) {
|
||||
cardTextEl.textContent = '';
|
||||
if (grade === 'F') {
|
||||
const pl = document.createElement('div');
|
||||
pl.className = 'qc-mission-reward-plaque qc-mission-reward-plaque--muted';
|
||||
const t = document.createElement('div');
|
||||
t.className = 'qc-mission-reward-plaque-title';
|
||||
t.textContent = 'ไม่ได้รับการ์ด';
|
||||
const s = document.createElement('div');
|
||||
s.className = 'qc-mission-reward-plaque-sub';
|
||||
s.textContent = 'เกรด F';
|
||||
pl.appendChild(t);
|
||||
pl.appendChild(s);
|
||||
cardTextEl.appendChild(pl);
|
||||
} else if (reward) {
|
||||
const pl = document.createElement('div');
|
||||
pl.className = 'qc-mission-reward-plaque';
|
||||
const t = document.createElement('div');
|
||||
t.className = 'qc-mission-reward-plaque-title';
|
||||
t.textContent = reward.th;
|
||||
const s = document.createElement('div');
|
||||
s.className = 'qc-mission-reward-plaque-sub';
|
||||
s.textContent = reward.en;
|
||||
pl.appendChild(t);
|
||||
pl.appendChild(s);
|
||||
cardTextEl.appendChild(pl);
|
||||
}
|
||||
}
|
||||
overlay.classList.remove('is-hidden');
|
||||
const btn = document.getElementById('btn-quiz-carry-mission-done');
|
||||
if (btn) {
|
||||
btn.onclick = function () {
|
||||
overlay.classList.add('is-hidden');
|
||||
if (previewMode && editorEmbedReturn) {
|
||||
showQuizCarryTimeupOnDeskLayer();
|
||||
} else {
|
||||
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function showQuizCarrySessionCompleteOverlay() {
|
||||
if (quizCarrySessionEnded) return;
|
||||
quizCarrySessionEnded = true;
|
||||
@@ -3311,6 +3729,14 @@
|
||||
if (qEl) qEl.textContent = playQuizText;
|
||||
const phaseEl = document.getElementById('quiz-game-phase-label');
|
||||
if (phaseEl) phaseEl.textContent = 'ครบ ' + String(quizCarryRoundsCompleted) + ' ข้อ';
|
||||
const missionOv = document.getElementById('quiz-carry-mission-overlay');
|
||||
if (missionOv && quizCarryUseMissionSummaryOverlay) {
|
||||
showQuizCarryMissionSummaryOverlay({
|
||||
forceGradeF: !!(playQuizPlayerLocal && playQuizPlayerLocal.eliminated),
|
||||
});
|
||||
renderPlayQuizScoreboard(playLiveQuizScores);
|
||||
return;
|
||||
}
|
||||
const ov = document.getElementById('gauntlet-ended-overlay');
|
||||
const msgEl = document.getElementById('gauntlet-ended-message');
|
||||
const titleEl = document.getElementById('gauntlet-ended-title');
|
||||
@@ -3547,6 +3973,8 @@
|
||||
quizCarryRoundsCompleted = 0;
|
||||
quizCarrySessionEnded = false;
|
||||
hideQuizCarryEmbedCountdownOverlay();
|
||||
hideQuizCarryTimeupOnDeskLayer();
|
||||
hideQuizCarryResultEndLayer();
|
||||
me.quizCarryHeld = null;
|
||||
others.forEach((o) => {
|
||||
if (!o) return;
|
||||
@@ -3593,6 +4021,8 @@
|
||||
async function loadQuizCarryPoolAndStart() {
|
||||
quizCarryRoundsCompleted = 0;
|
||||
quizCarrySessionEnded = false;
|
||||
hideQuizCarryTimeupOnDeskLayer();
|
||||
hideQuizCarryResultEndLayer();
|
||||
quizCarrySessionLength = 0;
|
||||
let pool = [];
|
||||
let s = {};
|
||||
@@ -8639,9 +9069,25 @@
|
||||
|
||||
if (portrait) {
|
||||
const cid = me.characterId || getPlayCharacterId();
|
||||
const fr = getCharacterFrame(cid, me.direction || 'down', Date.now(), !!me.isWalking);
|
||||
const fb = fr && fr.src ? fr : getCharacterImg(cid, 'down');
|
||||
if (fb && fb.src) portrait.src = fb.src;
|
||||
const dir = me.direction || 'down';
|
||||
const nowT = Date.now();
|
||||
const walk = !!me.isWalking;
|
||||
const rawImg = getAvatarImg(cid, dir, nowT, walk);
|
||||
const tint = me.playTint || playTintFromPeerId(String(myId != null ? myId : 'me'));
|
||||
const comp = rawImg && cid && tint
|
||||
? getPlayTintedAvatarSource(rawImg, cid, dir, nowT, walk, tint)
|
||||
: rawImg;
|
||||
if (comp && comp.tagName === 'CANVAS' && comp.width > 0) {
|
||||
try {
|
||||
portrait.src = comp.toDataURL('image/png');
|
||||
} catch (e) {
|
||||
if (rawImg && rawImg.src) portrait.src = rawImg.src;
|
||||
}
|
||||
} else if (comp && comp.src) {
|
||||
portrait.src = comp.src;
|
||||
} else if (rawImg && rawImg.src) {
|
||||
portrait.src = rawImg.src;
|
||||
}
|
||||
}
|
||||
if (statusEl) {
|
||||
if (isQuizCarry() && quizCarrySessionEnded) {
|
||||
@@ -9509,6 +9955,8 @@
|
||||
syncPlayQuizMapPanel();
|
||||
syncQuizCarryEmbedQuestionStrip();
|
||||
}
|
||||
/* timeup ต้องซิงก์ทุกเฟรมแม้จบเซสชัน — อย่าให้อยู่ในเงื่อนไขด้านบนอย่างเดียว */
|
||||
syncQuizCarryTimeupDeskLayerPosition();
|
||||
if (previewFillBots && mapData && !useCyberPlayHud()) {
|
||||
const human = countPlayHumans();
|
||||
const bots = [...others.keys()].filter(isPreviewBotId).length;
|
||||
|
||||
@@ -266,8 +266,36 @@
|
||||
return -1;
|
||||
}
|
||||
|
||||
function isUploadedCharAssetId(id) {
|
||||
if (!id) return false;
|
||||
const s = String(id).trim();
|
||||
return /^char-\d/i.test(s);
|
||||
}
|
||||
|
||||
function useMultiFrameIdleSpriteSheets(id) {
|
||||
if (!id) return true;
|
||||
return !isUploadedCharAssetId(id);
|
||||
}
|
||||
|
||||
function ensureCharacterIdleAnim(id, dir) {
|
||||
const d = dir || 'down';
|
||||
if (!useMultiFrameIdleSpriteSheets(id)) {
|
||||
const key0 = String(id) + '_' + d + '_idle';
|
||||
var anim0 = characterIdleAnimations[key0];
|
||||
if (!anim0) {
|
||||
anim0 = { frames: [], fallback: null };
|
||||
characterIdleAnimations[key0] = anim0;
|
||||
var enc0 = encodeURIComponent(id);
|
||||
for (var ii = 0; ii < CHARACTER_ANIM_FRAMES; ii++) {
|
||||
var im0 = new Image();
|
||||
im0.src = BASE + '/img/characters/' + enc0 + '_' + d + '_' + ii + '.png';
|
||||
anim0.frames.push(im0);
|
||||
}
|
||||
anim0.fallback = new Image();
|
||||
anim0.fallback.src = BASE + '/img/characters/' + enc0 + '_' + d + '_idle.png';
|
||||
}
|
||||
return anim0;
|
||||
}
|
||||
const key = id + '_' + d + '_idle';
|
||||
var anim = characterIdleAnimations[key];
|
||||
if (!anim) {
|
||||
|
||||
@@ -27,6 +27,78 @@
|
||||
display: block;
|
||||
touch-action: none;
|
||||
}
|
||||
/* หลังกดรับหลักฐานในหน้าสรุป — BG ดำ alpha ทั้งจอ + timeup ติดโซนโต๊ะเหมือน #quiz-map-question-panel */
|
||||
#quiz-carry-timeup-desk-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 65;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#quiz-carry-timeup-desk-layer.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.qc-timeup-dim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: rgba(0, 0, 0, 0.58);
|
||||
pointer-events: none;
|
||||
}
|
||||
.qc-timeup-desk-anchor {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
padding: clamp(6px, 1.5vw, 14px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
.qc-timeup-txt-img {
|
||||
width: min(94%, 520px);
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
filter: drop-shadow(0 10px 36px rgba(0, 0, 0, 0.65)) drop-shadow(0 0 20px rgba(255, 60, 80, 0.15));
|
||||
}
|
||||
/* หลัง timeup 5s — mock จบ: result-complete / result-gameover เต็มพื้นที่แคนวาส */
|
||||
#quiz-carry-result-end-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 66;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#quiz-carry-result-end-layer.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.qc-result-end-dim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: rgba(0, 0, 0, 0.62);
|
||||
pointer-events: none;
|
||||
}
|
||||
.qc-result-end-img {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(96vw, 920px);
|
||||
max-height: min(88vh, 640px);
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(0 12px 40px rgba(0, 0, 0, 0.7));
|
||||
}
|
||||
/* Quiz carry — ปุ่ม GRAB มุมขวาล่าง (เทียบ F) · mockup ≈ 10–12% ความสูงจอ */
|
||||
#quiz-carry-grab-btn {
|
||||
position: absolute;
|
||||
@@ -526,6 +598,299 @@
|
||||
border: 2px solid rgba(247, 118, 140, 0.85);
|
||||
color: #fecdd3;
|
||||
}
|
||||
/* quiz_carry — สรุปผลภารกิจ (mock) · พื้นหลังแยกจากเนื้อหา กันลาย PNG [4th]/[5th] ทับ + หัวซ้ำกับ artwork */
|
||||
#quiz-carry-mission-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10090;
|
||||
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;
|
||||
font-family: 'Orbitron', 'Segoe UI', sans-serif;
|
||||
}
|
||||
#quiz-carry-mission-overlay.is-hidden { display: none !important; }
|
||||
.qc-mission-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(4, 6, 14, 0.78);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.qc-mission-panel {
|
||||
position: relative;
|
||||
width: min(96vw, 720px);
|
||||
max-width: 100%;
|
||||
max-height: min(92vh, 860px);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
filter: drop-shadow(0 0 22px rgba(0, 255, 240, 0.18));
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.qc-mission-panel-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
background-image: url('/Game/img/quiz-carry/popup-result.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
background-size: 100% auto;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.qc-mission-panel-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
/* เว้นตำแหน่งหัวข้อใน artwork — ไม่ซ้ำกับ <h2> sr-only */
|
||||
padding: clamp(72px, 16vw, 112px) clamp(14px, 3.2vw, 26px) clamp(14px, 2.8vw, 22px);
|
||||
gap: clamp(8px, 1.6vw, 14px);
|
||||
}
|
||||
.qc-mission-rank-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
gap: clamp(6px, 1.5vw, 14px);
|
||||
flex: 0 0 auto;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.qc-mission-rank-cell {
|
||||
flex: 0 0 auto;
|
||||
width: clamp(68px, 16vw, 104px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-align: center;
|
||||
color: #e2e8f0;
|
||||
min-width: 0;
|
||||
}
|
||||
/* แท็กอันดับ mock: [1st] … [5th] สีทอง — อยู่เหนือกรอบ ไม่ทับอวาตาร์ */
|
||||
.qc-mission-rank-tag {
|
||||
flex: 0 0 auto;
|
||||
font-size: clamp(0.5rem, 1.35vw, 0.65rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
color: #f6e08a;
|
||||
text-shadow: 0 0 10px rgba(246, 224, 138, 0.45);
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.qc-mission-rank-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 88px;
|
||||
aspect-ratio: 1;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 255, 240, 0.45);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 79, 180, 0.2),
|
||||
0 0 14px rgba(0, 255, 240, 0.18);
|
||||
background: rgba(10, 12, 24, 0.95);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.qc-mission-rank-avatar {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 72%;
|
||||
height: 72%;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
.qc-mission-rank-nick {
|
||||
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;
|
||||
}
|
||||
.qc-mission-rank-score {
|
||||
font-size: clamp(0.62rem, 1.85vw, 0.78rem);
|
||||
font-weight: 800;
|
||||
color: #f6e08a;
|
||||
text-shadow: 0 0 10px rgba(246, 224, 138, 0.35);
|
||||
}
|
||||
.qc-mission-total {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: clamp(0.65rem, 1.85vw, 0.8rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
flex: 0 0 auto;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
.qc-mission-total-nums {
|
||||
color: #f6e08a;
|
||||
font-weight: 800;
|
||||
text-shadow: 0 0 8px rgba(246, 224, 138, 0.25);
|
||||
}
|
||||
.qc-mission-bottom {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
|
||||
gap: clamp(10px, 2.2vw, 20px);
|
||||
align-items: start;
|
||||
flex: 0 0 auto;
|
||||
isolation: isolate;
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.qc-mission-bottom { grid-template-columns: 1fr; }
|
||||
}
|
||||
.qc-mission-section-h {
|
||||
margin: 0 0 8px;
|
||||
font-size: clamp(0.68rem, 1.9vw, 0.82rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
color: #f1f5f9;
|
||||
text-align: center;
|
||||
}
|
||||
.qc-mission-grade-block,
|
||||
.qc-mission-bonus-block {
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.qc-mission-grade {
|
||||
font-size: clamp(2.4rem, 11vw, 4rem);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.02em;
|
||||
color: #f8fafc;
|
||||
text-shadow: 0 0 28px rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
.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--f { color: #f7768e; text-shadow: 0 0 22px rgba(247, 118, 140, 0.45); }
|
||||
.qc-mission-bonus-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: clamp(10px, 2.5vw, 18px);
|
||||
min-height: 0;
|
||||
}
|
||||
.qc-mission-bonus-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.qc-mission-reward-plaque {
|
||||
flex: 0 1 auto;
|
||||
min-width: 100px;
|
||||
max-width: 160px;
|
||||
padding: 8px 10px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0, 255, 240, 0.55);
|
||||
box-shadow: 0 0 14px rgba(0, 255, 240, 0.15), inset 0 0 20px rgba(0, 40, 48, 0.35);
|
||||
background: linear-gradient(165deg, rgba(14, 18, 36, 0.98), rgba(8, 10, 22, 0.99));
|
||||
text-align: center;
|
||||
}
|
||||
.qc-mission-reward-plaque--muted {
|
||||
border-color: rgba(148, 153, 168, 0.4);
|
||||
box-shadow: none;
|
||||
opacity: 0.92;
|
||||
}
|
||||
.qc-mission-reward-plaque-title {
|
||||
font-size: clamp(0.68rem, 1.8vw, 0.8rem);
|
||||
font-weight: 800;
|
||||
color: #f1f5f9;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.qc-mission-reward-plaque-sub {
|
||||
margin-top: 4px;
|
||||
font-size: clamp(0.58rem, 1.45vw, 0.68rem);
|
||||
font-weight: 600;
|
||||
color: #7af8ff;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.qc-mission-check {
|
||||
width: clamp(22px, 6vw, 32px);
|
||||
height: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.qc-mission-success-txt {
|
||||
font-size: clamp(0.72rem, 2.1vw, 0.88rem);
|
||||
font-weight: 700;
|
||||
color: #9ece6a;
|
||||
}
|
||||
.qc-mission-card-text {
|
||||
flex: 0 1 auto;
|
||||
text-align: center;
|
||||
font-size: clamp(0.68rem, 1.9vw, 0.82rem);
|
||||
color: #c0caf5;
|
||||
line-height: 1.45;
|
||||
min-width: 0;
|
||||
max-width: min(200px, 42vw);
|
||||
}
|
||||
.qc-mission-reward-sub {
|
||||
font-size: 0.78em;
|
||||
opacity: 0.85;
|
||||
font-weight: 600;
|
||||
}
|
||||
.qc-mission-panel-inner #btn-quiz-carry-mission-done {
|
||||
flex: 0 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
#btn-quiz-carry-mission-done {
|
||||
margin: clamp(10px, 2.5vw, 16px) auto 0;
|
||||
display: block;
|
||||
width: min(92%, 320px);
|
||||
height: clamp(44px, 11vw, 56px);
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
background: url('/Game/img/quiz-carry/btn-done.png') center / contain no-repeat;
|
||||
background-color: transparent;
|
||||
}
|
||||
#btn-quiz-carry-mission-done:focus-visible {
|
||||
outline: 2px solid rgba(0, 255, 240, 0.7);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.qc-mission-sr {
|
||||
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;
|
||||
clip-path: inset(50%) !important;
|
||||
border: 0 !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
#gauntlet-ended-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -966,6 +1331,16 @@
|
||||
<img id="quiz-map-q-feedback-score" alt="" width="120" height="48" decoding="async" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="quiz-carry-timeup-desk-layer" class="is-hidden" aria-hidden="true">
|
||||
<div class="qc-timeup-dim" aria-hidden="true"></div>
|
||||
<div class="qc-timeup-desk-anchor">
|
||||
<img id="quiz-carry-timeup-txt-img" class="qc-timeup-txt-img" src="/Game/img/quiz-carry/timeup-txt.png" alt="" width="480" height="120" decoding="async" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="quiz-carry-result-end-layer" class="is-hidden" aria-hidden="true" role="img" aria-label="ผลจบภารกิจ">
|
||||
<div class="qc-result-end-dim" aria-hidden="true"></div>
|
||||
<img id="quiz-carry-result-end-img" class="qc-result-end-img" src="" alt="" width="900" height="520" decoding="async" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="play-quiz-scoreboard" class="is-hidden" aria-live="polite">
|
||||
@@ -1057,9 +1432,37 @@
|
||||
<button type="button" id="btn-gauntlet-ended-lobby">กลับห้อง (ล็อบบี้) · Back to room lobby</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="quiz-carry-mission-overlay" class="is-hidden" role="dialog" aria-modal="true" aria-labelledby="qc-mission-title">
|
||||
<div class="qc-mission-backdrop" aria-hidden="true"></div>
|
||||
<div class="qc-mission-panel">
|
||||
<div class="qc-mission-panel-bg" aria-hidden="true"></div>
|
||||
<div class="qc-mission-panel-inner">
|
||||
<h2 id="qc-mission-title" class="qc-mission-sr">สรุปผลภารกิจ</h2>
|
||||
<div id="qc-mission-rank-row" class="qc-mission-rank-row"></div>
|
||||
<p id="qc-mission-total-line" class="qc-mission-total"></p>
|
||||
<div class="qc-mission-bottom">
|
||||
<div class="qc-mission-grade-block">
|
||||
<div class="qc-mission-section-h">ระดับความสำเร็จ</div>
|
||||
<div id="qc-mission-grade" class="qc-mission-grade" aria-live="polite">A</div>
|
||||
</div>
|
||||
<div class="qc-mission-bonus-block">
|
||||
<div class="qc-mission-section-h">โบนัสพิเศษ</div>
|
||||
<div id="qc-mission-bonus-row" class="qc-mission-bonus-row">
|
||||
<div class="qc-mission-bonus-inline">
|
||||
<img id="qc-mission-check" class="qc-mission-check" src="/Game/img/quiz-carry/result-check.png" alt="" width="28" height="28" loading="lazy" decoding="async" />
|
||||
<span id="qc-mission-success-txt" class="qc-mission-success-txt">ภารกิจสำเร็จ</span>
|
||||
</div>
|
||||
<div id="qc-mission-card-text" class="qc-mission-card-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="btn-quiz-carry-mission-done"><span class="qc-mission-sr">รับหลักฐาน · Done</span></button>
|
||||
</div>
|
||||
</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.165"></script>
|
||||
<script src="js/play.js?v=0.182"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
const previewMode = params.get('preview') === '1';
|
||||
const forceDefaultCharacter = params.get('defaultChar') === '1';
|
||||
const editorEmbedReturn = params.get('editorEmbed') === '1';
|
||||
const playMapIdFromQuery = (params.get('map') || '').trim();
|
||||
/** สรุปภารกิจแบบ mock (popup-result ฯลฯ) — ใช้เฉพาะฉากนี้จาก editor (id ใน URL เช่น editor.html?id=mnorwqx1) */
|
||||
const quizCarryMissionSummaryMapId = 'mnorwqx1';
|
||||
const quizCarryUseMissionSummaryOverlay = playMapIdFromQuery === quizCarryMissionSummaryMapId;
|
||||
/** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */
|
||||
const LEGACY_PLACEHOLDER_CHARACTER_ID = 'Chatest';
|
||||
let firstCharacterDefaultResolved = null;
|
||||
@@ -159,8 +163,39 @@
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** ตัวละครอัปโหลดจากระบบมักเป็น char- + ตัวเลข (อาจมี suffix) — ไม่มี idle strip / idle layer ครบทุกทิศ */
|
||||
function isUploadedCharAssetId(id) {
|
||||
if (!id) return false;
|
||||
const s = String(id).trim();
|
||||
return /^char-\d/i.test(s);
|
||||
}
|
||||
|
||||
function useMultiFrameIdleSpriteSheets(id) {
|
||||
if (!id) return true;
|
||||
return !isUploadedCharAssetId(id);
|
||||
}
|
||||
|
||||
function ensureCharacterIdleAnim(id, dir) {
|
||||
const d = dir || 'down';
|
||||
/* char-* ไม่มี *_idle_0.png — ใช้ strip เดิน dir_0..3 เป็น idle ต่อทิศ (มีลูป) แทนรูปเดียวที่มักไม่มี */
|
||||
if (!useMultiFrameIdleSpriteSheets(id)) {
|
||||
const key = String(id) + '_' + d + '_idle';
|
||||
let anim = characterIdleAnimations[key];
|
||||
if (!anim) {
|
||||
anim = { frames: [], fallback: null };
|
||||
characterIdleAnimations[key] = anim;
|
||||
const enc = encodeURIComponent(id);
|
||||
for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) {
|
||||
const img = new Image();
|
||||
img.src = BASE + '/img/characters/' + enc + '_' + d + '_' + i + '.png';
|
||||
anim.frames.push(img);
|
||||
}
|
||||
const fb = new Image();
|
||||
fb.src = BASE + '/img/characters/' + enc + '_' + d + '_idle.png';
|
||||
anim.fallback = fb;
|
||||
}
|
||||
return anim;
|
||||
}
|
||||
const key = id + '_' + d + '_idle';
|
||||
let anim = characterIdleAnimations[key];
|
||||
if (!anim) {
|
||||
@@ -179,6 +214,7 @@
|
||||
return anim;
|
||||
}
|
||||
|
||||
/** ลูป idle ขณะยืน — ใช้จังหวะเวลาเหมือนเดิน (CHARACTER_ANIM_FRAME_MS) */
|
||||
function idleAnimPhaseIndex(now) {
|
||||
const t = typeof now === 'number' ? now : Date.now();
|
||||
return Math.floor(t / CHARACTER_ANIM_FRAME_MS) % CHARACTER_ANIM_FRAMES;
|
||||
@@ -357,6 +393,7 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
/** เลเยอร์ idle: id_<dir>_idle_<n>_layer_<name>.png หรือ id_<dir>_idle_layer_<name>.png */
|
||||
function layerUrlCandidatesIdle(id, dir, layerName, frameIndex) {
|
||||
const enc = encodeURIComponent(id);
|
||||
const base = BASE + '/img/characters/' + enc + '_' + dir + '_idle';
|
||||
@@ -468,13 +505,27 @@
|
||||
let anyTintColorLayer = false;
|
||||
let skippedShadowWhilePending = false;
|
||||
const diskLayers = playCharDiskLayersSet(id);
|
||||
/* ยืน (opts.idle): ลอง compose จาก *_idle_*_layer_* เหมือนหน้า character upload — รวม char-* ที่อัปโหลด idle เลเยอร์; ถ้าไม่มีไฟล์จะ missing → ไม่มีเลเยอร์ย้อม → getPlayTintedAvatarSource fallback ไป walk compose */
|
||||
const idleLayerStripes = !!useIdle;
|
||||
const resDir = dir;
|
||||
const resFi = frameIndex;
|
||||
for (let li = 0; li < PLAY_LAYER_ORDER.length; li++) {
|
||||
const layerName = PLAY_LAYER_ORDER[li];
|
||||
if (diskLayers && layerName !== 'shadow' && !diskLayers.has(layerName)) continue;
|
||||
/* ห้าม fallback face จากทิศ down เมื่อเป็น up/left/right — จะได้ตาอยู่หลังหัวเพราะวางหน้าหน้าบนร่างหันหลัง */
|
||||
const urls = layerName === 'shadow'
|
||||
? (useIdle ? shadowUrlCandidatesIdle(id, dir, frameIndex) : shadowUrlCandidates(id, dir, frameIndex))
|
||||
: (useIdle ? layerUrlCandidatesIdle(id, dir, layerName, frameIndex) : layerUrlCandidates(id, dir, layerName, frameIndex));
|
||||
/* char-* หันหลัง (up): ไม่มี face ทิศนี้ — ข้ามกันตาหน้าซ้อนหลังหัว; ซ้าย/ขวา/หน้าใช้ face ตามทิศได้ */
|
||||
if (layerName === 'face' && isUploadedCharAssetId(id) && dir === 'up') {
|
||||
continue;
|
||||
}
|
||||
let urls;
|
||||
if (layerName === 'shadow') {
|
||||
urls = idleLayerStripes
|
||||
? shadowUrlCandidatesIdle(id, dir, frameIndex)
|
||||
: shadowUrlCandidates(id, resDir, resFi);
|
||||
} else {
|
||||
urls = idleLayerStripes
|
||||
? layerUrlCandidatesIdle(id, dir, layerName, frameIndex)
|
||||
: layerUrlCandidates(id, resDir, layerName, resFi);
|
||||
}
|
||||
const r = resolvePlayLayerImage(urls);
|
||||
/* เงาโหลดช้า/404 บ่อย — อย่าให้บล็อกทั้งคอมโพส (ไม่งั้นตกไป fallback แถบแนวตั้งแทนเลเยอร์จริง) */
|
||||
if (r.status === 'pending' && layerName === 'shadow') {
|
||||
@@ -502,23 +553,24 @@
|
||||
|
||||
if (playCharLayerDiscoveryPending(characterId)) return rawImg;
|
||||
|
||||
if (!isWalking && characterIdleSpritesVisible(characterId, dirn)) {
|
||||
return rawImg;
|
||||
}
|
||||
|
||||
const apiFlag = playCharLayerFromApi(characterId);
|
||||
/* เหมือนเดิม: มีเลเยอร์แยกไฟล์เมื่อ API บอก hasLayerFiles หรือ probe เจอ — gen สีทำบน canvas จาก *_layer_* เท่านั้น */
|
||||
const charHasLayerFiles = apiFlag === true
|
||||
|| (apiFlag == null && playCharAnyDirectionLayered(characterId));
|
||||
/* ct3: ไม่ใช้ face จาก down บนทิศอื่น (แก้ตาหลังหัว) */
|
||||
|
||||
/* มี PNG idle รวม — ใช้เมื่อไม่มีระบบเลเยอร์สี (ถ้ามีเลเยอร์ต้อง compose ไม่ return raw ขาว) */
|
||||
if (!charHasLayerFiles && !isWalking && characterIdleSpritesVisible(characterId, dirn)) {
|
||||
return rawImg;
|
||||
}
|
||||
const cacheKeyWalk = [characterId, dirn, frameIndexWalk, tint.head, tint.hair, tint.body, 'ct3'].join('|');
|
||||
|
||||
if (charHasLayerFiles) {
|
||||
if (!isWalking) {
|
||||
const cacheKeyIdle = [characterId, dirn, idlePhase, tint.head, tint.hair, tint.body, 'ct3', 'idleL'].join('|');
|
||||
const composeFrame = isUploadedCharAssetId(characterId) ? 0 : idlePhase;
|
||||
const cacheKeyIdle = [characterId, dirn, composeFrame, tint.head, tint.hair, tint.body, 'ct3', 'idleL'].join('|');
|
||||
const hitI = playLayerCompositeCache.get(cacheKeyIdle);
|
||||
if (hitI) return hitI;
|
||||
const packI = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, idlePhase, tint, { idle: true });
|
||||
const packI = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, composeFrame, tint, { idle: true });
|
||||
if (packI && packI.canvas) {
|
||||
if (!packI.skipCache) playLayerCompositeCache.set(cacheKeyIdle, packI.canvas);
|
||||
return packI.canvas;
|
||||
@@ -556,7 +608,8 @@
|
||||
const t = typeof now === 'number' ? now : Date.now();
|
||||
if (!isWalking) {
|
||||
const idleAnim = ensureCharacterIdleAnim(id, dir);
|
||||
const idlePhase = idleAnimPhaseIndex(t);
|
||||
let idlePhase = idleAnimPhaseIndex(t);
|
||||
if (isUploadedCharAssetId(id)) idlePhase = 0;
|
||||
const idleFi = pickLoadedWalkFrameIndex(idleAnim, idlePhase);
|
||||
if (idleFi >= 0) return idleAnim.frames[idleFi];
|
||||
const idfb = idleAnim.fallback;
|
||||
@@ -704,6 +757,8 @@
|
||||
let quizCarrySessionLength = 0;
|
||||
let quizCarryRoundsCompleted = 0;
|
||||
let quizCarrySessionEnded = false;
|
||||
/** หลัง timeup บนโต๊ะ (embed): รอ 5s แล้วโชว์ result-complete / result-gameover */
|
||||
let quizCarryResultEndTimer = null;
|
||||
/** embed พรีวิว: รอ Ready / START ของโฮสต์ก่อนนับ 3–2–1 */
|
||||
let quizCarryPregameActive = false;
|
||||
let playHostId = null;
|
||||
@@ -1459,6 +1514,72 @@
|
||||
fitQuizMapQuestionFontToPanel(panel, textEl, startFontPx, mnFont);
|
||||
}
|
||||
|
||||
/** โซนเดียวกับแผงคำถามบนแผนที่ — ยึด timeup ให้ตรงกล่อง #quiz-map-question-panel (ห้ามคำนวณซ้ำแล้วคลาด) */
|
||||
function syncQuizCarryTimeupDeskLayerPosition() {
|
||||
const layer = document.getElementById('quiz-carry-timeup-desk-layer');
|
||||
if (!layer || layer.classList.contains('is-hidden')) return;
|
||||
const anchor = layer.querySelector('.qc-timeup-desk-anchor')
|
||||
|| layer.querySelector('.qc-timeup-desk-inner')
|
||||
|| (() => {
|
||||
const img = document.getElementById('quiz-carry-timeup-txt-img');
|
||||
return img && img.parentElement && img.parentElement !== layer ? img.parentElement : null;
|
||||
})();
|
||||
if (!anchor || !canvas) return;
|
||||
const clearToFullStack = () => {
|
||||
anchor.style.left = '0';
|
||||
anchor.style.top = '0';
|
||||
anchor.style.width = '100%';
|
||||
anchor.style.height = '100%';
|
||||
};
|
||||
const stack = document.getElementById('play-canvas-stack');
|
||||
const panel = document.getElementById('quiz-map-question-panel');
|
||||
if (stack && panel && !panel.classList.contains('is-hidden')) {
|
||||
try {
|
||||
const sr = stack.getBoundingClientRect();
|
||||
const pr = panel.getBoundingClientRect();
|
||||
const rw = pr.width;
|
||||
const rh = pr.height;
|
||||
if (rw >= 8 && rh >= 8 && sr.width > 0 && sr.height > 0) {
|
||||
anchor.style.left = Math.round(pr.left - sr.left) + 'px';
|
||||
anchor.style.top = Math.round(pr.top - sr.top) + 'px';
|
||||
anchor.style.width = Math.round(rw) + 'px';
|
||||
anchor.style.height = Math.round(rh) + 'px';
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* fallback ด้านล่าง */ }
|
||||
}
|
||||
const pl = panel && panel.style && String(panel.style.left || '').trim();
|
||||
const pt = panel && panel.style && String(panel.style.top || '').trim();
|
||||
const pw = panel && panel.style && String(panel.style.width || '').trim();
|
||||
const ph = panel && panel.style && String(panel.style.height || '').trim();
|
||||
if (pl && pt && pw && ph) {
|
||||
anchor.style.left = pl;
|
||||
anchor.style.top = pt;
|
||||
anchor.style.width = pw;
|
||||
anchor.style.height = ph;
|
||||
return;
|
||||
}
|
||||
if (!mapData || mapData.gameType !== 'quiz_carry') {
|
||||
clearToFullStack();
|
||||
return;
|
||||
}
|
||||
const bounds = getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData);
|
||||
if (!bounds) {
|
||||
clearToFullStack();
|
||||
return;
|
||||
}
|
||||
const camX = me.x * tileSize;
|
||||
const camY = me.y * tileSize;
|
||||
const left = (bounds.minX * tileSize - camX) * zoom + canvas.width / 2;
|
||||
const top = (bounds.minY * tileSize - camY) * zoom + canvas.height / 2;
|
||||
const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zoom;
|
||||
const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zoom;
|
||||
anchor.style.left = Math.round(left) + 'px';
|
||||
anchor.style.top = Math.round(top) + 'px';
|
||||
anchor.style.width = Math.round(Math.max(48, wPx)) + 'px';
|
||||
anchor.style.height = Math.round(Math.max(40, hPx)) + 'px';
|
||||
}
|
||||
|
||||
function carryEmbedCountdownAnchorResolved() {
|
||||
if (!mapData || !isQuizCarry()) return 'screen';
|
||||
const v = mapData.carryEmbedCountdownAnchor;
|
||||
@@ -3285,6 +3406,306 @@
|
||||
quizCarryPickNextQuestion();
|
||||
}
|
||||
|
||||
function playQuizCarryAvatarUrlForMission(characterId) {
|
||||
if (!characterId) return '';
|
||||
return BASE + '/img/characters/' + encodeURIComponent(String(characterId)) + '_down.png';
|
||||
}
|
||||
|
||||
/** อวาตาร์สรุปภารกิจ / DOM — ใช้สี gen เดียวกับในเกม (เลเยอร์ + tint) */
|
||||
function applyQuizCarryRankAvatarTint(imgEl, characterId, peerId) {
|
||||
if (!imgEl || !characterId) return;
|
||||
let attempts = 0;
|
||||
const fallbackUrl = playQuizCarryAvatarUrlForMission(characterId);
|
||||
const tick = () => {
|
||||
attempts++;
|
||||
const nowT = Date.now();
|
||||
const rawImg = getCharacterFrame(characterId, 'down', nowT, false) || getCharacterImg(characterId, 'down');
|
||||
if (!rawImg) {
|
||||
if (fallbackUrl) imgEl.src = fallbackUrl;
|
||||
return;
|
||||
}
|
||||
if ((!rawImg.complete || !rawImg.naturalWidth) && attempts < 28) {
|
||||
if (fallbackUrl) imgEl.src = fallbackUrl + '?p=' + String(attempts);
|
||||
setTimeout(tick, 90);
|
||||
return;
|
||||
}
|
||||
if (!rawImg.naturalWidth) {
|
||||
if (fallbackUrl) imgEl.src = fallbackUrl;
|
||||
return;
|
||||
}
|
||||
const tint = playTintFromPeerId(peerId != null ? String(peerId) : String(characterId));
|
||||
const out = getPlayTintedAvatarSource(rawImg, characterId, 'down', nowT, false, tint);
|
||||
if (out && out.tagName === 'CANVAS' && out.width > 0) {
|
||||
try {
|
||||
imgEl.src = out.toDataURL('image/png');
|
||||
return;
|
||||
} catch (e) { /* ต่อไปลอง walk หรือ URL */ }
|
||||
}
|
||||
if (out && out.src && out !== rawImg) {
|
||||
imgEl.src = out.src;
|
||||
return;
|
||||
}
|
||||
if (attempts < 28 && playCharLayerDiscoveryPending(characterId)) {
|
||||
setTimeout(tick, 120);
|
||||
return;
|
||||
}
|
||||
imgEl.src = rawImg.src || fallbackUrl;
|
||||
};
|
||||
tick();
|
||||
}
|
||||
|
||||
/** เกรดจากค่าเฉลี่ยคะแนนทีม (0–100 หลังปัด) — A 80–100, B 60–79, C 0–59 */
|
||||
function quizCarryGradeFromTeamAverage(avgRounded) {
|
||||
if (avgRounded >= 80) return 'A';
|
||||
if (avgRounded >= 60) return 'B';
|
||||
return 'C';
|
||||
}
|
||||
|
||||
function quizCarryRollCardRewardForGrade(grade) {
|
||||
const u = Math.random() * 100;
|
||||
if (grade === 'A') {
|
||||
if (u < 30) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' };
|
||||
if (u < 80) return { th: 'การ์ด Rare', en: 'Rare card' };
|
||||
return { th: 'การ์ดธรรมดา', en: 'Common card' };
|
||||
}
|
||||
if (grade === 'B') {
|
||||
if (u < 20) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' };
|
||||
if (u < 50) return { th: 'การ์ด Rare', en: 'Rare card' };
|
||||
return { th: 'การ์ดธรรมดา', en: 'Common card' };
|
||||
}
|
||||
if (grade === 'C') {
|
||||
if (u < 20) return { th: 'การ์ด Rare', en: 'Rare card' };
|
||||
return { th: 'การ์ดธรรมดา', en: 'Common card' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function quizCarryBuildMissionRankList() {
|
||||
const ranks = [];
|
||||
if (myId != null) {
|
||||
ranks.push({
|
||||
id: myId,
|
||||
nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ',
|
||||
score: Math.max(0, Number(playLiveQuizScores[myId]) || 0),
|
||||
characterId: me.characterId || getPlayCharacterId(),
|
||||
});
|
||||
}
|
||||
others.forEach((o, id) => {
|
||||
if (!o) return;
|
||||
ranks.push({
|
||||
id,
|
||||
nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id),
|
||||
score: Math.max(0, Number(playLiveQuizScores[id]) || 0),
|
||||
characterId: o.characterId || null,
|
||||
});
|
||||
});
|
||||
ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th'));
|
||||
return ranks.slice(0, 5);
|
||||
}
|
||||
|
||||
function cancelQuizCarryResultEndAfterTimeup() {
|
||||
if (quizCarryResultEndTimer != null) {
|
||||
clearTimeout(quizCarryResultEndTimer);
|
||||
quizCarryResultEndTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** มีผู้เล่นอย่างน้อยหนึ่งคนได้คะแนน > 0 (= ส่งคำตอบถูกที่ฮับอย่างน้อย 1 ครั้ง; คะแนนต่อข้อคงที่ QUIZ_CARRY_POINTS_PER_CORRECT) */
|
||||
function quizCarryAnyPlayerHasCorrectAnswer() {
|
||||
if (!playLiveQuizScores || typeof playLiveQuizScores !== 'object') return false;
|
||||
const keys = Object.keys(playLiveQuizScores);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (Math.max(0, Number(playLiveQuizScores[keys[i]]) || 0) > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hideQuizCarryResultEndLayer() {
|
||||
const el = document.getElementById('quiz-carry-result-end-layer');
|
||||
if (!el) return;
|
||||
el.classList.add('is-hidden');
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
function showQuizCarryResultEndLayer(useComplete) {
|
||||
const el = document.getElementById('quiz-carry-result-end-layer');
|
||||
const img = document.getElementById('quiz-carry-result-end-img');
|
||||
if (!el || !img) return;
|
||||
const fname = useComplete ? 'result-complete.png' : 'result-gameover.png';
|
||||
img.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now());
|
||||
el.classList.remove('is-hidden');
|
||||
el.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
/** หลังแสดง timeup บนโต๊ะ (embed): รอ 5 วินาที แล้วโชว์ mock จบตามว่ามีคนตอบถูกหรือไม่ */
|
||||
function scheduleQuizCarryResultEndAfterTimeup() {
|
||||
cancelQuizCarryResultEndAfterTimeup();
|
||||
hideQuizCarryResultEndLayer();
|
||||
quizCarryResultEndTimer = setTimeout(function () {
|
||||
quizCarryResultEndTimer = null;
|
||||
const anyOk = quizCarryAnyPlayerHasCorrectAnswer();
|
||||
hideQuizCarryTimeupOnDeskLayer();
|
||||
showQuizCarryResultEndLayer(anyOk);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function hideQuizCarryTimeupOnDeskLayer() {
|
||||
cancelQuizCarryResultEndAfterTimeup();
|
||||
const el = document.getElementById('quiz-carry-timeup-desk-layer');
|
||||
if (!el) return;
|
||||
const anchor = el.querySelector('.qc-timeup-desk-anchor') || el.querySelector('.qc-timeup-desk-inner');
|
||||
if (anchor) {
|
||||
anchor.style.left = '';
|
||||
anchor.style.top = '';
|
||||
anchor.style.width = '';
|
||||
anchor.style.height = '';
|
||||
}
|
||||
el.classList.add('is-hidden');
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
/** หลังกดรับหลักฐาน (embed preview) — แสดง timeup-txt กลางโต๊ะบนแคนวาส แยกจากป๊อปสรุปคะแนน */
|
||||
function showQuizCarryTimeupOnDeskLayer() {
|
||||
const el = document.getElementById('quiz-carry-timeup-desk-layer');
|
||||
if (!el) return;
|
||||
const img = document.getElementById('quiz-carry-timeup-txt-img');
|
||||
if (img) {
|
||||
img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now());
|
||||
}
|
||||
el.classList.remove('is-hidden');
|
||||
el.setAttribute('aria-hidden', 'false');
|
||||
try {
|
||||
syncQuizCarryTimeupDeskLayerPosition();
|
||||
} catch (e) { /* ignore */ }
|
||||
if (previewMode && editorEmbedReturn) {
|
||||
scheduleQuizCarryResultEndAfterTimeup();
|
||||
}
|
||||
}
|
||||
|
||||
function showQuizCarryMissionSummaryOverlay(opts) {
|
||||
opts = opts || {};
|
||||
const forceF = !!opts.forceGradeF;
|
||||
const overlay = document.getElementById('quiz-carry-mission-overlay');
|
||||
if (!overlay) return;
|
||||
hideQuizCarryTimeupOnDeskLayer();
|
||||
const ranks = quizCarryBuildMissionRankList();
|
||||
const parts = ranks.map((r) => Math.max(0, Number(r.score) || 0));
|
||||
const total = parts.reduce((a, b) => a + b, 0);
|
||||
const n = ranks.length;
|
||||
const avgForGrade = n ? Math.min(100, Math.max(0, Math.round(total / n))) : 0;
|
||||
const grade = forceF ? 'F' : quizCarryGradeFromTeamAverage(avgForGrade);
|
||||
const reward = grade === 'F' ? null : quizCarryRollCardRewardForGrade(grade);
|
||||
|
||||
const rowEl = document.getElementById('qc-mission-rank-row');
|
||||
const totalEl = document.getElementById('qc-mission-total-line');
|
||||
const gradeEl = document.getElementById('qc-mission-grade');
|
||||
const cardTextEl = document.getElementById('qc-mission-card-text');
|
||||
const checkEl = document.getElementById('qc-mission-check');
|
||||
const successTxt = document.getElementById('qc-mission-success-txt');
|
||||
const rankTagLabels = ['[1st]', '[2nd]', '[3rd]', '[4th]', '[5th]'];
|
||||
if (rowEl) {
|
||||
rowEl.innerHTML = '';
|
||||
ranks.forEach((r, idx) => {
|
||||
const rank = idx + 1;
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'qc-mission-rank-cell';
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'qc-mission-rank-tag';
|
||||
tag.textContent = rankTagLabels[rank - 1] || ('[' + rank + ']');
|
||||
const frame = document.createElement('div');
|
||||
frame.className = 'qc-mission-rank-frame';
|
||||
const img = document.createElement('img');
|
||||
img.className = 'qc-mission-rank-avatar';
|
||||
img.alt = '';
|
||||
applyQuizCarryRankAvatarTint(img, r.characterId, r.id);
|
||||
img.onerror = function () {
|
||||
this.onerror = null;
|
||||
this.src = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect fill="%2318223b" width="64" height="64"/><text x="32" y="36" fill="%237aa2f7" font-size="10" text-anchor="middle">?</text></svg>');
|
||||
};
|
||||
frame.appendChild(img);
|
||||
const nick = document.createElement('div');
|
||||
nick.className = 'qc-mission-rank-nick';
|
||||
nick.textContent = r.nickname;
|
||||
nick.title = r.nickname;
|
||||
const sc = document.createElement('div');
|
||||
sc.className = 'qc-mission-rank-score';
|
||||
sc.textContent = String(r.score);
|
||||
cell.appendChild(tag);
|
||||
cell.appendChild(frame);
|
||||
cell.appendChild(nick);
|
||||
cell.appendChild(sc);
|
||||
rowEl.appendChild(cell);
|
||||
});
|
||||
}
|
||||
if (totalEl) {
|
||||
totalEl.textContent = '';
|
||||
if (!n) {
|
||||
totalEl.textContent = '—';
|
||||
} else {
|
||||
const lab = document.createElement('span');
|
||||
lab.textContent = 'คะแนนรวม ';
|
||||
const nums = document.createElement('span');
|
||||
nums.className = 'qc-mission-total-nums';
|
||||
nums.textContent = '(' + parts.join('+') + ') = ' + total;
|
||||
const tail = document.createElement('span');
|
||||
tail.textContent = ' · เฉลี่ยทีม ' + avgForGrade + '/100';
|
||||
totalEl.appendChild(lab);
|
||||
totalEl.appendChild(nums);
|
||||
totalEl.appendChild(tail);
|
||||
}
|
||||
}
|
||||
if (gradeEl) {
|
||||
gradeEl.textContent = grade;
|
||||
gradeEl.className = 'qc-mission-grade qc-mission-grade--' + String(grade).toLowerCase();
|
||||
}
|
||||
const okMission = grade !== 'F';
|
||||
if (checkEl) checkEl.style.display = okMission ? '' : 'none';
|
||||
if (successTxt) {
|
||||
successTxt.style.display = okMission ? '' : 'none';
|
||||
if (okMission) successTxt.textContent = 'ภารกิจสำเร็จ';
|
||||
}
|
||||
if (cardTextEl) {
|
||||
cardTextEl.textContent = '';
|
||||
if (grade === 'F') {
|
||||
const pl = document.createElement('div');
|
||||
pl.className = 'qc-mission-reward-plaque qc-mission-reward-plaque--muted';
|
||||
const t = document.createElement('div');
|
||||
t.className = 'qc-mission-reward-plaque-title';
|
||||
t.textContent = 'ไม่ได้รับการ์ด';
|
||||
const s = document.createElement('div');
|
||||
s.className = 'qc-mission-reward-plaque-sub';
|
||||
s.textContent = 'เกรด F';
|
||||
pl.appendChild(t);
|
||||
pl.appendChild(s);
|
||||
cardTextEl.appendChild(pl);
|
||||
} else if (reward) {
|
||||
const pl = document.createElement('div');
|
||||
pl.className = 'qc-mission-reward-plaque';
|
||||
const t = document.createElement('div');
|
||||
t.className = 'qc-mission-reward-plaque-title';
|
||||
t.textContent = reward.th;
|
||||
const s = document.createElement('div');
|
||||
s.className = 'qc-mission-reward-plaque-sub';
|
||||
s.textContent = reward.en;
|
||||
pl.appendChild(t);
|
||||
pl.appendChild(s);
|
||||
cardTextEl.appendChild(pl);
|
||||
}
|
||||
}
|
||||
overlay.classList.remove('is-hidden');
|
||||
const btn = document.getElementById('btn-quiz-carry-mission-done');
|
||||
if (btn) {
|
||||
btn.onclick = function () {
|
||||
overlay.classList.add('is-hidden');
|
||||
if (previewMode && editorEmbedReturn) {
|
||||
showQuizCarryTimeupOnDeskLayer();
|
||||
} else {
|
||||
window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function showQuizCarrySessionCompleteOverlay() {
|
||||
if (quizCarrySessionEnded) return;
|
||||
quizCarrySessionEnded = true;
|
||||
@@ -3308,6 +3729,14 @@
|
||||
if (qEl) qEl.textContent = playQuizText;
|
||||
const phaseEl = document.getElementById('quiz-game-phase-label');
|
||||
if (phaseEl) phaseEl.textContent = 'ครบ ' + String(quizCarryRoundsCompleted) + ' ข้อ';
|
||||
const missionOv = document.getElementById('quiz-carry-mission-overlay');
|
||||
if (missionOv && quizCarryUseMissionSummaryOverlay) {
|
||||
showQuizCarryMissionSummaryOverlay({
|
||||
forceGradeF: !!(playQuizPlayerLocal && playQuizPlayerLocal.eliminated),
|
||||
});
|
||||
renderPlayQuizScoreboard(playLiveQuizScores);
|
||||
return;
|
||||
}
|
||||
const ov = document.getElementById('gauntlet-ended-overlay');
|
||||
const msgEl = document.getElementById('gauntlet-ended-message');
|
||||
const titleEl = document.getElementById('gauntlet-ended-title');
|
||||
@@ -3544,6 +3973,8 @@
|
||||
quizCarryRoundsCompleted = 0;
|
||||
quizCarrySessionEnded = false;
|
||||
hideQuizCarryEmbedCountdownOverlay();
|
||||
hideQuizCarryTimeupOnDeskLayer();
|
||||
hideQuizCarryResultEndLayer();
|
||||
me.quizCarryHeld = null;
|
||||
others.forEach((o) => {
|
||||
if (!o) return;
|
||||
@@ -3590,6 +4021,8 @@
|
||||
async function loadQuizCarryPoolAndStart() {
|
||||
quizCarryRoundsCompleted = 0;
|
||||
quizCarrySessionEnded = false;
|
||||
hideQuizCarryTimeupOnDeskLayer();
|
||||
hideQuizCarryResultEndLayer();
|
||||
quizCarrySessionLength = 0;
|
||||
let pool = [];
|
||||
let s = {};
|
||||
@@ -7748,6 +8181,55 @@
|
||||
return quizTilesFootprintPlay(px, py).has(tx + ',' + ty);
|
||||
}
|
||||
|
||||
/**
|
||||
* กล่องตรวจโซนห้ามซ้อน (blockPlayer) — วางเหมือนชนกำแพง: กลางแนวนอน + ชิดเท้า
|
||||
* blockPlayerSeparationW/H: 0 = ใช้เต็มความกว้าง/สูงตัว · ค่าอื่น = จำกัด 1..cw / 1..ch
|
||||
* รองรับ legacy blockPlayerSeparation เมื่อยังไม่มี W/H ใน JSON
|
||||
*/
|
||||
function resolveBlockPlayerNoOverlapBoxWH(md) {
|
||||
const m = md || mapData;
|
||||
if (!m) return { cw: 1, ch: 1, boxW: 1, boxH: 1 };
|
||||
const { cw, ch } = getCharacterFootprintWH(m);
|
||||
let w = m.blockPlayerSeparationW != null && m.blockPlayerSeparationW !== '' ? Math.floor(Number(m.blockPlayerSeparationW)) : NaN;
|
||||
let h = m.blockPlayerSeparationH != null && m.blockPlayerSeparationH !== '' ? Math.floor(Number(m.blockPlayerSeparationH)) : NaN;
|
||||
const leg = m.blockPlayerSeparation != null && m.blockPlayerSeparation !== '' ? Math.floor(Number(m.blockPlayerSeparation)) : NaN;
|
||||
const hasW = Number.isFinite(w) && w >= 0;
|
||||
const hasH = Number.isFinite(h) && h >= 0;
|
||||
if (!hasW && !hasH && Number.isFinite(leg) && leg >= 1) {
|
||||
w = Math.min(leg, cw);
|
||||
h = Math.min(leg, ch);
|
||||
} else {
|
||||
if (!hasW) w = 0;
|
||||
if (!hasH) h = 0;
|
||||
}
|
||||
const boxW = !w || w <= 0 ? cw : Math.max(1, Math.min(cw, w));
|
||||
const boxH = !h || h <= 0 ? ch : Math.max(1, Math.min(ch, h));
|
||||
return { cw, ch, boxW, boxH };
|
||||
}
|
||||
|
||||
/** ช่องกริดที่ถือว่า “ผู้เล่นที่ (px,py) กินพื้นที่” สำหรับตรวจโซน blockPlayer — กล่องเดียวกับแนวชนกำแพง */
|
||||
function quizTilesBlockPlayerPeerFootprintPlay(px, py) {
|
||||
const s = new Set();
|
||||
if (!mapData) return s;
|
||||
if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s;
|
||||
const { cw, ch, boxW, boxH } = resolveBlockPlayerNoOverlapBoxWH(mapData);
|
||||
const w = mapData.width || 20, h = mapData.height || 15;
|
||||
const minTx = Math.floor(px);
|
||||
const minTy = Math.floor(py);
|
||||
const offX = minTx + Math.floor((cw - boxW) / 2);
|
||||
const offY = minTy + (ch - boxH);
|
||||
for (let ty = offY; ty < offY + boxH; ty++) {
|
||||
for (let tx = offX; tx < offX + boxW; tx++) {
|
||||
if (tx >= 0 && ty >= 0 && tx < w && ty < h) s.add(tx + ',' + ty);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function playPeerBlockPlayerOccupiesTile(px, py, tx, ty) {
|
||||
return quizTilesBlockPlayerPeerFootprintPlay(px, py).has(tx + ',' + ty);
|
||||
}
|
||||
|
||||
/** Footprint ชนกำแพง (objects=1) เท่านั้น — hub / interactive / blockPlayer / quiz ใช้ quizTilesFootprintPlay = เต็ม characterCells */
|
||||
function quizTilesWallCollisionFootprintPlay(px, py) {
|
||||
const s = new Set();
|
||||
@@ -7858,7 +8340,7 @@
|
||||
const tx = +p[0], ty = +p[1];
|
||||
if (!bp[ty] || bp[ty][tx] !== 1) continue;
|
||||
for (const [, o] of others) {
|
||||
if (playEntityFootprintContainsTile(o.x, o.y, tx, ty)) return false;
|
||||
if (playPeerBlockPlayerOccupiesTile(o.x, o.y, tx, ty)) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7898,9 +8380,9 @@
|
||||
if (!bp[ty] || bp[ty][tx] !== 1) continue;
|
||||
for (const [, peer] of others) {
|
||||
if (o && peer === o) continue;
|
||||
if (playEntityFootprintContainsTile(peer.x, peer.y, tx, ty)) return false;
|
||||
if (playPeerBlockPlayerOccupiesTile(peer.x, peer.y, tx, ty)) return false;
|
||||
}
|
||||
if (o && playEntityFootprintContainsTile(me.x, me.y, tx, ty)) return false;
|
||||
if (o && playPeerBlockPlayerOccupiesTile(me.x, me.y, tx, ty)) return false;
|
||||
}
|
||||
}
|
||||
if (isQuiz()) {
|
||||
@@ -8587,9 +9069,25 @@
|
||||
|
||||
if (portrait) {
|
||||
const cid = me.characterId || getPlayCharacterId();
|
||||
const fr = getCharacterFrame(cid, me.direction || 'down', Date.now(), !!me.isWalking);
|
||||
const fb = fr && fr.src ? fr : getCharacterImg(cid, 'down');
|
||||
if (fb && fb.src) portrait.src = fb.src;
|
||||
const dir = me.direction || 'down';
|
||||
const nowT = Date.now();
|
||||
const walk = !!me.isWalking;
|
||||
const rawImg = getAvatarImg(cid, dir, nowT, walk);
|
||||
const tint = me.playTint || playTintFromPeerId(String(myId != null ? myId : 'me'));
|
||||
const comp = rawImg && cid && tint
|
||||
? getPlayTintedAvatarSource(rawImg, cid, dir, nowT, walk, tint)
|
||||
: rawImg;
|
||||
if (comp && comp.tagName === 'CANVAS' && comp.width > 0) {
|
||||
try {
|
||||
portrait.src = comp.toDataURL('image/png');
|
||||
} catch (e) {
|
||||
if (rawImg && rawImg.src) portrait.src = rawImg.src;
|
||||
}
|
||||
} else if (comp && comp.src) {
|
||||
portrait.src = comp.src;
|
||||
} else if (rawImg && rawImg.src) {
|
||||
portrait.src = rawImg.src;
|
||||
}
|
||||
}
|
||||
if (statusEl) {
|
||||
if (isQuizCarry() && quizCarrySessionEnded) {
|
||||
@@ -9457,6 +9955,8 @@
|
||||
syncPlayQuizMapPanel();
|
||||
syncQuizCarryEmbedQuestionStrip();
|
||||
}
|
||||
/* timeup ต้องซิงก์ทุกเฟรมแม้จบเซสชัน — อย่าให้อยู่ในเงื่อนไขด้านบนอย่างเดียว */
|
||||
syncQuizCarryTimeupDeskLayerPosition();
|
||||
if (previewFillBots && mapData && !useCyberPlayHud()) {
|
||||
const human = countPlayHumans();
|
||||
const bots = [...others.keys()].filter(isPreviewBotId).length;
|
||||
|
||||
@@ -1043,7 +1043,7 @@
|
||||
</div>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0122"></script>
|
||||
<script src="js/room-lobby.js?v=0.0185"></script>
|
||||
<script src="js/room-lobby.js?v=0.0188"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -126,7 +126,9 @@ function buildCharacterLayerManifest(id, files) {
|
||||
return { ok: true, id, byDir, byDirIdle };
|
||||
}
|
||||
|
||||
const GAUNTLET_ASSETS_DIR = path.join(__dirname, 'data', 'gauntlet-assets');
|
||||
/** ต้องอยู่ใต้ public/img/ เพื่อให้ nginx (alias /Game/ → Game/public) เสิร์ฟ GET /Game/img/gauntlet-assets/* ได้ — เดิมเก็บใน data/ จะถูกคัดลอกมาครั้งแรก */
|
||||
const GAUNTLET_ASSETS_DIR = path.join(__dirname, 'public', 'img', 'gauntlet-assets');
|
||||
const GAUNTLET_ASSETS_DIR_LEGACY = path.join(__dirname, 'data', 'gauntlet-assets');
|
||||
const GAUNTLET_ASSETS_META_PATH = path.join(__dirname, 'data', 'gauntlet-assets-meta.json');
|
||||
/** ต้องอยู่ใต้ public/ เพื่อให้ nginx (alias /Game/ → Game/public) เสิร์ฟรูปได้ — อย่าใช้ data/ */
|
||||
const QUIZ_CARRY_PLAQUE_ASSETS_DIR = path.join(__dirname, 'public', 'img', 'quiz-carry-plaque-assets');
|
||||
@@ -730,8 +732,31 @@ function saveGameTimingToFile(d) {
|
||||
}
|
||||
}
|
||||
|
||||
function migrateGauntletAssetsFromLegacyIfNeeded() {
|
||||
try {
|
||||
if (!fs.existsSync(GAUNTLET_ASSETS_DIR_LEGACY)) return;
|
||||
if (!fs.existsSync(GAUNTLET_ASSETS_DIR)) fs.mkdirSync(GAUNTLET_ASSETS_DIR, { recursive: true });
|
||||
const names = fs.readdirSync(GAUNTLET_ASSETS_DIR_LEGACY);
|
||||
names.forEach((f) => {
|
||||
if (!safeGauntletStoredFilename(f)) return;
|
||||
const src = path.join(GAUNTLET_ASSETS_DIR_LEGACY, f);
|
||||
const dest = path.join(GAUNTLET_ASSETS_DIR, f);
|
||||
if (fs.existsSync(dest)) return;
|
||||
try {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log('[gauntlet-assets] migrated', f, 'from data/ to public/img/gauntlet-assets/');
|
||||
} catch (e) {
|
||||
console.error('[gauntlet-assets] migrate failed', f, e && e.message);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('migrateGauntletAssetsFromLegacyIfNeeded', e && e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureGauntletAssetsDir() {
|
||||
if (!fs.existsSync(GAUNTLET_ASSETS_DIR)) fs.mkdirSync(GAUNTLET_ASSETS_DIR, { recursive: true });
|
||||
migrateGauntletAssetsFromLegacyIfNeeded();
|
||||
}
|
||||
|
||||
function loadGauntletAssetsMeta() {
|
||||
|
||||