Commit 3e2a893c authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(game): announce new achievements the moment a game ends

Achievements were only checked when the achievements screen was opened, so a
new one was never celebrated. After every game the HUD now refreshes the player
from the server (the purse counts up, a level-up fires if earned), runs the
achievement check and drops a gold banner for each new one with its coin
reward, the reward jingle and a haptic.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 28883801
...@@ -6,6 +6,8 @@ import { assetImg } from './theme.js'; ...@@ -6,6 +6,8 @@ import { assetImg } from './theme.js';
import { icon, gicon, crown } from './icons.js'; import { icon, gicon, crown } from './icons.js';
import { countUp, haptic } from './feel.js'; import { countUp, haptic } from './feel.js';
import * as juice from './juice.js'; import * as juice from './juice.js';
import * as net from './net.js';
import { getLang } from './i18n.js';
import * as audio from './audio.js'; import * as audio from './audio.js';
let hudEl, tabBar; let hudEl, tabBar;
...@@ -32,6 +34,7 @@ export function init(guestMode = false) { ...@@ -32,6 +34,7 @@ export function init(guestMode = false) {
bus.on('coins:earned', animateCoins); bus.on('coins:earned', animateCoins);
bus.on('xp:earned', animateXp); bus.on('xp:earned', animateXp);
bus.on('tournament:pending-updated', updateTournamentBadge); bus.on('tournament:pending-updated', updateTournamentBadge);
bus.on('game:ended', afterGame);
bus.on('tournament:paired', () => updateTournamentBadge([1])); bus.on('tournament:paired', () => updateTournamentBadge([1]));
} }
...@@ -200,6 +203,59 @@ function updateHud() { ...@@ -200,6 +203,59 @@ function updateHud() {
let lastLevel = 0; let lastLevel = 0;
/**
* After every game: pull the player from the server (the purse counts up and a
* level-up fires if one was earned), then check achievements and announce each
* new one. Guests have no achievements.
*/
let afterGameTimer = null;
function afterGame() {
if (isGuestMode || !store.get('auth.token')) return;
clearTimeout(afterGameTimer);
afterGameTimer = setTimeout(async () => {
try {
const fresh = await net.get('profile.php');
if (fresh && !fresh.error) store.set('player', { ...store.get('player'), ...fresh });
} catch (e) {}
try {
const res = await net.post('achievements.php', { action: 'check' });
const ids = res?.newly_completed || [];
if (!ids.length) return;
const all = await net.get('achievements.php');
const list = (all?.achievements || []).filter(a => ids.includes(a.id));
list.forEach((a, i) => setTimeout(() => announceAchievement(a), 900 + i * 2600));
} catch (e) {}
}, 1800);
}
/** A gold banner that drops from the top: an achievement just unlocked. */
function announceAchievement(a) {
const name = (getLang() === 'ar' && a.name_ar) ? a.name_ar : (a.name || '');
const el = document.createElement('div');
el.className = 'ach-pop';
el.setAttribute('role', 'status');
el.innerHTML = `
<span class="ach-pop-badge">${gicon('medal', 46)}</span>
<span class="ach-pop-text">
<span class="ach-pop-kind">${t('hud.achievement_unlocked')}</span>
<span class="ach-pop-name">${escapeHtml(name)}</span>
</span>
${a.coins_reward ? `<span class="ach-pop-reward">${gicon('coin', 24)}<span class="num">+${a.coins_reward}</span></span>` : ''}
<style>
.ach-pop { position: fixed; top: calc(var(--safe-top) + 10px); inset-inline: 12px; z-index: 350; display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: 18px; border: 2px solid var(--outline); background: linear-gradient(180deg, #FFE47E, #F2A01A); box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.6), 0 5px 0 var(--btn-gold-edge), 0 14px 26px rgba(0, 0, 0, 0.5); animation: achDrop 2400ms var(--ease-out) forwards; max-width: 520px; margin-inline: auto; }
@keyframes achDrop { 0% { transform: translateY(-140%) scale(0.9); } 12% { transform: translateY(6%) scale(1.03); } 18% { transform: none; } 86% { transform: none; opacity: 1; } 100% { transform: translateY(-140%); opacity: 0; } }
.ach-pop-badge { display: flex; animation: wiggle 1.2s ease-in-out infinite; }
.ach-pop-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.ach-pop-kind { font-family: var(--font-display); font-size: 13px; color: #7A3E00; }
.ach-pop-name { font-family: var(--font-display); font-size: 19px; color: #fff; text-shadow: var(--text-outline); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ach-pop-reward { display: inline-flex; align-items: center; gap: 2px; font-family: var(--font-display); font-size: 18px; color: #fff; text-shadow: var(--text-outline); }
</style>`;
document.body.appendChild(el);
audio.play('reward', 'reward');
haptic('success');
setTimeout(() => el.remove(), 2500);
}
/** Full-screen level-up: rays, the crown, the new level in a gold star. */ /** Full-screen level-up: rays, the crown, the new level in a gold star. */
function celebrateLevel(level) { function celebrateLevel(level) {
if (document.querySelector('.lvl-up')) return; if (document.querySelector('.lvl-up')) return;
......
...@@ -1000,7 +1000,8 @@ const strings = { ...@@ -1000,7 +1000,8 @@ const strings = {
'splash.tip_5': 'نصيحة: في الطاولة لا تترك قطعة وحيدة أمام خصمك', 'splash.tip_5': 'نصيحة: في الطاولة لا تترك قطعة وحيدة أمام خصمك',
'hud.level_up': 'مستوى جديد!', 'hud.level_up': 'مستوى جديد!',
'hud.level_up_sub': 'وصلت إلى المستوى {n}', 'hud.level_up_sub': 'وصلت إلى المستوى {n}',
'hud.great': 'رائع!' 'hud.great': 'رائع!',
'hud.achievement_unlocked': 'إنجاز جديد!'
}, },
en: { en: {
'app.name': 'EL3AB', 'app.name': 'EL3AB',
...@@ -2001,7 +2002,8 @@ const strings = { ...@@ -2001,7 +2002,8 @@ const strings = {
'splash.tip_5': 'Tip: in backgammon, don\'t leave a single checker exposed', 'splash.tip_5': 'Tip: in backgammon, don\'t leave a single checker exposed',
'hud.level_up': 'Level up!', 'hud.level_up': 'Level up!',
'hud.level_up_sub': 'You reached level {n}', 'hud.level_up_sub': 'You reached level {n}',
'hud.great': 'Awesome!' 'hud.great': 'Awesome!',
'hud.achievement_unlocked': 'Achievement unlocked!'
} }
}; };
......
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