diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index a459fe0..22da66f 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -2708,31 +2708,49 @@ quizWrongGhostImg = img; return img; } - /** วาด "ผี" คุมทับตัวผู้เล่นในแมป mng8a80o เมื่อตอบผิด — ครอบทั้งตัวละครเลย */ - function drawQuizWrongGhostOverlayPlay(ax, ay, entId) { - if (!isQuizQuestionMissionUiMapPlay()) return; + /** ตรวจว่า peer/me ตอบผิดในแมป mng8a80o แล้ว → คุมผีแทน avatar */ + function isQuizWrongGhostActiveForEnt(entId) { + if (!isQuizQuestionMissionUiMapPlay()) return false; const sid = String(entId == null ? '' : entId); const isMeEnt = (myId != null && sid === String(myId)); - const wrong = isMeEnt + return isMeEnt ? !!(playQuizPlayerLocal && (playQuizPlayerLocal.cannotTrue || playQuizPlayerLocal.cannotFalse)) : !!playQuizEverWrong[sid]; - if (!wrong) return; + } + + /** วาด "ผี" แทน avatar ทั้งตัวในแมป mng8a80o เมื่อตอบผิด — คุมผีเลย */ + function drawQuizWrongGhostOverlayPlay(ax, ay, entId, nameLabel) { const img = getQuizWrongGhostImg(); if (!img || !img.complete || !img.naturalWidth) return; - /* ผีครอบทับ avatar ทั้งตัว — กว้าง ~2.4 tile, สูงตามสัดส่วน, ลอยนิดๆ */ const tile = (typeof tileSize === 'number' && tileSize > 0) ? tileSize : 32; - const wPx = tile * 2.4; + /* ผีขนาดเท่าตัวละครเดิม: avatar ปกติ ~1 tile กว้าง, ~1.6 tile สูง → ผีกว้าง ~1.4, สูงตามสัดส่วน */ + const wPx = tile * 1.4; const ratio = img.naturalHeight / Math.max(1, img.naturalWidth); const hPx = wPx * ratio; - const tBob = Math.sin(performance.now() / 320) * (tile * 0.05); - /* ทับตรงกลางตัว avatar — avatar กว้าง 1 tile, สูง ~1.6 tile (วาดจากมุมบนซ้าย) */ - const avatarCenterY = ay + tile * 0.55; + const sid = String(entId == null ? '' : entId); + const tBob = Math.sin(performance.now() / 360 + (sid ? sid.charCodeAt(0) || 0 : 0)) * (tile * 0.06); + /* วาดให้ "เท้าผี" ตรงกับเท้า avatar (avatar สูง 1.6 tile จากจุดบนซ้าย ay) */ + const groundY = ay + tile * 1.6; const drawX = ax + (tile - wPx) / 2; - const drawY = avatarCenterY - hPx / 2 + tBob; + const drawY = groundY - hPx + tBob; ctx.save(); - ctx.globalAlpha = 0.85; + ctx.globalAlpha = 0.98; ctx.drawImage(img, drawX, drawY, wPx, hPx); ctx.restore(); + /* ชื่อใต้ผี (ถ้ามี — เพื่อระบุว่าเป็นใคร) */ + if (nameLabel) { + ctx.save(); + ctx.font = '600 12px system-ui, "Segoe UI", "Kanit", sans-serif'; + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(255,255,255,0.95)'; + ctx.strokeStyle = 'rgba(0,0,0,0.85)'; + ctx.lineWidth = 3; + const nx = ax + tile / 2; + const ny = groundY + 14; + ctx.strokeText(nameLabel, nx, ny); + ctx.fillText(nameLabel, nx, ny); + ctx.restore(); + } } /** เกมถูก/ผิด — ตรงกับ server QUIZ_TF_POINTS_PER_CORRECT */ const QUIZ_TF_POINTS_PER_CORRECT = 10; @@ -18471,22 +18489,32 @@ if (shouldGauntletCrownHeistSkipAvatarDrawPlay(o, false, off.ax, off.ay)) return; const axO = safeX(o.x) + off.ax; const ayO = safeY(o.y) + off.ay; - if (playQuizAvatarHideWhileOverlappingAnswerZone(o, id, axO, ayO)) return; + /* ผีต้องโชว์เสมอ แม้ทับโซนคำตอบ (เดินไปไหนก็ได้) */ + const ghostActiveO = isQuizWrongGhostActiveForEnt(id); + if (!ghostActiveO && playQuizAvatarHideWhileOverlappingAnswerZone(o, id, axO, ayO)) return; if (botOut) ctx.save(); if (botOut) ctx.globalAlpha = 0.4; - drawAvatar(axO, ayO, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o), isGauntletCrownHeistMapPlay() ? (o.gauntletCrownPenaltyFxUntil || 0) : 0); - drawQuizWrongGhostOverlayPlay(axO, ayO, id); + /* ตอบผิดในแมป mng8a80o → แทน avatar ด้วยผีเลย */ + if (ghostActiveO) { + drawQuizWrongGhostOverlayPlay(axO, ayO, id, peerName); + } else { + drawAvatar(axO, ayO, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o), isGauntletCrownHeistMapPlay() ? (o.gauntletCrownPenaltyFxUntil || 0) : 0); + } if (botOut) ctx.restore(); } else { if (shouldGauntletCrownHeistSkipAvatarDrawPlay(me, true, 0, 0)) return; const axMe = safeX(me.x); const ayMe = safeY(me.y); - if (playQuizAvatarHideWhileOverlappingAnswerZone(me, myId, axMe, ayMe)) return; + const ghostActiveMe = isQuizWrongGhostActiveForEnt(myId); + if (!ghostActiveMe && playQuizAvatarHideWhileOverlappingAnswerZone(me, myId, axMe, ayMe)) return; if (isJumpSurvive() && jumpSurviveEliminated) ctx.save(); if (isJumpSurvive() && jumpSurviveEliminated) ctx.globalAlpha = 0.4; const faceDirMe = isGauntletFaceRightMapMno9kb07() ? 'right' : me.direction; - drawAvatar(axMe, ayMe, true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me), isGauntletCrownHeistMapPlay() ? (me.gauntletCrownPenaltyFxUntil || 0) : 0); - drawQuizWrongGhostOverlayPlay(axMe, ayMe, myId); + if (ghostActiveMe) { + drawQuizWrongGhostOverlayPlay(axMe, ayMe, myId, me.nickname + meTag); + } else { + drawAvatar(axMe, ayMe, true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me), isGauntletCrownHeistMapPlay() ? (me.gauntletCrownPenaltyFxUntil || 0) : 0); + } if (isJumpSurvive() && jumpSurviveEliminated) ctx.restore(); } }); diff --git a/www/html/Game/public/js/room-lobby.js b/www/html/Game/public/js/room-lobby.js index b555dee..e77d9ec 100644 --- a/www/html/Game/public/js/room-lobby.js +++ b/www/html/Game/public/js/room-lobby.js @@ -2750,7 +2750,22 @@ socket.on('peer-display-name', applyPeerDisplayNameUpdate); socket.on('connect', () => { - socket.emit('join-space', { spaceId, nickname: getProfileDisplayName(), characterId: getStoredCharacterId() }, (res) => { + /* ส่งค่าสีที่เลือกใน Main-Lobby ติดไปด้วย — server จะใช้ค่านี้ถ้ายังว่าง (กันโชว์สีแดง default ตอนเข้าใหม่) */ + var savedThemeIdx = null; + var savedSkinIdx = null; + try { + var t = parseInt(localStorage.getItem('lobbyThemeColor'), 10); + var s = parseInt(localStorage.getItem('lobbySkinTone'), 10); + if (t >= 1 && t <= 8) savedThemeIdx = t; + if (s >= 1 && s <= 3) savedSkinIdx = s; + } catch (e) { /* ignore */ } + socket.emit('join-space', { + spaceId, + nickname: getProfileDisplayName(), + characterId: getStoredCharacterId(), + desiredLobbyColorThemeIndex: savedThemeIdx, + desiredLobbySkinToneIndex: savedSkinIdx, + }, (res) => { if (!res || !res.ok) { var joinErr = (res && res.error) || 'เข้าไม่ได้'; if (/เริ่มคดี|ไม่รับผู้เล่น/.test(joinErr)) { diff --git a/www/html/Game/public/play.html b/www/html/Game/public/play.html index e484740..bd45c05 100644 --- a/www/html/Game/public/play.html +++ b/www/html/Game/public/play.html @@ -3214,7 +3214,7 @@ - +
v —
diff --git a/www/html/Game/public/room-lobby.html b/www/html/Game/public/room-lobby.html index e623398..9e852f7 100644 --- a/www/html/Game/public/room-lobby.html +++ b/www/html/Game/public/room-lobby.html @@ -1576,7 +1576,7 @@ - +
v —
diff --git a/www/html/Game/server.js b/www/html/Game/server.js index 7292f70..e62b3ac 100644 --- a/www/html/Game/server.js +++ b/www/html/Game/server.js @@ -4639,7 +4639,7 @@ function spaceAllowsQuizCarryLobbyRelaxed(space) { } io.on('connection', (socket) => { - socket.on('join-space', ({ spaceId, nickname, characterId, playMapId }, cb) => { + socket.on('join-space', ({ spaceId, nickname, characterId, playMapId, desiredLobbyColorThemeIndex, desiredLobbySkinToneIndex }, cb) => { const space = spaces.get(spaceId); if (!space || !space.mapData) return cb && cb({ ok: false, error: 'ไม่พบห้อง' }); const maxPlayers = space.maxPlayers || 10; @@ -4674,11 +4674,20 @@ io.on('connection', (socket) => { const spawnJoinOrder = space.peers.size; const spawnPt = pickSpawnForJoin(mdJoin, spawnJoinOrder); const bbStartBalloons = Math.max(1, Math.min(12, Math.floor(Number(mdJoin.balloonBossBalloonsPerPlayer)) || 3)); + /* ใช้สีที่ client เลือกไว้ใน Main-Lobby ถ้าผ่านการตรวจสอบ + ยังไม่ถูกใช้ในห้องนี้ */ + let chosenThemeIdx = parseInt(desiredLobbyColorThemeIndex, 10); + if (!(chosenThemeIdx >= 1 && chosenThemeIdx <= LOBBY_THEME_COUNT) || getTakenLobbyThemeIndices(space).has(chosenThemeIdx)) { + chosenThemeIdx = pickFreeLobbyThemeIndex(space); + } + let chosenSkinIdx = parseInt(desiredLobbySkinToneIndex, 10); + if (!(chosenSkinIdx >= 1 && chosenSkinIdx <= LOBBY_SKIN_COUNT)) { + chosenSkinIdx = pickLobbySkinIndexForSlot(spawnJoinOrder); + } const peer = { id: socket.id, x: +spawnPt.x, y: +spawnPt.y, direction: 'down', nickname: nickname || 'ผู้เล่น', ready: false, characterId: characterId || null, voiceMicOn: true, spawnJoinOrder, - lobbyColorThemeIndex: pickFreeLobbyThemeIndex(space), - lobbySkinToneIndex: pickLobbySkinIndexForSlot(spawnJoinOrder), + lobbyColorThemeIndex: chosenThemeIdx, + lobbySkinToneIndex: chosenSkinIdx, gauntletJumpTicks: 0, gauntletScore: 0, gauntletJumpPending: false, gauntletEliminated: false, spaceShooterScore: 0, balloonBossScore: 0, balloonBossBossDmg: 0, balloonBossBalloons: mdJoin.gameType === 'balloon_boss' ? bbStartBalloons : 5, balloonBossEliminated: false, };