update more flow and fix bugs

This commit is contained in:
2026-06-12 06:26:20 +00:00
parent b14469f7d7
commit 0c2f5119e1
9 changed files with 407 additions and 22 deletions
+259
View File
@@ -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 ต้องยาว 8128 (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);
+15 -2
View File
@@ -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",
+1 -1
View File
@@ -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)">&times;</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">&times;</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>' +
+14 -5
View File
@@ -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)">&times;</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">&times;</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>' +
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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>
+2 -2
View File
@@ -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;
+112 -8
View File
@@ -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();
+1 -1
View File
@@ -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>