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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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",
|
||||
|
||||
+126
-23
@@ -487,10 +487,15 @@
|
||||
|
||||
.artifact-bank__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
|
||||
gap: 0.65rem;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
interface MuseumPathCelebrationProps {
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export function MuseumPathCelebration({ imageUrl }: MuseumPathCelebrationProps) {
|
||||
return (
|
||||
<div className="museum-path-celebration museum-path-celebration--fade-in" role="status">
|
||||
<img
|
||||
className="museum-path-celebration__bg"
|
||||
src={encodeURI(imageUrl)}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="museum-path-celebration__scrim" aria-hidden="true" />
|
||||
<div className="museum-path-celebration__content">
|
||||
<p className="museum-path-celebration__eyebrow">You did it!</p>
|
||||
<h1 className="museum-path-celebration__title">Ted escaped the museum!</h1>
|
||||
<p className="museum-path-celebration__subtitle">You found the right path through the displays.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<string | null>(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 (
|
||||
<div className="museum-path-scene">
|
||||
<div className="sarcophagus-panel museum-path-panel">
|
||||
{phase === 'solved' && (
|
||||
<MuseumPathCelebration imageUrl={mediaConfig.tedCelebrateImageUrl} />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`sarcophagus-panel museum-path-panel ${phase === 'solved' ? 'museum-path-panel--fade-out' : ''}`}
|
||||
aria-hidden={phase === 'solved'}
|
||||
>
|
||||
<header className="lock-header museum-path__header">
|
||||
<h1>Museum Path</h1>
|
||||
<p className="lock-subtitle">
|
||||
@@ -296,19 +309,14 @@ export function MuseumPathPuzzle() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{phase === 'solved' ? (
|
||||
<p className="museum-path__success" role="status">
|
||||
You found the path! Ted escaped the museum!
|
||||
</p>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={`unlock-btn museum-path__check ${jiggle ? 'unlock-btn--jiggle' : ''}`}
|
||||
onClick={handleCheckPath}
|
||||
disabled={phase === 'solved'}
|
||||
>
|
||||
CHECK PATH
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<MediaConfig> | 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,
|
||||
|
||||
Reference in New Issue
Block a user