update flow design 1.3
This commit is contained in:
@@ -358,18 +358,24 @@
|
||||
#testimony-overlay .es3-cards {
|
||||
left: calc(200 / var(--es3-w) * 100%);
|
||||
top: calc(635 / var(--es3-h) * 100%);
|
||||
width: calc(800 / var(--es3-w) * 100%);
|
||||
height: calc(380 / var(--es3-h) * 100%);
|
||||
/* ขยายให้พอ 3 ใบเรียงพอดี = 252*3 + 22*2 = 800px stage */
|
||||
width: 800px;
|
||||
height: 380px;
|
||||
display: flex;
|
||||
gap: calc(22 / var(--es3-w) * 100%);
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 22px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#testimony-overlay .es3-card {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
/* คงสัดส่วน portrait — ไม่ยืดเต็มจอเมื่อเก็บได้แค่ใบเดียว; 252×380 stage px ตรงตามดีไซน์ */
|
||||
flex: 0 0 252px;
|
||||
width: 252px;
|
||||
min-width: 252px;
|
||||
max-width: 252px;
|
||||
height: 380px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #cfd5dd 0%, #9ea7b4 100%);
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
|
||||
@@ -718,16 +724,18 @@
|
||||
}
|
||||
|
||||
#testimony-overlay .es-reveal-wrap .es-players {
|
||||
left: 120px;
|
||||
top: 300px;
|
||||
width: 1680px;
|
||||
height: 650px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
column-gap: 24px;
|
||||
padding: 0 70px;
|
||||
left: 200px;
|
||||
top: 290px;
|
||||
width: 1520px;
|
||||
height: 720px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
column-gap: 32px;
|
||||
row-gap: 18px;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
align-items: start;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
@@ -771,19 +779,19 @@
|
||||
|
||||
#testimony-overlay .es-reveal-wrap .es-card {
|
||||
position: relative;
|
||||
width: 220px;
|
||||
max-width: 220px;
|
||||
height: 275px;
|
||||
width: 252px;
|
||||
min-width: 252px;
|
||||
max-width: 252px;
|
||||
height: 320px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, #cfd5dd 0%, #b2bac4 100%);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #cfd5dd 0%, #9ea7b4 100%);
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
color: #1b2030;
|
||||
border: 3px solid rgba(150, 200, 255, 0.7);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#testimony-overlay .es-reveal-wrap .es-card.rarity-common {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 413 KiB |
@@ -813,8 +813,8 @@
|
||||
const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {};
|
||||
let lineInArt = Number(raw.finishLineInFinishArtFrac);
|
||||
if (!Number.isFinite(lineInArt)) {
|
||||
/* mno9kb07: จบที่ประตูตู้เซฟ (ขวาของ finish art) ไม่ใช่แถบ FINISH ด้านซ้าย */
|
||||
lineInArt = gauntletCrownRunwayBgMapEligiblePlay() ? 0.72 : 0.12;
|
||||
/* mno9kb07: จบที่ "แถบ FINISH" สีเหลือง (กลาง-ซ้ายของ finish art) — ไม่ใช่ตู้เซฟ */
|
||||
lineInArt = gauntletCrownRunwayBgMapEligiblePlay() ? 0.22 : 0.12;
|
||||
}
|
||||
lineInArt = Math.max(0, Math.min(0.92, lineInArt));
|
||||
const finishLineWorldX = g.finishStart + g.w4Safe * lineInArt;
|
||||
@@ -831,13 +831,15 @@
|
||||
legacy = Math.max(0.08, Math.min(0.96, legacy));
|
||||
return Math.max(0.12, Math.min(0.62, 0.14 + legacy * 0.42));
|
||||
}
|
||||
return gauntletCrownRunwayBgMapEligiblePlay() ? 0.5 : 0.34;
|
||||
/* mno9kb07: หยุดเลื่อนที่ ~0.42 (เส้น FINISH เลื่อนมาอยู่กลางจอ) */
|
||||
return gauntletCrownRunwayBgMapEligiblePlay() ? 0.42 : 0.34;
|
||||
}
|
||||
|
||||
function gauntletCrownRunwayObstacleFadeStartViewportFracPlay() {
|
||||
const raw = mapData && mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {};
|
||||
let v = Number(raw.obstacleFadeStartViewportFrac);
|
||||
if (!Number.isFinite(v)) v = 0.9;
|
||||
/* mno9kb07: เริ่มจางอุปสรรคให้เร็วขึ้น (1.0 = ตอนเส้น FINISH ยังอยู่ขอบขวา) เพื่อให้อุปสรรคหายก่อนถึงเส้นชัย */
|
||||
if (!Number.isFinite(v)) v = gauntletCrownRunwayBgMapEligiblePlay() ? 1.05 : 0.9;
|
||||
return Math.max(0.45, Math.min(1.15, v));
|
||||
}
|
||||
|
||||
@@ -2695,6 +2697,41 @@
|
||||
let playLiveQuizScores = {};
|
||||
/** เกมถูก/ผิด (multiplayer): ใครเคยตอบผิดในเซสชันนี้ — ใช้โชว์แสตมป์ในสรุปภารกิจ mng8a80o */
|
||||
let playQuizEverWrong = {};
|
||||
/** ผีตามตัวเมื่อตอบผิด — ใช้เฉพาะแมป mng8a80o (เป็นสัญลักษณ์/บทลงโทษเชิงสนุก) */
|
||||
const QUIZ_WRONG_GHOST_URL = BASE + '/img/QUESTION/ghost/ghost-front.png';
|
||||
let quizWrongGhostImg = null;
|
||||
function getQuizWrongGhostImg() {
|
||||
if (quizWrongGhostImg) return quizWrongGhostImg;
|
||||
const img = new Image();
|
||||
img.decoding = 'async';
|
||||
img.src = QUIZ_WRONG_GHOST_URL;
|
||||
quizWrongGhostImg = img;
|
||||
return img;
|
||||
}
|
||||
/** วาด "ผี" คุมหัวผู้เล่นในแมป mng8a80o เมื่อตอบผิด (ใช้กับ me/peers) */
|
||||
function drawQuizWrongGhostOverlayPlay(ax, ay, entId) {
|
||||
if (!isQuizQuestionMissionUiMapPlay()) return;
|
||||
const sid = String(entId == null ? '' : entId);
|
||||
const isMeEnt = (myId != null && sid === String(myId));
|
||||
const wrong = isMeEnt
|
||||
? !!(playQuizPlayerLocal && (playQuizPlayerLocal.cannotTrue || playQuizPlayerLocal.cannotFalse))
|
||||
: !!playQuizEverWrong[sid];
|
||||
if (!wrong) return;
|
||||
const img = getQuizWrongGhostImg();
|
||||
if (!img || !img.complete || !img.naturalWidth) return;
|
||||
/* ขนาด: ~1.6 tile (ใหญ่กว่าหัวเล็กน้อย แต่ไม่บัง avatar ทั้งตัว); ลอยเหนือหัว */
|
||||
const tile = (typeof tileSize === 'number' && tileSize > 0) ? tileSize : 32;
|
||||
const wPx = tile * 1.6;
|
||||
const ratio = img.naturalHeight / Math.max(1, img.naturalWidth);
|
||||
const hPx = wPx * ratio;
|
||||
const tBob = Math.sin(performance.now() / 280) * (tile * 0.06);
|
||||
const drawX = ax + (tile - wPx) / 2;
|
||||
const drawY = ay - hPx * 0.78 + tBob;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.92;
|
||||
ctx.drawImage(img, drawX, drawY, wPx, hPx);
|
||||
ctx.restore();
|
||||
}
|
||||
/** เกมถูก/ผิด — ตรงกับ server QUIZ_TF_POINTS_PER_CORRECT */
|
||||
const QUIZ_TF_POINTS_PER_CORRECT = 10;
|
||||
const QUIZ_TF_SCORE_PLUS_URL = BASE + '/img/QUESTION/score+.png';
|
||||
@@ -8340,6 +8377,7 @@
|
||||
return;
|
||||
}
|
||||
const tint = getPlayTintForMissionAvatar(peerId != null ? peerId : ((myId != null && myId !== '') ? myId : 'local'));
|
||||
const isMePeer = (peerId != null && myId != null && String(peerId) === String(myId));
|
||||
const out = resolvePlayDrawCharSource(characterId, 'down', nowT, false, tint, isMePeer);
|
||||
if (out && out.tagName === 'CANVAS' && out.width > 0) {
|
||||
try {
|
||||
@@ -18435,6 +18473,7 @@
|
||||
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);
|
||||
if (botOut) ctx.restore();
|
||||
} else {
|
||||
if (shouldGauntletCrownHeistSkipAvatarDrawPlay(me, true, 0, 0)) return;
|
||||
@@ -18445,6 +18484,7 @@
|
||||
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 (isJumpSurvive() && jumpSurviveEliminated) ctx.restore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4600,7 +4600,20 @@
|
||||
}
|
||||
|
||||
/* ===== ห้องสรุปหลักฐาน — การไต่สวน (เลือกหลักฐาน 2 ใบ/ปากคำ ก่อนโหวต) ===== */
|
||||
var tmState = { round: 0, members: [], picks: [], submitted: false, mode: 'select', submittedPicksById: {} };
|
||||
var tmState = { round: 0, members: [], picks: [], submitted: false, mode: 'select', submittedPicksById: {}, ownedSlots: [], requiredPicks: 2 };
|
||||
|
||||
/** สล็อตหลักฐานที่ผู้เล่นเก็บได้สำหรับ suspect ตามรอบ (เช่น [0,2]) */
|
||||
function tmOwnedSlotsForRound(round) {
|
||||
var i = Math.floor(Number(round));
|
||||
if (Number.isNaN(i) || i < 0 || i > 2) i = 0;
|
||||
var row = (myPlayerEvidence && myPlayerEvidence[i]) || [];
|
||||
var out = [];
|
||||
row.forEach(function (s) {
|
||||
var v = Math.floor(Number(s));
|
||||
if (v >= 0 && v <= 2 && out.indexOf(v) === -1) out.push(v);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
var SUSPECT_NAMES_EN = ['SOMCHAI', 'ARIN', 'JIRAPHA'];
|
||||
|
||||
function ensureTestimonyStyle() {
|
||||
@@ -4739,31 +4752,49 @@
|
||||
var card = e.target.closest('.es3-card'); if (!card || tmState.submitted) return;
|
||||
var idx = parseInt(card.getAttribute('data-card-index'), 10);
|
||||
if (!(idx >= 0)) return;
|
||||
var maxPicks = Math.max(1, Math.min(2, tmState.requiredPicks || 2));
|
||||
var pos = tmState.picks.indexOf(idx);
|
||||
if (pos >= 0) { tmState.picks.splice(pos, 1); card.classList.remove('is-selected'); }
|
||||
else { if (tmState.picks.length >= 2) return; tmState.picks.push(idx); card.classList.add('is-selected'); }
|
||||
else { if (tmState.picks.length >= maxPicks) return; tmState.picks.push(idx); card.classList.add('is-selected'); }
|
||||
es3UpdateCount();
|
||||
});
|
||||
var submitEl = ov.querySelector('#es3BtnSubmit');
|
||||
if (submitEl) submitEl.addEventListener('click', function () {
|
||||
if (tmState.picks.length !== 2 || tmState.submitted) return;
|
||||
socket.emit('testimony-submit', { picks: tmState.picks.slice(0, 2) }, function (res) {
|
||||
var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2));
|
||||
if (tmState.picks.length !== req || tmState.submitted) return;
|
||||
socket.emit('testimony-submit', { picks: tmState.picks.slice(0, req) }, function (res) {
|
||||
if (res && res.ok) {
|
||||
tmState.submitted = true;
|
||||
if (!tmState.submittedPicksById) tmState.submittedPicksById = {};
|
||||
tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, 2);
|
||||
tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, req);
|
||||
es3SetStatus('ส่งหลักฐานแล้ว — รอผู้เล่นอื่น...');
|
||||
var sub = document.getElementById('es3BtnSubmit'); if (sub) sub.disabled = true;
|
||||
} else appendLobbySystemChat('— ส่งหลักฐานไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
|
||||
});
|
||||
});
|
||||
var openEl = ov.querySelector('#es3BtnOpen');
|
||||
if (openEl) {
|
||||
openEl.addEventListener('click', function () {
|
||||
function autoSubmitThenReveal() {
|
||||
var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2));
|
||||
var doReveal = function () {
|
||||
socket.emit('testimony-reveal', {}, function (res) { if (!(res && res.ok)) appendLobbySystemChat('— เปิดเผยไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : '')); });
|
||||
});
|
||||
};
|
||||
/* ถ้ายังไม่ submit แต่เลือกครบตาม required แล้ว — submit ให้ก่อน reveal */
|
||||
if (!tmState.submitted && tmState.ownedSlots.length > 0 && tmState.picks.length === req) {
|
||||
socket.emit('testimony-submit', { picks: tmState.picks.slice(0, req) }, function (res) {
|
||||
if (res && res.ok) {
|
||||
tmState.submitted = true;
|
||||
tmState.submittedPicksById[socket.id] = tmState.picks.slice(0, req);
|
||||
}
|
||||
doReveal();
|
||||
});
|
||||
} else {
|
||||
doReveal();
|
||||
}
|
||||
}
|
||||
if (openEl) {
|
||||
openEl.addEventListener('click', autoSubmitThenReveal);
|
||||
openEl.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.click(); }
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); autoSubmitThenReveal(); }
|
||||
});
|
||||
}
|
||||
var readyEl = ov.querySelector('#esBtnReady');
|
||||
@@ -4788,8 +4819,9 @@
|
||||
}
|
||||
|
||||
function es3UpdateCount() {
|
||||
var el = document.getElementById('es3-count'); if (el) el.textContent = 'เลือกหลักฐาน ' + tmState.picks.length + '/2';
|
||||
var sub = document.getElementById('es3BtnSubmit'); if (sub) sub.disabled = (tmState.picks.length !== 2 || tmState.submitted);
|
||||
var req = Math.max(1, Math.min(2, tmState.requiredPicks || 2));
|
||||
var el = document.getElementById('es3-count'); if (el) el.textContent = 'เลือกหลักฐาน ' + tmState.picks.length + '/' + req;
|
||||
var sub = document.getElementById('es3BtnSubmit'); if (sub) sub.disabled = (tmState.picks.length !== req || tmState.submitted);
|
||||
}
|
||||
|
||||
function esRevealCardEl(c, linkName, cardIdx) {
|
||||
@@ -4924,6 +4956,8 @@
|
||||
|
||||
function openTestimony(data) {
|
||||
if (typeof closeSuspectOverlay === 'function') closeSuspectOverlay();
|
||||
/* ซิงค์หลักฐานของตัวเองจาก server ก่อน (กันรอบที่ play จบใหม่ๆ แล้ว state ใน room-lobby ยังไม่อัปเดต) */
|
||||
if (data && Array.isArray(data.myPlayerEvidence)) applyMyPlayerEvidence(data.myPlayerEvidence);
|
||||
var ov = ensureTestimonyOverlay();
|
||||
tmState.round = (data && typeof data.round === 'number') ? data.round : 0;
|
||||
tmState.members = (data && Array.isArray(data.members)) ? data.members : [];
|
||||
@@ -4931,6 +4965,9 @@
|
||||
tmState.submitted = false;
|
||||
tmState.submittedPicksById = {};
|
||||
tmState.mode = 'select';
|
||||
tmState.ownedSlots = tmOwnedSlotsForRound(tmState.round);
|
||||
/* ต้องเลือกหลักฐาน = min(2, จำนวนที่เก็บได้) — ถ้าได้ 1 ใบ ส่ง 1 ใบพอ */
|
||||
tmState.requiredPicks = Math.max(1, Math.min(2, tmState.ownedSlots.length));
|
||||
var SC = '/Main-Lobby/IMAGE/Showcard/';
|
||||
var CS = '/Main-Lobby/IMAGE/Choose%20a%20suspect/';
|
||||
var sd = tmSuspectData(tmState.round);
|
||||
@@ -4953,7 +4990,26 @@
|
||||
}
|
||||
es3SetStatus('กำลังรอ..ผู้เล่นเลือกหลักฐาน');
|
||||
var cardsRoot = document.getElementById('es3-cards'); cardsRoot.textContent = '';
|
||||
(sd.cards || []).forEach(function (c, i) { cardsRoot.appendChild(es3EvidenceCardEl(c, sd.linkName, i, true)); });
|
||||
/* แสดงเฉพาะการ์ดที่ผู้เล่นเก็บได้จริง (ตามสล็อตจาก myPlayerEvidence) */
|
||||
if (tmState.ownedSlots.length === 0) {
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'es3-empty-hint';
|
||||
empty.textContent = 'ยังไม่มีหลักฐานของ ' + (sd.linkName || ('ผู้ต้องสงสัย ' + (tmState.round + 1))) + ' — รอเพื่อนเลือกหลักฐาน';
|
||||
cardsRoot.appendChild(empty);
|
||||
/* auto-submit ว่างเพื่อไม่บล็อกรอบ */
|
||||
socket.emit('testimony-submit', { picks: [] }, function (res) {
|
||||
if (res && res.ok) {
|
||||
tmState.submitted = true;
|
||||
tmState.submittedPicksById[socket.id] = [];
|
||||
es3SetStatus('ไม่มีหลักฐาน — ข้ามอัตโนมัติ');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tmState.ownedSlots.forEach(function (slot) {
|
||||
var c = (sd.cards || [])[slot];
|
||||
if (c) cardsRoot.appendChild(es3EvidenceCardEl(c, sd.linkName, slot, true));
|
||||
});
|
||||
}
|
||||
es3UpdateCount();
|
||||
es3RenderMembers([], [], false);
|
||||
ov.classList.remove('is-hidden');
|
||||
|
||||
@@ -3214,7 +3214,7 @@
|
||||
<script src="/app-base.js?v=2"></script>
|
||||
<script src="/Game/socket.io/socket.io.js"></script>
|
||||
<script src="js/version.js?v=0.0306"></script>
|
||||
<script src="js/play.js?v=0.0465"></script>
|
||||
<script src="js/play.js?v=0.0468"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<link rel="stylesheet" href="css/profile-popup.css?v=6">
|
||||
<link rel="stylesheet" href="css/customize-popup.css?v=42">
|
||||
<link rel="stylesheet" href="css/leaderboard-popup.css?v=2">
|
||||
<link rel="stylesheet" href="css/testimony-overlay.css?v=8">
|
||||
<link rel="stylesheet" href="css/testimony-overlay.css?v=11">
|
||||
<link rel="stylesheet" href="css/evidence-view-overlay.css?v=12">
|
||||
<style>
|
||||
html, body.room-lobby-page { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; min-height: 100%; }
|
||||
@@ -1576,7 +1576,7 @@
|
||||
<script src="js/display-name.js?v=2"></script>
|
||||
<script src="js/version.js?v=0.0122"></script>
|
||||
<script src="js/customize-popup.js?v=31" data-customize-triggers="" data-customize-asset-base="img/03-5-Customize"></script>
|
||||
<script src="js/room-lobby.js?v=0.0243"></script>
|
||||
<script src="js/room-lobby.js?v=0.0246"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+118
-13
@@ -2088,12 +2088,14 @@ function emitTrialVoteUpdate(sid, space) {
|
||||
io.to(sid).emit('trial-vote-update', {
|
||||
counts,
|
||||
voted: Object.keys(votes).length,
|
||||
totalPlayers: space.peers ? space.peers.size : 0,
|
||||
totalPlayers: testimonyTotalParticipants(space),
|
||||
});
|
||||
}
|
||||
|
||||
function computeAndEmitTrialResult(sid, space) {
|
||||
space.trialPhase = 'revealed';
|
||||
/* กันบอทบางตัวยังไม่โหวต — โหวตให้หมดก่อนรวมผล */
|
||||
autoVoteBotsTrial(space);
|
||||
const counts = [0, 0, 0];
|
||||
const votes = space.trialVotes || {};
|
||||
const culprit = (typeof space.culpritIndex === 'number') ? space.culpritIndex : 0;
|
||||
@@ -2109,7 +2111,12 @@ function computeAndEmitTrialResult(sid, space) {
|
||||
});
|
||||
const winnerNames = winners.map((id) => {
|
||||
const p = space.peers.get(id);
|
||||
return (p && p.nickname) ? p.nickname : String(id).slice(0, 6);
|
||||
if (p && p.nickname) return p.nickname;
|
||||
if (typeof id === 'string' && id.indexOf(TESTIMONY_BOT_PREFIX) === 0) {
|
||||
const slot = parseInt(id.slice(TESTIMONY_BOT_PREFIX.length), 10);
|
||||
return Number.isFinite(slot) ? ('บอท ' + (slot + 1)) : 'บอท';
|
||||
}
|
||||
return String(id).slice(0, 6);
|
||||
});
|
||||
io.to(sid).emit('trial-result', {
|
||||
culpritIndex: culprit,
|
||||
@@ -2121,23 +2128,75 @@ function computeAndEmitTrialResult(sid, space) {
|
||||
}
|
||||
|
||||
/* ===== ห้องสรุปหลักฐาน — การไต่สวน (ปากคำ 3 รอบ ก่อนโหวต) ===== */
|
||||
/** id บอทใน testimony/trial — สอดคล้องกับ slot บน lobby (__lobby_bot_0..N) */
|
||||
const TESTIMONY_BOT_PREFIX = '__lobby_bot_';
|
||||
|
||||
function testimonyBotIds(space) {
|
||||
const n = effectiveBotSlotCount(space);
|
||||
const out = [];
|
||||
for (let i = 0; i < n; i++) out.push(TESTIMONY_BOT_PREFIX + i);
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildTestimonyMembers(space) {
|
||||
const arr = [];
|
||||
space.peers.forEach((p, id) => {
|
||||
arr.push({ id, nickname: (p && p.nickname) ? p.nickname : String(id).slice(0, 6), isHost: id === space.hostId });
|
||||
arr.push({ id, nickname: (p && p.nickname) ? p.nickname : String(id).slice(0, 6), isHost: id === space.hostId, isBot: false });
|
||||
});
|
||||
testimonyBotIds(space).forEach((bid, i) => {
|
||||
arr.push({ id: bid, nickname: 'บอท ' + (i + 1), isHost: false, isBot: true });
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
|
||||
/** จำนวนผู้เล่นทั้งหมดที่ต้อง submit/vote (รวมบอท) */
|
||||
function testimonyTotalParticipants(space) {
|
||||
return (space.peers ? space.peers.size : 0) + effectiveBotSlotCount(space);
|
||||
}
|
||||
|
||||
/** จำนวนการ์ดที่บอทควรเลือก — เท่ากับ max ของ "ผู้เล่นจริง" ในรอบ (กันบอทเลือก 2 ขณะคนเลือก 1) */
|
||||
function botTestimonyPickCount(space) {
|
||||
const round = typeof space.testimonyRound === 'number' ? space.testimonyRound : 0;
|
||||
let maxOwned = 0;
|
||||
space.peers.forEach((_p, pid) => {
|
||||
const ev = clonePlayerEvidence(space, pid);
|
||||
const len = Array.isArray(ev[round]) ? ev[round].length : 0;
|
||||
if (len > maxOwned) maxOwned = len;
|
||||
});
|
||||
/* ถ้าผู้เล่นจริงไม่มีหลักฐานเลย — ให้บอทเลือก 1 ใบ (กันรอบติด) */
|
||||
if (maxOwned === 0) return 1;
|
||||
return Math.max(1, Math.min(2, maxOwned));
|
||||
}
|
||||
|
||||
/** สุ่ม picks ให้บอทในรอบ testimony ปัจจุบัน — จำนวนใบเท่ากับผู้เล่นจริง */
|
||||
function autoSubmitBotTestimonyPicks(space) {
|
||||
if (!space.testimonySubmitted) space.testimonySubmitted = {};
|
||||
const slots = [0, 1, 2];
|
||||
const want = botTestimonyPickCount(space);
|
||||
testimonyBotIds(space).forEach((bid) => {
|
||||
if (space.testimonySubmitted[bid] != null) return;
|
||||
const pool = slots.slice();
|
||||
const picks = [];
|
||||
while (picks.length < want && pool.length) {
|
||||
const idx = Math.floor(Math.random() * pool.length);
|
||||
picks.push(pool.splice(idx, 1)[0]);
|
||||
}
|
||||
picks.sort((a, b) => a - b);
|
||||
space.testimonySubmitted[bid] = picks;
|
||||
});
|
||||
}
|
||||
|
||||
function emitTestimonyOpen(sid, space) {
|
||||
space.testimonySubmitted = {};
|
||||
space.testimonyReadyNext = {};
|
||||
space.testimonyRevealed = false;
|
||||
/* บอทส่งหลักฐานทันทีเมื่อเปิด round */
|
||||
autoSubmitBotTestimonyPicks(space);
|
||||
const base = {
|
||||
round: space.testimonyRound,
|
||||
suspectIndex: space.testimonyRound,
|
||||
members: buildTestimonyMembers(space),
|
||||
totalPlayers: space.peers.size,
|
||||
totalPlayers: testimonyTotalParticipants(space),
|
||||
hostId: space.hostId,
|
||||
cardMinigames: space.suspectCardMinigames || [],
|
||||
};
|
||||
@@ -2150,22 +2209,47 @@ function emitTestimonyOpen(sid, space) {
|
||||
suspectProgress: playerSuspectProgressFromEvidence(space, pid),
|
||||
});
|
||||
});
|
||||
/* ถ้ามีแต่บอท (ไม่มีผู้เล่นจริง) ก็เปิด reveal เลย — แต่ปกติจะมีคนกด reveal เอง */
|
||||
}
|
||||
|
||||
function emitTestimonyStatus(sid, space) {
|
||||
io.to(sid).emit('testimony-status', {
|
||||
round: space.testimonyRound,
|
||||
submitted: Object.keys(space.testimonySubmitted || {}),
|
||||
totalPlayers: space.peers.size,
|
||||
totalPlayers: testimonyTotalParticipants(space),
|
||||
});
|
||||
}
|
||||
|
||||
function doTestimonyReveal(sid, space) {
|
||||
space.testimonyRevealed = true;
|
||||
/* กันกรณีบอทยังไม่ submit (เผื่อ edge case) */
|
||||
autoSubmitBotTestimonyPicks(space);
|
||||
/* บอทพร้อมไปรอบถัดไปทันที (auto-ready) */
|
||||
if (!space.testimonyReadyNext) space.testimonyReadyNext = {};
|
||||
testimonyBotIds(space).forEach((bid) => { space.testimonyReadyNext[bid] = true; });
|
||||
io.to(sid).emit('testimony-reveal', {
|
||||
round: space.testimonyRound,
|
||||
picks: space.testimonySubmitted || {},
|
||||
members: buildTestimonyMembers(space),
|
||||
ready: Object.keys(space.testimonyReadyNext),
|
||||
totalPlayers: testimonyTotalParticipants(space),
|
||||
});
|
||||
}
|
||||
|
||||
/** สุ่มโหวตชี้คนร้ายให้บอท — สมจริงๆ ก็คือเลือกใครก็ได้ใน 3 คน (มี bias ให้ตัวร้ายจริง 40% เพื่อให้สนุก) */
|
||||
function autoVoteBotsTrial(space) {
|
||||
if (!space.trialVotes) space.trialVotes = {};
|
||||
const culprit = (typeof space.culpritIndex === 'number') ? space.culpritIndex : Math.floor(Math.random() * 3);
|
||||
testimonyBotIds(space).forEach((bid) => {
|
||||
if (space.trialVotes[bid] != null) return;
|
||||
/* 40% โหวตถูก, 60% โหวตสุ่ม */
|
||||
let pick;
|
||||
if (Math.random() < 0.4) {
|
||||
pick = culprit;
|
||||
} else {
|
||||
pick = Math.floor(Math.random() * 3);
|
||||
}
|
||||
space.trialVotes[bid] = pick;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2175,6 +2259,8 @@ function advanceTestimony(sid, space) {
|
||||
space.testimonyActive = false;
|
||||
space.trialPhase = 'voting';
|
||||
space.trialVotes = {};
|
||||
/* บอทโหวตทันทีเมื่อเข้าหน้าโหวต */
|
||||
autoVoteBotsTrial(space);
|
||||
space.peers.forEach((_p, pid) => {
|
||||
const sock = io.sockets.sockets.get(pid);
|
||||
if (!sock) return;
|
||||
@@ -4902,7 +4988,7 @@ io.on('connection', (socket) => {
|
||||
reply({ ok: true });
|
||||
});
|
||||
|
||||
// ผู้เล่นส่งหลักฐาน 2 ใบในปากคำปัจจุบัน
|
||||
// ผู้เล่นส่งหลักฐาน 1–2 ใบในปากคำปัจจุบัน — ใบที่ส่งต้องเป็นใบที่ตัวเองเก็บได้จริง
|
||||
socket.on('testimony-submit', (data, cb) => {
|
||||
const reply = typeof cb === 'function' ? cb : () => {};
|
||||
const sid = socket.data.spaceId;
|
||||
@@ -4911,13 +4997,30 @@ io.on('connection', (socket) => {
|
||||
if (!space.testimonyActive) return reply({ ok: false, error: 'ไม่อยู่ในขั้นไต่สวน' });
|
||||
if (space.testimonyRevealed) return reply({ ok: false, error: 'ปากคำนี้เปิดเผยแล้ว' });
|
||||
const raw = (data && Array.isArray(data.picks)) ? data.picks : [];
|
||||
const owned = normalizeEvidenceSlotList(clonePlayerEvidence(space, socket.id)[space.testimonyRound] || []);
|
||||
const required = Math.max(1, Math.min(2, owned.length));
|
||||
const picks = [];
|
||||
raw.forEach((n) => { const v = Math.floor(Number(n)); if (v >= 0 && v <= 8 && picks.indexOf(v) === -1) picks.push(v); });
|
||||
if (picks.length !== 2) return reply({ ok: false, error: 'ต้องเลือกหลักฐาน 2 ใบ' });
|
||||
raw.forEach((n) => {
|
||||
const v = Math.floor(Number(n));
|
||||
if (v >= 0 && v <= 2 && picks.indexOf(v) === -1 && owned.indexOf(v) >= 0) picks.push(v);
|
||||
});
|
||||
if (owned.length === 0) {
|
||||
/* ไม่มีหลักฐานของ suspect รอบนี้ — ข้ามได้ (auto-submit ว่าง) */
|
||||
if (!space.testimonySubmitted) space.testimonySubmitted = {};
|
||||
space.testimonySubmitted[socket.id] = [];
|
||||
emitTestimonyStatus(sid, space);
|
||||
const totalP0 = testimonyTotalParticipants(space);
|
||||
if (Object.keys(space.testimonySubmitted).length >= totalP0 && totalP0 > 0) doTestimonyReveal(sid, space);
|
||||
return reply({ ok: true, skipped: true });
|
||||
}
|
||||
if (picks.length !== required) {
|
||||
return reply({ ok: false, error: 'ต้องเลือกหลักฐาน ' + required + ' ใบ (ตามจำนวนที่เก็บได้)' });
|
||||
}
|
||||
if (!space.testimonySubmitted) space.testimonySubmitted = {};
|
||||
space.testimonySubmitted[socket.id] = picks.slice(0, 2);
|
||||
space.testimonySubmitted[socket.id] = picks.slice(0, required);
|
||||
emitTestimonyStatus(sid, space);
|
||||
if (Object.keys(space.testimonySubmitted).length >= space.peers.size && space.peers.size > 0) doTestimonyReveal(sid, space);
|
||||
const totalP = testimonyTotalParticipants(space);
|
||||
if (Object.keys(space.testimonySubmitted).length >= totalP && totalP > 0) doTestimonyReveal(sid, space);
|
||||
reply({ ok: true });
|
||||
});
|
||||
|
||||
@@ -4942,8 +5045,9 @@ io.on('connection', (socket) => {
|
||||
if (!space.testimonyActive || !space.testimonyRevealed) return reply({ ok: false, error: 'ยังเปิดเผยไม่ได้' });
|
||||
if (!space.testimonyReadyNext) space.testimonyReadyNext = {};
|
||||
space.testimonyReadyNext[socket.id] = true;
|
||||
io.to(sid).emit('testimony-ready-update', { ready: Object.keys(space.testimonyReadyNext), totalPlayers: space.peers.size });
|
||||
if (Object.keys(space.testimonyReadyNext).length >= space.peers.size && space.peers.size > 0) advanceTestimony(sid, space);
|
||||
const totalRN = testimonyTotalParticipants(space);
|
||||
io.to(sid).emit('testimony-ready-update', { ready: Object.keys(space.testimonyReadyNext), totalPlayers: totalRN });
|
||||
if (Object.keys(space.testimonyReadyNext).length >= totalRN && totalRN > 0) advanceTestimony(sid, space);
|
||||
reply({ ok: true });
|
||||
});
|
||||
|
||||
@@ -4959,7 +5063,8 @@ io.on('connection', (socket) => {
|
||||
space.trialVotes = space.trialVotes || {};
|
||||
space.trialVotes[socket.id] = idx;
|
||||
emitTrialVoteUpdate(sid, space);
|
||||
if (Object.keys(space.trialVotes).length >= space.peers.size && space.peers.size > 0) {
|
||||
const totalTV = testimonyTotalParticipants(space);
|
||||
if (Object.keys(space.trialVotes).length >= totalTV && totalTV > 0) {
|
||||
computeAndEmitTrialResult(sid, space);
|
||||
}
|
||||
reply({ ok: true, index: idx });
|
||||
|
||||
Reference in New Issue
Block a user