diff --git a/www/html/Admin/api/leaderboard.php b/www/html/Admin/api/leaderboard.php index 503b879..a8b2233 100644 --- a/www/html/Admin/api/leaderboard.php +++ b/www/html/Admin/api/leaderboard.php @@ -19,15 +19,44 @@ if ($limit > 200) $limit = 200; $caseId = preg_replace('/[^0-9]/', '', (string) ($_GET['caseId'] ?? '')); +/* ชื่อคดี (caseId -> name) จาก Game/data/case-media.json */ +function lb_case_names(): array +{ + $path = __DIR__ . '/../../Game/data/case-media.json'; + if (!is_file($path)) return []; + $j = json_decode((string) @file_get_contents($path), true); + $cases = (is_array($j) && isset($j['cases']) && is_array($j['cases'])) ? $j['cases'] : []; + $out = []; + foreach ($cases as $cid => $c) { + $nm = is_array($c) ? trim((string) ($c['name'] ?? '')) : ''; + if ($nm !== '') $out[(string) $cid] = $nm; + } + return $out; +} +$caseNames = lb_case_names(); + $store = read_store(); $accounts = (isset($store['accounts']) && is_array($store['accounts'])) ? $store['accounts'] : []; $rows = []; foreach ($accounts as $a) { + $bestCaseId = ''; if ($caseId !== '') { + /* เจาะจงคดี */ $score = max(0, (int) (($a['scoreByCase'][$caseId] ?? 0))); + $bestCaseId = $caseId; } else { - $score = max(0, (int) ($a['score'] ?? 0)); + /* กระดานรวม — คะแนนสูงสุดของ user จากคดีไหนก็ได้ */ + $score = 0; + $sbc = (isset($a['scoreByCase']) && is_array($a['scoreByCase'])) ? $a['scoreByCase'] : []; + foreach ($sbc as $cid => $sc) { + $sc = max(0, (int) $sc); + if ($sc > $score) { $score = $sc; $bestCaseId = (string) $cid; } + } + if ($score <= 0) { /* legacy: ไม่มีคะแนนรายคดี ใช้คะแนนรวม (ไม่มีชื่อคดี) */ + $score = max(0, (int) ($a['score'] ?? 0)); + $bestCaseId = ''; + } } if ($score <= 0) { continue; @@ -39,6 +68,8 @@ foreach ($accounts as $a) { 'name' => $name, 'score' => $score, 'coins' => max(0, (int) ($a['coins'] ?? 0)), + 'caseId' => $bestCaseId, + 'caseName' => ($bestCaseId !== '' && isset($caseNames[$bestCaseId])) ? $caseNames[$bestCaseId] : '', 'key' => (string) ($a['providerUserId'] ?? ''), ]; } @@ -57,7 +88,7 @@ $myKey = trim((string) ($_GET['playerKey'] ?? '')); if ($myKey !== '') { for ($i = 0; $i < $total; $i++) { if ($rows[$i]['key'] === $myKey) { - $me = ['rank' => $i + 1, 'name' => $rows[$i]['name'], 'score' => $rows[$i]['score'], 'coins' => $rows[$i]['coins']]; + $me = ['rank' => $i + 1, 'name' => $rows[$i]['name'], 'score' => $rows[$i]['score'], 'coins' => $rows[$i]['coins'], 'caseId' => $rows[$i]['caseId'], 'caseName' => $rows[$i]['caseName']]; break; } } @@ -71,6 +102,8 @@ for ($i = 0; $i < $n; $i++) { 'name' => $rows[$i]['name'], 'score' => $rows[$i]['score'], 'coins' => $rows[$i]['coins'], + 'caseId' => $rows[$i]['caseId'], + 'caseName' => $rows[$i]['caseName'], ]; } diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json index 477e73f..2ef2120 100644 --- a/www/html/Admin/private/store.json +++ b/www/html/Admin/private/store.json @@ -38,9 +38,9 @@ "providerUserId": "p_1775109142385_wq7wfy1p32j", "notes": "auto: player-coins", "blocked": false, - "coins": 545, + "coins": 565, "createdAt": "2026-04-02T05:52:21+00:00", - "updatedAt": "2026-06-17T07:52:36+00:00", + "updatedAt": "2026-06-17T08:31:26+00:00", "daily": { "anchorMs": 1781197200000, "claimedDays": [ @@ -54,7 +54,7 @@ ], "lockUntilMs": 1781715600000 }, - "score": 470, + "score": 490, "scoreByCase": { "1": 10, "10": 120, @@ -63,11 +63,14 @@ "13": 20, "14": 20, "9": 40, - "12": 40, + "12": 60, "15": 20, "4": 10 }, - "lbName": "Q" + "lbName": "Q", + "achievements": { + "d1_minigame_solver": 2 + } }, { "id": "1d64c56fadb64a93eae68a1d", @@ -173,6 +176,21 @@ "coins": 0, "createdAt": "2026-06-16T05:42:47+00:00", "updatedAt": "2026-06-16T05:42:47+00:00" + }, + { + "id": "0b016b9f790d0b90d99d7ba6", + "email": "", + "displayName": "Guest", + "loginType": "guest", + "providerUserId": "achvtest_12345678", + "notes": "auto: achievements", + "blocked": false, + "coins": 0, + "achievements": { + "d1_minigame_solver": 3 + }, + "createdAt": "2026-06-17T08:22:51+00:00", + "updatedAt": "2026-06-17T08:22:51+00:00" } ] } \ No newline at end of file diff --git a/www/html/Game/server.js b/www/html/Game/server.js index 61ff41e..66039b2 100644 --- a/www/html/Game/server.js +++ b/www/html/Game/server.js @@ -1760,6 +1760,7 @@ function computeRunGradeForEvidence(space) { /* ===== แจกเหรียญตามอันดับ + คะแนนสะสม (high score) แบบ server-authoritative ===== */ const GAME_AWARD_URL = process.env.GAME_AWARD_URL || 'https://srv1361159.hstgr.cloud/Admin/api/game-award.php'; +const ACHIEVEMENTS_URL = process.env.ACHIEVEMENTS_URL || GAME_AWARD_URL.replace('game-award.php', 'achievements.php'); const GAME_AWARD_SECRET_PATH = path.join(__dirname, '..', 'Admin', 'private', 'game-award-secret.txt'); let __gameAwardSecret = null; function gameAwardSecret() { @@ -1807,6 +1808,25 @@ function submitGameAwards(awards) { } catch (e) { console.error('[game-award] throw', e && e.message); } } +/* บวก progress achievement (server-authoritative ผ่าน achievements.php — secret เดียวกับ game-award) + items: [{ playerKey, id, inc }] — หนึ่ง POST ต่อรายการ (achievements.php รับทีละ id) */ +function submitAchievementProgress(items) { + const secret = gameAwardSecret(); + if (!secret || !Array.isArray(items) || !items.length) return; + items.forEach((it) => { + if (!it || !it.playerKey || !it.id) return; + try { + fetch(ACHIEVEMENTS_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'progress', secret, playerKey: it.playerKey, id: it.id, inc: it.inc || 1 }), + }).then((r) => r.json()).then((d) => { + if (!d || !d.ok) console.error('[achv] failed', it.id, d); + }).catch((e) => console.error('[achv] error', e && e.message)); + } catch (e) { console.error('[achv] throw', e && e.message); } + }); +} + /* แจกเหรียญตามอันดับคะแนน (เฉพาะผู้รอดชีวิต ผู้ตาย = 0) + คะแนนสะสมเข้า leaderboard — ครั้งเดียวต่อรอบ */ function awardMinigameRankCoins(sid, space) { if (!space || space.coinAwardDoneForRun) return; @@ -1829,6 +1849,9 @@ function awardMinigameRankCoins(sid, space) { const awards = rows.filter((r) => r.coins > 0 && r.playerKey).map((r) => ({ playerKey: r.playerKey, nickname: r.nickname, coins: r.coins, caseId: caseId })); submitGameAwards(awards); + /* Achievement: d1 Minigame Solver — รอดมินิเกมสำเร็จ (เฉพาะผู้รอดชีวิตที่เป็นผู้เล่นจริง) */ + submitAchievementProgress(survivors.filter((r) => r.playerKey).map((r) => ({ playerKey: r.playerKey, id: 'd1_minigame_solver', inc: 1 }))); + io.to(sid).emit('minigame-coins', { gameType, coinsTable: MINIGAME_RANK_COINS, @@ -3636,6 +3659,34 @@ function computeAndEmitTrialResult(sid, space) { disruptorId, hasDisruptor: !!disruptorId, }); + + /* ===== Achievement auto-track ===== */ + try { + const achv = []; + /* a1 First Deduction (target 1) + a5 Truth Hunter (target 20) — ผู้โหวตถูก (ผู้เล่นจริงเท่านั้น) */ + winners.forEach((id) => { + const p = space.peers.get(id); + if (p && p.playerKey) { + achv.push({ playerKey: p.playerKey, id: 'a1_first_deduction', inc: 1 }); + achv.push({ playerKey: p.playerKey, id: 'a5_truth_hunter', inc: 1 }); + } + }); + /* ตัวป่วน — นับเฉพาะผู้เล่นจริง (มี playerKey) */ + if (disruptorId && space.disruptorPlayerKey) { + const dKey = space.disruptorPlayerKey; + achv.push({ playerKey: dKey, id: 'e2_the_impostor', inc: 1 }); /* target 1 */ + achv.push({ playerKey: dKey, id: 'e4_agent_of_chaos', inc: 1 }); /* target 10 */ + /* ตัวป่วนชนะ = ทีมโหวตผิดคน (ไม่จับคนร้ายตัวจริง) */ + let mv = 0; for (let k = 1; k <= 2; k++) { if (counts[k] > counts[mv]) mv = k; } + const anyV = (counts[0] + counts[1] + counts[2]) > 0; + if (anyV && mv !== culprit) { + achv.push({ playerKey: dKey, id: 'e3_master_of_doubt', inc: 1 }); /* target 1 */ + achv.push({ playerKey: dKey, id: 'e5_slippery_eel', inc: 1 }); /* target 5 */ + } + } + submitAchievementProgress(achv); + } catch (e) { console.error('[achv] trial-result', e && e.message); } + space.silencedPlayerId = null; } diff --git a/www/html/Main-Lobby/index.html b/www/html/Main-Lobby/index.html index c1723a3..1f90e16 100644 --- a/www/html/Main-Lobby/index.html +++ b/www/html/Main-Lobby/index.html @@ -231,6 +231,6 @@ - + diff --git a/www/html/Main-Lobby/lobby.js b/www/html/Main-Lobby/lobby.js index 3e0f208..729921a 100644 --- a/www/html/Main-Lobby/lobby.js +++ b/www/html/Main-Lobby/lobby.js @@ -268,14 +268,15 @@ var rank = parseInt(r.rank, 10) || (i + 1); var name = lbEscapeHtml(r.name || 'ผู้เล่น'); var score = Math.max(0, parseInt(r.score, 10) || 0); - var coins = Math.max(0, parseInt(r.coins, 10) || 0); + var caseName = lbEscapeHtml((r.caseName || '').toString()); + var caseLabel = caseName ? ('(คดี : ' + caseName + ')') : ''; var head = rank <= 3 ? '' : '' + rank + ''; html += '
  • ' + head + '
    ' + '' + name + '' + - '(เหรียญ : ' + coins + ')' + + '' + caseLabel + '' + '
    ' + '' + score + '' + '
  • ';