Commit 432f8feb authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: daily rewards streak tracking + achievements system

Daily rewards:
- Server now tracks streak, prevents double-claim, resets on missed day
- GET endpoint returns current state (streak, claimed status, today's reward)
- Updates profile daily_streak + last_daily_reward columns
- Frontend fetches state from server instead of relying on local store

Achievements:
- New scene with category filters, progress bars, tier badges
- POST check action recalculates all progress from player stats
- game.php now tracks win_streak and checks achievements on game end
- Rewards (coins/XP) auto-granted when achievement completes

Challenges:
- Claim tracking via economy_transactions prevents double-claims
- Fixed column name (reason, not description)
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 8b31a26d
This diff is collapsed.
...@@ -114,12 +114,28 @@ function getDailyChallenges($db, string $userId): void { ...@@ -114,12 +114,28 @@ function getDailyChallenges($db, string $userId): void {
'daily_claimed' => 0, 'daily_claimed' => 0,
]; ];
// Check which challenges have been claimed today
$claimedToday = $sdb->get('economy_transactions', [
'player_id' => 'eq.' . $userId,
'type' => 'eq.challenge_reward',
'created_at' => 'gte.' . urlencode($todayStart),
'select' => 'reason'
]);
$claimedIds = [];
if (is_array($claimedToday) && !isset($claimedToday['error'])) {
foreach ($claimedToday as $ct) {
if (preg_match('/Daily challenge: (.+)/', $ct['reason'] ?? '', $m)) {
$claimedIds[] = $m[1];
}
}
}
foreach ($dailyChallenges as &$c) { foreach ($dailyChallenges as &$c) {
$c['reward_coins'] = intval($c['reward_coins'] * (1 + $level * 0.1)); $c['reward_coins'] = intval($c['reward_coins'] * (1 + $level * 0.1));
$c['reward_xp'] = intval($c['reward_xp'] * (1 + $level * 0.05)); $c['reward_xp'] = intval($c['reward_xp'] * (1 + $level * 0.05));
$c['progress'] = min($progressMap[$c['type']] ?? 0, $c['target']); $c['progress'] = min($progressMap[$c['type']] ?? 0, $c['target']);
$c['completed'] = $c['progress'] >= $c['target']; $c['completed'] = $c['progress'] >= $c['target'];
$c['claimed'] = false; // TODO: track claims per day $c['claimed'] = in_array($c['id'], $claimedIds);
$c['date'] = $today; $c['date'] = $today;
} }
...@@ -142,6 +158,20 @@ function claimChallenge($db, string $userId, array $input): void { ...@@ -142,6 +158,20 @@ function claimChallenge($db, string $userId, array $input): void {
if (!$challengeId) jsonError('challenge_id required'); if (!$challengeId) jsonError('challenge_id required');
// Prevent double claim
$today = date('Y-m-d') . 'T00:00:00+00:00';
$sdb = supabaseService();
$existing = $sdb->get('economy_transactions', [
'player_id' => 'eq.' . $userId,
'type' => 'eq.challenge_reward',
'reason' => 'eq.Daily challenge: ' . $challengeId,
'created_at' => 'gte.' . urlencode($today),
'limit' => 1
]);
if (is_array($existing) && !isset($existing['error']) && !empty($existing)) {
jsonError('Already claimed this challenge today');
}
// Grant rewards // Grant rewards
$profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'coins,xp', 'limit' => 1]); $profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'coins,xp', 'limit' => 1]);
$profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : ['coins' => 0, 'xp' => 0]; $profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : ['coins' => 0, 'xp' => 0];
...@@ -152,7 +182,6 @@ function claimChallenge($db, string $userId, array $input): void { ...@@ -152,7 +182,6 @@ function claimChallenge($db, string $userId, array $input): void {
$db->update('profiles', ['coins' => $newCoins, 'xp' => $newXp], ['id' => 'eq.' . $userId]); $db->update('profiles', ['coins' => $newCoins, 'xp' => $newXp], ['id' => 'eq.' . $userId]);
// Record transaction // Record transaction
$sdb = supabaseService();
$sdb->insert('economy_transactions', [ $sdb->insert('economy_transactions', [
'player_id' => $userId, 'player_id' => $userId,
'type' => 'challenge_reward', 'type' => 'challenge_reward',
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization'); header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; } if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
...@@ -12,36 +12,141 @@ require_once __DIR__ . '/../includes/auth.php'; ...@@ -12,36 +12,141 @@ require_once __DIR__ . '/../includes/auth.php';
$token = requireAuth(); $token = requireAuth();
$userId = getUserId($token); $userId = getUserId($token);
$input = getInput(); $method = $_SERVER['REQUEST_METHOD'];
$action = $input['action'] ?? 'claim';
$db = supabase($token); $db = supabase($token);
$sdb = supabaseService();
if ($action === 'claim') { $DAY_REWARDS = [50, 75, 100, 125, 150, 200, 300];
$profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'coins', 'limit' => 1]);
if ($method === 'GET') {
$profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'daily_streak,last_daily_reward,coins', 'limit' => 1]);
$profile = is_array($profiles) && !empty($profiles) && !isset($profiles['error']) ? $profiles[0] : null; $profile = is_array($profiles) && !empty($profiles) && !isset($profiles['error']) ? $profiles[0] : null;
if (!$profile) jsonError('Profile not found');
$streak = $profile['daily_streak'] ?? 0;
$lastClaim = $profile['last_daily_reward'] ?? null;
$today = gmdate('Y-m-d');
$yesterday = gmdate('Y-m-d', strtotime('-1 day'));
$alreadyClaimed = $lastClaim && substr($lastClaim, 0, 10) === $today;
// If last claim was before yesterday, streak resets
if ($lastClaim && substr($lastClaim, 0, 10) < $yesterday) {
$streak = 0;
}
$dayIndex = $streak % 7;
$todayReward = $DAY_REWARDS[$dayIndex];
jsonResponse([
'streak' => $streak,
'already_claimed' => $alreadyClaimed,
'today_reward' => $todayReward,
'day_index' => $dayIndex,
'last_claim' => $lastClaim
]);
}
if ($method === 'POST') {
$input = getInput();
$action = $input['action'] ?? 'claim';
if ($action !== 'claim') jsonError('Invalid action');
$profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'daily_streak,last_daily_reward,coins', 'limit' => 1]);
$profile = is_array($profiles) && !empty($profiles) && !isset($profiles['error']) ? $profiles[0] : null;
if (!$profile) jsonError('Profile not found'); if (!$profile) jsonError('Profile not found');
$reward = 100; $streak = $profile['daily_streak'] ?? 0;
$newCoins = ($profile['coins'] ?? 0) + $reward; $lastClaim = $profile['last_daily_reward'] ?? null;
$today = gmdate('Y-m-d');
$yesterday = gmdate('Y-m-d', strtotime('-1 day'));
$now = gmdate('c');
$db->update('profiles', ['coins' => $newCoins], ['id' => 'eq.' . $userId]); // Check if already claimed today
if ($lastClaim && substr($lastClaim, 0, 10) === $today) {
jsonError('Already claimed today');
}
$sdb = supabaseService(); // Calculate new streak
if (!$lastClaim || substr($lastClaim, 0, 10) < $yesterday) {
// Streak broken (or first time) — reset to day 1
$streak = 1;
} else {
// Last claim was yesterday — streak continues
$streak = $streak + 1;
}
// Day index (0-6) determines reward amount
$dayIndex = ($streak - 1) % 7;
$reward = $DAY_REWARDS[$dayIndex];
$currentCoins = $profile['coins'] ?? 0;
$newCoins = $currentCoins + $reward;
// Update profile: coins, streak, last_daily_reward
$db->update('profiles', [
'coins' => $newCoins,
'daily_streak' => $streak,
'last_daily_reward' => $now
], ['id' => 'eq.' . $userId]);
// Record transaction
$sdb->insert('economy_transactions', [ $sdb->insert('economy_transactions', [
'player_id' => $userId, 'player_id' => $userId,
'type' => 'daily_reward', 'type' => 'daily_reward',
'currency' => 'coins', 'currency' => 'coins',
'amount' => $reward, 'amount' => $reward,
'balance_after' => $newCoins, 'balance_after' => $newCoins,
'description' => 'Daily reward' 'reason' => 'Daily reward day ' . $streak
]); ]);
// Check streak achievements
checkAchievements($sdb, $userId, 'daily_streak', $streak);
jsonResponse([ jsonResponse([
'coins' => $reward, 'coins' => $reward,
'total_coins' => $newCoins 'total_coins' => $newCoins,
'streak' => $streak,
'day_index' => $dayIndex
]); ]);
} }
jsonError('Invalid action'); jsonError('Method not allowed', 405);
function checkAchievements($sdb, string $userId, string $type, int $value): void {
$achievements = $sdb->get('achievements', ['select' => 'id,condition,coins_reward,xp_reward']);
if (!is_array($achievements) || isset($achievements['error'])) return;
foreach ($achievements as $a) {
$condition = $a['condition'];
if (is_string($condition)) $condition = json_decode($condition, true);
if (!$condition || ($condition['type'] ?? '') !== $type) continue;
if ($value < ($condition['count'] ?? PHP_INT_MAX)) continue;
// Check if already earned
$existing = $sdb->get('player_achievements', [
'player_id' => 'eq.' . $userId,
'achievement_id' => 'eq.' . $a['id'],
'limit' => 1
]);
if (is_array($existing) && !isset($existing['error']) && !empty($existing)) {
if (($existing[0]['completed'] ?? false)) continue;
// Update progress
$sdb->update('player_achievements', [
'progress' => $value,
'completed' => true,
'completed_at' => gmdate('c')
], ['player_id' => 'eq.' . $userId, 'achievement_id' => 'eq.' . $a['id']]);
} else {
$sdb->insert('player_achievements', [
'player_id' => $userId,
'achievement_id' => $a['id'],
'progress' => $value,
'completed' => true,
'completed_at' => gmdate('c')
]);
}
}
}
...@@ -165,7 +165,7 @@ function handleComplete($db, string $userId, array $input): void { ...@@ -165,7 +165,7 @@ function handleComplete($db, string $userId, array $input): void {
// Calculate Elo rating change // Calculate Elo rating change
$ratingCol = getRatingColumn($timeControl); $ratingCol = getRatingColumn($timeControl);
$profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => $ratingCol . ',games_played,total_wins,total_draws,total_losses,coins,xp,level', 'limit' => 1]); $profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => $ratingCol . ',games_played,total_wins,total_draws,total_losses,win_streak,coins,xp,level', 'limit' => 1]);
$profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : null; $profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : null;
if ($profile) { if ($profile) {
...@@ -175,9 +175,16 @@ function handleComplete($db, string $userId, array $input): void { ...@@ -175,9 +175,16 @@ function handleComplete($db, string $userId, array $input): void {
$ratingChange = $newRating - $playerRating; $ratingChange = $newRating - $playerRating;
$updates = [$ratingCol => $newRating, 'games_played' => ($profile['games_played'] ?? 0) + 1]; $updates = [$ratingCol => $newRating, 'games_played' => ($profile['games_played'] ?? 0) + 1];
if ($result === 'win') $updates['total_wins'] = ($profile['total_wins'] ?? 0) + 1; if ($result === 'win') {
elseif ($result === 'draw') $updates['total_draws'] = ($profile['total_draws'] ?? 0) + 1; $updates['total_wins'] = ($profile['total_wins'] ?? 0) + 1;
else $updates['total_losses'] = ($profile['total_losses'] ?? 0) + 1; $updates['win_streak'] = ($profile['win_streak'] ?? 0) + 1;
} elseif ($result === 'draw') {
$updates['total_draws'] = ($profile['total_draws'] ?? 0) + 1;
$updates['win_streak'] = 0;
} else {
$updates['total_losses'] = ($profile['total_losses'] ?? 0) + 1;
$updates['win_streak'] = 0;
}
$db->update('profiles', $updates, ['id' => 'eq.' . $userId]); $db->update('profiles', $updates, ['id' => 'eq.' . $userId]);
...@@ -219,6 +226,9 @@ function handleComplete($db, string $userId, array $input): void { ...@@ -219,6 +226,9 @@ function handleComplete($db, string $userId, array $input): void {
// Tournament result reporting hook // Tournament result reporting hook
reportTournamentResult($db, $matchId, $result, $userId); reportTournamentResult($db, $matchId, $result, $userId);
// Check achievements after game completion
checkGameAchievements($sdb, $userId, $updates, $newRating);
jsonResponse(['success' => true, 'result' => $result, 'rating_before' => $playerRating, 'rating_after' => $newRating, 'rating_change' => $ratingChange, 'coins_earned' => $coins]); jsonResponse(['success' => true, 'result' => $result, 'rating_before' => $playerRating, 'rating_after' => $newRating, 'rating_change' => $ratingChange, 'coins_earned' => $coins]);
} }
...@@ -283,3 +293,66 @@ function reportTournamentResult($db, string $matchId, string $result, string $us ...@@ -283,3 +293,66 @@ function reportTournamentResult($db, string $matchId, string $result, string $us
@file_get_contents($url, false, $ctx); @file_get_contents($url, false, $ctx);
} }
function checkGameAchievements($sdb, string $userId, array $profileUpdates, int $newRating): void {
$achievements = $sdb->get('achievements', ['select' => 'id,condition,coins_reward,xp_reward']);
if (!is_array($achievements) || isset($achievements['error'])) return;
$stats = [
'wins' => $profileUpdates['total_wins'] ?? 0,
'games_played' => $profileUpdates['games_played'] ?? 0,
'win_streak' => $profileUpdates['win_streak'] ?? 0,
'elo' => $newRating,
];
foreach ($achievements as $a) {
$condition = $a['condition'];
if (is_string($condition)) $condition = json_decode($condition, true);
if (!$condition) continue;
$type = $condition['type'] ?? '';
$target = $condition['count'] ?? 1;
if (!isset($stats[$type])) continue;
if ($stats[$type] < $target) continue;
// Check if already completed
$existing = $sdb->get('player_achievements', [
'player_id' => 'eq.' . $userId,
'achievement_id' => 'eq.' . $a['id'],
'limit' => 1
]);
if (is_array($existing) && !isset($existing['error']) && !empty($existing)) {
if ($existing[0]['completed'] ?? false) continue;
$sdb->update('player_achievements', [
'progress' => $stats[$type],
'completed' => true,
'completed_at' => gmdate('c')
], ['player_id' => 'eq.' . $userId, 'achievement_id' => 'eq.' . $a['id']]);
} else {
$sdb->insert('player_achievements', [
'player_id' => $userId,
'achievement_id' => $a['id'],
'progress' => $stats[$type],
'completed' => true,
'completed_at' => gmdate('c')
]);
}
// Grant rewards
$coins = intval($a['coins_reward'] ?? 0);
if ($coins > 0) {
$sdb->rpc('award_coins', ['p_player_id' => $userId, 'p_amount' => $coins, 'p_reason' => 'Achievement: ' . $a['id']]);
}
$xp = intval($a['xp_reward'] ?? 0);
if ($xp > 0) {
$profiles = $sdb->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'xp,level', 'limit' => 1]);
if (is_array($profiles) && !empty($profiles)) {
$currentXp = ($profiles[0]['xp'] ?? 0) + $xp;
$newLevel = floor($currentXp / 500) + 1;
$sdb->update('profiles', ['xp' => $currentXp, 'level' => $newLevel], ['id' => 'eq.' . $userId]);
}
}
}
}
...@@ -43,9 +43,9 @@ export function mountTable(el) { ...@@ -43,9 +43,9 @@ export function mountTable(el) {
<span class="qb-icon" style="background:linear-gradient(135deg,#1e40af,#3b82f6);">${emoji('lightning', '⚡', 20)}</span> <span class="qb-icon" style="background:linear-gradient(135deg,#1e40af,#3b82f6);">${emoji('lightning', '⚡', 20)}</span>
<span class="qb-label">تحديات</span> <span class="qb-label">تحديات</span>
</button> </button>
<button class="quick-btn" id="btn-battlepass"> <button class="quick-btn" id="btn-achievements">
<span class="qb-icon" style="background:linear-gradient(135deg,#5b21b6,#8b5cf6);">🎖️</span> <span class="qb-icon" style="background:linear-gradient(135deg,#854d0e,#ca8a04);">${emoji('trophy', '🏆', 20)}</span>
<span class="qb-label">الموسم</span> <span class="qb-label">إنجازات</span>
</button> </button>
<button class="quick-btn breathe-glow" id="btn-daily-reward"> <button class="quick-btn breathe-glow" id="btn-daily-reward">
<span class="qb-icon" style="background:linear-gradient(135deg,#92400e,#e4ac38);">${emoji('gift', '🎁', 20)}</span> <span class="qb-icon" style="background:linear-gradient(135deg,#92400e,#e4ac38);">${emoji('gift', '🎁', 20)}</span>
...@@ -232,9 +232,9 @@ export function mountTable(el) { ...@@ -232,9 +232,9 @@ export function mountTable(el) {
audio.play('click'); audio.play('click');
scene.push('daily-challenges'); scene.push('daily-challenges');
}); });
el.querySelector('#btn-battlepass')?.addEventListener('click', () => { el.querySelector('#btn-achievements')?.addEventListener('click', () => {
audio.play('click'); audio.play('click');
// TODO: battle pass scene scene.push('achievements');
}); });
el.querySelector('#btn-daily-reward')?.addEventListener('click', () => { el.querySelector('#btn-daily-reward')?.addEventListener('click', () => {
audio.play('click'); audio.play('click');
......
import * as scene from '../../core/scene.js'; import * as scene from '../../core/scene.js';
import { mountDaily } from './scenes/daily.js'; import { mountDaily } from './scenes/daily.js';
import { mountChallenges } from './scenes/challenges.js'; import { mountChallenges } from './scenes/challenges.js';
import { mountAchievements } from './scenes/achievements.js';
scene.register('daily-reward', mountDaily); scene.register('daily-reward', mountDaily);
scene.register('daily-challenges', mountChallenges); scene.register('daily-challenges', mountChallenges);
scene.register('achievements', mountAchievements);
import * as net from '../../../core/net.js';
import * as audio from '../../../core/audio.js';
import * as scene from '../../../core/scene.js';
import * as juice from '../../../core/juice.js';
import { emoji } from '../../../core/theme.js';
const CATEGORY_LABELS = {
gameplay: 'اللعب',
social: 'اجتماعي',
progression: 'التقدم',
collection: 'جمع',
};
const TIER_COLORS = {
1: '#94a3b8',
2: '#22c55e',
3: '#E4AC38',
4: '#a855f7',
};
export async function mountAchievements(el) {
el.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;background:#0a0a1a;color:#64748b;">جاري التحميل...</div>`;
let achievements = [];
let stats = { total: 0, completed: 0 };
try {
const data = await net.get('achievements.php');
if (data && !data.error) {
achievements = data.achievements || [];
stats = data.stats || stats;
}
} catch (e) {}
// Also trigger a check to update progress
net.post('achievements.php', { action: 'check' }).then(res => {
if (res && res.newly_completed && res.newly_completed.length > 0) {
juice.hapticSuccess();
audio.play('reward');
// Re-mount to show updated state
mountAchievements(el);
}
}).catch(() => {});
render(el, achievements, stats);
}
function render(el, achievements, stats) {
const categories = [...new Set(achievements.map(a => a.category))];
const progressPct = stats.total > 0 ? Math.round((stats.completed / stats.total) * 100) : 0;
el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:#0a0a1a;">
<!-- Header -->
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button>
<span style="font-size:16px;font-weight:700;color:#f8fafc;">${emoji('trophy', '🏆', 16)} الإنجازات</span>
<span style="margin-inline-start:auto;font-size:12px;color:#64748b;">${stats.completed}/${stats.total}</span>
</div>
<!-- Progress bar -->
<div style="padding:12px 16px 0;">
<div style="background:#1a1a2e;border-radius:99px;height:8px;overflow:hidden;">
<div style="background:linear-gradient(90deg,#E4AC38,#FFCC66);height:100%;width:${progressPct}%;border-radius:99px;transition:width 0.5s;"></div>
</div>
<div style="display:flex;justify-content:space-between;margin-top:4px;">
<span style="font-size:10px;color:#64748b;">${progressPct}% مكتمل</span>
<span style="font-size:10px;color:#E4AC38;">${stats.completed} إنجاز</span>
</div>
</div>
<!-- Category filter -->
<div style="display:flex;gap:6px;padding:12px 16px;overflow-x:auto;">
<button class="ach-filter active" data-cat="all">الكل</button>
${categories.map(c => `<button class="ach-filter" data-cat="${c}">${CATEGORY_LABELS[c] || c}</button>`).join('')}
</div>
<!-- Achievement list -->
<div id="ach-list" style="flex:1;overflow-y:auto;padding:0 16px 16px;display:flex;flex-direction:column;gap:8px;">
${renderList(achievements, 'all')}
</div>
</div>
<style>
.ach-filter{background:#1a1a2e;border:1px solid rgba(255,255,255,0.06);color:#94a3b8;font-size:11px;padding:6px 12px;border-radius:99px;cursor:pointer;white-space:nowrap;font-family:inherit;font-weight:600;}
.ach-filter.active{background:#E4AC38;color:#1a1a1a;border-color:#E4AC38;}
.ach-card{display:flex;align-items:center;gap:12px;padding:12px;background:#0f0f1e;border-radius:12px;border:1px solid rgba(255,255,255,0.04);}
.ach-card.completed{border-color:rgba(34,197,94,0.3);background:#0a1a0a;}
</style>
`;
el.querySelector('#back-btn').addEventListener('click', () => { audio.play('click'); scene.pop(); });
el.querySelectorAll('.ach-filter').forEach(btn => {
btn.addEventListener('click', () => {
el.querySelectorAll('.ach-filter').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
el.querySelector('#ach-list').innerHTML = renderList(achievements, btn.dataset.cat);
});
});
}
function renderList(achievements, category) {
const filtered = category === 'all' ? achievements : achievements.filter(a => a.category === category);
if (filtered.length === 0) {
return `<div style="text-align:center;padding:32px;color:#475569;font-size:13px;">لا توجد إنجازات في هذه الفئة</div>`;
}
return filtered.map(a => {
const pct = a.target > 0 ? Math.min(100, Math.round((a.progress / a.target) * 100)) : 0;
const tierColor = TIER_COLORS[a.tier] || TIER_COLORS[1];
return `
<div class="ach-card ${a.completed ? 'completed' : ''}">
<div style="width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;
background:${a.completed ? 'linear-gradient(135deg,#166534,#22c55e)' : '#1a1a2e'};
border:2px solid ${a.completed ? '#22c55e' : tierColor};">
${a.completed ? '✓' : tierIcon(a.tier)}
</div>
<div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:600;color:${a.completed ? '#34D399' : '#f8fafc'};margin-bottom:2px;">${a.name}</div>
<div style="font-size:11px;color:#64748b;margin-bottom:4px;">${a.description}</div>
${!a.completed ? `
<div style="display:flex;align-items:center;gap:6px;">
<div style="flex:1;background:#1a1a2e;border-radius:99px;height:4px;overflow:hidden;">
<div style="background:${tierColor};height:100%;width:${pct}%;border-radius:99px;"></div>
</div>
<span style="font-size:10px;color:#64748b;white-space:nowrap;">${a.progress}/${a.target}</span>
</div>
` : `
<div style="font-size:10px;color:#22c55e;">✓ مكتمل</div>
`}
</div>
<div style="text-align:center;min-width:40px;">
<div style="font-size:11px;font-weight:700;color:#E4AC38;">${a.coins_reward}</div>
<div style="font-size:9px;color:#64748b;">عملة</div>
</div>
</div>
`;
}).join('');
}
function tierIcon(tier) {
const icons = { 1: '⭐', 2: '🌟', 3: '💫', 4: '👑' };
return icons[tier] || '⭐';
}
...@@ -10,12 +10,36 @@ import { emoji } from '../../../core/theme.js'; ...@@ -10,12 +10,36 @@ import { emoji } from '../../../core/theme.js';
const DAY_REWARDS = [50, 75, 100, 125, 150, 200, 300]; const DAY_REWARDS = [50, 75, 100, 125, 150, 200, 300];
export async function mountDaily(el) { export async function mountDaily(el) {
el.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;background:#0a0a1a;color:#64748b;">جاري التحميل...</div>`;
let streak = 0;
let alreadyClaimed = false;
let dayIndex = 0;
let todayReward = 50;
try {
const data = await net.get('daily-reward.php');
if (data && !data.error) {
streak = data.streak || 0;
alreadyClaimed = data.already_claimed || false;
dayIndex = data.day_index || 0;
todayReward = data.today_reward || DAY_REWARDS[0];
}
} catch (e) {
// Fallback to local store
const player = store.get('player') || {}; const player = store.get('player') || {};
const lastClaim = player.last_daily_reward || null; const lastClaim = player.last_daily_reward || null;
const today = new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10);
const alreadyClaimed = lastClaim && lastClaim.slice(0, 10) === today; alreadyClaimed = lastClaim && lastClaim.slice(0, 10) === today;
const streak = player.daily_streak || 0; streak = player.daily_streak || 0;
dayIndex = streak % 7;
todayReward = DAY_REWARDS[dayIndex];
}
render(el, streak, alreadyClaimed, dayIndex, todayReward);
}
function render(el, streak, alreadyClaimed, dayIndex, todayReward) {
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:#0a0a1a;"> <div style="display:flex;flex-direction:column;height:100%;background:#0a0a1a;">
<!-- Header --> <!-- Header -->
...@@ -34,11 +58,9 @@ export async function mountDaily(el) { ...@@ -34,11 +58,9 @@ export async function mountDaily(el) {
<!-- 7-day timeline --> <!-- 7-day timeline -->
<div style="display:flex;gap:6px;width:100%;max-width:340px;justify-content:center;"> <div style="display:flex;gap:6px;width:100%;max-width:340px;justify-content:center;">
${DAY_REWARDS.map((coins, i) => { ${DAY_REWARDS.map((coins, i) => {
const dayNum = i + 1; const isPast = i < dayIndex || (i === dayIndex && alreadyClaimed);
const isPast = i < streak; const isToday = i === dayIndex && !alreadyClaimed;
const isToday = i === streak; const claimed = i < dayIndex || (i === dayIndex && alreadyClaimed);
const isFuture = i > streak;
const claimed = isPast || (isToday && alreadyClaimed);
return ` return `
<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;"> <div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;">
<div style="width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700; <div style="width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;
...@@ -47,7 +69,7 @@ export async function mountDaily(el) { ...@@ -47,7 +69,7 @@ export async function mountDaily(el) {
color:${claimed ? '#34D399' : isToday ? '#1a1a1a' : '#64748b'};"> color:${claimed ? '#34D399' : isToday ? '#1a1a1a' : '#64748b'};">
${claimed ? '✓' : coins} ${claimed ? '✓' : coins}
</div> </div>
<span style="font-size:9px;color:${isToday ? '#E4AC38' : '#475569'};font-weight:${isToday ? '700' : '400'};">يوم ${dayNum}</span> <span style="font-size:9px;color:${isToday ? '#E4AC38' : '#475569'};font-weight:${isToday ? '700' : '400'};">يوم ${i + 1}</span>
</div> </div>
`; `;
}).join('')} }).join('')}
...@@ -62,7 +84,7 @@ export async function mountDaily(el) { ...@@ -62,7 +84,7 @@ export async function mountDaily(el) {
` : ` ` : `
<div style="font-size:56px;margin-bottom:8px;animation:float 3s ease-in-out infinite;">${emoji('gift', '🎁', 56)}</div> <div style="font-size:56px;margin-bottom:8px;animation:float 3s ease-in-out infinite;">${emoji('gift', '🎁', 56)}</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;margin-bottom:4px;">مكافأة اليوم</div> <div style="font-size:16px;font-weight:700;color:#f8fafc;margin-bottom:4px;">مكافأة اليوم</div>
<div style="font-size:28px;font-weight:800;color:#E4AC38;margin-bottom:16px;">${DAY_REWARDS[Math.min(streak, 6)]} ${emoji('coin', '🪙', 24)}</div> <div style="font-size:28px;font-weight:800;color:#E4AC38;margin-bottom:16px;">${todayReward} ${emoji('coin', '🪙', 24)}</div>
<button class="btn btn-primary" id="claim-btn" style="font-size:16px;padding:14px 48px;">استلم المكافأة</button> <button class="btn btn-primary" id="claim-btn" style="font-size:16px;padding:14px 48px;">استلم المكافأة</button>
`} `}
</div> </div>
...@@ -87,7 +109,7 @@ export async function mountDaily(el) { ...@@ -87,7 +109,7 @@ export async function mountDaily(el) {
const data = await net.post('daily-reward.php', { action: 'claim' }); const data = await net.post('daily-reward.php', { action: 'claim' });
if (data.error) { if (data.error) {
claimBtn.textContent = data.error; claimBtn.textContent = data.error;
claimBtn.disabled = false; setTimeout(() => { claimBtn.textContent = 'استلم المكافأة'; claimBtn.disabled = false; }, 2000);
return; return;
} }
...@@ -96,15 +118,19 @@ export async function mountDaily(el) { ...@@ -96,15 +118,19 @@ export async function mountDaily(el) {
juice.coinBurst(window.innerWidth / 2, window.innerHeight / 2, 10); juice.coinBurst(window.innerWidth / 2, window.innerHeight / 2, 10);
juice.coinFlyTo(window.innerWidth / 2, window.innerHeight / 2, '#hud-coins', 5); juice.coinFlyTo(window.innerWidth / 2, window.innerHeight / 2, '#hud-coins', 5);
// Update UI to claimed state // Update local store
claimBtn.textContent = ' تم!'; const player = store.get('player') || {};
claimBtn.style.background = '#34D399'; store.set('player', {
...player,
coins: data.total_coins,
daily_streak: data.streak,
last_daily_reward: new Date().toISOString()
});
bus.emit('coins:earned', { amount: data.coins });
bus.emit('coins:earned', { amount: data.coins || 100 }); // Re-render with claimed state
const player = store.get('player'); render(el, data.streak, true, data.day_index, data.coins);
if (player) {
store.set('player', { ...player, coins: data.total_coins || (player.coins || 0) + (data.coins || 100), last_daily_reward: new Date().toISOString() });
}
} catch (e) { } catch (e) {
claimBtn.textContent = 'فشل حاول مرة أخرى'; claimBtn.textContent = 'فشل حاول مرة أخرى';
claimBtn.disabled = false; claimBtn.disabled = false;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment