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:
+97
-2
@@ -2,6 +2,7 @@
|
||||
import type { PageProps } from "./$types";
|
||||
import { browser } from "$app/environment";
|
||||
import { enhance } from "$app/forms";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import VerseDisplay from "$lib/components/VerseDisplay.svelte";
|
||||
import SearchInput from "$lib/components/SearchInput.svelte";
|
||||
@@ -13,7 +14,7 @@
|
||||
import DevButtons from "$lib/components/DevButtons.svelte";
|
||||
import AuthModal from "$lib/components/AuthModal.svelte";
|
||||
|
||||
import { evaluateGuess } from "$lib/utils/game";
|
||||
import { evaluateGuess, getFirstLetter } from "$lib/utils/game";
|
||||
import {
|
||||
generateShareText,
|
||||
shareResult,
|
||||
@@ -75,6 +76,62 @@
|
||||
!persistence.chapterGuessCompleted,
|
||||
);
|
||||
|
||||
let knownTestament = $derived(
|
||||
persistence.guesses.some((g) => g.testamentMatch)
|
||||
? correctBook?.testament
|
||||
: null,
|
||||
);
|
||||
let knownSection = $derived(
|
||||
persistence.guesses.some((g) => g.sectionMatch)
|
||||
? correctBook?.section
|
||||
: null,
|
||||
);
|
||||
let knownFirstLetter = $derived(
|
||||
persistence.guesses.some((g) => g.firstLetterMatch)
|
||||
? getFirstLetter(correctBook?.name ?? "").toUpperCase()
|
||||
: null,
|
||||
);
|
||||
|
||||
let testamentVisible = $state(false);
|
||||
let sectionVisible = $state(false);
|
||||
let firstLetterVisible = $state(false);
|
||||
let showHints = $state(false);
|
||||
|
||||
// On page load, show hints that are already known without animation
|
||||
onMount(() => {
|
||||
if (knownTestament) testamentVisible = true;
|
||||
if (knownSection) sectionVisible = true;
|
||||
if (knownFirstLetter) firstLetterVisible = true;
|
||||
|
||||
const winCount = Object.keys(localStorage).filter(
|
||||
(k) => k.startsWith("bibdle-win-tracked-") && localStorage.getItem(k) === "true"
|
||||
).length;
|
||||
showHints = winCount < 3;
|
||||
});
|
||||
|
||||
// Fade in newly revealed hints after the guess animation completes
|
||||
$effect(() => {
|
||||
if (!knownTestament || testamentVisible) return;
|
||||
const id = setTimeout(() => {
|
||||
testamentVisible = true;
|
||||
}, 2800);
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
$effect(() => {
|
||||
if (!knownSection || sectionVisible) return;
|
||||
const id = setTimeout(() => {
|
||||
sectionVisible = true;
|
||||
}, 2800);
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
$effect(() => {
|
||||
if (!knownFirstLetter || firstLetterVisible) return;
|
||||
const id = setTimeout(() => {
|
||||
firstLetterVisible = true;
|
||||
}, 2800);
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
|
||||
async function submitGuess(bookId: string) {
|
||||
if (persistence.guesses.some((g) => g.book.id === bookId)) return;
|
||||
|
||||
@@ -318,6 +375,42 @@
|
||||
<div class="animate-fade-in-up animate-delay-400">
|
||||
<GamePrompt guessCount={persistence.guesses.length} />
|
||||
|
||||
{#if showHints && (knownTestament || knownSection || knownFirstLetter)}
|
||||
<div
|
||||
class="text-xs uppercase tracking-widest font-bold text-center text-gray-500 dark:text-gray-400 flex flex-col gap-1 mb-4 -mt-2"
|
||||
>
|
||||
{#if knownTestament}
|
||||
<p
|
||||
style="transition: opacity 0.5s ease; opacity: {testamentVisible
|
||||
? 1
|
||||
: 0};"
|
||||
>
|
||||
It is in the {knownTestament === "old"
|
||||
? "Old"
|
||||
: "New"} Testament.
|
||||
</p>
|
||||
{/if}
|
||||
{#if knownSection}
|
||||
<p
|
||||
style="transition: opacity 0.5s ease; opacity: {sectionVisible
|
||||
? 1
|
||||
: 0};"
|
||||
>
|
||||
It is in the {knownSection} section.
|
||||
</p>
|
||||
{/if}
|
||||
{#if knownFirstLetter}
|
||||
<p
|
||||
style="transition: opacity 0.5s ease; opacity: {firstLetterVisible
|
||||
? 1
|
||||
: 0};"
|
||||
>
|
||||
The book's name starts with "{knownFirstLetter}".
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<SearchInput
|
||||
bind:searchQuery
|
||||
{guessedIds}
|
||||
@@ -356,7 +449,9 @@
|
||||
</div>
|
||||
|
||||
{#if isWon}
|
||||
<hr class="animate-fade-in-up animate-delay-800 border-gray-300 dark:border-gray-600" />
|
||||
<hr
|
||||
class="animate-fade-in-up animate-delay-800 border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
<div class="animate-fade-in-up animate-delay-800">
|
||||
<a
|
||||
href="https://discord.gg/yWQXbGK8SD"
|
||||
|
||||
Reference in New Issue
Block a user