update 7 minigame 1.1
This commit is contained in:
@@ -892,12 +892,14 @@ code {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.input-coins {
|
||||
.input-coins,
|
||||
.input-score {
|
||||
width: 5.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.btn-coins-save {
|
||||
.btn-coins-save,
|
||||
.btn-score-save {
|
||||
font-size: 0.74rem;
|
||||
padding: 0.32rem 0.55rem;
|
||||
margin-top: 0.3rem;
|
||||
|
||||
@@ -3895,6 +3895,11 @@
|
||||
var bb = Number(data.balloonBossMissionTimeSec);
|
||||
inpT.value = String(Number.isFinite(bb) && bb > 0 ? Math.floor(bb) : 0);
|
||||
}
|
||||
var inpBp = el('mega-virus-balloons-per-player');
|
||||
if (inpBp && Object.prototype.hasOwnProperty.call(data, 'balloonBossBalloonsPerPlayer')) {
|
||||
var bp = Number(data.balloonBossBalloonsPerPlayer);
|
||||
inpBp.value = String(Number.isFinite(bp) && bp > 0 ? Math.min(12, Math.floor(bp)) : 0);
|
||||
}
|
||||
var bossInp = el('mega-virus-boss-url');
|
||||
if (bossInp && Object.prototype.hasOwnProperty.call(data, 'balloonBossBossImageUrl')) {
|
||||
bossInp.value = data.balloonBossBossImageUrl != null ? String(data.balloonBossBossImageUrl) : '';
|
||||
@@ -3939,12 +3944,16 @@
|
||||
var fbUrl = normalizeMegaVirusAssetUrl(el('mega-virus-balloon-fallback-url') && el('mega-virus-balloon-fallback-url').value
|
||||
? String(el('mega-virus-balloon-fallback-url').value).trim()
|
||||
: '');
|
||||
var balloonsPer = el('mega-virus-balloons-per-player') ? parseInt(String(el('mega-virus-balloons-per-player').value), 10) : 0;
|
||||
if (Number.isNaN(balloonsPer) || balloonsPer < 0) balloonsPer = 0;
|
||||
balloonsPer = Math.max(0, Math.min(12, balloonsPer));
|
||||
gameTimingFetch('GET')
|
||||
.then(function (data) {
|
||||
data.balloonBossMissionTimeSec = limSs;
|
||||
data.balloonBossBossImageUrl = bossUrl;
|
||||
data.balloonBossPlayerBalloonImageUrls = balloonUrls;
|
||||
data.balloonBossPlayerBalloonFallbackUrl = fbUrl;
|
||||
data.balloonBossBalloonsPerPlayer = balloonsPer;
|
||||
return gameTimingFetch('PUT', data);
|
||||
})
|
||||
.then(function (res) {
|
||||
@@ -4477,6 +4486,7 @@
|
||||
if (name === 'mega-virus') loadMegaVirusPanel();
|
||||
if (name === 'game-timing') loadGameTimingPanel();
|
||||
if (name === 'stack-game') loadStackGamePanel();
|
||||
if (name === 'highscore') loadHighscore();
|
||||
}
|
||||
|
||||
function boot() {
|
||||
@@ -4902,6 +4912,71 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ===== กระดานผู้นำ (High Score) ===== */
|
||||
function renderHighscore(rows) {
|
||||
var tbEl = el('table-highscore');
|
||||
if (!tbEl) return;
|
||||
var tb = tbEl.querySelector('tbody');
|
||||
tb.innerHTML = '';
|
||||
(rows || []).forEach(function (r) {
|
||||
var rank = parseInt(r.rank, 10) || 0;
|
||||
var score = Math.max(0, parseInt(r.score, 10) || 0);
|
||||
var coins = Math.max(0, parseInt(r.coins, 10) || 0);
|
||||
var medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : ('#' + rank);
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML =
|
||||
'<td>' + medal + '</td>' +
|
||||
'<td>' + escapeHtml(r.name || 'ผู้เล่น') + (r.blocked ? ' <span class="msg error">บล็อก</span>' : '') + '</td>' +
|
||||
'<td class="td-coins">' +
|
||||
'<input type="number" class="input-score" min="0" step="1" value="' + score + '" data-id="' + escapeAttr(r.id) + '" aria-label="คะแนน">' +
|
||||
'<button type="button" class="btn btn-primary btn-score-save" data-id="' + escapeAttr(r.id) + '">บันทึก</button>' +
|
||||
'</td>' +
|
||||
'<td>' + coins + '</td>' +
|
||||
'<td><button type="button" class="btn btn-ghost btn-score-reset" data-id="' + escapeAttr(r.id) + '">รีเซ็ต 0</button></td>';
|
||||
tb.appendChild(tr);
|
||||
});
|
||||
tb.querySelectorAll('.btn-score-save').forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
var id = b.getAttribute('data-id');
|
||||
var row = b.closest('tr');
|
||||
var inp = row ? row.querySelector('input.input-score') : null;
|
||||
var score = Math.max(0, parseInt(inp && inp.value, 10) || 0);
|
||||
api('accounts.php', { method: 'PATCH', body: { id: id, score: score } })
|
||||
.then(function () { setMsg('highscore-msg', 'บันทึกคะแนนแล้ว', 'ok'); return loadHighscore(); })
|
||||
.catch(function (err) { setMsg('highscore-msg', err.message, 'error'); });
|
||||
});
|
||||
});
|
||||
tb.querySelectorAll('.btn-score-reset').forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
var id = b.getAttribute('data-id');
|
||||
if (!confirm('รีเซ็ตคะแนนคนนี้เป็น 0?')) return;
|
||||
api('leaderboard-admin.php', { method: 'POST', body: { action: 'reset', id: id } })
|
||||
.then(function () { return loadHighscore(); })
|
||||
.catch(function (err) { setMsg('highscore-msg', err.message, 'error'); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadHighscore() {
|
||||
return api('leaderboard-admin.php').then(function (r) {
|
||||
renderHighscore(r.rows);
|
||||
var n = (r.rows && r.rows.length) || 0;
|
||||
setMsg('highscore-msg', n ? ('ผู้เล่นมีคะแนน ' + r.total + ' คน') : 'ยังไม่มีผู้เล่นทำคะแนน', '');
|
||||
}).catch(function (err) { setMsg('highscore-msg', err.message, 'error'); });
|
||||
}
|
||||
|
||||
(function bindHighscoreControls() {
|
||||
var refresh = el('btn-highscore-refresh');
|
||||
if (refresh) refresh.addEventListener('click', function () { loadHighscore(); });
|
||||
var resetAll = el('btn-highscore-reset-all');
|
||||
if (resetAll) resetAll.addEventListener('click', function () {
|
||||
if (!confirm('รีเซ็ตคะแนนสะสมของผู้เล่นทุกคนเป็น 0? (ย้อนกลับไม่ได้)')) return;
|
||||
api('leaderboard-admin.php', { method: 'POST', body: { action: 'resetAll' } })
|
||||
.then(function (r) { setMsg('highscore-msg', 'รีเซ็ตแล้ว ' + (r.reset || 0) + ' คน', 'ok'); return loadHighscore(); })
|
||||
.catch(function (err) { setMsg('highscore-msg', err.message, 'error'); });
|
||||
});
|
||||
})();
|
||||
|
||||
el('form-account-add').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var fd = new FormData(e.target);
|
||||
|
||||
@@ -8,6 +8,7 @@ require_login();
|
||||
function normalize_account_row(array $a): array
|
||||
{
|
||||
$a['coins'] = max(0, (int)($a['coins'] ?? 0));
|
||||
$a['score'] = max(0, (int)($a['score'] ?? 0));
|
||||
return $a;
|
||||
}
|
||||
|
||||
@@ -80,6 +81,9 @@ if ($method === 'PATCH') {
|
||||
if (array_key_exists('coins', $body)) {
|
||||
$store['accounts'][$i]['coins'] = max(0, (int)$body['coins']);
|
||||
}
|
||||
if (array_key_exists('score', $body)) {
|
||||
$store['accounts'][$i]['score'] = max(0, (int)$body['score']);
|
||||
}
|
||||
if (array_key_exists('coinsDelta', $body)) {
|
||||
$cur = max(0, (int)($store['accounts'][$i]['coins'] ?? 0));
|
||||
$store['accounts'][$i]['coins'] = max(0, $cur + (int)$body['coinsDelta']);
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* ภายในเซิร์ฟเวอร์เท่านั้น — มอบเหรียญ + คะแนนสะสม (high score) ให้ผู้เล่นหลังจบมินิเกม
|
||||
* เรียกจาก Game server (Node) เท่านั้น โดยตรวจ secret ร่วม (Admin/private/game-award-secret.txt)
|
||||
*
|
||||
* POST JSON: {
|
||||
* "secret": "<shared secret>",
|
||||
* "awards": [ { "playerKey": "...", "nickname": "...", "coins": 10 }, ... ]
|
||||
* }
|
||||
* - บวก coins (ใช้จ่ายได้) และ score (สะสมถาวร ไม่ลด) เท่ากับจำนวนเหรียญที่ได้
|
||||
* - cap ต่อรายการ 0–100 กันยิงมั่ว
|
||||
*/
|
||||
require __DIR__ . '/_common.php';
|
||||
|
||||
date_default_timezone_set('Asia/Bangkok');
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
json_response(['ok' => false, 'error' => 'Use POST'], 405);
|
||||
}
|
||||
|
||||
$secretFile = ADMIN_PRIVATE_DIR . '/game-award-secret.txt';
|
||||
$expected = is_file($secretFile) ? trim((string) @file_get_contents($secretFile)) : '';
|
||||
|
||||
$body = require_json_body();
|
||||
$secret = (string) ($body['secret'] ?? '');
|
||||
if ($expected === '' || strlen($secret) < 16 || !hash_equals($expected, $secret)) {
|
||||
json_response(['ok' => false, 'error' => 'unauthorized'], 403);
|
||||
}
|
||||
|
||||
$awards = (isset($body['awards']) && is_array($body['awards'])) ? $body['awards'] : [];
|
||||
if (!$awards) {
|
||||
json_response(['ok' => true, 'updated' => 0]);
|
||||
}
|
||||
|
||||
$store = read_store();
|
||||
if (!isset($store['accounts']) || !is_array($store['accounts'])) {
|
||||
$store['accounts'] = [];
|
||||
}
|
||||
|
||||
/* index บัญชี guest ตาม playerKey */
|
||||
$byKey = [];
|
||||
foreach ($store['accounts'] as $i => $a) {
|
||||
if (($a['loginType'] ?? '') === 'guest') {
|
||||
$byKey[(string) ($a['providerUserId'] ?? '')] = $i;
|
||||
}
|
||||
}
|
||||
|
||||
$updated = 0;
|
||||
foreach ($awards as $aw) {
|
||||
if (!is_array($aw)) {
|
||||
continue;
|
||||
}
|
||||
$key = trim((string) ($aw['playerKey'] ?? ''));
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key)) {
|
||||
continue;
|
||||
}
|
||||
$coins = (int) ($aw['coins'] ?? 0);
|
||||
if ($coins < 0) $coins = 0;
|
||||
if ($coins > 100) $coins = 100;
|
||||
$nick = trim((string) ($aw['nickname'] ?? ''));
|
||||
if (function_exists('mb_substr')) {
|
||||
$nick = $nick !== '' ? mb_substr($nick, 0, 24) : '';
|
||||
} else {
|
||||
$nick = $nick !== '' ? substr($nick, 0, 24) : '';
|
||||
}
|
||||
|
||||
$caseId = preg_replace('/[^0-9]/', '', (string) ($aw['caseId'] ?? ''));
|
||||
$noScore = !empty($aw['noScore']); // Fund ฯลฯ: บวกแค่ coins ไม่บวก high-score
|
||||
|
||||
if (isset($byKey[$key])) {
|
||||
$i = $byKey[$key];
|
||||
if (!empty($store['accounts'][$i]['blocked'])) {
|
||||
continue;
|
||||
}
|
||||
$store['accounts'][$i]['coins'] = max(0, (int) ($store['accounts'][$i]['coins'] ?? 0)) + $coins;
|
||||
if (!$noScore) {
|
||||
$store['accounts'][$i]['score'] = max(0, (int) ($store['accounts'][$i]['score'] ?? 0)) + $coins;
|
||||
}
|
||||
if (!$noScore && $caseId !== '') {
|
||||
if (!isset($store['accounts'][$i]['scoreByCase']) || !is_array($store['accounts'][$i]['scoreByCase'])) {
|
||||
$store['accounts'][$i]['scoreByCase'] = [];
|
||||
}
|
||||
$store['accounts'][$i]['scoreByCase'][$caseId] = max(0, (int) ($store['accounts'][$i]['scoreByCase'][$caseId] ?? 0)) + $coins;
|
||||
}
|
||||
if ($nick !== '') {
|
||||
$store['accounts'][$i]['lbName'] = $nick;
|
||||
if (($store['accounts'][$i]['displayName'] ?? '') === 'Guest' || ($store['accounts'][$i]['displayName'] ?? '') === '') {
|
||||
$store['accounts'][$i]['displayName'] = $nick;
|
||||
}
|
||||
}
|
||||
$store['accounts'][$i]['updatedAt'] = gmdate('c');
|
||||
} else {
|
||||
$store['accounts'][] = [
|
||||
'id' => new_id(),
|
||||
'email' => '',
|
||||
'displayName' => ($nick !== '' ? $nick : 'Guest'),
|
||||
'loginType' => 'guest',
|
||||
'providerUserId' => $key,
|
||||
'notes' => 'auto: game-award',
|
||||
'blocked' => false,
|
||||
'coins' => $coins,
|
||||
'score' => ($noScore ? 0 : $coins),
|
||||
'scoreByCase' => ((!$noScore && $caseId !== '') ? [$caseId => $coins] : []),
|
||||
'lbName' => $nick,
|
||||
'createdAt' => gmdate('c'),
|
||||
'updatedAt' => gmdate('c'),
|
||||
];
|
||||
$byKey[$key] = count($store['accounts']) - 1;
|
||||
}
|
||||
$updated++;
|
||||
}
|
||||
|
||||
if (!write_store($store)) {
|
||||
json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500);
|
||||
}
|
||||
|
||||
json_response(['ok' => true, 'updated' => $updated]);
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* แอดมินเท่านั้น — จัดการกระดานผู้นำ (High Score)
|
||||
* GET → รายชื่อบัญชีเรียงตามคะแนนสะสม (มาก→น้อย)
|
||||
* POST { action: 'resetAll' } → ล้างคะแนนทุกคนเป็น 0
|
||||
* POST { action: 'reset', id } → ล้างคะแนนคนเดียวเป็น 0
|
||||
* (การตั้งคะแนนรายคน ใช้ accounts.php PATCH { id, score })
|
||||
*/
|
||||
require __DIR__ . '/_common.php';
|
||||
require_login();
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
if ($method === 'GET') {
|
||||
$store = read_store();
|
||||
$rows = [];
|
||||
foreach (($store['accounts'] ?? []) as $a) {
|
||||
$score = max(0, (int) ($a['score'] ?? 0));
|
||||
$name = trim((string) ($a['lbName'] ?? ''));
|
||||
if ($name === '') $name = trim((string) ($a['displayName'] ?? ''));
|
||||
if ($name === '') $name = 'ผู้เล่น';
|
||||
$rows[] = [
|
||||
'id' => (string) ($a['id'] ?? ''),
|
||||
'name' => $name,
|
||||
'score' => $score,
|
||||
'coins' => max(0, (int) ($a['coins'] ?? 0)),
|
||||
'blocked' => !empty($a['blocked']),
|
||||
'loginType' => (string) ($a['loginType'] ?? ''),
|
||||
'key' => (string) ($a['providerUserId'] ?? ''),
|
||||
];
|
||||
}
|
||||
usort($rows, function ($x, $y) {
|
||||
if ($y['score'] !== $x['score']) {
|
||||
return $y['score'] - $x['score'];
|
||||
}
|
||||
return strcmp((string) $x['name'], (string) $y['name']);
|
||||
});
|
||||
foreach ($rows as $i => &$r) {
|
||||
$r['rank'] = $i + 1;
|
||||
}
|
||||
unset($r);
|
||||
json_response(['ok' => true, 'rows' => $rows, 'total' => count($rows)]);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$body = require_json_body();
|
||||
$action = (string) ($body['action'] ?? '');
|
||||
|
||||
if ($action === 'resetAll') {
|
||||
$store = read_store();
|
||||
$n = 0;
|
||||
foreach (($store['accounts'] ?? []) as $i => $a) {
|
||||
if ((int) ($a['score'] ?? 0) !== 0) {
|
||||
$store['accounts'][$i]['score'] = 0;
|
||||
$store['accounts'][$i]['updatedAt'] = gmdate('c');
|
||||
$n++;
|
||||
}
|
||||
}
|
||||
if (!write_store($store)) {
|
||||
json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500);
|
||||
}
|
||||
json_response(['ok' => true, 'reset' => $n]);
|
||||
}
|
||||
|
||||
if ($action === 'reset') {
|
||||
$id = trim((string) ($body['id'] ?? ''));
|
||||
if ($id === '') {
|
||||
json_response(['ok' => false, 'error' => 'ระบุ id'], 400);
|
||||
}
|
||||
$store = read_store();
|
||||
$found = false;
|
||||
foreach (($store['accounts'] ?? []) as $i => $a) {
|
||||
if (($a['id'] ?? '') === $id) {
|
||||
$store['accounts'][$i]['score'] = 0;
|
||||
$store['accounts'][$i]['updatedAt'] = gmdate('c');
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
json_response(['ok' => false, 'error' => 'ไม่พบบัญชี'], 404);
|
||||
}
|
||||
if (!write_store($store)) {
|
||||
json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500);
|
||||
}
|
||||
json_response(['ok' => true]);
|
||||
}
|
||||
|
||||
json_response(['ok' => false, 'error' => 'action ไม่ถูกต้อง'], 400);
|
||||
}
|
||||
|
||||
json_response(['ok' => false, 'error' => 'Use GET or POST'], 405);
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* สาธารณะ (อ่านอย่างเดียว) — กระดานผู้นำ High Score
|
||||
* GET ?limit=50&playerKey=<optional>
|
||||
* คืน { ok, total, top: [ { rank, name, score, coins } ], me?: { rank, name, score } }
|
||||
* จัดอันดับจากค่า score สะสม (ไม่ลดเมื่อใช้เหรียญ)
|
||||
*/
|
||||
require __DIR__ . '/_common.php';
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'GET') {
|
||||
json_response(['ok' => false, 'error' => 'Use GET'], 405);
|
||||
}
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 50);
|
||||
if ($limit < 1) $limit = 1;
|
||||
if ($limit > 200) $limit = 200;
|
||||
|
||||
$caseId = preg_replace('/[^0-9]/', '', (string) ($_GET['caseId'] ?? ''));
|
||||
|
||||
$store = read_store();
|
||||
$accounts = (isset($store['accounts']) && is_array($store['accounts'])) ? $store['accounts'] : [];
|
||||
|
||||
$rows = [];
|
||||
foreach ($accounts as $a) {
|
||||
if ($caseId !== '') {
|
||||
$score = max(0, (int) (($a['scoreByCase'][$caseId] ?? 0)));
|
||||
} else {
|
||||
$score = max(0, (int) ($a['score'] ?? 0));
|
||||
}
|
||||
if ($score <= 0) {
|
||||
continue;
|
||||
}
|
||||
$name = trim((string) ($a['lbName'] ?? ''));
|
||||
if ($name === '') $name = trim((string) ($a['displayName'] ?? ''));
|
||||
if ($name === '' || $name === 'Guest') $name = 'ผู้เล่น';
|
||||
$rows[] = [
|
||||
'name' => $name,
|
||||
'score' => $score,
|
||||
'coins' => max(0, (int) ($a['coins'] ?? 0)),
|
||||
'key' => (string) ($a['providerUserId'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
usort($rows, function ($x, $y) {
|
||||
if ($y['score'] !== $x['score']) {
|
||||
return $y['score'] - $x['score'];
|
||||
}
|
||||
return strcmp((string) $x['name'], (string) $y['name']);
|
||||
});
|
||||
|
||||
$total = count($rows);
|
||||
|
||||
$me = null;
|
||||
$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']];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$top = [];
|
||||
$n = min($limit, $total);
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
$top[] = [
|
||||
'rank' => $i + 1,
|
||||
'name' => $rows[$i]['name'],
|
||||
'score' => $rows[$i]['score'],
|
||||
'coins' => $rows[$i]['coins'],
|
||||
];
|
||||
}
|
||||
|
||||
json_response(['ok' => true, 'total' => $total, 'top' => $top, 'me' => $me]);
|
||||
@@ -118,6 +118,7 @@
|
||||
<div class="admin-tab-sep" aria-hidden="true"><span>ระบบ & บัญชี</span></div>
|
||||
<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>
|
||||
<button type="button" class="tab" data-tab="highscore" role="tab" id="tab-highscore" aria-controls="tab-panel-highscore"><span class="tab-label">กระดานผู้นำ</span><span class="tab-desc">High Score · คะแนนสะสม</span></button>
|
||||
<button type="button" class="tab" data-tab="admins" role="tab" id="tab-admins" aria-controls="tab-panel-admins"><span class="tab-label">แอดมิน</span><span class="tab-desc">สิทธิ์ระบบ</span></button>
|
||||
<button type="button" class="tab" data-tab="test-mode" role="tab" id="tab-test-mode" aria-controls="tab-panel-test-mode"><span class="tab-label">Test Mode</span><span class="tab-desc">เปิด hotkeys ทดสอบเกม</span></button>
|
||||
</nav>
|
||||
@@ -775,6 +776,7 @@
|
||||
<legend>เวลา & รูป</legend>
|
||||
<div class="form-grid form-inline quiz-timing-grid">
|
||||
<label title="0 = ใช้ 120 วิเมื่อแมปไม่กำหนด · ตั้งอย่างน้อย 10 ถ้าต้องการกำหนดเอง">เวลารอบ (วินาที) <input type="number" id="mega-virus-mission-sec" min="0" max="7200" step="5" value="0"></label>
|
||||
<label title="จำนวนลูกโป่ง (ชีวิต) ต่อผู้เล่น · 0 = ใช้ค่าจากแมป หรือ default 3 · แมปกำหนดทับค่านี้">ลูกโป่งต่อคน <input type="number" id="mega-virus-balloons-per-player" min="0" max="12" step="1" value="0"></label>
|
||||
</div>
|
||||
<div class="form-grid form-inline" style="margin-top:0.75rem;align-items:flex-end;gap:0.75rem;flex-wrap:wrap">
|
||||
<label class="space-shooter-ship-url-label" style="flex:1;min-width:14rem">รูปบอส (URL) <input type="text" id="mega-virus-boss-url" maxlength="500" spellcheck="false" placeholder="/Game/img/MegaVirus/boss.png" autocomplete="off"></label>
|
||||
@@ -1019,6 +1021,30 @@
|
||||
<p id="accounts-msg" class="msg" role="status"></p>
|
||||
</section>
|
||||
|
||||
<section id="tab-panel-highscore" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-highscore">
|
||||
<h2>กระดานผู้นำ — High Score</h2>
|
||||
<p class="muted">อันดับจาก <strong>คะแนนสะสม</strong> ที่ผู้เล่นได้จากการติดอันดับในมินิเกม (สะสมถาวร ไม่ลดเมื่อใช้เหรียญ) · แก้ไข/รีเซ็ตได้</p>
|
||||
<div class="form-inline" style="margin-bottom:10px; display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<button type="button" class="btn btn-ghost" id="btn-highscore-refresh">รีเฟรช</button>
|
||||
<button type="button" class="btn btn-danger" id="btn-highscore-reset-all">รีเซ็ตคะแนนทั้งหมด</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table" id="table-highscore">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>อันดับ</th>
|
||||
<th>ชื่อผู้เล่น</th>
|
||||
<th>คะแนนสะสม</th>
|
||||
<th>COINS</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p id="highscore-msg" class="msg" role="status"></p>
|
||||
</section>
|
||||
|
||||
<section id="tab-panel-admins" class="tab-panel card" hidden role="tabpanel" aria-labelledby="tab-admins">
|
||||
<h2>บัญชีแอดมิน</h2>
|
||||
<p class="muted">เฉพาะ <strong>super admin</strong> เท่านั้นที่เพิ่ม/ลบแอดมินได้</p>
|
||||
@@ -1091,6 +1117,6 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="admin.js?v=81"></script>
|
||||
<script src="admin.js?v=82"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
cfac0faaa01bdd06ac50fd763625e96816f032ff047d3bb2cf9697e917fe71fd
|
||||
@@ -33,14 +33,14 @@
|
||||
{
|
||||
"id": "483a104becd7a5f92c0e5cad",
|
||||
"email": "",
|
||||
"displayName": "Guest",
|
||||
"displayName": "Q",
|
||||
"loginType": "guest",
|
||||
"providerUserId": "p_1775109142385_wq7wfy1p32j",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 10,
|
||||
"coins": 110,
|
||||
"createdAt": "2026-04-02T05:52:21+00:00",
|
||||
"updatedAt": "2026-06-12T06:21:53+00:00",
|
||||
"updatedAt": "2026-06-12T08:29:33+00:00",
|
||||
"daily": {
|
||||
"anchorMs": 1781197200000,
|
||||
"claimedDays": [
|
||||
@@ -53,7 +53,14 @@
|
||||
false
|
||||
],
|
||||
"lockUntilMs": 1781283600000
|
||||
}
|
||||
},
|
||||
"score": 100,
|
||||
"scoreByCase": {
|
||||
"1": 10,
|
||||
"10": 70,
|
||||
"8": 20
|
||||
},
|
||||
"lbName": "Q"
|
||||
},
|
||||
{
|
||||
"id": "1d64c56fadb64a93eae68a1d",
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
"gauntletTickMs": 220,
|
||||
"gauntletJumpTicks": 16,
|
||||
"gauntletTimeLimitSec": 0,
|
||||
"gauntletLaneImageUrls": [],
|
||||
"gauntletLaneImageUrls": [
|
||||
"/Game/img/gauntlet-assets/obstacles-2.png",
|
||||
"/Game/img/gauntlet-assets/obstacles-3.png",
|
||||
"/Game/img/gauntlet-assets/obstacles-4.png",
|
||||
"/Game/img/gauntlet-assets/obstacles-5.png"
|
||||
],
|
||||
"gauntletLaserTopUrl": "",
|
||||
"gauntletLaserBottomUrl": "",
|
||||
"gauntletLaserLineUrl": "",
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
"label": "obstacles-5",
|
||||
"updatedAt": 1777530889730
|
||||
},
|
||||
"gauntlet-2a75a958cd5e5e8a.png": {
|
||||
"label": "obstacles-1",
|
||||
"updatedAt": 1777530896068
|
||||
},
|
||||
"gauntlet-dd51baed17270995.png": {
|
||||
"label": "buttonlazer",
|
||||
"updatedAt": 1777569238420
|
||||
|
||||
@@ -1024,3 +1024,59 @@
|
||||
transform: scale(1.02);
|
||||
transition: filter 0.15s ease, transform 0.1s ease;
|
||||
}
|
||||
|
||||
/* ปุ่ม host แยก (ขวา) — ใช้รูปจริง: btn-next-suspect (ปากคำ 1-2) / btn-start-vote (ปากคำสุดท้าย) */
|
||||
#testimony-overlay .es-reveal-wrap .es-proceed {
|
||||
position: absolute;
|
||||
left: 1240px;
|
||||
top: 951px;
|
||||
width: 360px;
|
||||
height: 96px;
|
||||
z-index: 11;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s ease, transform 0.1s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
#testimony-overlay .es-reveal-wrap .es-proceed[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#testimony-overlay .es-reveal-wrap .es-proceed img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
/* ค่าเริ่มต้น: โชว์ "ผู้ต้องสงสัยถัดไป" — ปากคำสุดท้าย (is-last): โชว์ "เริ่มพิจารณาคดี" */
|
||||
#testimony-overlay .es-reveal-wrap .es-proceed .es-proceed-trial {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#testimony-overlay .es-reveal-wrap .es-proceed.is-last .es-proceed-next {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#testimony-overlay .es-reveal-wrap .es-proceed.is-last .es-proceed-trial {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#testimony-overlay .es-reveal-wrap .es-proceed:not(:disabled) {
|
||||
filter: drop-shadow(0 0 12px rgba(83, 224, 255, 0.5));
|
||||
}
|
||||
|
||||
#testimony-overlay .es-reveal-wrap .es-proceed:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
#testimony-overlay .es-reveal-wrap .es-proceed:not(:disabled):hover {
|
||||
filter: drop-shadow(0 0 18px rgba(83, 224, 255, 0.85)) brightness(1.1);
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
+266
-57
@@ -839,6 +839,8 @@
|
||||
let gauntletCrownRunwayBgSpeedPxPerSec = 48;
|
||||
let gauntletCrownRunwayBgScrollPx = 0;
|
||||
let gauntletCrownRunwayBgFinishLatched = false;
|
||||
/** ส่งสัญญาณ "ถึงเส้นชัย" ให้ server แล้วหรือยัง (กันส่งซ้ำ) — server จะหยุด obstacle/collision */
|
||||
let gauntletFinishPhaseSignaled = false;
|
||||
/** >0 = หยุดเลื่อนแล้ว (timestamp ms) รอ POST_STOP ก่อน latch */
|
||||
let gauntletCrownRunwayBgStripFreezeSinceMs = 0;
|
||||
/** คอลัมน์แผนที่ที่ล็อกตอน freeze — แนบตัวนำแทนขอบขวาสูงสุดของแมป */
|
||||
@@ -1275,25 +1277,48 @@
|
||||
return screenFrac <= gauntletCrownRunwayFinishStopViewportFracPlay() + 0.008;
|
||||
}
|
||||
|
||||
/** จัด scroll ให้จุดอ้างอิงใน FINISH ตรงคอลัมน์แผนที่ (หรือ finishStopViewportFrac บนจอ) */
|
||||
function gauntletCrownRunwaySnapScrollToFinishStopPlay() {
|
||||
if (!mapData || !canvas || !gauntletCrownRunwayBgDrawActivePlay()) return;
|
||||
/** ค่าตำแหน่ง scroll ที่ทำให้เส้น FINISH หยุดตรงจุด (คืน null ถ้าคำนวณไม่ได้) — ใช้ทั้ง snap และ clamp กันวิ่งเลยเส้นชัย */
|
||||
function gauntletCrownRunwayFinishStopScrollPxPlay() {
|
||||
if (!mapData || !canvas || !gauntletCrownRunwayBgDrawActivePlay()) return null;
|
||||
const g = gauntletCrownRunwayScrollGeometryPlay();
|
||||
if (!g) return;
|
||||
if (!g) return null;
|
||||
const finishLineStripX = gauntletCrownRunwayFinishLineStripXPlay(g);
|
||||
if (finishLineStripX == null) return;
|
||||
if (finishLineStripX == null) return null;
|
||||
const alignWorldX = gauntletCrownRunwayFinishAlignMapWorldXPlay();
|
||||
if (alignWorldX != null) {
|
||||
gauntletCrownRunwayBgScrollPx = finishLineStripX - alignWorldX;
|
||||
return;
|
||||
return finishLineStripX - alignWorldX;
|
||||
}
|
||||
const stopVp = gauntletCrownRunwayFinishStopViewportFracPlay();
|
||||
const targetScrollView = finishLineStripX - stopVp * g.cwWorld;
|
||||
const zGaRaw = computePlayCameraZDrawPlay();
|
||||
const gCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zGaRaw);
|
||||
if (!gCam) return;
|
||||
if (!gCam) return null;
|
||||
const worldMinX = gCam.px - canvas.width / (2 * zGaRaw);
|
||||
gauntletCrownRunwayBgScrollPx = targetScrollView - worldMinX;
|
||||
return targetScrollView - worldMinX;
|
||||
}
|
||||
|
||||
/** จัด scroll ให้จุดอ้างอิงใน FINISH ตรงคอลัมน์แผนที่ (หรือ finishStopViewportFrac บนจอ) */
|
||||
function gauntletCrownRunwaySnapScrollToFinishStopPlay() {
|
||||
const t = gauntletCrownRunwayFinishStopScrollPxPlay();
|
||||
if (t == null) return;
|
||||
gauntletCrownRunwayBgScrollPx = t;
|
||||
}
|
||||
|
||||
/** ลำดับการหยุดที่เส้นชัย: ล็อก align (ครั้งเดียว) → snap เป๊ะ → เริ่ม freeze → (latch+mission ทีหลังใน tick) */
|
||||
function gauntletCrownRunwayBeginFinishStopPlay() {
|
||||
if (gauntletCrownRunwayFinishAlignLatchedWorldX == null) {
|
||||
const leadAlign = gauntletCrownRunwayLeadingEntityAlignWorldXPlay();
|
||||
if (leadAlign != null) gauntletCrownRunwayFinishAlignLatchedWorldX = leadAlign;
|
||||
}
|
||||
gauntletCrownRunwaySnapScrollToFinishStopPlay();
|
||||
gauntletCrownRunwayBgStripFreezeSinceMs = Date.now();
|
||||
/* แจ้ง server ครั้งเดียว: ถึงเส้นชัยแล้ว → หยุด obstacle + collision (กัน -10 ตอน obstacle หายแล้ว) */
|
||||
if (!gauntletFinishPhaseSignaled) {
|
||||
gauntletFinishPhaseSignaled = true;
|
||||
try { socket.emit('gauntlet-finish-phase'); } catch (_e) { /* ignore */ }
|
||||
}
|
||||
try { syncGauntletCrownJumpButton(); } catch (_sj) { /* ignore */ }
|
||||
try { draw(); } catch (_d) { /* ignore */ }
|
||||
}
|
||||
|
||||
/** โหมดจอเท่านั้น: กล้องเลื่อนหลัง freeze ทำให้ FINISH หลุด — รีสแนปทุกเฟรม */
|
||||
@@ -1343,6 +1368,7 @@
|
||||
gauntletCrownRunwayBgImgs = [null, null, null, null, null];
|
||||
gauntletCrownRunwayBgScrollPx = 0;
|
||||
gauntletCrownRunwayBgFinishLatched = false;
|
||||
gauntletFinishPhaseSignaled = false;
|
||||
gauntletCrownRunwayBgStripFreezeSinceMs = 0;
|
||||
gauntletCrownRunwayFinishAlignLatchedWorldX = null;
|
||||
gauntletCrownRunwayClientMissionShown = false;
|
||||
@@ -1382,6 +1408,8 @@
|
||||
|
||||
function tickGauntletCrownRunwayBgPlay(dtSec) {
|
||||
if (!gauntletCrownRunwayBgDrawActivePlay()) return;
|
||||
/* หยุดเลื่อน BG ระหว่างคำถามพิเศษ (special-quiz freeze) — กัน BG/เส้นชัยเดินตอนค้างตอบ */
|
||||
if (specialQuizFreeze) return;
|
||||
/** พรีรัน (howto / countdown) — ห้ามเลื่อนก่อน GO · Mega Virus ใช้ shell เดียวกัน */
|
||||
if (usesCrownLobbyShellPlay() && gauntletCrownPregamePhase !== 'live') return;
|
||||
if (gauntletCrownRunwayBgFinishLatched) return;
|
||||
@@ -1431,7 +1459,17 @@
|
||||
} catch (_d) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
gauntletCrownRunwayBgScrollPx += (gauntletCrownRunwayBgSpeedPxPerSec || 48) * dtSec;
|
||||
const gcrInc = (gauntletCrownRunwayBgSpeedPxPerSec || 48) * dtSec;
|
||||
/* ถ้าก้าวถัดไป "จะถึง/เลย" จุดหยุด FINISH → หยุดเป๊ะตรงนั้น (ไม่วิ่งเลย) แล้วเริ่มจบเกมทันที */
|
||||
const gcrLiveStopPx = gauntletCrownRunwayFinishStopScrollPxPlay();
|
||||
if (gauntletCrownRunwayBgMapEligiblePlay() && gcrLiveStopPx != null
|
||||
&& gcrLiveStopPx >= gauntletCrownRunwayBgScrollPx - 0.01
|
||||
&& gauntletCrownRunwayBgScrollPx + gcrInc >= gcrLiveStopPx) {
|
||||
gauntletCrownRunwayBgScrollPx = gcrLiveStopPx;
|
||||
gauntletCrownRunwayBeginFinishStopPlay();
|
||||
return;
|
||||
}
|
||||
gauntletCrownRunwayBgScrollPx += gcrInc;
|
||||
}
|
||||
|
||||
/** stack + แมป mnn93hpi — พื้นหลัง intro+loop เลื่อนแนวตั้ง (stackTowerBgScroll ในแมป) */
|
||||
@@ -1836,9 +1874,19 @@
|
||||
const CHARACTER_ANIM_FRAME_MS = 200;
|
||||
|
||||
/** จังหวะเดินลูปคงที่ 4 เฟรม — อย่า modulo ด้วยจำนวนเฟรมที่โหลดได้แล้ว (ไม่งั้นเฟรมกลางหาย/ลูปสั้นลง) */
|
||||
/** true เมื่ออยู่ในเกมวิ่งหลบ (gauntlet runner) ช่วงเล่นจริง — ใช้เร่งจังหวะขาให้ดูเหมือน "วิ่ง" */
|
||||
function gauntletRunnerAnimActivePlay() {
|
||||
try {
|
||||
return isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live' && gauntletCrownRunwayAvatarRunAllowedPlay();
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
|
||||
function walkAnimPhaseIndex(now, isWalking) {
|
||||
const t = isWalking ? (typeof now === 'number' ? now : Date.now()) : 0;
|
||||
return Math.floor(t / CHARACTER_ANIM_FRAME_MS) % CHARACTER_ANIM_FRAMES;
|
||||
if (!isWalking) return 0;
|
||||
const t = (typeof now === 'number' ? now : Date.now());
|
||||
/* gauntlet วิ่งหลบ: ย่นเวลาเฟรม ~1.85× ให้ขาสลับเร็ว = วิ่ง (ไม่ใช่เดิน) */
|
||||
const frameMs = gauntletRunnerAnimActivePlay() ? CHARACTER_ANIM_FRAME_MS * 0.54 : CHARACTER_ANIM_FRAME_MS;
|
||||
return Math.floor(t / frameMs) % CHARACTER_ANIM_FRAMES;
|
||||
}
|
||||
|
||||
/** เลือกเฟรมสูงสุดที่โหลดแล้วและ <= phase (เดิมถอย) */
|
||||
@@ -2681,6 +2729,20 @@
|
||||
return getStoredCharacterId();
|
||||
}
|
||||
|
||||
/** playerKey ของผู้เล่น (ผูกบัญชีเหรียญ/คะแนน) — สร้างถ้ายังไม่มี เหมือน Main-Lobby */
|
||||
function getJdPlayerKey() {
|
||||
try {
|
||||
let k = (localStorage.getItem('jdPlayerKey') || '').trim();
|
||||
if (!k || k.length < 8) {
|
||||
k = 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14);
|
||||
localStorage.setItem('jdPlayerKey', k);
|
||||
}
|
||||
return k;
|
||||
} catch (e) {
|
||||
return 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14);
|
||||
}
|
||||
}
|
||||
|
||||
/** บอทพรีวิว: เลือกรหัสคนละตัวจาก roster (ไม่ซ้ำผู้เล่นถ้ามีตัวเลือก) */
|
||||
function pickPreviewBotCharacterId(botSlotIndex) {
|
||||
const idx = Math.max(0, Math.floor(Number(botSlotIndex)) || 0);
|
||||
@@ -9038,6 +9100,43 @@
|
||||
return ranks.slice(0, 5);
|
||||
}
|
||||
|
||||
/** payload สำหรับ overlay สรุปผลกลาง (gauntlet-crown-mission-overlay) — ให้ MG4 ใช้หน้าจบแบบเดียวกับเกมอื่น */
|
||||
function quizCarryBuildGcmMissionPayload(opts) {
|
||||
opts = opts || {};
|
||||
const ranks = quizCarryBuildMissionRankList();
|
||||
const ranked = ranks.map(function (row, idx) {
|
||||
const pos = idx + 1;
|
||||
const sc = Math.max(0, Number(row.score) || 0);
|
||||
return {
|
||||
id: row.id, nickname: row.nickname, characterId: row.characterId,
|
||||
baseScore: sc, eliminated: false,
|
||||
rank: pos, rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos),
|
||||
rankBonus: 0, finalScore: sc,
|
||||
};
|
||||
});
|
||||
const totalParts = ranked.map(function (r) { return r.baseScore; });
|
||||
const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0);
|
||||
const n = ranked.length || 1;
|
||||
const averageScore = Math.min(100, Math.max(0, Math.round(totalSum / n)));
|
||||
const grade = opts.forceGradeF ? 'F' : quizCarryGradeFromTeamAverage(averageScore);
|
||||
const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade);
|
||||
return {
|
||||
ranked: ranked, totalSum: totalSum, averageScore: averageScore,
|
||||
grade: grade, rewardCard: rewardCard, totalParts: totalParts,
|
||||
uiSkin: 'quiz_carry', survivorCount: opts.forceGradeF ? 0 : ranked.length,
|
||||
participantCount: ranked.length,
|
||||
};
|
||||
}
|
||||
|
||||
/** ตั้งรูปพื้นหลังหน้าจบของ overlay กลางเป็นชุด quiz-carry (cut) */
|
||||
function applyQuizCarryGcmMissionBg() {
|
||||
const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg');
|
||||
if (resBg) {
|
||||
resBg.src = BASE + '/img/quiz-carry/popup-result.png';
|
||||
resBg.onerror = function () { this.onerror = null; this.src = BASE + '/img/gauntlet-assets/popup-result.png'; };
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEmbedPreviewLobbyReturnTimer() {
|
||||
if (embedPreviewLobbyReturnTimer != null) {
|
||||
clearTimeout(embedPreviewLobbyReturnTimer);
|
||||
@@ -9410,7 +9509,8 @@
|
||||
quizCarrySessionCompleteResultToSummaryT = setTimeout(function () {
|
||||
quizCarrySessionCompleteResultToSummaryT = null;
|
||||
hideQuizCarryResultEndLayer();
|
||||
showQuizCarryMissionSummaryOverlay(summaryOpts);
|
||||
/* ใช้หน้าจบ overlay กลาง (เหมือนเกมอื่น) skin quiz_carry แทน overlay เฉพาะเดิม */
|
||||
showGauntletCrownMissionOverlay(quizCarryBuildGcmMissionPayload(summaryOpts));
|
||||
}, QUIZ_CARRY_SESSION_END_SPLASH_MS);
|
||||
renderPlayQuizScoreboard(playLiveQuizScores);
|
||||
return;
|
||||
@@ -11787,6 +11887,7 @@
|
||||
if (mission.uiSkin === 'violent_crime') return isSpaceShooterMissionUiMapPlay();
|
||||
if (mission.uiSkin === 'mega_virus') return isMegaVirusMissionShellMapPlay();
|
||||
if (mission.uiSkin === 'stack_tower') return isStackTowerMissionUiMapPlay();
|
||||
if (mission.uiSkin === 'quiz_carry') return quizCarryEmbedMissionFlowActive() || isQuizCarry();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -11802,6 +11903,7 @@
|
||||
if (skin === 'violent_crime') return violentCrimeAssetUrl(file);
|
||||
if (skin === 'mega_virus') return megaVirusAssetUrl(file);
|
||||
if (skin === 'stack_tower') return stackTowerAssetUrl(file);
|
||||
if (skin === 'quiz_carry') return BASE + '/img/quiz-carry/' + String(file || '').replace(/^\//, '');
|
||||
return questionMissionAssetUrl(file);
|
||||
}
|
||||
|
||||
@@ -12455,6 +12557,16 @@
|
||||
specialQuizIconImgs[key] = img;
|
||||
return img;
|
||||
}
|
||||
const specialQuizGlowImgs = {};
|
||||
/** แสง glow (08-event light-*) วางหลังไอคอน — ให้ดูเป็นของเก็บเรืองแสงบนเส้นทาง */
|
||||
function specialQuizGetGlowImg(type) {
|
||||
const key = type === 'police' ? 'police' : 'lawyer';
|
||||
if (specialQuizGlowImgs[key]) return specialQuizGlowImgs[key];
|
||||
const img = new Image();
|
||||
img.src = '/Game/img/special-quiz/light-' + key + '.png';
|
||||
specialQuizGlowImgs[key] = img;
|
||||
return img;
|
||||
}
|
||||
function setSpecialQuizIconFromServer(payload) {
|
||||
if (!payload || payload.x == null || payload.y == null) { specialQuizIcon = null; return; }
|
||||
specialQuizIcon = {
|
||||
@@ -12517,6 +12629,19 @@
|
||||
const bob = Math.sin(Date.now() / 320) * size * 0.06;
|
||||
const cy = sy + bob;
|
||||
ctx.save();
|
||||
/* glow 08-event (light-*) หมุน+เต้นหลังไอคอน — collectible เรืองแสง */
|
||||
const glowImg = specialQuizGetGlowImg(specialQuizIcon.iconType);
|
||||
if (glowImg && glowImg.complete && glowImg.naturalWidth > 0) {
|
||||
const pulse = 0.82 + Math.sin(Date.now() / 300) * 0.18;
|
||||
const gsz = size * 2.5 * pulse;
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
ctx.globalAlpha = 0.9;
|
||||
ctx.translate(sx, cy);
|
||||
ctx.rotate(Date.now() / 2600);
|
||||
ctx.drawImage(glowImg, -gsz / 2, -gsz / 2, gsz, gsz);
|
||||
ctx.restore();
|
||||
}
|
||||
const r = size * 0.62;
|
||||
const grad = ctx.createRadialGradient(sx, cy, r * 0.18, sx, cy, r);
|
||||
grad.addColorStop(0, 'rgba(122, 162, 247, 0.5)');
|
||||
@@ -12742,7 +12867,9 @@
|
||||
extendCurrentMinigameTimeClient(addSec);
|
||||
}
|
||||
if (coins > 0 && card.effectKey === 'fund') {
|
||||
addOwnCoinsViaApi(coins);
|
||||
/* server แจกเหรียญแล้ว (game-award.php) — แค่ดึงยอดล่าสุดมาแสดง; ถ้าไม่ใช่ (เผื่อ fallback) ค่อยบวกเอง */
|
||||
if (data.coinsServerAwarded) resyncOwnCoins();
|
||||
else addOwnCoinsViaApi(coins);
|
||||
}
|
||||
/* ถ้า overlay ควิซยังเปิด (การ์ดเพิ่งได้จากควิซ) — แผง «เล่นต่อ» โชว์การ์ดอยู่แล้ว ไม่ต้องเด้งซ้ำ */
|
||||
const sqOv = specialQuizOverlayEl();
|
||||
@@ -12770,6 +12897,21 @@
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/** ดึงยอดเหรียญล่าสุดจาก server มาอัปเดต cache (ใช้หลัง server แจกเหรียญเอง เช่น Fund) */
|
||||
function resyncOwnCoins() {
|
||||
let key = '';
|
||||
try { key = (localStorage.getItem('jdPlayerKey') || '').trim(); } catch (e) { key = ''; }
|
||||
if (!key || key.length < 8) return;
|
||||
const url = (typeof appPath === 'function' ? appPath('/Admin/api/player-coins.php') : '/Admin/api/player-coins.php') + '?playerKey=' + encodeURIComponent(key);
|
||||
try {
|
||||
fetch(url, { credentials: 'omit' }).then(function (r) { return r.json(); }).then(function (d) {
|
||||
if (d && d.ok && typeof d.coins === 'number') {
|
||||
try { localStorage.setItem('jdCoins', String(d.coins)); } catch (e) { /* ignore */ }
|
||||
}
|
||||
}).catch(function () { /* ignore */ });
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/** +วินาที ให้มินิเกมที่กำลังเล่น (jump/shooter/quiz carry) — mng8a80o จัดการฝั่ง server */
|
||||
function extendCurrentMinigameTimeClient(sec) {
|
||||
const ms = Math.max(0, sec * 1000);
|
||||
@@ -13221,7 +13363,7 @@
|
||||
if (previewMode && editorEmbedReturn) {
|
||||
ov.classList.add('is-hidden');
|
||||
if (gcmHead) gcmHead.classList.remove('sr-only');
|
||||
if (mission && (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus' || mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime')) {
|
||||
if (mission && (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus' || mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'quiz_carry')) {
|
||||
cancelQuizCarryResultEndAfterTimeup();
|
||||
hideQuizCarryTimeupOnDeskLayer();
|
||||
hideQuizCarryResultEndLayer();
|
||||
@@ -17780,6 +17922,7 @@
|
||||
nickname: joinNick,
|
||||
characterId: getPlayCharacterId(),
|
||||
playMapId: joinPlayMapId || undefined,
|
||||
playerKey: getJdPlayerKey(),
|
||||
}, (res) => {
|
||||
if (!res || !res.ok) {
|
||||
const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้';
|
||||
@@ -18299,10 +18442,47 @@
|
||||
}
|
||||
});
|
||||
|
||||
function showMinigameCoinsToast(mine) {
|
||||
let el = document.getElementById('minigame-coins-toast');
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.id = 'minigame-coins-toast';
|
||||
el.style.cssText = 'position:fixed;left:50%;top:13%;transform:translateX(-50%);z-index:99999;padding:13px 26px;border-radius:14px;font-weight:700;text-align:center;pointer-events:none;box-shadow:0 6px 24px rgba(0,0,0,.45);transition:opacity .35s;opacity:0;font-size:20px;line-height:1.3;';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
const survived = mine.survived !== false;
|
||||
const coins = Math.max(0, Number(mine.coins) || 0);
|
||||
if (survived && coins > 0) {
|
||||
el.style.background = 'linear-gradient(135deg,#1b9e4b,#13c06a)';
|
||||
el.style.color = '#fff';
|
||||
el.innerHTML = '🪙 +' + coins + ' เหรียญ<br><span style="font-size:.72em;font-weight:500;opacity:.92;">คะแนนสะสม +' + coins + '</span>';
|
||||
} else if (survived) {
|
||||
el.style.background = 'linear-gradient(135deg,#555,#777)';
|
||||
el.style.color = '#fff';
|
||||
el.textContent = 'รอบนี้ไม่ติดอันดับรางวัล';
|
||||
} else {
|
||||
el.style.background = 'linear-gradient(135deg,#a11,#d33)';
|
||||
el.style.color = '#fff';
|
||||
el.textContent = '💀 ตายแล้ว — ไม่ได้รับเหรียญ';
|
||||
}
|
||||
el.style.opacity = '1';
|
||||
if (window.__miniCoinsToastT) clearTimeout(window.__miniCoinsToastT);
|
||||
window.__miniCoinsToastT = setTimeout(() => { el.style.opacity = '0'; }, 4200);
|
||||
}
|
||||
|
||||
socket.on('minigame-coins', (data) => {
|
||||
try {
|
||||
if (!data || !Array.isArray(data.results)) return;
|
||||
const mine = data.results.find((r) => r.id === myId);
|
||||
if (mine) showMinigameCoinsToast(mine);
|
||||
} catch (e) { /* ignore */ }
|
||||
});
|
||||
|
||||
socket.on('gauntlet-ended', (data) => {
|
||||
cancelQuizCarryResultEndAfterTimeup();
|
||||
gauntletEndsAtMs = null;
|
||||
gauntletCrownRunwayBgFinishLatched = false;
|
||||
gauntletFinishPhaseSignaled = false;
|
||||
gauntletCrownRunwayBgStripFreezeSinceMs = 0;
|
||||
gauntletCrownRunwayFinishAlignLatchedWorldX = null;
|
||||
gauntletCrownRunwayClientMissionShown = false;
|
||||
@@ -19531,52 +19711,70 @@
|
||||
|
||||
let playPath = [];
|
||||
|
||||
/** เลเซอร์ = รูป cut obstacles-1.png (emitter หัว + ลำแสง + emitter ท้าย ในรูปเดียว) ยืดเต็มความสูง
|
||||
ถ้ารูปยังไม่โหลด → fallback ลำแสงแดงวาดเอง */
|
||||
function drawGauntletLaserColumnScreen(rx, ry, rw, rh) {
|
||||
if (rh <= 1 || rw <= 0) return;
|
||||
const cx = rx + rw / 2;
|
||||
const spriteRec = ensureGauntletAssetImage(BASE + '/img/gauntlet-assets/obstacles-1.png');
|
||||
if (spriteRec && spriteRec.img.complete && spriteRec.img.naturalWidth > 0) {
|
||||
const drawW = Math.max(14, rw * 1.5);
|
||||
try { ctx.drawImage(spriteRec.img, cx - drawW / 2, ry, drawW, rh); } catch (e) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
ctx.save();
|
||||
const coreW = Math.max(2, rw * 0.30);
|
||||
const glowW = Math.max(coreW + 4, rw * 0.92);
|
||||
const prevComp = ctx.globalCompositeOperation;
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
/* glow รอบนอก */
|
||||
let g1 = ctx.createLinearGradient(cx - glowW / 2, 0, cx + glowW / 2, 0);
|
||||
g1.addColorStop(0, 'rgba(255,40,60,0)');
|
||||
g1.addColorStop(0.5, 'rgba(255,55,75,0.5)');
|
||||
g1.addColorStop(1, 'rgba(255,40,60,0)');
|
||||
ctx.fillStyle = g1;
|
||||
ctx.fillRect(cx - glowW / 2, ry, glowW, rh);
|
||||
/* แกนสว่าง */
|
||||
let g2 = ctx.createLinearGradient(cx - coreW / 2, 0, cx + coreW / 2, 0);
|
||||
g2.addColorStop(0, 'rgba(255,110,130,0.85)');
|
||||
g2.addColorStop(0.5, 'rgba(255,240,245,0.98)');
|
||||
g2.addColorStop(1, 'rgba(255,110,130,0.85)');
|
||||
ctx.fillStyle = g2;
|
||||
ctx.fillRect(cx - coreW / 2, ry, coreW, rh);
|
||||
ctx.globalCompositeOperation = prevComp;
|
||||
/* หัว/ท้าย emitter — รูปจริง gauntlet-dd51 (gauntletLaserTopUrl/BottomUrl) ที่บน-ล่างสุดของลำแสง
|
||||
กล่องเป็นสีเข้ม → ใส่ glow แดงข้างหลังให้เด่นบนพื้นมืด (ผนัง museum) */
|
||||
const topRec = gauntletLaserTopUrl ? ensureGauntletAssetImage(gauntletLaserTopUrl) : null;
|
||||
const botRec = gauntletLaserBottomUrl ? ensureGauntletAssetImage(gauntletLaserBottomUrl) : null;
|
||||
const lineRec = gauntletLaserLineUrl ? ensureGauntletAssetImage(gauntletLaserLineUrl) : null;
|
||||
let topH = 0;
|
||||
let botH = 0;
|
||||
const capW = Math.max(18, rw * 2.0);
|
||||
function laserEmitterGlow(gy) {
|
||||
const rad = capW * 0.8;
|
||||
const pc = ctx.globalCompositeOperation;
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
const gg = ctx.createRadialGradient(cx, gy, 0, cx, gy, rad);
|
||||
gg.addColorStop(0, 'rgba(255,120,140,0.9)');
|
||||
gg.addColorStop(0.5, 'rgba(255,55,75,0.55)');
|
||||
gg.addColorStop(1, 'rgba(255,40,60,0)');
|
||||
ctx.fillStyle = gg;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, gy, rad, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.globalCompositeOperation = pc;
|
||||
}
|
||||
if (topRec && topRec.img.complete && topRec.img.naturalWidth > 0) {
|
||||
topH = Math.min(rh * 0.4, rw * topRec.img.naturalHeight / topRec.img.naturalWidth);
|
||||
const ch = capW * topRec.img.naturalHeight / topRec.img.naturalWidth;
|
||||
laserEmitterGlow(ry + ch * 0.35);
|
||||
try { ctx.drawImage(topRec.img, cx - capW / 2, ry - ch * 0.1, capW, ch); } catch (e) { /* ignore */ }
|
||||
}
|
||||
if (botRec && botRec.img.complete && botRec.img.naturalWidth > 0) {
|
||||
botH = Math.min(rh * 0.4, rw * botRec.img.naturalHeight / botRec.img.naturalWidth);
|
||||
}
|
||||
const lineReady = !!(lineRec && lineRec.img.complete && lineRec.img.naturalWidth > 0 && rh > 1);
|
||||
/* ไม่เติมสีคอลัมน์ทึบเมื่อมีรูปเส้นแล้ว — กันทึบโปร่งซ้าย/ขวาเลเซอร์ดูเหมือนแถบ UI/scrollbar */
|
||||
if (!lineReady) {
|
||||
ctx.fillStyle = gauntletLaserFillColor;
|
||||
ctx.fillRect(rx, ry, rw, rh);
|
||||
}
|
||||
/* เส้นกลาง tile ทั้งความสูงคอลัมน์ แล้วค่อยวาดหัว/ท้ายทับ — ให้ลำแสงต่อเนื่องแบบสินทรัพย์รวม (รูปอ้างอิง) */
|
||||
if (lineReady) {
|
||||
const iw = lineRec.img.naturalWidth;
|
||||
const ih = lineRec.img.naturalHeight;
|
||||
const scale = rw / iw;
|
||||
const step = Math.max(1, ih * scale);
|
||||
let y = ry;
|
||||
while (y < ry + rh) {
|
||||
const piece = Math.min(step, ry + rh - y);
|
||||
const srcH = piece / scale;
|
||||
try {
|
||||
ctx.drawImage(lineRec.img, 0, 0, iw, srcH, rx, y, rw, piece);
|
||||
} catch (e) { /* ignore */ }
|
||||
y += piece;
|
||||
}
|
||||
}
|
||||
if (topH > 0 && topRec) {
|
||||
try { ctx.drawImage(topRec.img, rx, ry, rw, topH); } catch (e) { /* ignore */ }
|
||||
}
|
||||
if (botH > 0 && botRec) {
|
||||
try { ctx.drawImage(botRec.img, rx, ry + rh - botH, rw, botH); } catch (e) { /* ignore */ }
|
||||
}
|
||||
const lw = Number(gauntletLaserLineWidthPx) || 0;
|
||||
if (lw > 0) {
|
||||
ctx.strokeStyle = gauntletLaserStrokeColor;
|
||||
ctx.lineWidth = lw;
|
||||
ctx.strokeRect(rx + lw / 2, ry + lw / 2, rw - lw, rh - lw);
|
||||
const ch = capW * botRec.img.naturalHeight / botRec.img.naturalWidth;
|
||||
laserEmitterGlow(ry + rh - ch * 0.35);
|
||||
/* ท้าย — กลับหัวรูป (เครื่องยิงหันขึ้น) วางชิดล่างสุด */
|
||||
ctx.save();
|
||||
ctx.translate(cx, ry + rh - ch * 0.9);
|
||||
ctx.scale(1, -1);
|
||||
try { ctx.drawImage(botRec.img, -capW / 2, 0, capW, ch); } catch (e) { /* ignore */ }
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -20189,7 +20387,7 @@
|
||||
}
|
||||
|
||||
/** แถบ Cyber SCORE + สรุปผล — หน้า idle down + tint ต่อผู้เล่น */
|
||||
function setCyberHudScoreAvatarImg(avImg, row) {
|
||||
function setCyberHudScoreAvatarImg(avImg, row, skipReuse) {
|
||||
const cid = row.characterId ? String(row.characterId) : '';
|
||||
if (!cid) {
|
||||
avImg.src = defaultAvatarImg.src;
|
||||
@@ -20201,7 +20399,9 @@
|
||||
avImg.src = cached;
|
||||
return;
|
||||
}
|
||||
const hudSrc = findPlayCyberHudScoreAvatarSrcForPeer(row.id);
|
||||
/* skipReuse: ตอน retry (เช่น crop หัวยังไม่เสร็จ) อย่าอ่าน src ปัจจุบันจาก DOM กลับมา
|
||||
ไม่งั้นจะได้ "รูปเต็มตัวชั่วคราว" กลับมาแล้ว cache ค้าง (human โชว์เต็มตัวคนเดียว) */
|
||||
const hudSrc = skipReuse ? '' : findPlayCyberHudScoreAvatarSrcForPeer(row.id);
|
||||
if (hudSrc) {
|
||||
avImg.src = hudSrc;
|
||||
cyberHudScoreAvatarUrlCache.set(cacheKey, hudSrc);
|
||||
@@ -20230,8 +20430,16 @@
|
||||
cyberHudScoreAvatarUrlCache.set(cacheKey, lobbyFace);
|
||||
return;
|
||||
}
|
||||
/* crop ยังไม่เสร็จ (async) → ตั้งเต็มตัวชั่วคราว ไม่ cache → retry จะได้หน้า crop */
|
||||
/* crop ยังไม่เสร็จ (async) → ตั้งเต็มตัวชั่วคราว แล้ว retry จนได้หัว (กัน human โชว์เต็มตัวคนเดียว)
|
||||
จำกัดจำนวน retry กัน loop ถ้า crop ล้มเหลวถาวร */
|
||||
avImg.src = lobbySrc;
|
||||
var hudAvTries = avImg.__hudAvHeadTries || 0;
|
||||
if (hudAvTries < 12) {
|
||||
avImg.__hudAvHeadTries = hudAvTries + 1;
|
||||
setTimeout(function () {
|
||||
if (avImg.isConnected) setCyberHudScoreAvatarImg(avImg, row, true);
|
||||
}, 200);
|
||||
}
|
||||
return;
|
||||
}
|
||||
avImg.src = lobbySrc;
|
||||
@@ -21630,6 +21838,7 @@
|
||||
}
|
||||
} else if (o.kind === 'laser' && typeof o.drawX === 'number') {
|
||||
if (o.drawX < stx - 2 || o.drawX > enx + 2) continue;
|
||||
/* ใช้โซนที่ editor กำหนด (gauntletLaserRowStart..End เช่น 4..18) — หัว/ท้าย emitter อยู่ตามนั้น */
|
||||
const { y0: laserRow0, y1: laserRow1 } = gauntletLaserRowHitRange(o, h);
|
||||
const wx0 = o.drawX * tileSize;
|
||||
const wx1 = (o.drawX + 1) * tileSize;
|
||||
|
||||
@@ -1882,6 +1882,22 @@
|
||||
if (Number.isNaN(idx) || idx < 0 || idx > 2) return;
|
||||
var img = card.querySelector(':scope > img');
|
||||
if (img) img.src = getSuspectPickImageUrl(idx);
|
||||
/* ปุ่มแว่นขยาย — ดูการ์ดเต็ม + ปิดได้ (ไม่ชนการเลือก/โหวต) */
|
||||
if (!card.querySelector('.suspect-zoom-btn')) {
|
||||
if (getComputedStyle(card).position === 'static') card.style.position = 'relative';
|
||||
var zb = document.createElement('button');
|
||||
zb.type = 'button';
|
||||
zb.className = 'suspect-zoom-btn';
|
||||
zb.setAttribute('aria-label', 'ดูการ์ดเต็ม');
|
||||
zb.textContent = '🔍';
|
||||
zb.style.cssText = 'position:absolute;top:9px;right:9px;z-index:8;width:34px;height:34px;border:none;border-radius:50%;cursor:pointer;font-size:16px;line-height:1;display:flex;align-items:center;justify-content:center;background:rgba(10,16,30,.74);color:#9fe9ff;box-shadow:0 2px 8px rgba(0,0,0,.45)';
|
||||
zb.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
var i = parseInt(card.getAttribute('data-index'), 10);
|
||||
openEvidenceCardLightbox(getSuspectPickImageUrl(i), '', 'common');
|
||||
});
|
||||
card.appendChild(zb);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (caseMediaData) paint();
|
||||
@@ -2956,6 +2972,7 @@
|
||||
spaceId,
|
||||
nickname: getProfileDisplayName(),
|
||||
characterId: getStoredCharacterId(),
|
||||
playerKey: ensurePlayerKey(),
|
||||
desiredLobbyColorThemeIndex: savedThemeIdx,
|
||||
desiredLobbySkinToneIndex: savedSkinIdx,
|
||||
}, (res) => {
|
||||
@@ -4340,6 +4357,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* กระดานผู้นำในเกม — คะแนนจริง "แยกต่อคดี" จาก server (ว่าง → fallback mock) */
|
||||
var lobbyRankRealLeaders = null;
|
||||
function fetchLobbyRankLeaders() {
|
||||
var caseId = '';
|
||||
try { caseId = String(getDetectiveCaseId() || ''); } catch (e) { caseId = ''; }
|
||||
var base = (typeof appPath === 'function' ? appPath('/Admin/api/leaderboard.php') : '/Admin/api/leaderboard.php');
|
||||
var url = base + '?limit=10&caseId=' + encodeURIComponent(caseId) + '&playerKey=' + encodeURIComponent(ensurePlayerKey());
|
||||
fetch(url, { credentials: 'omit' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (!d || !d.ok || !Array.isArray(d.top)) return;
|
||||
lobbyRankRealLeaders = d.top.map(function (e) {
|
||||
return { name: e.name || 'ผู้เล่น', score: Math.max(0, parseInt(e.score, 10) || 0) };
|
||||
});
|
||||
var ov = document.getElementById('lobby-rank-overlay');
|
||||
if (ov && !ov.classList.contains('is-hidden')) renderLobbyRankModalContent();
|
||||
})
|
||||
.catch(function () { /* ออฟไลน์ → ใช้ mock */ });
|
||||
}
|
||||
|
||||
function getLobbyPlayerAvatarSrc() {
|
||||
try {
|
||||
const composed = (localStorage.getItem('jdCharLobbyIdleDown') || '').trim();
|
||||
@@ -4399,7 +4436,8 @@
|
||||
function renderLobbyRankModalContent() {
|
||||
const titleEl = document.getElementById('lobby-rank-case-title');
|
||||
if (titleEl) titleEl.textContent = getLobbyRankCaseTitle();
|
||||
const sorted = [...LOBBY_RANK_MOCK_LEADERS].sort((a, b) => b.score - a.score);
|
||||
const rankSrc = (lobbyRankRealLeaders && lobbyRankRealLeaders.length) ? lobbyRankRealLeaders : LOBBY_RANK_MOCK_LEADERS;
|
||||
const sorted = [...rankSrc].sort((a, b) => b.score - a.score);
|
||||
const podium = document.getElementById('lobby-rank-podium');
|
||||
const list = document.getElementById('lobby-rank-list');
|
||||
const selfFoot = document.getElementById('lobby-rank-self');
|
||||
@@ -4479,6 +4517,7 @@
|
||||
if (!ov) return;
|
||||
ov.classList.remove('is-hidden');
|
||||
ov.setAttribute('aria-hidden', 'false');
|
||||
fetchLobbyRankLeaders();
|
||||
syncLobbyRankScale();
|
||||
renderLobbyRankModalContent();
|
||||
requestAnimationFrame(() => {
|
||||
@@ -5138,6 +5177,11 @@
|
||||
'<img class="ready-active" src="' + SC + 'btn-ready-active.png" alt="">' +
|
||||
'<img class="ready-start-vote" src="' + SC + 'btn-start-vote.png" alt="">' +
|
||||
'</button>' +
|
||||
/* ปุ่ม host แยกต่างหาก (ขวา) — ไปปากคำถัดไป / เริ่มพิจารณาคดี */
|
||||
'<button type="button" class="layer es-proceed btn" id="esBtnProceed" hidden disabled aria-label="ไปต่อ">' +
|
||||
'<img class="es-proceed-next" src="' + SC + 'btn-next-suspect.png" alt="ผู้ต้องสงสัยถัดไป">' +
|
||||
'<img class="es-proceed-trial" src="' + SC + 'btn-start-vote.png" alt="เริ่มพิจารณาคดี">' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="es3-mic btn" id="es3-mic" aria-label="ไมค์">🎤</button>' +
|
||||
'</div>' +
|
||||
@@ -5195,23 +5239,10 @@
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onActionBtnTrigger(); }
|
||||
});
|
||||
}
|
||||
/* ปุ่ม READY — ทุกคนกดยืนยันของตัวเอง (ไม่ทำหน้าที่ไปต่อแล้ว) */
|
||||
var readyEl = ov.querySelector('#esBtnReady');
|
||||
if (readyEl) readyEl.addEventListener('click', function () {
|
||||
if (this.disabled) return;
|
||||
var state = this.getAttribute('data-state') || 'ready';
|
||||
if (state === 'start-vote') {
|
||||
/* host กดเริ่มพิจารณาคดี — ไปปากคำถัดไป/หน้าโหวต */
|
||||
this.disabled = true;
|
||||
socket.emit('testimony-start-vote', {}, function (res) {
|
||||
if (!(res && res.ok)) {
|
||||
var rd = document.getElementById('esBtnReady');
|
||||
if (rd) rd.disabled = false;
|
||||
appendLobbySystemChat('— เริ่มพิจารณาคดีไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
/* state === 'ready' — กดยืนยัน READY ของตัวเอง */
|
||||
this.classList.add('is-active');
|
||||
this.disabled = true;
|
||||
tmState.iReadied = true;
|
||||
@@ -5224,6 +5255,19 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
/* ปุ่ม host แยก — ไปปากคำถัดไป / เริ่มพิจารณาคดี (เปิดเมื่อทุกคน READY) */
|
||||
var proceedEl = ov.querySelector('#esBtnProceed');
|
||||
if (proceedEl) proceedEl.addEventListener('click', function () {
|
||||
if (this.disabled || this.hidden) return;
|
||||
this.disabled = true;
|
||||
socket.emit('testimony-start-vote', {}, function (res) {
|
||||
if (!(res && res.ok)) {
|
||||
var pe = document.getElementById('esBtnProceed');
|
||||
if (pe) pe.disabled = false;
|
||||
appendLobbySystemChat('— ไปต่อไม่สำเร็จ' + (res && res.error ? (' · ' + res.error) : ''));
|
||||
}
|
||||
});
|
||||
});
|
||||
return ov;
|
||||
}
|
||||
|
||||
@@ -5597,39 +5641,40 @@
|
||||
- ทุกคน READY แล้ว: host เห็น btn-start-vote.png (กดได้); คนอื่น disabled */
|
||||
function esUpdateRevealActionButton() {
|
||||
var btn = document.getElementById('esBtnReady');
|
||||
if (!btn) return;
|
||||
var proceed = document.getElementById('esBtnProceed');
|
||||
var isHost = !!(tmState.hostId && tmState.hostId === socket.id);
|
||||
var total = (tmState.members || []).length;
|
||||
var readyCount = (tmState.readyIds || []).length;
|
||||
var allReady = total > 0 && readyCount >= total;
|
||||
var meReadied = !!tmState.iReadied || ((tmState.readyIds || []).indexOf(socket.id) >= 0);
|
||||
|
||||
btn.classList.remove('is-start-vote', 'is-active');
|
||||
btn.disabled = false;
|
||||
/* ปุ่ม READY (กลาง) — แค่ยืนยันของตัวเอง */
|
||||
if (btn) {
|
||||
btn.classList.remove('is-start-vote', 'is-active');
|
||||
btn.setAttribute('data-state', 'ready');
|
||||
if (meReadied) {
|
||||
btn.classList.add('is-active');
|
||||
btn.disabled = true;
|
||||
btn.setAttribute('aria-label', 'READY แล้ว');
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.setAttribute('aria-label', 'READY');
|
||||
}
|
||||
}
|
||||
|
||||
if (allReady && isHost) {
|
||||
/* host เห็นปุ่มเริ่มพิจารณาคดี */
|
||||
btn.setAttribute('data-state', 'start-vote');
|
||||
btn.setAttribute('aria-label', 'เริ่มพิจารณาคดี');
|
||||
btn.classList.add('is-start-vote');
|
||||
btn.disabled = false;
|
||||
} else if (allReady && !isHost) {
|
||||
/* non-host: รอ host */
|
||||
btn.setAttribute('data-state', 'ready');
|
||||
btn.setAttribute('aria-label', 'รอ host เริ่มพิจารณาคดี');
|
||||
btn.classList.add('is-active');
|
||||
btn.disabled = true;
|
||||
} else if (meReadied) {
|
||||
/* ยังรอคนอื่น */
|
||||
btn.setAttribute('data-state', 'ready');
|
||||
btn.setAttribute('aria-label', 'รอผู้เล่นอื่น');
|
||||
btn.classList.add('is-active');
|
||||
btn.disabled = true;
|
||||
} else {
|
||||
/* ยังไม่ได้กด */
|
||||
btn.setAttribute('data-state', 'ready');
|
||||
btn.setAttribute('aria-label', 'READY');
|
||||
btn.disabled = false;
|
||||
/* ปุ่มไปต่อ (ขวา) — host เท่านั้น, เปิดเมื่อทุกคน READY; ป้ายเปลี่ยนตามปากคำสุดท้าย */
|
||||
if (proceed) {
|
||||
var isLast = (Number(tmState.round) || 0) >= 2;
|
||||
proceed.classList.toggle('is-last', isLast);
|
||||
proceed.setAttribute('aria-label', isLast ? 'เริ่มพิจารณาคดี' : 'ผู้ต้องสงสัยถัดไป');
|
||||
if (isHost) {
|
||||
proceed.hidden = false;
|
||||
proceed.disabled = !allReady;
|
||||
proceed.classList.toggle('is-enabled', allReady);
|
||||
} else {
|
||||
proceed.hidden = true;
|
||||
proceed.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4906,7 +4906,7 @@
|
||||
<div class="quiz-carry-pregame-card">
|
||||
<h2 id="quiz-carry-pregame-sr-title" class="quiz-carry-pregame-sr-only">ประลองความรู้ คำศัพท์ในกระบวนการยุติธรรม — How to play</h2>
|
||||
<div class="quiz-carry-pregame-art">
|
||||
<img id="quiz-carry-pregame-howto-img" class="quiz-carry-pregame-howto-img" src="/Game/img/quiz-carry/howto.png" width="900" height="520" alt="HOW TO PLAY — ประลองความรู้ คำศัพท์ในกระบวนการยุติธรรม" loading="lazy" decoding="async" />
|
||||
<img id="quiz-carry-pregame-howto-img" class="quiz-carry-pregame-howto-img" src="/Game/img/quiz-carry/popup-Howto.png" width="1454" height="854" alt="HOW TO PLAY — ประลองความรู้ คำศัพท์ในกระบวนการยุติธรรม" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
<div class="quiz-carry-pregame-footer">
|
||||
<p id="quiz-carry-pregame-status" class="quiz-carry-pregame-status" aria-live="polite"></p>
|
||||
@@ -5152,7 +5152,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.00610200038"></script>
|
||||
<script src="js/play.js?v=0.00610200052"></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=15">
|
||||
<link rel="stylesheet" href="css/testimony-overlay.css?v=17">
|
||||
<link rel="stylesheet" href="css/evidence-view-overlay.css?v=13">
|
||||
<style>
|
||||
html, body.room-lobby-page { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; min-height: 100%; }
|
||||
@@ -1613,7 +1613,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.0271"></script>
|
||||
<script src="js/room-lobby.js?v=0.0275"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+161
-11
@@ -509,7 +509,7 @@ function getSpecialQuizSet(settings, level, caseId) {
|
||||
|
||||
/* ===== Special Quiz ในเกม — ไอคอนสุ่มในฉาก + เซสชันถาม-ตอบ multiplayer (ทุกคนต้องตอบถูก) ===== */
|
||||
/** เฉพาะ 4 มินิเกมนี้ (ตอนสืบสวน) ที่ไอคอนพิเศษจะสุ่มโผล่ */
|
||||
const SPECIAL_QUIZ_ELIGIBLE_MAP_IDS = ['mng8a80o', 'mnorwqx1', 'mnptfts2', 'mnpz6rkp'];
|
||||
const SPECIAL_QUIZ_ELIGIBLE_MAP_IDS = ['mng8a80o', 'mnorwqx1', 'mnptfts2', 'mnpz6rkp', 'mno9kb07'];
|
||||
const SPECIAL_QUIZ_ICON_TYPES = ['lawyer', 'police'];
|
||||
/** โอกาสโผล่ = 1 ใน 3 ของการเล่นแต่ละรอบ */
|
||||
const SPECIAL_QUIZ_SPAWN_CHANCE = 1 / 3;
|
||||
@@ -861,13 +861,17 @@ function specialCardClientPayload(card) {
|
||||
};
|
||||
}
|
||||
|
||||
/** เพดานเวลาที่ Time Dilation สะสมได้ต่อรอบ (กันการ์ดซ้อนยืดเวลาเกินเหตุ) */
|
||||
const TIME_DILATION_MAX_PENDING_SEC = 30;
|
||||
|
||||
/** ใช้การ์ดที่ทำงาน "ทันที" (minigame / now) — ส่วนที่เหลือเข้าคิวรอ flow ที่ถูก */
|
||||
function applySpecialCardImmediate(sid, space, card) {
|
||||
const def = specialCardDef(card && card.cardId);
|
||||
if (!def) return;
|
||||
if (def.when === 'minigame' && def.key === 'time_dilation') {
|
||||
/* +10 วิให้รอบ quiz (mng8a80o) ตอน resume — มินิเกมอื่นฝั่ง client ขยายเอง */
|
||||
space.timeDilationPendingSec = (space.timeDilationPendingSec || 0) + (def.addSec || 10);
|
||||
/* +10 วิให้รอบ quiz (mng8a80o) ตอน resume — มินิเกมอื่นฝั่ง client ขยายเอง
|
||||
cap กันการ์ดซ้อนหลายใบจนเวลายืดเกินเหตุ */
|
||||
space.timeDilationPendingSec = Math.min(TIME_DILATION_MAX_PENDING_SEC, (space.timeDilationPendingSec || 0) + (def.addSec || 10));
|
||||
io.to(sid).emit('special-card-applied', {
|
||||
card: specialCardClientPayload(card),
|
||||
addSec: def.addSec || 10,
|
||||
@@ -876,10 +880,17 @@ function applySpecialCardImmediate(sid, space, card) {
|
||||
return;
|
||||
}
|
||||
if (def.when === 'now' && def.key === 'fund') {
|
||||
/* +COINS — แต่ละ client บวกเหรียญของตัวเองผ่าน PHP (server ไม่รู้ playerKey) */
|
||||
/* +COINS — server แจกเองผ่าน game-award.php (มี secret + รู้ playerKey จาก join) กัน client ปลอม
|
||||
noScore: ไม่ให้เหรียญ Fund ไปปั่นคะแนน high-score (เป็นโชค ไม่ใช่ฝีมือ) */
|
||||
const coins = def.coins || 10;
|
||||
const awards = [...space.peers.values()]
|
||||
.filter((p) => p.playerKey)
|
||||
.map((p) => ({ playerKey: p.playerKey, nickname: (p.nickname || '').trim() || 'ผู้เล่น', coins: coins, noScore: true }));
|
||||
submitGameAwards(awards);
|
||||
io.to(sid).emit('special-card-applied', {
|
||||
card: specialCardClientPayload(card),
|
||||
coins: def.coins || 10,
|
||||
coins: coins,
|
||||
coinsServerAwarded: true,
|
||||
});
|
||||
consumePendingSpecialCard(space, card.cardId);
|
||||
return;
|
||||
@@ -1548,6 +1559,82 @@ function computeRunGradeForEvidence(space) {
|
||||
} catch (e) { return 'C'; }
|
||||
}
|
||||
|
||||
/* ===== แจกเหรียญตามอันดับ + คะแนนสะสม (high score) แบบ server-authoritative ===== */
|
||||
const GAME_AWARD_URL = process.env.GAME_AWARD_URL || 'https://srv1361159.hstgr.cloud/Admin/api/game-award.php';
|
||||
const GAME_AWARD_SECRET_PATH = path.join(__dirname, '..', 'Admin', 'private', 'game-award-secret.txt');
|
||||
let __gameAwardSecret = null;
|
||||
function gameAwardSecret() {
|
||||
if (__gameAwardSecret != null) return __gameAwardSecret;
|
||||
try { __gameAwardSecret = String(fs.readFileSync(GAME_AWARD_SECRET_PATH, 'utf8')).trim(); }
|
||||
catch (e) { __gameAwardSecret = ''; }
|
||||
return __gameAwardSecret;
|
||||
}
|
||||
/* เหรียญตามอันดับผู้รอดชีวิต: อันดับ 1→10, 2→8, 3→6, 4→4, 5→2, 6+→0 */
|
||||
const MINIGAME_RANK_COINS = [10, 8, 6, 4, 2, 0];
|
||||
|
||||
/* คะแนน + สถานะรอด/ตาย ของผู้เล่นต่อ gameType ตอนจบมินิเกม */
|
||||
function minigamePlayerScoreSurvived(space, p, gameType) {
|
||||
switch (gameType) {
|
||||
case 'quiz': {
|
||||
const st = space.quizSession && space.quizSession.players ? space.quizSession.players[p.id] : null;
|
||||
return { score: st ? Math.max(0, Number(st.score) | 0) : 0, survived: st ? !st.eliminated : true };
|
||||
}
|
||||
case 'gauntlet':
|
||||
case 'jump_survive':
|
||||
return { score: Math.max(0, p.gauntletScore | 0), survived: !p.gauntletEliminated };
|
||||
case 'space_shooter':
|
||||
return { score: Math.max(0, p.spaceShooterScore | 0), survived: true };
|
||||
case 'balloon_boss':
|
||||
return { score: Math.max(0, p.balloonBossScore | 0), survived: !p.balloonBossEliminated };
|
||||
default: /* stack, quiz_carry — client รายงานคะแนน/รอดเอง ตอนจบ */
|
||||
return { score: Math.max(0, p.reportedMiniScore | 0), survived: p.reportedMiniSurvived !== false };
|
||||
}
|
||||
}
|
||||
|
||||
/* ยิงไป PHP (มี secret) เพื่อบวกเหรียญ+คะแนนเข้าบัญชีผู้เล่นจริง (client ปลอมไม่ได้) */
|
||||
function submitGameAwards(awards) {
|
||||
const secret = gameAwardSecret();
|
||||
if (!secret || !Array.isArray(awards) || !awards.length) return;
|
||||
try {
|
||||
fetch(GAME_AWARD_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ secret, awards }),
|
||||
}).then((r) => r.json()).then((d) => {
|
||||
if (!d || !d.ok) console.error('[game-award] failed', d);
|
||||
}).catch((e) => console.error('[game-award] error', e && e.message));
|
||||
} catch (e) { console.error('[game-award] throw', e && e.message); }
|
||||
}
|
||||
|
||||
/* แจกเหรียญตามอันดับคะแนน (เฉพาะผู้รอดชีวิต ผู้ตาย = 0) + คะแนนสะสมเข้า leaderboard — ครั้งเดียวต่อรอบ */
|
||||
function awardMinigameRankCoins(sid, space) {
|
||||
if (!space || space.coinAwardDoneForRun) return;
|
||||
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
|
||||
const gameType = md && md.gameType;
|
||||
if (!gameType || gameType === 'zep' || gameType === 'quiz_battle' || serverMapIsPostCaseLobbyB(space)) return;
|
||||
const peers = [...space.peers.values()];
|
||||
if (!peers.length) return;
|
||||
space.coinAwardDoneForRun = true;
|
||||
|
||||
const rows = peers.map((p) => {
|
||||
const ss = minigamePlayerScoreSurvived(space, p, gameType);
|
||||
return { id: p.id, playerKey: p.playerKey || '', nickname: (p.nickname || '').trim() || 'ผู้เล่น', score: ss.score, survived: !!ss.survived, coins: 0 };
|
||||
});
|
||||
/* ผู้รอดชีวิตเรียงคะแนนมาก→น้อย แล้วให้เหรียญตามอันดับ */
|
||||
const survivors = rows.filter((r) => r.survived).sort((a, b) => b.score - a.score);
|
||||
survivors.forEach((r, i) => { r.coins = i < MINIGAME_RANK_COINS.length ? MINIGAME_RANK_COINS[i] : 0; });
|
||||
|
||||
const caseId = String(space.caseId || space.detectiveLobbyCaseId || '');
|
||||
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);
|
||||
|
||||
io.to(sid).emit('minigame-coins', {
|
||||
gameType,
|
||||
coinsTable: MINIGAME_RANK_COINS,
|
||||
results: rows.map((r) => ({ id: r.id, nickname: r.nickname, score: r.score, survived: r.survived, coins: r.coins })),
|
||||
});
|
||||
}
|
||||
|
||||
/* card object ที่เก็บในแฟ้มหลักฐาน */
|
||||
function sanitizeStoredEvidenceCard(c) {
|
||||
if (!c || typeof c !== 'object') return null;
|
||||
@@ -1716,6 +1803,8 @@ function defaultGameTiming() {
|
||||
balloonBossPlayerBalloonImageUrls: ['', '', '', '', '', ''],
|
||||
/** กรอบฟองรอบผู้เล่น / ลูกโป่ง fallback เริ่มต้น — Artboard 9 (วง cyan +หาง) */
|
||||
balloonBossPlayerBalloonFallbackUrl: '/Game/img/MegaVirus/Artboard 9.png',
|
||||
/** จำนวนลูกโป่งต่อคน (0 = ใช้ค่าจากแมป หรือ default 3) */
|
||||
balloonBossBalloonsPerPlayer: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2097,6 +2186,9 @@ function loadGameTiming() {
|
||||
balloonBossPlayerBalloonFallbackUrl: Object.prototype.hasOwnProperty.call(j, 'balloonBossPlayerBalloonFallbackUrl')
|
||||
? sanitizeGauntletAssetUrl(j.balloonBossPlayerBalloonFallbackUrl)
|
||||
: '',
|
||||
balloonBossBalloonsPerPlayer: Object.prototype.hasOwnProperty.call(j, 'balloonBossBalloonsPerPlayer')
|
||||
? Math.max(0, Math.min(12, Math.floor(Number(j.balloonBossBalloonsPerPlayer)) || 0))
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
} catch (e) { console.error('loadGameTiming', e.message); }
|
||||
@@ -2105,6 +2197,13 @@ function loadGameTiming() {
|
||||
|
||||
let runtimeGameTiming = loadGameTiming();
|
||||
|
||||
/** จำนวนลูกโป่งต่อคน Mega Virus — แมปกำหนดก่อน, ไม่งั้นใช้ค่ากลางจาก admin, สุดท้าย default 3 (clamp 1-12) */
|
||||
function balloonBossBalloonsForMap(md) {
|
||||
const fromMap = Math.floor(Number(md && md.balloonBossBalloonsPerPlayer)) || 0;
|
||||
const fromCfg = Math.floor(Number(runtimeGameTiming && runtimeGameTiming.balloonBossBalloonsPerPlayer)) || 0;
|
||||
return Math.max(1, Math.min(12, fromMap || fromCfg || 3));
|
||||
}
|
||||
|
||||
function getGauntletTickMs() {
|
||||
return runtimeGameTiming.gauntletTickMs;
|
||||
}
|
||||
@@ -2225,6 +2324,13 @@ function saveGameTimingToFile(d) {
|
||||
const balloonBossPlayerBalloonFallbackUrl = Object.prototype.hasOwnProperty.call(d, 'balloonBossPlayerBalloonFallbackUrl')
|
||||
? sanitizeGauntletAssetUrl(d.balloonBossPlayerBalloonFallbackUrl)
|
||||
: prevBbFb;
|
||||
/* จำนวนลูกโป่งต่อคน (0 = ใช้ค่าจากแมป หรือ default 3) */
|
||||
const prevBbCount = Object.prototype.hasOwnProperty.call(prev, 'balloonBossBalloonsPerPlayer')
|
||||
? Math.max(0, Math.min(12, Math.floor(Number(prev.balloonBossBalloonsPerPlayer)) || 0))
|
||||
: 0;
|
||||
const balloonBossBalloonsPerPlayer = Object.prototype.hasOwnProperty.call(d, 'balloonBossBalloonsPerPlayer')
|
||||
? Math.max(0, Math.min(12, Math.floor(Number(d.balloonBossBalloonsPerPlayer)) || 0))
|
||||
: prevBbCount;
|
||||
const prevStackTowerSec = Object.prototype.hasOwnProperty.call(prev, 'stackTowerMissionTimeSec')
|
||||
? prev.stackTowerMissionTimeSec
|
||||
: 90;
|
||||
@@ -2287,6 +2393,7 @@ function saveGameTimingToFile(d) {
|
||||
balloonBossBossImageUrl,
|
||||
balloonBossPlayerBalloonImageUrls,
|
||||
balloonBossPlayerBalloonFallbackUrl,
|
||||
balloonBossBalloonsPerPlayer,
|
||||
};
|
||||
fs.writeFileSync(GAME_TIMING_PATH, JSON.stringify(out, null, 2), 'utf8');
|
||||
runtimeGameTiming = out;
|
||||
@@ -3018,6 +3125,9 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
|
||||
space.suspectPhaseActive = false;
|
||||
space.detectiveMinigameActive = true;
|
||||
space.detectiveAwardDoneForRun = false;
|
||||
space.coinAwardDoneForRun = false;
|
||||
/* รีเซ็ตคะแนนที่ client รายงาน (stack/quiz_carry) ของรอบก่อน */
|
||||
space.peers.forEach((p) => { p.reportedMiniScore = 0; p.reportedMiniSurvived = true; });
|
||||
/* Card 3 Ban — ผู้ที่ถูกแบนไม่ได้เล่นมินิเกมรอบนี้ (1 รอบ) */
|
||||
space.bannedThisRunPlayerId = space.bannedPlayerId || null;
|
||||
space.bannedPlayerId = null;
|
||||
@@ -3121,6 +3231,8 @@ function beginDetectiveSuspectMinigame(sid, space, cardEntry, selectedIndex) {
|
||||
|
||||
function returnDetectiveSpaceToLobbyB(sid, space, message, awardOptions) {
|
||||
ensurePostCaseLobbyMapLoaded();
|
||||
/* แจกเหรียญตามอันดับ + คะแนนสะสม ก่อนล้างสถานะมินิเกม (คะแนน/ผู้รอดยังครบ) */
|
||||
awardMinigameRankCoins(sid, space);
|
||||
clearSpaceQuizTimers(space);
|
||||
clearSpecialQuizTimers(space);
|
||||
space.specialQuiz = null;
|
||||
@@ -5862,6 +5974,7 @@ function newGauntletRunState(space) {
|
||||
spawnAcc: 0,
|
||||
nextSpawnIn: 3,
|
||||
crownRunHeld: crown,
|
||||
finishPhase: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6049,12 +6162,26 @@ function runGauntletTick(sid, space) {
|
||||
const h = md.height || 15;
|
||||
const gr = space.gauntletRun;
|
||||
if (!gr) return;
|
||||
/* หยุด run ระหว่างตอบคำถามพิเศษ (special-quiz) — เลื่อน endsAt ไปด้วยให้เวลาไม่เดิน, ไม่ไหล obstacle/ชน */
|
||||
if (space.specialQuizAwaitContinue || (space.specialQuiz && space.specialQuiz.triggered && !space.specialQuiz.done)) {
|
||||
if (gr.endsAt != null) gr.endsAt += getGauntletTickMs();
|
||||
emitGauntletSync(sid, space);
|
||||
return;
|
||||
}
|
||||
ensureGauntletEndsAtIfNeeded(space);
|
||||
if (gr.endsAt != null && Date.now() >= gr.endsAt) {
|
||||
endGauntletGame(sid, space, 'time');
|
||||
return;
|
||||
}
|
||||
|
||||
/* ถึงเส้นชัยแล้ว (client แจ้ง) — หยุดสร้าง/ชน obstacle ให้ทุกคนเดินเข้าเส้นชัยได้ ไม่มี -10 อีก */
|
||||
if (gr.finishPhase) {
|
||||
if (gr.obstacles.length) { gr.obstacles = []; }
|
||||
space.peers.forEach((p) => { if ((p.gauntletJumpTicks || 0) > 0) p.gauntletJumpTicks--; });
|
||||
emitGauntletSync(sid, space);
|
||||
return;
|
||||
}
|
||||
|
||||
if (gr.crownRunHeld) {
|
||||
space.peers.forEach((p) => {
|
||||
p.gauntletJumpPending = false;
|
||||
@@ -6800,7 +6927,7 @@ io.on('connection', (socket) => {
|
||||
if (qbAllAnswered(room)) qbReveal(room);
|
||||
});
|
||||
|
||||
socket.on('join-space', ({ spaceId, nickname, characterId, playMapId, desiredLobbyColorThemeIndex, desiredLobbySkinToneIndex }, cb) => {
|
||||
socket.on('join-space', ({ spaceId, nickname, characterId, playMapId, playerKey, desiredLobbyColorThemeIndex, desiredLobbySkinToneIndex }, cb) => {
|
||||
const space = spaces.get(spaceId);
|
||||
if (!space || !space.mapData) return cb && cb({ ok: false, error: 'ไม่พบห้อง' });
|
||||
const maxPlayers = space.maxPlayers || 10;
|
||||
@@ -6834,7 +6961,7 @@ io.on('connection', (socket) => {
|
||||
const mdJoin = (space.mapId && maps.get(space.mapId)) || space.mapData;
|
||||
const spawnJoinOrder = space.peers.size;
|
||||
const spawnPt = pickSpawnForJoin(mdJoin, spawnJoinOrder);
|
||||
const bbStartBalloons = Math.max(1, Math.min(12, Math.floor(Number(mdJoin.balloonBossBalloonsPerPlayer)) || 3));
|
||||
const bbStartBalloons = balloonBossBalloonsForMap(mdJoin);
|
||||
/* ใช้สีที่ client เลือกไว้ใน Main-Lobby — คนจริงสำคัญกว่าบอท */
|
||||
let chosenThemeIdx = parseInt(desiredLobbyColorThemeIndex, 10);
|
||||
if (chosenThemeIdx >= 1 && chosenThemeIdx <= LOBBY_THEME_COUNT) {
|
||||
@@ -6867,6 +6994,8 @@ io.on('connection', (socket) => {
|
||||
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,
|
||||
playerKey: (typeof playerKey === 'string' && /^[a-zA-Z0-9_-]{8,128}$/.test(playerKey)) ? playerKey : '',
|
||||
reportedMiniScore: 0, reportedMiniSurvived: true,
|
||||
};
|
||||
if (mdJoin.gameType === 'quiz_battle' && quizBattlePathModeActiveServer(mdJoin)) {
|
||||
const sn = quizBattleSpawnWorldFromJoinOrderServer(mdJoin, spawnJoinOrder);
|
||||
@@ -7209,13 +7338,19 @@ io.on('connection', (socket) => {
|
||||
reply(started);
|
||||
});
|
||||
|
||||
socket.on('detective-minigame-finished', (_data, cb) => {
|
||||
socket.on('detective-minigame-finished', (data, cb) => {
|
||||
const reply = typeof cb === 'function' ? cb : () => {};
|
||||
const sid = socket.data.spaceId;
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space || !space.peers.has(socket.id)) {
|
||||
return reply({ ok: false, error: 'ไม่อยู่ในห้อง' });
|
||||
}
|
||||
/* client รายงานคะแนน/รอด ของตัวเอง (ใช้กับเกมที่ server ไม่ได้นับคะแนน เช่น stack/quiz_carry) */
|
||||
const finPeer = space.peers.get(socket.id);
|
||||
if (finPeer && data && typeof data === 'object') {
|
||||
if (Number.isFinite(Number(data.score))) finPeer.reportedMiniScore = Math.max(0, Math.floor(Number(data.score)));
|
||||
if (data.survived === false) finPeer.reportedMiniSurvived = false;
|
||||
}
|
||||
if (!space.detectiveMinigameActive) {
|
||||
if (serverMapIsPostCaseLobbyB(space)) {
|
||||
const grant = grantDetectiveEvidenceForCurrentRun(sid, space, [socket.id]);
|
||||
@@ -7522,7 +7657,8 @@ io.on('connection', (socket) => {
|
||||
if (detectiveB && lobbyLevel && caseId && !isLobbyADetectiveStart) {
|
||||
return reply({ ok: false, error: 'ย้ายไป LobbyB ไม่ได้ — ต้องอยู่ฉาก LobbyA (หรือล็อบบี้ชื่อ LobbyA) และเซิร์ฟต้องมีไฟล์แผนที่ LobbyB' });
|
||||
}
|
||||
if (!lobbyHostStandingInStartArea(space, socket.id)) {
|
||||
/* detective LobbyA→LobbyB: ไม่บังคับยืนโซนส้ม (หน้าจอเลือกคดีบังแผนที่ ขยับตัวไม่ได้) */
|
||||
if (!isLobbyADetectiveStart && !lobbyHostStandingInStartArea(space, socket.id)) {
|
||||
return reply({ ok: false, error: 'ยืนในพื้นที่เริ่มเกม (สีส้มในเอดิเตอร์) ก่อนกดเริ่ม' });
|
||||
}
|
||||
if (isLobbyADetectiveStart) {
|
||||
@@ -7708,6 +7844,20 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
/* client แจ้งว่าเส้นชัยมาถึงแล้ว (BG ถึง finish) → เข้าโหมดจบ: หยุด obstacle + collision */
|
||||
socket.on('gauntlet-finish-phase', () => {
|
||||
const sid = socket.data.spaceId;
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
if (!space || !space.peers.has(socket.id)) return;
|
||||
const md = (space.mapId && maps.get(space.mapId)) || space.mapData;
|
||||
if (!md || md.gameType !== 'gauntlet') return;
|
||||
if (space.gauntletRun) {
|
||||
space.gauntletRun.finishPhase = true;
|
||||
space.gauntletRun.obstacles = [];
|
||||
emitGauntletSync(sid, space);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('gauntlet-jump', () => {
|
||||
const sid = socket.data.spaceId;
|
||||
const space = sid ? spaces.get(sid) : null;
|
||||
@@ -7922,7 +8072,7 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
}
|
||||
if (p && md && md.gameType === 'balloon_boss' && data) {
|
||||
const bbDefaultBalloons = Math.max(1, Math.min(12, Math.floor(Number(md.balloonBossBalloonsPerPlayer)) || 3));
|
||||
const bbDefaultBalloons = balloonBossBalloonsForMap(md);
|
||||
if (data.balloonBossScore != null) {
|
||||
const ns = Math.floor(Number(data.balloonBossScore));
|
||||
if (Number.isFinite(ns) && ns >= 0) {
|
||||
@@ -7960,7 +8110,7 @@ io.on('connection', (socket) => {
|
||||
const out = { id: socket.id, x: nx, y: ny, direction: p.direction, characterId: p.characterId };
|
||||
if (md && md.gameType === 'space_shooter') out.spaceShooterScore = Math.max(0, p.spaceShooterScore | 0);
|
||||
if (md && md.gameType === 'balloon_boss') {
|
||||
const bbDef = Math.max(1, Math.min(12, Math.floor(Number(md.balloonBossBalloonsPerPlayer)) || 3));
|
||||
const bbDef = balloonBossBalloonsForMap(md);
|
||||
out.balloonBossScore = Math.max(0, p.balloonBossScore | 0);
|
||||
out.balloonBossBossDmg = Math.max(0, p.balloonBossBossDmg | 0);
|
||||
out.balloonBossBalloons = typeof p.balloonBossBalloons === 'number' && Number.isFinite(p.balloonBossBalloons)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -230,6 +230,6 @@
|
||||
<script src="daily-popup.js?v=26" data-daily-trigger="#btn-daily" data-daily-asset-base="IMAGE/Daily" data-daily-test-reset-seconds="0"></script>
|
||||
<script src="../Game/js/customize-popup.js?v=31" data-customize-triggers="#btn-cloth" data-customize-asset-base="/Game/img/03-5-Customize"></script>
|
||||
<script src="../Game/js/profile-popup.js?v=6" data-profile-trigger="#btn-myprofile" data-profile-asset-base="/Game/img/03-6-Profile"></script>
|
||||
<script src="lobby.js?v=0.0190"></script>
|
||||
<script src="lobby.js?v=0.0191"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -252,10 +252,56 @@
|
||||
.catch(function () { /* offline */ });
|
||||
}
|
||||
|
||||
function lbEscapeHtml(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
/** วาดกระดานผู้นำจากข้อมูลจริง (top = [{rank,name,score,coins}]) */
|
||||
function renderLeaderboardRows(top) {
|
||||
var listEl = document.querySelector('.lobby-leaderboard-list');
|
||||
if (!listEl || !Array.isArray(top) || !top.length) return; /* ว่าง → คงตัวอย่างเดิมไว้ */
|
||||
var html = '';
|
||||
for (var i = 0; i < top.length; i++) {
|
||||
var r = top[i] || {};
|
||||
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 head = rank <= 3
|
||||
? '<img src="IMAGE/leaderboard-' + rank + '.png" alt="" class="lobby-rank-icon" role="presentation" decoding="async">'
|
||||
: '<span class="lobby-rank-num">' + rank + '</span>';
|
||||
html += '<li>' + head +
|
||||
'<div class="lobby-rank-info">' +
|
||||
'<span class="lobby-rank-name">' + name + '</span>' +
|
||||
'<span class="lobby-rank-case">(เหรียญ : ' + coins + ')</span>' +
|
||||
'</div>' +
|
||||
'<span class="lobby-rank-score">' + score + '</span>' +
|
||||
'</li>';
|
||||
}
|
||||
listEl.innerHTML = html;
|
||||
scheduleLeaderboardPlacement();
|
||||
}
|
||||
|
||||
/** ดึงกระดานผู้นำจากเซิร์ฟเวอร์ (อันดับตามคะแนนสะสม high score) */
|
||||
function fetchAndRenderLeaderboard() {
|
||||
var base = (typeof appPath === 'function' ? appPath('/Admin/api/leaderboard.php') : '/Admin/api/leaderboard.php');
|
||||
var url = base + '?limit=50&playerKey=' + encodeURIComponent(ensurePlayerKey());
|
||||
fetch(url, { credentials: 'omit' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (!d || !d.ok || !Array.isArray(d.top)) return;
|
||||
renderLeaderboardRows(d.top);
|
||||
})
|
||||
.catch(function () { /* ออฟไลน์ → คงตัวอย่างเดิม */ });
|
||||
}
|
||||
|
||||
/** รีเฟรชหลังเลือกตัวละคร / สลับแท็บ / ย้อนกลับบน tablet (bfcache) */
|
||||
function syncMainLobbyCharacterUi() {
|
||||
applyProfileTexts();
|
||||
syncCoinsFromServer();
|
||||
fetchAndRenderLeaderboard();
|
||||
function applyCharAssets(urlOrNull) {
|
||||
applyProfileAvatar(urlOrNull);
|
||||
applyCenterCharacterResolved(urlOrNull);
|
||||
|
||||
Reference in New Issue
Block a user