update more flow and fix bugs
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* สาธารณะ — ระบบรางวัลล็อกอินรายวัน (Daily Login Reward) แบบ server-authoritative
|
||||
*
|
||||
* GET ?playerKey=... → คืนสถานะปัจจุบัน { claimedDays, currentDay, msUntilReset, coins, rewards }
|
||||
* POST { playerKey, day } → เคลมรางวัลของวันนั้น (server ตรวจสอบ + บวกเหรียญจริง กันยิงซ้ำ/ฟาร์ม)
|
||||
*
|
||||
* เก็บสถานะไว้ในบัญชี guest (providerUserId == playerKey) ใต้คีย์ "daily"
|
||||
* รีเซ็ตทุกเที่ยงคืนตามเวลาไทย (Asia/Bangkok)
|
||||
*/
|
||||
require __DIR__ . '/_common.php';
|
||||
|
||||
date_default_timezone_set('Asia/Bangkok');
|
||||
|
||||
const DAILY_TOTAL_DAYS = 7;
|
||||
// จำนวนเหรียญต่อวัน (วันที่ 7 ให้เยอะสุด) — server เป็นเจ้าของค่านี้ ฝั่ง client เลือกเองไม่ได้
|
||||
const DAILY_REWARDS = [10, 15, 20, 25, 30, 50, 100];
|
||||
|
||||
function daily_now_ms(): int
|
||||
{
|
||||
return (int) round(microtime(true) * 1000);
|
||||
}
|
||||
|
||||
/** เที่ยงคืนของ "วันนี้" (เวลาไทย) เป็น ms */
|
||||
function daily_midnight_ms(int $nowMs): int
|
||||
{
|
||||
$t = (int) floor($nowMs / 1000);
|
||||
$mid = strtotime('today 00:00:00', $t);
|
||||
return ($mid !== false ? $mid : $t) * 1000;
|
||||
}
|
||||
|
||||
/** เที่ยงคืนของ "พรุ่งนี้" (เวลาไทย) เป็น ms */
|
||||
function daily_next_midnight_ms(int $nowMs): int
|
||||
{
|
||||
$t = (int) floor($nowMs / 1000);
|
||||
$mid = strtotime('tomorrow 00:00:00', $t);
|
||||
return ($mid !== false ? $mid : $t + 86400) * 1000;
|
||||
}
|
||||
|
||||
function daily_default_state(int $nowMs): array
|
||||
{
|
||||
return [
|
||||
'anchorMs' => daily_midnight_ms($nowMs),
|
||||
'claimedDays' => array_fill(0, DAILY_TOTAL_DAYS, false),
|
||||
'lockUntilMs' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
/** ทำให้ state ถูกต้อง (รีเซ็ตรอบใหม่ถ้าถึงเวลา) แล้วคำนวณ currentDay/msUntilReset */
|
||||
function daily_compute(array &$daily, int $nowMs): array
|
||||
{
|
||||
if (!is_array($daily['claimedDays'] ?? null) || count($daily['claimedDays']) !== DAILY_TOTAL_DAYS) {
|
||||
$daily = daily_default_state($nowMs);
|
||||
}
|
||||
// normalize types
|
||||
$claimed = [];
|
||||
for ($i = 0; $i < DAILY_TOTAL_DAYS; $i++) {
|
||||
$claimed[$i] = !empty($daily['claimedDays'][$i]);
|
||||
}
|
||||
$daily['claimedDays'] = $claimed;
|
||||
$daily['anchorMs'] = (int) ($daily['anchorMs'] ?? daily_midnight_ms($nowMs));
|
||||
$daily['lockUntilMs'] = (int) ($daily['lockUntilMs'] ?? 0);
|
||||
|
||||
$changed = false;
|
||||
|
||||
// วันแรกที่ยังไม่เคลม
|
||||
$firstUnclaimed = 0;
|
||||
for ($i = 0; $i < DAILY_TOTAL_DAYS; $i++) {
|
||||
if (!$claimed[$i]) { $firstUnclaimed = $i + 1; break; }
|
||||
}
|
||||
|
||||
$nowMidnight = daily_midnight_ms($nowMs);
|
||||
$remainMs = max(0, daily_next_midnight_ms($nowMs) - $nowMs);
|
||||
|
||||
// self-heal: เคลมครบแต่ไม่มี lock → ตั้ง lock ถึงเที่ยงคืนถัดไป
|
||||
if (!$firstUnclaimed && $daily['lockUntilMs'] <= 0) {
|
||||
$daily['lockUntilMs'] = daily_next_midnight_ms($nowMs);
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// เคลมครบ 7 วัน + ถึงเวลารีเซ็ตแล้ว → เริ่มรอบใหม่
|
||||
if (!$firstUnclaimed && $daily['lockUntilMs'] > 0 && $nowMs >= $daily['lockUntilMs']) {
|
||||
$daily['anchorMs'] = $nowMidnight;
|
||||
$daily['claimedDays'] = array_fill(0, DAILY_TOTAL_DAYS, false);
|
||||
$daily['lockUntilMs'] = 0;
|
||||
$claimed = $daily['claimedDays'];
|
||||
$firstUnclaimed = 1;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// วันที่ปลดล็อกตามเวลา (เพิ่มทีละวันทุกเที่ยงคืน)
|
||||
$anchorMidnight = daily_midnight_ms((int) $daily['anchorMs']);
|
||||
$dayIndex = (int) max(0, floor(($nowMidnight - $anchorMidnight) / 86400000));
|
||||
$unlockedByTime = min(DAILY_TOTAL_DAYS, $dayIndex + 1);
|
||||
|
||||
$lockActive = $daily['lockUntilMs'] > $nowMs;
|
||||
if (!$lockActive && $daily['lockUntilMs'] > 0) {
|
||||
$daily['lockUntilMs'] = 0;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
$currentDay = 0;
|
||||
if ($firstUnclaimed > 0 && $firstUnclaimed <= $unlockedByTime && !$lockActive) {
|
||||
$currentDay = $firstUnclaimed;
|
||||
}
|
||||
|
||||
return [
|
||||
'changed' => $changed,
|
||||
'currentDay' => $currentDay,
|
||||
'msUntilReset' => $remainMs,
|
||||
'firstUnclaimed' => $firstUnclaimed,
|
||||
];
|
||||
}
|
||||
|
||||
function daily_public_state(array $daily, array $computed, int $coins, int $nowMs): array
|
||||
{
|
||||
return [
|
||||
'ok' => true,
|
||||
'claimedDays' => array_values($daily['claimedDays']),
|
||||
'anchorMs' => (int) $daily['anchorMs'],
|
||||
'lockUntilMs' => (int) $daily['lockUntilMs'],
|
||||
'currentDay' => (int) $computed['currentDay'],
|
||||
'msUntilReset' => (int) $computed['msUntilReset'],
|
||||
'serverNowMs' => $nowMs,
|
||||
'coins' => $coins,
|
||||
'rewards' => DAILY_REWARDS,
|
||||
];
|
||||
}
|
||||
|
||||
function daily_validate_key(string $key): void
|
||||
{
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key)) {
|
||||
json_response(['ok' => false, 'error' => 'playerKey ต้องยาว 8–128 (a-z 0-9 _ -)'], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/** หา index บัญชี guest ตาม playerKey (คืน -1 ถ้าไม่มี) */
|
||||
function daily_find_account(array $store, string $key): int
|
||||
{
|
||||
foreach ($store['accounts'] ?? [] as $i => $a) {
|
||||
if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
$nowMs = daily_now_ms();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$key = trim((string) ($_GET['playerKey'] ?? ''));
|
||||
daily_validate_key($key);
|
||||
|
||||
$store = read_store();
|
||||
$idx = daily_find_account($store, $key);
|
||||
|
||||
if ($idx < 0) {
|
||||
// ยังไม่มีบัญชี → คืน state เริ่มต้น (ไม่เขียน store จนกว่าจะเคลมจริง)
|
||||
$daily = daily_default_state($nowMs);
|
||||
$computed = daily_compute($daily, $nowMs);
|
||||
json_response(daily_public_state($daily, $computed, 0, $nowMs));
|
||||
}
|
||||
|
||||
$acc = $store['accounts'][$idx];
|
||||
$daily = is_array($acc['daily'] ?? null) ? $acc['daily'] : daily_default_state($nowMs);
|
||||
$computed = daily_compute($daily, $nowMs);
|
||||
$coins = max(0, (int) ($acc['coins'] ?? 0));
|
||||
|
||||
if ($computed['changed']) {
|
||||
$store['accounts'][$idx]['daily'] = $daily;
|
||||
$store['accounts'][$idx]['updatedAt'] = gmdate('c');
|
||||
write_store($store);
|
||||
}
|
||||
|
||||
json_response(daily_public_state($daily, $computed, $coins, $nowMs));
|
||||
}
|
||||
|
||||
if ($method !== 'POST') {
|
||||
json_response(['ok' => false, 'error' => 'Use GET or POST'], 405);
|
||||
}
|
||||
|
||||
// ---- POST: เคลมรางวัล ----
|
||||
$body = require_json_body();
|
||||
$key = trim((string) ($body['playerKey'] ?? ''));
|
||||
daily_validate_key($key);
|
||||
|
||||
$day = (int) ($body['day'] ?? 0);
|
||||
if ($day < 1 || $day > DAILY_TOTAL_DAYS) {
|
||||
json_response(['ok' => false, 'error' => 'day ต้องอยู่ระหว่าง 1–' . DAILY_TOTAL_DAYS], 400);
|
||||
}
|
||||
|
||||
$store = read_store();
|
||||
$idx = daily_find_account($store, $key);
|
||||
|
||||
if ($idx < 0) {
|
||||
// สร้างบัญชี guest ใหม่พร้อม daily state
|
||||
$idx = count($store['accounts'] ?? []);
|
||||
$store['accounts'][] = [
|
||||
'id' => new_id(),
|
||||
'email' => '',
|
||||
'displayName' => 'Guest',
|
||||
'loginType' => 'guest',
|
||||
'providerUserId' => $key,
|
||||
'notes' => 'auto: daily-reward',
|
||||
'blocked' => false,
|
||||
'coins' => 0,
|
||||
'daily' => daily_default_state($nowMs),
|
||||
'createdAt' => gmdate('c'),
|
||||
'updatedAt' => gmdate('c'),
|
||||
];
|
||||
}
|
||||
|
||||
$acc = $store['accounts'][$idx];
|
||||
if (!empty($acc['blocked'])) {
|
||||
json_response(['ok' => false, 'error' => 'บัญชีถูกระงับ'], 403);
|
||||
}
|
||||
|
||||
$daily = is_array($acc['daily'] ?? null) ? $acc['daily'] : daily_default_state($nowMs);
|
||||
$computed = daily_compute($daily, $nowMs);
|
||||
|
||||
// ตรวจสอบ server-side: วันที่เคลมต้องเป็น "วันที่กดได้" จริงในตอนนี้
|
||||
if ($computed['currentDay'] !== $day) {
|
||||
$coinsNow = max(0, (int) ($acc['coins'] ?? 0));
|
||||
$resp = daily_public_state($daily, $computed, $coinsNow, $nowMs);
|
||||
$resp['ok'] = false;
|
||||
$resp['error'] = $daily['claimedDays'][$day - 1]
|
||||
? 'รับรางวัลวันนี้ไปแล้ว รอรีเซ็ตรอบถัดไป'
|
||||
: 'ยังเปิดรับรางวัลวันนี้ไม่ได้';
|
||||
// เขียน state ที่ normalize แล้ว (เผื่อมีการรีเซ็ต) ก่อนตอบ
|
||||
$store['accounts'][$idx]['daily'] = $daily;
|
||||
$store['accounts'][$idx]['updatedAt'] = gmdate('c');
|
||||
write_store($store);
|
||||
json_response($resp, 409);
|
||||
}
|
||||
|
||||
// ผ่านการตรวจสอบ → เคลม + บวกเหรียญจริง
|
||||
$reward = DAILY_REWARDS[$day - 1] ?? 0;
|
||||
$daily['claimedDays'][$day - 1] = true;
|
||||
$daily['lockUntilMs'] = daily_next_midnight_ms($nowMs);
|
||||
|
||||
$coins = max(0, (int) ($acc['coins'] ?? 0)) + $reward;
|
||||
$store['accounts'][$idx]['coins'] = $coins;
|
||||
$store['accounts'][$idx]['daily'] = $daily;
|
||||
$store['accounts'][$idx]['updatedAt'] = gmdate('c');
|
||||
|
||||
if (!write_store($store)) {
|
||||
json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500);
|
||||
}
|
||||
|
||||
// คำนวณสถานะใหม่หลังเคลม (currentDay จะเลื่อนเป็น 0 เพราะ lock แล้ว)
|
||||
$computedAfter = daily_compute($daily, $nowMs);
|
||||
$resp = daily_public_state($daily, $computedAfter, $coins, $nowMs);
|
||||
$resp['claimedDay'] = $day;
|
||||
$resp['reward'] = $reward;
|
||||
$resp['added'] = $reward;
|
||||
json_response($resp);
|
||||
@@ -38,9 +38,22 @@
|
||||
"providerUserId": "p_1775109142385_wq7wfy1p32j",
|
||||
"notes": "auto: player-coins",
|
||||
"blocked": false,
|
||||
"coins": 0,
|
||||
"coins": 10,
|
||||
"createdAt": "2026-04-02T05:52:21+00:00",
|
||||
"updatedAt": "2026-04-02T05:52:21+00:00"
|
||||
"updatedAt": "2026-06-12T06:21:53+00:00",
|
||||
"daily": {
|
||||
"anchorMs": 1781197200000,
|
||||
"claimedDays": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"lockUntilMs": 1781283600000
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1d64c56fadb64a93eae68a1d",
|
||||
|
||||
@@ -378,7 +378,7 @@
|
||||
ov.id = 'evidence-card-lightbox';
|
||||
ov.style.cssText = 'position:fixed;inset:0;z-index:100001;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(5,8,18,.88);backdrop-filter:blur(4px)';
|
||||
ov.innerHTML =
|
||||
'<button type="button" class="ecl-close" aria-label="ปิด" style="position:absolute;top:22px;right:26px;width:46px;height:46px;border:none;border-radius:50%;cursor:pointer;font:900 26px/1 system-ui,sans-serif;color:#0a0e1a;background:linear-gradient(180deg,#ffd666,#f0b429);box-shadow:0 8px 22px rgba(0,0,0,.45)">×</button>' +
|
||||
'<button type="button" class="ecl-close" aria-label="ปิด" style="position:absolute;top:22px;right:26px;width:46px;height:46px;display:flex;align-items:center;justify-content:center;padding:0;border:none;border-radius:50%;cursor:pointer;font:900 26px/1 system-ui,sans-serif;color:#0a0e1a;background:linear-gradient(180deg,#ffd666,#f0b429);box-shadow:0 8px 22px rgba(0,0,0,.45)"><span style="display:block;margin-top:-2px">×</span></button>' +
|
||||
'<div class="ecl-stage" style="display:flex;flex-direction:column;align-items:center;gap:14px;max-width:92vw">' +
|
||||
'<img class="ecl-img" alt="" style="display:block;width:auto;height:auto;max-width:min(86vw,520px);max-height:78vh;border-radius:16px" />' +
|
||||
'<div class="ecl-cap" style="font:800 16px/1.3 Kanit,system-ui,sans-serif;color:#e7ecff;text-align:center;max-width:90vw"></div>' +
|
||||
|
||||
@@ -4903,7 +4903,7 @@
|
||||
appendLobbySystemChat('— เปิดเผยผล: คนร้ายคือผู้ต้องสงสัยหมายเลข ' + (culprit + 1) + ' · ผู้ตอบถูก: ' + names);
|
||||
// โชว์คะแนนโหวตสักครู่ แล้วไปหน้าผลเต็มเสมอ (ถูก = เรือนจำไซเบอร์ / ผิด/ไม่โหวต = ภารกิจล้มเหลว)
|
||||
if (trialFinalTimer) clearTimeout(trialFinalTimer);
|
||||
trialFinalTimer = setTimeout(function () { showFinalResult(correct, culprit); }, 2600);
|
||||
trialFinalTimer = setTimeout(function () { showFinalResult(correct, culprit, anyVotes); }, 2600);
|
||||
}
|
||||
|
||||
var trialFinalTimer = null;
|
||||
@@ -4919,10 +4919,12 @@
|
||||
'#trial-final-culprit{position:absolute;left:50%;top:55%;transform:translate(-50%,-50%);height:52%;width:auto;z-index:2;display:none;filter:drop-shadow(0 0 20px rgba(255,200,80,.55))}' +
|
||||
// หัวข้อ "ภารกิจล้มเหลว" (ตอนตอบผิด) วางทับ lose-bg
|
||||
'#trial-final-txt{position:absolute;left:50%;top:7%;transform:translateX(-50%);width:780px;max-width:90vw;height:auto;z-index:2;display:none}' +
|
||||
/* ชนะ: ข้อความ "ยินดีด้วย ชนะ" วางช่วงล่างเหนือปุ่มกลับ (เลี่ยงป้าย "เรือนจำไซเบอร์" บนสุด) */
|
||||
'#trial-final-txt.final-txt-congrats{top:auto;bottom:21%;width:420px;filter:drop-shadow(0 2px 10px rgba(0,0,0,.7))}' +
|
||||
'#trial-final-home{position:absolute;left:50%;bottom:5%;transform:translateX(-50%);width:300px;height:104px;max-width:70vw;background:url(' + R + 'btn-gohome.png) center/contain no-repeat;border:none;cursor:pointer;font-size:0;color:transparent;z-index:3;filter:drop-shadow(0 4px 14px rgba(0,0,0,.6))}';
|
||||
document.head.appendChild(st);
|
||||
}
|
||||
function showFinalResult(correct, culprit) {
|
||||
function showFinalResult(correct, culprit, anyVotes) {
|
||||
injectFinalResultStyle();
|
||||
var R = '/Main-Lobby/IMAGE/Result/';
|
||||
var ov = document.getElementById('trial-final');
|
||||
@@ -4938,16 +4940,23 @@
|
||||
var culpEl = ov.querySelector('#trial-final-culprit');
|
||||
var txtEl = ov.querySelector('#trial-final-txt');
|
||||
if (correct) {
|
||||
/* ชนะ — จับคนร้ายจริง: เรือนจำไซเบอร์ + คนร้ายในคุก + ข้อความ "ยินดีด้วย ทีมนักสืบ ชนะ!" */
|
||||
ov.style.backgroundImage = 'url(' + R + 'cyber-prison.png)';
|
||||
var culpIdx = (typeof culprit === 'number') ? culprit : 0;
|
||||
if (culpEl) {
|
||||
culpEl.src = getCulpritPrisonImageUrl(culpIdx);
|
||||
culpEl.style.display = 'block';
|
||||
}
|
||||
if (txtEl) txtEl.style.display = 'none';
|
||||
if (txtEl) { txtEl.src = R + 'txt-congrats.png'; txtEl.className = 'final-txt-congrats'; txtEl.style.display = 'block'; }
|
||||
} else if (anyVotes) {
|
||||
/* จับแพะ — โหวตผิดคน: "อุปส์..ตัวป่วนชนะ ความจริงถูกบิดเบือน!" (troll-win) */
|
||||
ov.style.backgroundImage = 'url(' + R + 'troll-win-bg.png)';
|
||||
if (txtEl) { txtEl.src = R + 'txt-troll-win.png'; txtEl.className = ''; txtEl.style.display = 'block'; }
|
||||
if (culpEl) culpEl.style.display = 'none';
|
||||
} else {
|
||||
/* ไม่มีใครโหวต / ภารกิจล้มเหลว */
|
||||
ov.style.backgroundImage = 'url(' + R + 'lose-bg.png)';
|
||||
if (txtEl) { txtEl.src = R + 'txt-your-lose.png'; txtEl.style.display = 'block'; }
|
||||
if (txtEl) { txtEl.src = R + 'txt-your-lose.png'; txtEl.className = ''; txtEl.style.display = 'block'; }
|
||||
if (culpEl) culpEl.style.display = 'none';
|
||||
}
|
||||
ov.classList.remove('is-hidden');
|
||||
@@ -5302,7 +5311,7 @@
|
||||
ov.style.cssText = 'position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(5,8,18,.86);backdrop-filter:blur(4px)';
|
||||
ov.innerHTML =
|
||||
'<button type="button" class="ecl-close" aria-label="ปิด" ' +
|
||||
'style="position:absolute;top:22px;right:26px;width:46px;height:46px;border:none;border-radius:50%;cursor:pointer;font:900 26px/1 system-ui,sans-serif;color:#0a0e1a;background:linear-gradient(180deg,#ffd666,#f0b429);box-shadow:0 8px 22px rgba(0,0,0,.45)">×</button>' +
|
||||
'style="position:absolute;top:22px;right:26px;width:46px;height:46px;display:flex;align-items:center;justify-content:center;padding:0;border:none;border-radius:50%;cursor:pointer;font:900 26px/1 system-ui,sans-serif;color:#0a0e1a;background:linear-gradient(180deg,#ffd666,#f0b429);box-shadow:0 8px 22px rgba(0,0,0,.45)"><span style="display:block;margin-top:-2px">×</span></button>' +
|
||||
'<div class="ecl-stage" style="display:flex;flex-direction:column;align-items:center;gap:14px;max-width:92vw">' +
|
||||
'<img class="ecl-img" alt="" style="display:block;width:auto;height:auto;max-width:min(86vw,520px);max-height:78vh;border-radius:16px;animation:eclPop .3s cubic-bezier(.2,.9,.3,1.35)" />' +
|
||||
'<div class="ecl-cap" style="font:800 16px/1.3 Kanit,system-ui,sans-serif;color:#e7ecff;text-align:center;max-width:90vw"></div>' +
|
||||
|
||||
@@ -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.00610200037"></script>
|
||||
<script src="js/play.js?v=0.00610200038"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
.suspect-card-ui {
|
||||
position: absolute;
|
||||
left: 61%;
|
||||
bottom: 11%;
|
||||
bottom: 9%;
|
||||
transform: translateX(-50%);
|
||||
width: 368px;
|
||||
display: flex;
|
||||
@@ -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.0269"></script>
|
||||
<script src="js/room-lobby.js?v=0.0271"></script>
|
||||
<div class="version-tag">v —</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -836,7 +836,7 @@ const SPECIAL_CARD_DEFS = {
|
||||
4: { key: 'silence', when: 'pre_trial', th: 'โหวตปิดปากผู้เล่น 1 คน ห้ามโหวตในพิจารณาคดี' },
|
||||
5: { key: 'bail', when: 'trial_revote', th: 'จับผิดตัว ให้โหวตใหม่ได้ 1 ครั้ง' },
|
||||
6: { key: 'fund', when: 'now', coins: 10, th: 'ทุกคนรับ +10 COINS' },
|
||||
7: { key: 'free_evidence', when: 'after_game', evidence: 2, th: 'ทุกคนรับหลักฐานฟรี +2 ใบ' },
|
||||
7: { key: 'free_evidence', when: 'after_game', evidence: 1, th: 'ทุกคนรับหลักฐานฟรี +1 ใบ' },
|
||||
};
|
||||
|
||||
function specialCardDef(cardId) {
|
||||
@@ -2964,7 +2964,7 @@ function grantDetectiveEvidenceForCurrentRun(sid, space, playerIds) {
|
||||
const pend = space.pendingSpecialCard;
|
||||
if (pend && Number(pend.cardId) === 7 && !pend.consumed) {
|
||||
const def = specialCardDef(7);
|
||||
freeEvidenceCount = (def && def.evidence) || 2;
|
||||
freeEvidenceCount = (def && def.evidence) || 1;
|
||||
for (let n = 0; n < freeEvidenceCount; n++) {
|
||||
ids.forEach((pid) => {
|
||||
if (!space.peers.has(pid)) return;
|
||||
|
||||
@@ -57,6 +57,10 @@
|
||||
var normalResetMs = 24 * 60 * 60 * 1000;
|
||||
var activeResetMs = isFastTestMode ? testResetMs : normalResetMs;
|
||||
var DAILY_STORAGE_KEY = 'jdDailyProgressV3:shared';
|
||||
var DAILY_ENDPOINT = appPath('/Admin/api/daily-reward.php');
|
||||
var PLAYER_KEY_LS = 'jdPlayerKey';
|
||||
/* โหมดทดสอบเร็ว = ใช้ local อย่างเดียว, โหมดปกติ = server เป็นเจ้าของสถานะจริง (กันฟาร์ม + ให้เหรียญจริง) */
|
||||
var useServer = !isFastTestMode;
|
||||
|
||||
var state = {
|
||||
isOpen: false,
|
||||
@@ -175,6 +179,86 @@
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/* ===== ผูกกับ server (รางวัล + กันฟาร์ม) ===== */
|
||||
|
||||
function getPlayerKey() {
|
||||
var k;
|
||||
try { k = localStorage.getItem(PLAYER_KEY_LS); } catch (e) { k = ''; }
|
||||
if (!k || String(k).length < 8) {
|
||||
k = 'p_' + Date.now() + '_' + Math.random().toString(36).slice(2, 14);
|
||||
try { localStorage.setItem(PLAYER_KEY_LS, k); } catch (e2) { /* ignore */ }
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
/* อัปเดตยอดเหรียญที่โปรไฟล์ทันที (server บันทึกจริงไปแล้ว) */
|
||||
function applyCoins(coins) {
|
||||
var c = String(Math.max(0, parseInt(coins, 10) || 0));
|
||||
try { localStorage.setItem('jdCoins', c); } catch (e) { /* ignore */ }
|
||||
var el = document.getElementById('lobby-profile-coins');
|
||||
if (el) el.textContent = c;
|
||||
}
|
||||
|
||||
/* แปลง response ของ server → progress object รูปแบบเดียวกับ local */
|
||||
function progressFromServer(data) {
|
||||
if (!data || !Array.isArray(data.claimedDays) || data.claimedDays.length !== TOTAL_DAYS) return null;
|
||||
var anchor = Number(data.anchorMs);
|
||||
if (!isFinite(anchor) || anchor <= 0) anchor = createDefaultProgress(Date.now()).anchorMs;
|
||||
var lock = Number(data.lockUntilMs);
|
||||
if (!isFinite(lock) || lock < 0) lock = 0;
|
||||
return {
|
||||
anchorMs: anchor,
|
||||
claimedDays: data.claimedDays.map(function (v) { return !!v; }),
|
||||
lockUntilMs: lock,
|
||||
timingKey: 'normal'
|
||||
};
|
||||
}
|
||||
|
||||
/* ดึงสถานะจริงจาก server แล้วยึดเป็นหลัก (ออฟไลน์ → คงสถานะ local เดิม) */
|
||||
function fetchServerDaily(done) {
|
||||
if (!useServer) { if (done) done(false); return; }
|
||||
var key = getPlayerKey();
|
||||
fetch(DAILY_ENDPOINT + '?playerKey=' + encodeURIComponent(key), { credentials: 'omit' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (!d || !d.ok) { if (done) done(false); return; }
|
||||
var p = progressFromServer(d);
|
||||
if (p) {
|
||||
state.progress = p;
|
||||
saveProgress();
|
||||
syncProgress();
|
||||
if (refs.cardsEl) renderCards();
|
||||
}
|
||||
if (typeof d.coins === 'number') applyCoins(d.coins);
|
||||
if (done) done(true);
|
||||
})
|
||||
.catch(function () { if (done) done(false); });
|
||||
}
|
||||
|
||||
/* เคลมรางวัลผ่าน server — server ตรวจสอบ + บวกเหรียญจริง + กันยิงซ้ำ */
|
||||
function claimDayOnServer(day, onOk, onFail) {
|
||||
var key = getPlayerKey();
|
||||
fetch(DAILY_ENDPOINT, {
|
||||
method: 'POST',
|
||||
credentials: 'omit',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ playerKey: key, day: day })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
d = d || {};
|
||||
var p = progressFromServer(d);
|
||||
if (p) { state.progress = p; saveProgress(); }
|
||||
if (d.ok) {
|
||||
if (typeof d.coins === 'number') applyCoins(d.coins);
|
||||
if (onOk) onOk(d);
|
||||
} else {
|
||||
if (onFail) onFail(d);
|
||||
}
|
||||
})
|
||||
.catch(function () { if (onFail) onFail(null); });
|
||||
}
|
||||
|
||||
function syncProgress() {
|
||||
if (!state.progress) return false;
|
||||
var progress = state.progress;
|
||||
@@ -415,14 +499,32 @@
|
||||
if (!state.progress) return;
|
||||
if (day !== state.currentDay) return;
|
||||
if (state.progress.claimedDays[day - 1]) return;
|
||||
state.progress.claimedDays[day - 1] = true;
|
||||
state.progress.lockUntilMs = Date.now() + Math.max(1, state.msUntilReset || activeResetMs);
|
||||
saveProgress();
|
||||
syncProgress();
|
||||
state.claimedAnimDay = day;
|
||||
renderCards();
|
||||
state.claimedAnimDay = null;
|
||||
syncDailyDotIndicator();
|
||||
if (claimBtn.disabled) return;
|
||||
claimBtn.disabled = true;
|
||||
|
||||
function finishLocal(animate) {
|
||||
if (animate) state.claimedAnimDay = day;
|
||||
syncProgress();
|
||||
renderCards();
|
||||
state.claimedAnimDay = null;
|
||||
syncDailyDotIndicator();
|
||||
}
|
||||
|
||||
if (useServer) {
|
||||
claimDayOnServer(day, function () {
|
||||
// สำเร็จ: server บันทึก + บวกเหรียญแล้ว, progress อัปเดตจาก response
|
||||
finishLocal(true);
|
||||
}, function () {
|
||||
// server ปฏิเสธ/ออฟไลน์: ใช้ progress ล่าสุดที่ได้กลับมา (ถ้ามี) แล้ว render ใหม่
|
||||
finishLocal(false);
|
||||
});
|
||||
} else {
|
||||
// โหมดทดสอบเร็ว: local อย่างเดียว
|
||||
state.progress.claimedDays[day - 1] = true;
|
||||
state.progress.lockUntilMs = Date.now() + Math.max(1, state.msUntilReset || activeResetMs);
|
||||
saveProgress();
|
||||
finishLocal(true);
|
||||
}
|
||||
});
|
||||
inner.appendChild(claimBtn);
|
||||
} else {
|
||||
@@ -540,6 +642,7 @@
|
||||
state.isOpen = true;
|
||||
syncScale();
|
||||
startResetCountdown();
|
||||
fetchServerDaily(); /* ดึงสถานะจริงจาก server ทุกครั้งที่เปิด */
|
||||
}
|
||||
|
||||
function closePopup() {
|
||||
@@ -571,6 +674,7 @@
|
||||
|
||||
state.progress = loadProgress();
|
||||
syncProgress();
|
||||
fetchServerDaily(); /* ซิงก์สถานะจริง + ยอดเหรียญตั้งแต่โหลด (อัปเดต dot ให้ถูก) */
|
||||
|
||||
startDailyDotWatcher();
|
||||
bindTriggers();
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
|
||||
<script src="../app-base.js?v=2"></script>
|
||||
<script src="../Game/js/display-name.js?v=2"></script>
|
||||
<script src="daily-popup.js?v=25" data-daily-trigger="#btn-daily" data-daily-asset-base="IMAGE/Daily" data-daily-test-reset-seconds="0"></script>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user