mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-06-25 08:45:22 -04:00
feat: add achievements system, hint overlay, and progress page polish
Achievements system:
- Add src/lib/server/milestones.ts with full achievement definitions and
calculation logic (16 achievements: streaks, book set completions,
community milestones like Overachiever/Procrastinator/Outlier, and fun
ones like Prodigal Son, Extra Credit, Is This A Joke To You?)
- Wire calculateMilestones() into the progress page server load
- Replace the old ad-hoc milestone cards with a proper achievements grid
(3/4 col, uniform min-height cards, larger text)
- Change "With God, All Things Are Possible" from "every game solved in 1"
to "solve in 1 guess for each of the 66 books at least once"
Game page hint overlay:
- After a correct testament/section/first-letter match, display a subtle
text hint below the verse prompt (e.g. "It is in the Old Testament.")
- Hints fade in 2.8s after a guess (after the row flip animation)
- Hints are only shown to new players (fewer than 3 tracked wins) to
avoid being patronising to experienced players
Progress page:
- Hide Skill Growth chart with {#if false && showChart} pending rework
- Fix book tier colour scheme: explored=blue, mastered=purple, perfect=emerald
(was amber/emerald — now consistent across grid, legend, and stat cards)
- Simplify GuessesTable row colour: remove proximity gradient, use flat red
for wrong guesses
- Add "Come back tomorrow!" encouragement text in CountdownTimer for new
players (fewer than 3 wins)
- Fix GamePrompt text colour to always be gray-100
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,15 @@
|
||||
count: number;
|
||||
};
|
||||
|
||||
type Milestone = {
|
||||
id: string;
|
||||
name: string;
|
||||
emoji: string;
|
||||
description: string;
|
||||
achieved: boolean;
|
||||
achievedDate: string | null;
|
||||
};
|
||||
|
||||
type ProgressData = {
|
||||
completions: Array<{ date: string; guessCount: number }>;
|
||||
chartPoints: ChartPoint[];
|
||||
@@ -49,6 +58,7 @@
|
||||
days14: string | null;
|
||||
days30: string | null;
|
||||
};
|
||||
milestones: Milestone[];
|
||||
};
|
||||
|
||||
interface PageData {
|
||||
@@ -78,9 +88,9 @@
|
||||
function bookTileClass(tier: BookTier): string {
|
||||
switch (tier) {
|
||||
case "perfect":
|
||||
return "bg-amber-400 text-amber-900";
|
||||
return "bg-emerald-500 text-white";
|
||||
case "mastered":
|
||||
return "bg-emerald-600 text-white";
|
||||
return "bg-purple-600 text-white";
|
||||
case "explored":
|
||||
return "bg-blue-700 text-blue-100";
|
||||
default:
|
||||
@@ -288,14 +298,14 @@
|
||||
emoji="🏆"
|
||||
value={String(prog.booksMastered)}
|
||||
label="Books Mastered"
|
||||
colorClass="text-emerald-400"
|
||||
colorClass="text-purple-400"
|
||||
suffix="/ 66"
|
||||
/>
|
||||
<ProgressStatCard
|
||||
emoji="⭐"
|
||||
value={String(prog.booksPerfect)}
|
||||
label="Books Perfected"
|
||||
colorClass="text-amber-400"
|
||||
colorClass="text-emerald-400"
|
||||
suffix="/ 66"
|
||||
/>
|
||||
</div>
|
||||
@@ -322,7 +332,7 @@
|
||||
class="flex items-center gap-1 text-xs text-gray-400"
|
||||
>
|
||||
<span
|
||||
class="inline-block w-5 h-5 rounded bg-emerald-600"
|
||||
class="inline-block w-5 h-5 rounded bg-purple-600"
|
||||
></span>
|
||||
Mastered
|
||||
</span>
|
||||
@@ -330,7 +340,7 @@
|
||||
class="flex items-center gap-1 text-xs text-gray-400"
|
||||
>
|
||||
<span
|
||||
class="inline-block w-5 h-5 rounded bg-amber-400"
|
||||
class="inline-block w-5 h-5 rounded bg-emerald-500"
|
||||
></span>
|
||||
Perfect
|
||||
</span>
|
||||
@@ -358,11 +368,11 @@
|
||||
<p class="text-xs text-gray-500 mt-3 leading-relaxed">
|
||||
<span class="text-blue-400 font-medium">Explored</span>
|
||||
— played at least once<br />
|
||||
<span class="text-emerald-400 font-medium"
|
||||
<span class="text-purple-400 font-medium"
|
||||
>Mastered</span
|
||||
>
|
||||
— avg ≤ 3 guesses over 2+ plays<br />
|
||||
<span class="text-amber-400 font-medium">Perfect</span> —
|
||||
<span class="text-emerald-400 font-medium">Perfect</span> —
|
||||
mastered and guessed in 1 at least once
|
||||
</p>
|
||||
</Container>
|
||||
@@ -373,8 +383,8 @@
|
||||
<ActivityCalendar completions={prog.completions} />
|
||||
</div>
|
||||
|
||||
<!-- Skill Growth Chart -->
|
||||
{#if showChart}
|
||||
<!-- Skill Growth Chart (hidden, needs rework) -->
|
||||
{#if false && showChart}
|
||||
<div class="mb-6">
|
||||
<Container class="p-4 md:p-6 w-full">
|
||||
<div class="w-full">
|
||||
@@ -499,108 +509,33 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Milestones -->
|
||||
{#if prog.bestSingleGame || prog.streakMilestones.days7 || prog.streakMilestones.days14 || prog.streakMilestones.days30}
|
||||
<!-- Achievements -->
|
||||
{#if prog.milestones.length > 0}
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-100 mb-3">
|
||||
Milestones
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{#if prog.bestSingleGame}
|
||||
<Container class="p-4">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl mb-1">⚡</div>
|
||||
<div
|
||||
class="text-sm font-bold text-yellow-300 leading-tight"
|
||||
>
|
||||
{prog.bestSingleGame.bookName}
|
||||
<h2 class="text-xl font-bold text-gray-100 mb-3">🏆 Achievements</h2>
|
||||
<div class="grid grid-cols-3 md:grid-cols-4 gap-2 md:gap-3">
|
||||
{#each prog.milestones.filter(m => m.achieved) as milestone (milestone.id)}
|
||||
<Container class="p-3 min-h-[130px]">
|
||||
<div class="text-center flex flex-col items-center justify-center h-full">
|
||||
<div class="text-2xl mb-1">{milestone.emoji}</div>
|
||||
<div class="text-sm font-bold text-yellow-300 leading-tight mb-1">
|
||||
{milestone.name}
|
||||
</div>
|
||||
<div
|
||||
class="text-xs text-gray-300 font-medium mt-1"
|
||||
>
|
||||
First 1-Guess Win
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] text-gray-500 mt-0.5"
|
||||
>
|
||||
{formatDate(prog.bestSingleGame.date)}
|
||||
<div class="text-xs text-gray-400 leading-tight">
|
||||
{milestone.description}
|
||||
</div>
|
||||
{#if milestone.achievedDate}
|
||||
<div class="text-[10px] text-gray-500 mt-1">
|
||||
{formatDate(milestone.achievedDate)}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-[10px] text-emerald-500 mt-1">Earned</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Container>
|
||||
{/if}
|
||||
{#if prog.streakMilestones.days7}
|
||||
<Container class="p-4">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl mb-1">🔥</div>
|
||||
<div
|
||||
class="text-sm font-bold text-orange-300 leading-tight"
|
||||
>
|
||||
7-Day Streak
|
||||
</div>
|
||||
<div
|
||||
class="text-xs text-gray-300 font-medium mt-1"
|
||||
>
|
||||
First Achieved
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] text-gray-500 mt-0.5"
|
||||
>
|
||||
{formatDate(
|
||||
prog.streakMilestones.days7,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
{/if}
|
||||
{#if prog.streakMilestones.days14}
|
||||
<Container class="p-4">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl mb-1">💥</div>
|
||||
<div
|
||||
class="text-sm font-bold text-orange-400 leading-tight"
|
||||
>
|
||||
14-Day Streak
|
||||
</div>
|
||||
<div
|
||||
class="text-xs text-gray-300 font-medium mt-1"
|
||||
>
|
||||
First Achieved
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] text-gray-500 mt-0.5"
|
||||
>
|
||||
{formatDate(
|
||||
prog.streakMilestones.days14,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
{/if}
|
||||
{#if prog.streakMilestones.days30}
|
||||
<Container class="p-4">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl mb-1">🏅</div>
|
||||
<div
|
||||
class="text-sm font-bold text-amber-300 leading-tight"
|
||||
>
|
||||
30-Day Streak
|
||||
</div>
|
||||
<div
|
||||
class="text-xs text-gray-300 font-medium mt-1"
|
||||
>
|
||||
First Achieved
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] text-gray-500 mt-0.5"
|
||||
>
|
||||
{formatDate(
|
||||
prog.streakMilestones.days30,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">{prog.milestones.filter(m => m.achieved).length} / {prog.milestones.length} achievements unlocked</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user