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);