diff --git a/www/html/Admin/api/daily-reward.php b/www/html/Admin/api/daily-reward.php new file mode 100644 index 0000000..bd3a0bb --- /dev/null +++ b/www/html/Admin/api/daily-reward.php @@ -0,0 +1,259 @@ + 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); diff --git a/www/html/Admin/private/store.json b/www/html/Admin/private/store.json index f49ac5e..9ef507a 100644 --- a/www/html/Admin/private/store.json +++ b/www/html/Admin/private/store.json @@ -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", diff --git a/www/html/Game/public/js/play.js b/www/html/Game/public/js/play.js index 2c8e661..50b1222 100644 --- a/www/html/Game/public/js/play.js +++ b/www/html/Game/public/js/play.js @@ -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 = - '' + + '' + '