Weekly prizes: top three of the Monday-to-Sunday Central week with 10+ finds get 3/2/1 POL as due prize drips (never finds, never against the pool), awarded once on the first tick after Sunday; Top Hunter badge; /api/prizes + winners on /leaders; admin award-by-hand
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+43
-3
@@ -18,17 +18,22 @@ const BADGES = [
|
||||
{ id: 'sweep', name: 'Full Sweep', icon: '\u{1F5FA}️', why: 'a find on every site' },
|
||||
{ id: 'lucky', name: 'Lucky Find', icon: '\u{1F48E}', why: 'one drip of 0.4 POL or more' },
|
||||
{ id: 'streak', name: 'On a Streak', icon: '\u{1F525}', why: 'finds on three days in a row' },
|
||||
{ id: 'top', name: 'Top Hunter', icon: '\u{1F3C6}', why: 'won a weekly prize as #1' },
|
||||
];
|
||||
const PRIZE_DEFAULTS = { weeklyPrizes: [3, 2, 1], weeklyMinFinds: 10 };
|
||||
function prizeRules() { const s = rewards.settings(); return { pol: Array.isArray(s.weeklyPrizes) && s.weeklyPrizes.length ? s.weeklyPrizes.map(Number) : PRIZE_DEFAULTS.weeklyPrizes, minFinds: Number(s.weeklyMinFinds) > 0 ? Number(s.weeklyMinFinds) : PRIZE_DEFAULTS.weeklyMinFinds }; }
|
||||
|
||||
function excluded() { const s = rewards.settings(); const list = Array.isArray(s.leaderboardExclude) ? s.leaderboardExclude : [1]; return new Set(list.map(Number)); }
|
||||
function finds() { return store.read('payouts', []).filter(p => p.status !== 'failed'); }
|
||||
function finds() { return store.read('payouts', []).filter(p => p.status !== 'failed' && !p.prize); } // prize drips are not finds
|
||||
|
||||
// Central day arithmetic on 'YYYY-MM-DD' keys
|
||||
function dayMinus(dayKey, n) { const [y, m, d] = dayKey.split('-').map(Number); const t = Date.UTC(y, m - 1, d) - n * 86400000; return new Date(t).toISOString().slice(0, 10); }
|
||||
// the Monday that starts a day's week (weeks run Monday to Sunday, Central)
|
||||
function weekOf(dayKey) { const [y, m, d] = dayKey.split('-').map(Number); const dow = (new Date(Date.UTC(y, m - 1, d)).getUTCDay() + 6) % 7; return dayMinus(dayKey, dow); }
|
||||
function inPeriod(p, period, today) {
|
||||
if (period === 'all') return true;
|
||||
if (period === 'month') return p.day.slice(0, 7) === today.slice(0, 7);
|
||||
return p.day >= dayMinus(today, 6); // week: the last seven Central days including today
|
||||
return weekOf(p.day) === weekOf(today); // week: this Monday-to-Sunday week
|
||||
}
|
||||
|
||||
function badgesFor(memberId, list) {
|
||||
@@ -43,6 +48,7 @@ function badgesFor(memberId, list) {
|
||||
if (mine.some(p => p.pol >= 0.4)) out.add('lucky');
|
||||
const days = new Set(mine.map(p => p.day));
|
||||
for (const d of days) { if (days.has(dayMinus(d, 1)) && days.has(dayMinus(d, 2))) { out.add('streak'); break; } }
|
||||
if (store.read('payouts', []).some(p => p.memberId === Number(memberId) && p.prize && p.prize.rank === 1 && p.status !== 'failed')) out.add('top');
|
||||
return BADGES.filter(b => out.has(b.id));
|
||||
}
|
||||
|
||||
@@ -58,6 +64,40 @@ function leaderboard(period, limit) {
|
||||
return rows.slice(0, limit || 25).map((r, i) => Object.assign(r, { rank: i + 1, pol: Math.round(r.pol * 10000) / 10000, badges: badgesFor(r.memberId, all).map(b => b.icon) }));
|
||||
}
|
||||
|
||||
// standings for one fixed week (Monday key), qualified by the minimum finds
|
||||
function weekRows(monday) {
|
||||
const ex = excluded(); const all = finds(); const end = dayMinus(monday, -6); const by = new Map();
|
||||
for (const p of all) {
|
||||
if (ex.has(p.memberId) || p.day < monday || p.day > end) continue;
|
||||
const r = by.get(p.memberId) || { memberId: p.memberId, who: p.username ? '@' + p.username : '#' + p.memberId, username: p.username || null, email: p.email, wallet: p.wallet, finds: 0, pol: 0, last: 0 };
|
||||
r.finds++; if (p.status === 'paid') r.pol += p.pol; r.last = Math.max(r.last, p.at); if (p.wallet) r.wallet = p.wallet; by.set(p.memberId, r);
|
||||
}
|
||||
return [...by.values()].sort((a, b) => b.finds - a.finds || b.pol - a.pol || a.last - b.last);
|
||||
}
|
||||
// award one week, once: prize drips go on the ledger as due, the faucet pays them like any drip
|
||||
function awardWeek(monday) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(monday || '') || weekOf(monday) !== monday) return { error: 'not a Monday key' };
|
||||
const done = store.read('prizes', []);
|
||||
if (done.some(x => x.week === monday)) return { skipped: 'already awarded', week: monday };
|
||||
const rules = prizeRules();
|
||||
const winners = weekRows(monday).filter(r => r.finds >= rules.minFinds).slice(0, rules.pol.length).map((r, i) => Object.assign({ rank: i + 1, prizePol: rules.pol[i] }, r));
|
||||
const day = ctDay(), at = Date.now();
|
||||
const recs = winners.map(w => ({ id: 'prize' + monday.replace(/-/g, '') + '-' + w.rank, memberId: w.memberId, email: w.email, wallet: w.wallet, username: w.username, missionId: 'prize:' + monday, site: 'Weekly prize #' + w.rank, pol: w.prizePol, day, at, status: 'due', tx: null, paidAt: null, error: null, prize: { week: monday, rank: w.rank } }));
|
||||
if (recs.length) store.update('payouts', [], all => { all.push(...recs); return all; });
|
||||
const entry = { week: monday, at, rules, winners: winners.map(w => ({ rank: w.rank, memberId: w.memberId, who: w.who, finds: w.finds, pol: w.pol, prizePol: w.prizePol })) };
|
||||
store.update('prizes', [], all => { all.push(entry); return all; });
|
||||
return entry;
|
||||
}
|
||||
// on every tick: the most recent completed week gets awarded on the first tick after it ends
|
||||
function awardDue(notify) {
|
||||
const last = dayMinus(weekOf(ctDay()), 7);
|
||||
if (store.read('prizes', []).some(x => x.week === last)) return null;
|
||||
const r = awardWeek(last);
|
||||
if (r && r.winners && r.winners.length && notify) notify('prize', r).catch(() => {});
|
||||
return r;
|
||||
}
|
||||
function prizes() { const all = store.read('prizes', []); return { rules: prizeRules(), week: weekOf(ctDay()), last: all.length ? all[all.length - 1] : null, history: all.slice(-8).reverse() }; }
|
||||
|
||||
function rankOf(memberId, period) {
|
||||
const rows = leaderboard(period, 100000); const i = rows.findIndex(r => r.memberId === Number(memberId));
|
||||
return i < 0 ? null : { rank: i + 1, of: rows.length, finds: rows[i].finds, pol: rows[i].pol };
|
||||
@@ -67,4 +107,4 @@ function rankOf(memberId, period) {
|
||||
// their instantadpay.com/join/<ref> link for everyone who signs up from it
|
||||
function shareLink(site, me) { const ref = me.username || me.memberId; return site + '/?r=' + encodeURIComponent(String(ref)); }
|
||||
|
||||
module.exports = { BADGES, badgesFor, leaderboard, rankOf, shareLink };
|
||||
module.exports = { BADGES, badgesFor, leaderboard, rankOf, shareLink, weekOf, weekRows, awardWeek, awardDue, prizes, prizeRules };
|
||||
|
||||
Reference in New Issue
Block a user