Commit 1b5018a6 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(challenges): sticker tiles, chunky gold bars, English titles

Each challenge shows its game's sticker (ludo_played, chess_played...) and
a ready one glows gold until collected. The all-done panel was never shown;
it now appears once every challenge is claimed. The API's fallback icons
and the stored templates use sticker names instead of emoji.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 3df90193
......@@ -38,15 +38,15 @@ function getDailyChallenges($db, string $userId): void {
$allChallenges = [];
if (is_array($templates) && !isset($templates['error'])) {
foreach ($templates as $t) {
$allChallenges[] = ['id' => $t['id'], 'title' => $t['title_ar'] ?? $t['title'], 'title_en' => $t['title'], 'target' => intval($t['target']), 'type' => $t['type'], 'reward_coins' => intval($t['reward_coins']), 'reward_xp' => intval($t['reward_xp']), 'icon' => $t['icon'] ?? '🎮'];
$allChallenges[] = ['id' => $t['id'], 'title' => $t['title_ar'] ?? $t['title'], 'title_en' => $t['title'], 'target' => intval($t['target']), 'type' => $t['type'], 'reward_coins' => intval($t['reward_coins']), 'reward_xp' => intval($t['reward_xp']), 'icon' => $t['icon'] ?? 'cards'];
}
}
if (empty($allChallenges)) {
// Fallback if DB is empty
$allChallenges = [
['id' => 'play_3', 'title' => 'العب 3 مباريات', 'title_en' => 'Play 3 games', 'target' => 3, 'type' => 'games_played', 'reward_coins' => 50, 'reward_xp' => 30, 'icon' => '🎮'],
['id' => 'win_2', 'title' => 'اربح مباراتين', 'title_en' => 'Win 2 games', 'target' => 2, 'type' => 'wins', 'reward_coins' => 80, 'reward_xp' => 50, 'icon' => '🏆'],
['id' => 'beat_bot', 'title' => 'اهزم بوت', 'title_en' => 'Beat a bot', 'target' => 1, 'type' => 'bot_wins', 'reward_coins' => 40, 'reward_xp' => 25, 'icon' => '🤖'],
['id' => 'play_3', 'title' => 'العب 3 مباريات', 'title_en' => 'Play 3 games', 'target' => 3, 'type' => 'games_played', 'reward_coins' => 50, 'reward_xp' => 30, 'icon' => 'cards'],
['id' => 'win_2', 'title' => 'اربح مباراتين', 'title_en' => 'Win 2 games', 'target' => 2, 'type' => 'wins', 'reward_coins' => 80, 'reward_xp' => 50, 'icon' => 'trophy'],
['id' => 'beat_bot', 'title' => 'اهزم بوت', 'title_en' => 'Beat a bot', 'target' => 1, 'type' => 'bot_wins', 'reward_coins' => 40, 'reward_xp' => 25, 'icon' => 'bot'],
];
}
......
......@@ -6,21 +6,23 @@ import * as juice from '../../../core/juice.js';
import * as bus from '../../../core/bus.js';
import * as store from '../../../core/store.js';
import * as scene from '../../../core/scene.js';
import { t } from '../../../core/i18n.js';
import { icon } from '../../../core/icons.js';
import { t, getLang } from '../../../core/i18n.js';
import { icon, gicon } from '../../../core/icons.js';
import { screenHead, wireBack, loadingRows, emptyState, esc } from '../../../core/ui.js';
// The server sends a typed symbol as each challenge's icon; the mark comes from
// the challenge type instead.
const TYPE_MARK = { games_played: 'cards', wins: 'trophy', bot_wins: 'bot', puzzles: 'puzzle', friends: 'users' };
// The mark comes from the challenge type: a game's own sticker for "play a
// game of X", otherwise the sticker for what is being counted.
const TYPE_MARK = { games_played: 'cards', wins: 'trophy', bot_wins: 'bot', puzzles: 'puzzle', puzzles_solved: 'puzzle', friends: 'friends' };
const GAME_KEYS = ['chess', 'ludo', 'domino', 'backgammon'];
const markFor = (type = '') => TYPE_MARK[type] || GAME_KEYS.find(g => type.startsWith(g + '_')) || 'star';
export async function mountChallenges(el) {
el.innerHTML = `
<div class="screen ch">
${screenHead(t('challenges.title'), { actions: '<span id="streak-badge" class="ch-streak"></span>' })}
<div id="challenges-list" class="rows">${loadingRows(3, 72)}</div>
<div id="all-done" class="ch-all-done" style="display:none;">
<span class="ch-all-done-mark">${icon('check-circle', 32)}</span>
<div id="all-done" class="ch-all-done" hidden>
<span class="ch-all-done-mark">${gicon('party', 72)}</span>
<div class="ch-all-done-title">${t('challenges.all_done')}</div>
</div>
</div>
......@@ -40,7 +42,7 @@ export async function mountChallenges(el) {
function renderChallenges(el, data) {
const { challenges, streak, streak_bonus } = data;
el.querySelector('#streak-badge').innerHTML = `${icon('flame', 16)}<span>${t('challenges.streak', { n: `<span class="num">${streak || 0}</span>` })}</span>`;
el.querySelector('#streak-badge').innerHTML = `${gicon('flame', 26)}<span>${t('challenges.streak', { n: `<span class="num">${streak || 0}</span>` })}</span>`;
const list = el.querySelector('#challenges-list');
list.innerHTML = challenges.map((c, i) => {
......@@ -48,23 +50,26 @@ function renderChallenges(el, data) {
const state = c.claimed ? 'ch-claimed' : c.completed ? 'ch-ready' : 'ch-open';
return `
<div class="row challenge-card ch-row ${state}" data-idx="${i}">
<span class="ch-tile">${icon(TYPE_MARK[c.type] || 'target', 22)}</span>
<span class="ch-tile">${gicon(markFor(c.type), 40)}</span>
<div class="row-main">
<div class="row-title ch-title">${esc(c.title)}</div>
<div class="row-title ch-title">${esc(getLang() === 'en' && c.title_en ? c.title_en : c.title)}</div>
<div class="ch-prog">
<span class="ch-bar"><span style="width:${pct}%"></span></span>
<span class="num ch-count">${c.progress}/${c.target}</span>
</div>
</div>
<div class="ch-end">
${c.claimed ? `<span class="ch-done" role="img" aria-label="${t('challenges.claimed')}">${icon('check', 20, { weight: 2.5 })}</span>` :
c.completed ? `<button class="btn btn-gold claim-btn" data-id="${esc(c.id)}" data-coins="${c.reward_coins}" data-xp="${c.reward_xp}">${icon('coin', 16)}<span class="num">${c.reward_coins}</span>${t('challenges.claim')}</button>` :
`<span class="ch-reward">${icon('coin', 16)}<span class="num">${c.reward_coins}</span></span>`}
${c.claimed ? `<span class="ch-done" role="img" aria-label="${t('challenges.claimed')}">${gicon('check', 36)}</span>` :
c.completed ? `<button class="btn btn-gold claim-btn" data-id="${esc(c.id)}" data-coins="${c.reward_coins}" data-xp="${c.reward_xp}">${gicon('coin', 24)}<span class="num">${c.reward_coins}</span>${t('challenges.claim')}</button>` :
`<span class="ch-reward">${gicon('coin', 24)}<span class="num">${c.reward_coins}</span></span>`}
</div>
</div>
`;
}).join('');
const allDone = () => { el.querySelector('#all-done').hidden = !(challenges.length && challenges.every(c => c.claimed)); };
allDone();
// Stagger animation
const cards = list.querySelectorAll('.challenge-card');
juice.stagger(Array.from(cards), juice.slideUpBounce, 0, 80);
......@@ -80,7 +85,7 @@ function renderChallenges(el, data) {
const coins = parseInt(btn.dataset.coins);
const xp = parseInt(btn.dataset.xp);
btn.innerHTML = `${icon('check', 18, { weight: 2.5 })}<span class="sr-only">${t('challenges.claimed')}</span>`;
btn.innerHTML = `${gicon('check', 28)}<span class="sr-only">${t('challenges.claimed')}</span>`;
btn.classList.add('ch-claimed-btn');
btn.disabled = true;
......@@ -93,6 +98,8 @@ function renderChallenges(el, data) {
await net.post('challenges.php', { action: 'claim', challenge_id: id, coins, xp });
bus.emit('coins:earned', { amount: coins });
bus.emit('xp:earned', { amount: xp });
const c = challenges.find(x => String(x.id) === id);
if (c) { c.claimed = true; allDone(); }
} catch (e) {}
});
});
......@@ -102,61 +109,79 @@ const STYLE = `
.ch-streak {
display: inline-flex;
align-items: center;
gap: 4px;
min-height: 32px;
padding: 0 10px;
border-radius: var(--r-md);
background: var(--back-800);
border: 1px solid var(--back-line);
font-size: var(--fs-sm);
font-weight: 800;
gap: 2px;
min-height: 40px;
padding: 0 12px 0 6px;
border-radius: 20px;
background: rgba(0, 0, 0, 0.5);
box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.6), 0 1px 0 rgba(255, 220, 160, 0.18);
font-family: var(--font-display);
font-size: var(--fs-md);
color: var(--text-primary);
text-shadow: var(--text-outline);
}
.ch-streak:empty { display: none; }
.ch-streak .i-flame { color: var(--gold); }
.ch-streak .gi { animation: idleBob 2.4s ease-in-out infinite; }
.ch-row { min-height: 72px; padding-block: var(--s-3); }
.ch-row { min-height: 80px; padding-block: var(--s-3); }
.ch-tile {
width: 44px;
height: 44px;
width: 58px;
height: 58px;
flex-shrink: 0;
border-radius: var(--r-md);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--back-800);
border: 1px solid var(--back-line);
color: var(--text-secondary);
background: radial-gradient(circle at 50% 35%, rgba(255, 220, 150, 0.25), rgba(0, 0, 0, 0.45) 70%);
box-shadow: 0 0 0 2px rgba(20, 8, 2, 0.8), inset 0 0 0 2px rgba(255, 210, 140, 0.22);
}
.ch-ready .ch-tile, .ch-claimed .ch-tile { color: var(--gold); background: var(--back-700); }
.ch-title { white-space: normal; }
.ch-claimed .ch-title { color: var(--text-secondary); }
.ch-ready .ch-tile { background: radial-gradient(circle at 50% 35%, #FFE98A, #E39A14 75%); box-shadow: 0 0 0 2px var(--outline), 0 0 18px rgba(255, 200, 80, 0.55); animation: chReady 1.6s ease-in-out infinite; }
.ch-claimed .ch-tile .gi { filter: grayscale(0.6) brightness(0.85); }
@keyframes chReady { 50% { transform: scale(1.06) rotate(-4deg); } }
.ch-title { white-space: normal; font-weight: 700; }
.ch-claimed .ch-title { color: var(--text-secondary); text-decoration: line-through; text-decoration-color: rgba(255, 244, 222, 0.35); }
.ch-prog { display: flex; align-items: center; gap: var(--s-2); margin-top: 6px; }
.ch-bar { flex: 1; height: 4px; border-radius: var(--r-full); background: var(--back-700); overflow: hidden; }
.ch-bar > span { display: block; height: 100%; border-radius: inherit; background: var(--gold); transition: width var(--dur-slow) var(--ease-out); }
.ch-claimed .ch-bar > span { background: var(--win); }
.ch-count { font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); white-space: nowrap; }
.ch-bar {
flex: 1;
height: 12px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.55);
box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.7), 0 1px 0 rgba(255, 220, 160, 0.2);
overflow: hidden;
}
.ch-bar > span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(180deg, var(--btn-gold-1), var(--btn-gold-2));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
animation: chFill 800ms var(--ease-out) 250ms both;
}
@keyframes chFill { from { width: 0; } }
.ch-claimed .ch-bar > span { background: linear-gradient(180deg, var(--btn-green-1), var(--btn-green-2)); }
.ch-count { font-family: var(--font-display); font-size: var(--fs-sm); color: var(--text-secondary); white-space: nowrap; text-shadow: var(--text-outline); }
.ch-end { flex-shrink: 0; display: flex; align-items: center; }
.ch-reward {
display: inline-flex;
align-items: center;
gap: 4px;
min-height: 28px;
padding: 0 8px;
border-radius: var(--r-sm);
background: var(--back-800);
border: 1px solid var(--back-line);
font-size: var(--fs-sm);
font-weight: 800;
min-height: 34px;
padding: 0 10px 0 4px;
border-radius: 17px;
background: rgba(0, 0, 0, 0.5);
box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.6);
font-family: var(--font-display);
font-size: 16px;
color: var(--text-primary);
text-shadow: var(--text-outline);
}
.ch-done { color: var(--win); display: flex; width: 44px; justify-content: center; }
.claim-btn { min-height: 44px; padding-inline: var(--s-3); font-size: var(--fs-sm); gap: 4px; }
.claim-btn .i-coin { color: var(--ink); }
.claim-btn.ch-claimed-btn { background: var(--back-700); color: var(--win); box-shadow: none; outline-color: var(--back-line); }
.ch-done { display: flex; width: 52px; justify-content: center; animation: popIn 360ms var(--ease-pop) both; }
.claim-btn { min-height: 48px; padding-inline: var(--s-3); gap: 4px; }
.claim-btn.ch-claimed-btn { filter: saturate(0.4) brightness(0.8); }
.ch-all-done[hidden] { display: none; }
.ch-all-done { display: flex; flex-direction: column; align-items: center; gap: var(--s-2); padding: var(--s-6); text-align: center; }
.ch-all-done-mark { color: var(--win); display: flex; }
.ch-all-done-title { font-size: var(--fs-md); font-weight: 700; color: var(--text-primary); }
.ch-all-done-mark { display: flex; animation: idleBob 3s ease-in-out infinite; filter: drop-shadow(0 4px 4px rgba(0, 0, 0, 0.5)); }
.ch-all-done-title { font-family: var(--font-display); font-size: var(--fs-lg); color: var(--text-primary); text-shadow: var(--text-outline); }
`;
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