From 89af525f22477d63368d9dc1e2be7e8d6a82ade3 Mon Sep 17 00:00:00 2001 From: Cory Locklear Date: Thu, 11 Jun 2026 10:10:05 -0400 Subject: [PATCH] Polish museum path puzzle UX for homelab testing. Add celebration overlay with Ted image, puzzle sounds, larger artifact thumbnails, and fix desktop artifact tray scrolling. --- .gitignore | 1 + public/media/config.json.example | 1 + scripts/setup-dev-media.sh | 8 ++ scripts/setup-prod-media.sh | 9 ++ src/App.css | 153 ++++++++++++++++++++++++++----- src/MuseumPathCelebration.tsx | 22 +++++ src/MuseumPathPuzzle.tsx | 38 +++++--- src/mediaConfig.ts | 4 + 8 files changed, 196 insertions(+), 40 deletions(-) create mode 100644 src/MuseumPathCelebration.tsx diff --git a/.gitignore b/.gitignore index b960fea..6308c20 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ dist-ssr # Local media copies (generated by scripts/setup-dev-media.sh) public/media/unlock.mp4 public/media/artifact-room-map.jpg +public/media/ted-celebrate.png public/media/artifacts/ public/media/celebration-pop.mp3 public/media/celebration-horn.mp3 diff --git a/public/media/config.json.example b/public/media/config.json.example index 3b18ed7..1d05213 100644 --- a/public/media/config.json.example +++ b/public/media/config.json.example @@ -1,6 +1,7 @@ { "unlockVideoUrl": "/media/unlock.mp4", "mapImageUrl": "/media/artifact-room-map.jpg", + "tedCelebrateImageUrl": "/media/ted-celebrate.png", "incorrectUrl": "/media/incorrect.mp3", "unlockConfirmUrl": "/media/unlock-confirm.mp3", "celebrationPopUrl": "/media/celebration-pop.mp3", diff --git a/scripts/setup-dev-media.sh b/scripts/setup-dev-media.sh index 81382f4..c929e75 100755 --- a/scripts/setup-dev-media.sh +++ b/scripts/setup-dev-media.sh @@ -26,6 +26,14 @@ cp "$INCORRECT_SOURCE" "$DEST_DIR/incorrect.mp3" cp "$CONFIRM_SOURCE" "$DEST_DIR/unlock-confirm.mp3" python3 "$ROOT/scripts/extract-artifact-room-map.py" "$PDF_SOURCE" "$DEST_DIR/artifact-room-map.jpg" python3 "$ROOT/scripts/extract-artifact-images.py" "$PDF_SOURCE" "$DEST_DIR/artifacts" +TED_CELEBRATE_SOURCE="${TED_CELEBRATE_SOURCE:-$SOURCE_DIR/ted-celebrate.png}" +if [[ -f "$TED_CELEBRATE_SOURCE" ]]; then + cp "$TED_CELEBRATE_SOURCE" "$DEST_DIR/ted-celebrate.png" +elif [[ -f "$ROOT/public/media/ted-celebrate.png" ]]; then + cp "$ROOT/public/media/ted-celebrate.png" "$DEST_DIR/ted-celebrate.png" +else + echo "Warning: ted-celebrate.png not found; puzzle 2 celebration image will be missing" >&2 +fi cp "$ROOT/public/media/config.json.example" "$DEST_DIR/config.json" echo "Copied media to $DEST_DIR/" diff --git a/scripts/setup-prod-media.sh b/scripts/setup-prod-media.sh index 903694d..adcda30 100755 --- a/scripts/setup-prod-media.sh +++ b/scripts/setup-prod-media.sh @@ -26,10 +26,19 @@ cp "$INCORRECT_SOURCE" "$DEST/incorrect.mp3" cp "$CONFIRM_SOURCE" "$DEST/unlock-confirm.mp3" python3 "$ROOT/scripts/extract-artifact-room-map.py" "$PDF_SOURCE" "$DEST/artifact-room-map.jpg" python3 "$ROOT/scripts/extract-artifact-images.py" "$PDF_SOURCE" "$DEST/artifacts" +TED_CELEBRATE_SOURCE="${TED_CELEBRATE_SOURCE:-$SOURCE_DIR/ted-celebrate.png}" +if [[ -f "$TED_CELEBRATE_SOURCE" ]]; then + cp "$TED_CELEBRATE_SOURCE" "$DEST/ted-celebrate.png" +elif [[ -f "$ROOT/public/media/ted-celebrate.png" ]]; then + cp "$ROOT/public/media/ted-celebrate.png" "$DEST/ted-celebrate.png" +else + echo "Warning: ted-celebrate.png not found; puzzle 2 celebration image will be missing" >&2 +fi cat >"$DEST/config.json" <<'EOF' { "unlockVideoUrl": "/media/unlock.mp4", "mapImageUrl": "/media/artifact-room-map.jpg", + "tedCelebrateImageUrl": "/media/ted-celebrate.png", "incorrectUrl": "/media/incorrect.mp3", "unlockConfirmUrl": "/media/unlock-confirm.mp3", "celebrationPopUrl": "/media/celebration-pop.mp3", diff --git a/src/App.css b/src/App.css index cf46c7a..ee42aa3 100644 --- a/src/App.css +++ b/src/App.css @@ -487,10 +487,15 @@ .artifact-bank__grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr)); - gap: 0.65rem; - max-height: 28rem; - overflow-y: auto; + grid-template-columns: repeat(auto-fill, minmax(8.75rem, 1fr)); + gap: 0.75rem; +} + +@media (max-width: 52rem) { + .artifact-bank__grid { + max-height: 28rem; + overflow-y: auto; + } } .path-slots { @@ -503,8 +508,8 @@ display: flex; align-items: center; gap: 0.65rem; - min-height: 4.5rem; - padding: 0.5rem 0.65rem; + min-height: 5.5rem; + padding: 0.55rem 0.75rem; background: linear-gradient(180deg, #2a2218 0%, #1f1a14 100%); border: 2px solid #4a3f32; border-radius: 0.5rem; @@ -561,7 +566,7 @@ } .artifact-card--overlay { - width: 6.5rem; + width: 8.75rem; opacity: 0.95; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); cursor: grabbing; @@ -575,8 +580,8 @@ } .artifact-card__image { - width: 3.25rem; - height: 3.25rem; + width: 5rem; + height: 5rem; object-fit: contain; flex-shrink: 0; background: #fff; @@ -584,12 +589,17 @@ } .path-slot .artifact-card__image { - width: 2.75rem; - height: 2.75rem; + width: 4rem; + height: 4rem; +} + +.artifact-card--overlay .artifact-card__image { + width: 5rem; + height: 5rem; } .artifact-card__name { - font-size: 0.78rem; + font-size: 0.85rem; line-height: 1.25; color: #f4e4bc; text-align: center; @@ -605,17 +615,110 @@ margin-top: 0.25rem; } -.museum-path__success { - margin: 0.25rem 0 0; - padding: 0.85rem 1.25rem; - font-family: Cinzel, Georgia, serif; - font-size: 1.1rem; - font-weight: 600; - letter-spacing: 0.04em; - color: #e8c547; - text-align: center; - background: rgba(92, 68, 28, 0.5); - border: 1px solid rgba(201, 162, 39, 0.55); - border-radius: 0.5rem; - animation: error-pop 0.25s ease-out; +.museum-path-panel--fade-out { + opacity: 0; + transform: scale(0.98); + pointer-events: none; + transition: + opacity 0.65s ease-out, + transform 0.65s ease-out; +} + +.museum-path-celebration { + position: fixed; + inset: 0; + z-index: 200; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.museum-path-celebration--fade-in { + opacity: 0; + animation: museum-celebration-in 0.9s ease-out 0.35s forwards; +} + +@keyframes museum-celebration-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.museum-path-celebration__bg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center top; +} + +.museum-path-celebration__scrim { + position: absolute; + inset: 0; + background: linear-gradient( + 180deg, + rgba(26, 21, 16, 0.35) 0%, + rgba(26, 21, 16, 0.55) 45%, + rgba(26, 21, 16, 0.72) 100% + ); +} + +.museum-path-celebration__content { + position: relative; + z-index: 1; + max-width: 36rem; + margin: 0 1.5rem; + padding: 2rem 2.25rem; + text-align: center; + background: rgba(45, 36, 25, 0.72); + border: 3px solid #c9a227; + border-radius: 1rem; + box-shadow: + inset 0 0 0 1px rgba(232, 197, 71, 0.35), + 0 16px 48px rgba(0, 0, 0, 0.5); + animation: museum-celebration-content-in 0.8s ease-out 0.7s both; +} + +@keyframes museum-celebration-content-in { + from { + opacity: 0; + transform: translateY(1.25rem) scale(0.96); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.museum-path-celebration__eyebrow { + margin: 0 0 0.65rem; + font-family: Cinzel, Georgia, serif; + font-size: clamp(1rem, 2.5vw, 1.2rem); + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #e8c547; +} + +.museum-path-celebration__title { + margin: 0 0 0.85rem; + font-family: Cinzel, Georgia, serif; + font-size: clamp(2rem, 6vw, 3rem); + font-weight: 700; + line-height: 1.15; + letter-spacing: 0.04em; + color: #f4e4bc; + text-shadow: 0 2px 12px rgba(0, 0, 0, 0.45); +} + +.museum-path-celebration__subtitle { + margin: 0; + font-size: clamp(1rem, 2.5vw, 1.15rem); + line-height: 1.45; + color: #c9b896; } diff --git a/src/MuseumPathCelebration.tsx b/src/MuseumPathCelebration.tsx new file mode 100644 index 0000000..8f170a1 --- /dev/null +++ b/src/MuseumPathCelebration.tsx @@ -0,0 +1,22 @@ +interface MuseumPathCelebrationProps { + imageUrl: string; +} + +export function MuseumPathCelebration({ imageUrl }: MuseumPathCelebrationProps) { + return ( +
+ + + ); +} diff --git a/src/MuseumPathPuzzle.tsx b/src/MuseumPathPuzzle.tsx index f9b4325..15ed7a7 100644 --- a/src/MuseumPathPuzzle.tsx +++ b/src/MuseumPathPuzzle.tsx @@ -13,12 +13,15 @@ import { import { CSS } from '@dnd-kit/utilities'; import confetti from 'canvas-confetti'; import { useCallback, useMemo, useState } from 'react'; +import { playCelebrationSounds, playIncorrectSound } from './mediaSounds'; import { CORRECT_PATH, SLOT_COUNT } from './puzzle2Config'; import { artifactImageUrl, useArtifactManifest, type ArtifactEntry, } from './useArtifactManifest'; +import { MuseumPathCelebration } from './MuseumPathCelebration'; +import { useMediaConfig } from './useMediaConfig'; type DropZoneId = 'bank' | `slot-${number}`; @@ -153,6 +156,7 @@ function PathSlot({ index, artifact }: { index: number; artifact: ArtifactEntry } export function MuseumPathPuzzle() { + const mediaConfig = useMediaConfig(); const manifest = useArtifactManifest(); const [slots, setSlots] = useState<(string | null)[]>(() => Array(SLOT_COUNT).fill(null)); const [activeSlug, setActiveSlug] = useState(null); @@ -214,12 +218,14 @@ export function MuseumPathPuzzle() { if (phase !== 'playing') return; if (!isPathCorrect(slots)) { + playIncorrectSound(mediaConfig.incorrectUrl); triggerJiggle(); return; } setShowError(false); setPhase('solved'); + playCelebrationSounds(mediaConfig); const confettiColors = ['#e8c547', '#c9a227', '#f4e4bc', '#d4a84b']; const duration = 2000; @@ -245,7 +251,7 @@ export function MuseumPathPuzzle() { } }; frame(); - }, [phase, slots, triggerJiggle]); + }, [mediaConfig, phase, slots, triggerJiggle]); if (!manifest) { return ( @@ -261,7 +267,14 @@ export function MuseumPathPuzzle() { return (
-
+ {phase === 'solved' && ( + + )} + +

Museum Path

@@ -296,19 +309,14 @@ export function MuseumPathPuzzle() {

)} - {phase === 'solved' ? ( -

- You found the path! Ted escaped the museum! -

- ) : ( - - )} +
); diff --git a/src/mediaConfig.ts b/src/mediaConfig.ts index 1cc2069..675d00f 100644 --- a/src/mediaConfig.ts +++ b/src/mediaConfig.ts @@ -1,6 +1,7 @@ export interface MediaConfig { unlockVideoUrl: string; mapImageUrl: string; + tedCelebrateImageUrl: string; incorrectUrl: string; unlockConfirmUrl: string; celebrationPopUrl: string; @@ -11,6 +12,7 @@ export interface MediaConfig { export const DEFAULT_MEDIA_CONFIG: MediaConfig = { unlockVideoUrl: '/media/unlock.mp4', mapImageUrl: '/media/artifact-room-map.jpg', + tedCelebrateImageUrl: '/media/ted-celebrate.png', incorrectUrl: '/media/incorrect.mp3', unlockConfirmUrl: '/media/unlock-confirm.mp3', celebrationPopUrl: '/media/celebration-pop.mp3', @@ -23,6 +25,8 @@ export function mergeMediaConfig(partial: Partial | null): MediaCon return { unlockVideoUrl: partial.unlockVideoUrl ?? DEFAULT_MEDIA_CONFIG.unlockVideoUrl, mapImageUrl: partial.mapImageUrl ?? DEFAULT_MEDIA_CONFIG.mapImageUrl, + tedCelebrateImageUrl: + partial.tedCelebrateImageUrl ?? DEFAULT_MEDIA_CONFIG.tedCelebrateImageUrl, incorrectUrl: partial.incorrectUrl ?? DEFAULT_MEDIA_CONFIG.incorrectUrl, unlockConfirmUrl: partial.unlockConfirmUrl ?? DEFAULT_MEDIA_CONFIG.unlockConfirmUrl, celebrationPopUrl: partial.celebrationPopUrl ?? DEFAULT_MEDIA_CONFIG.celebrationPopUrl,