| import { |
| Box, |
| Button, |
| Typography, |
| Chip, |
| Divider, |
| CircularProgress, |
| TextField, |
| Dialog, |
| DialogTitle, |
| DialogContent, |
| DialogActions, |
| useMediaQuery, |
| useTheme, |
| IconButton, |
| Tooltip, |
| } from "@mui/material"; |
| import { useNavigate } from "react-router-dom"; |
| import { TalkWithSarah } from "./TalkWithSarah"; |
| import { useState } from "react"; |
| import { useGame } from "../contexts/GameContext"; |
| import { storyApi } from "../utils/api"; |
| import { useSoundEffect } from "../hooks/useSoundEffect"; |
| import CloseIcon from "@mui/icons-material/Close"; |
| import CasinoOutlinedIcon from "@mui/icons-material/CasinoOutlined"; |
|
|
| const { initAudioContext } = storyApi; |
|
|
| |
| const RANDOM_PLACEHOLDERS = [ |
| "A dragon appears right above the hero...", |
| "Suddenly, all the trees start dancing the macarena...", |
| "A time-traveling pizza delivery guy shows up with a mysterious package...", |
| "The ground turns into jello and starts wobbling menacingly...", |
| "A choir of singing cats descends from the sky...", |
| "The hero's shadow detaches itself and starts doing stand-up comedy...", |
| "All the nearby rocks transform into vintage toasters...", |
| "A portal opens, and out steps the hero's evil twin made entirely of cheese...", |
| "The moon starts beatboxing an ominous rhythm...", |
| "Every nearby plant suddenly develops a British accent and starts having tea...", |
| "A giant rubber duck floats down from the sky...", |
| "The hero's sword turns into a bouquet of flowers...", |
| "A mysterious fog rolls in, bringing with it the scent of fresh cookies...", |
| "The hero's shoes start tap dancing on their own...", |
| "A unicorn gallops by, leaving a trail of glitter...", |
| "The sky turns neon green and starts flashing like a disco...", |
| "A talking squirrel offers the hero some sage advice...", |
| "The hero's reflection in the water winks and waves...", |
| "A giant marshmallow bounces across the landscape...", |
| "The hero's cape transforms into a pair of wings...", |
| "A parade of tiny elephants marches through the scene...", |
| "The sun suddenly dons sunglasses and starts singing...", |
| "A mysterious door appears, leading to a candy world...", |
| "The hero's hat turns into a magical top hat...", |
| "A rainbow-colored river flows uphill...", |
| "The hero's backpack starts floating and glowing...", |
| "A group of fairies begins to dance around the hero...", |
| "The hero's footsteps echo with musical notes...", |
| "A giant book opens, revealing a new adventure...", |
| "The hero's pet dragon starts juggling fireballs...", |
| "A spaceship lands and out steps an alien with a top hat...", |
| "The hero's backpack starts singing show tunes...", |
| "A rainbow appears and a leprechaun slides down it...", |
| "The hero's pet suddenly starts speaking in riddles...", |
| "A gust of wind brings a shower of confetti...", |
| "The hero's map transforms into a treasure map...", |
| "A giant clock appears, ticking backwards...", |
| "The hero's footsteps leave a trail of glowing footprints...", |
| "A parade of penguins marches by, playing instruments...", |
| "The hero's hat flies off and starts floating in mid-air...", |
| "A mysterious voice narrates the hero's every move...", |
| "The hero's shadow starts mimicking their actions in exaggerated ways...", |
| "A pikachu appears, looking curious...", |
| "A bublizarre jumps out from the bushes...", |
| "A pikachu flies by, leaving a trail of sparkles...", |
| "A salameche suddenly joins the hero's journey...", |
| "A carapuce watches from a distance, intrigued...", |
| ]; |
|
|
| |
| const formatTextWithBold = (text) => { |
| if (!text) return ""; |
| const parts = text.split(/(\*\*.*?\*\*)/g); |
| return parts.map((part, index) => { |
| if (part.startsWith("**") && part.endsWith("**")) { |
| return ( |
| <Chip |
| key={index} |
| label={part.slice(2, -2)} |
| size="small" |
| sx={{ |
| mx: 0.5, |
| fontSize: "1.1rem", |
| backgroundColor: "rgba(255, 255, 255, 0.1)", |
| color: "white", |
| }} |
| /> |
| ); |
| } |
| return part; |
| }); |
| }; |
|
|
| export function StoryChoices() { |
| const navigate = useNavigate(); |
| const [isSarahActive, setIsSarahActive] = useState(false); |
| const [sarahRecommendation, setSarahRecommendation] = useState(null); |
| const [showCustomDialog, setShowCustomDialog] = useState(false); |
| const [customChoice, setCustomChoice] = useState(""); |
| const [lastUsedPlaceholder, setLastUsedPlaceholder] = useState(""); |
| const [currentPlaceholder] = useState(() => { |
| const randomPlaceholder = |
| RANDOM_PLACEHOLDERS[ |
| Math.floor(Math.random() * RANDOM_PLACEHOLDERS.length) |
| ]; |
| setLastUsedPlaceholder(randomPlaceholder); |
| return randomPlaceholder; |
| }); |
| const { |
| choices, |
| onChoice, |
| isLoading, |
| isNarratorSpeaking, |
| stopNarration, |
| playNarration, |
| heroName, |
| getLastSegment, |
| isGameOver, |
| } = useGame(); |
|
|
| const theme = useTheme(); |
| const isMobile = useMediaQuery(theme.breakpoints.down("sm")); |
|
|
| |
| const playPageSound = useSoundEffect({ |
| basePath: "/sounds/page-flip-", |
| numSounds: 7, |
| volume: 0.5, |
| }); |
|
|
| |
| const playDiceSound = useSoundEffect({ |
| basePath: "/sounds/dice-", |
| numSounds: 3, |
| volume: 0.1, |
| enabled: true, |
| }); |
|
|
| const lastSegment = getLastSegment(); |
| const isLastStep = lastSegment?.is_last_step; |
| const isDeath = lastSegment?.isDeath; |
| const isVictory = lastSegment?.isVictory; |
| const storyText = lastSegment?.rawText || ""; |
|
|
| const getRandomPlaceholder = () => { |
| |
| const availablePlaceholders = RANDOM_PLACEHOLDERS.filter( |
| (p) => p !== lastUsedPlaceholder |
| ); |
| |
| const newPlaceholder = |
| availablePlaceholders[ |
| Math.floor(Math.random() * availablePlaceholders.length) |
| ]; |
| |
| setLastUsedPlaceholder(newPlaceholder); |
| return newPlaceholder; |
| }; |
|
|
| if (isGameOver()) { |
| return ( |
| <Box |
| sx={{ |
| display: "flex", |
| flexDirection: "column", |
| justifyContent: "center", |
| alignItems: "center", |
| gap: 2, |
| width: "100%", |
| }} |
| > |
| <Typography |
| variant="h2" |
| sx={{ |
| color: isVictory ? "#4CAF50" : "#f44336", |
| textAlign: "center", |
| textTransform: "uppercase", |
| }} |
| > |
| {isVictory ? "VICTORY" : "DEFEAT"} |
| </Typography> |
| </Box> |
| ); |
| } |
|
|
| if (!choices || choices.length === 0) return null; |
|
|
| return ( |
| <Box |
| data-story-choices |
| sx={{ |
| display: "flex", |
| flexDirection: isMobile ? "column" : "row", |
| justifyContent: "center", |
| alignItems: "center", |
| gap: 0.5, |
| width: "100%", |
| height: "100%", |
| }} |
| > |
| {isLoading ? ( |
| <CircularProgress |
| size={40} |
| sx={{ opacity: "0.2", color: "primary.main" }} |
| /> |
| ) : ( |
| <> |
| {/* {choices |
| .filter((_, index) => !isMobile || index === 0) |
| .map((choice, index) => ( |
| <Box |
| key={choice.id} |
| sx={{ |
| display: "flex", |
| flexDirection: "column", |
| alignItems: "center", |
| gap: 1, |
| minWidth: "fit-content", |
| maxWidth: isMobile ? "90%" : "30%", |
| }} |
| > |
| <Button |
| variant="contained" |
| size="large" |
| onClick={() => { |
| initAudioContext(); |
| playPageSound(); |
| stopNarration(); |
| onChoice(choice.id); |
| }} |
| disabled={isSarahActive || isLoading || isNarratorSpeaking} |
| sx={{ |
| width: "auto", |
| minWidth: "fit-content", |
| }} |
| > |
| {formatTextWithBold(choice.text)} |
| </Button> |
| </Box> |
| ))} */} |
| |
| <Box |
| sx={{ |
| display: "flex", |
| flexDirection: "column", |
| alignItems: "center", |
| gap: 1, |
| minWidth: "fit-content", |
| maxWidth: isMobile ? "90%" : "30%", |
| }} |
| > |
| <Button |
| variant="contained" |
| size="large" |
| onClick={() => { |
| initAudioContext(); |
| playPageSound(); |
| stopNarration(); |
| onChoice(choices[0].id); |
| }} |
| disabled={isSarahActive || isLoading || isNarratorSpeaking} |
| sx={{ |
| width: "auto", |
| minWidth: "fit-content", |
| }} |
| > |
| {formatTextWithBold(choices[0].text)} |
| </Button> |
| </Box> |
| |
| <Typography |
| variant="h6" |
| sx={{ |
| display: { xs: "none", sm: "block" }, |
| color: "rgba(255,255,255,0.5)", |
| fontWeight: "bold", |
| fontSize: "1.2rem", |
| mx: 2, |
| }} |
| > |
| OR |
| </Typography> |
| |
| <Box |
| sx={{ |
| display: "flex", |
| flexDirection: "column", |
| alignItems: "center", |
| gap: 1, |
| // ml: isMobile ? 0 : 4, |
| minWidth: "fit-content", |
| maxWidth: "30%", |
| }} |
| > |
| <Button |
| variant="contained" |
| size="large" |
| color="secondary" |
| onClick={() => setShowCustomDialog(true)} |
| disabled={isSarahActive || isLoading || isNarratorSpeaking} |
| sx={{ |
| width: "auto", |
| minWidth: "fit-content", |
| textTransform: "none", |
| }} |
| > |
| Write your own choice... |
| </Button> |
| </Box> |
| </> |
| )} |
|
|
| <Dialog |
| open={showCustomDialog} |
| onClose={() => setShowCustomDialog(false)} |
| maxWidth="md" |
| fullWidth |
| sx={{ |
| "& .MuiBackdrop-root": { |
| backgroundColor: "rgba(0, 0, 0, 0.95)", |
| }, |
| }} |
| PaperProps={{ |
| sx: { |
| backgroundColor: "transparent", |
| backgroundImage: "none", |
| boxShadow: "none", |
| m: isMobile ? 2 : 3, |
| maxHeight: isMobile ? "calc(100% - 32px)" : "calc(100% - 64px)", |
| }, |
| }} |
| > |
| <DialogTitle |
| sx={{ |
| pt: 2, |
| pb: 1, |
| textAlign: "left", |
| color: "text.primary", |
| fontSize: isMobile ? "1.25rem" : "1.5rem", |
| pl: 3, |
| }} |
| > |
| What happens next? |
| </DialogTitle> |
| <IconButton |
| onClick={() => setShowCustomDialog(false)} |
| sx={{ |
| position: "absolute", |
| right: 8, |
| top: 8, |
| color: "text.secondary", |
| }} |
| > |
| <CloseIcon /> |
| </IconButton> |
| <DialogContent |
| sx={{ |
| p: isMobile ? 2 : 3, |
| display: "flex", |
| flexDirection: "column", |
| gap: 2, |
| }} |
| > |
| <TextField |
| autoFocus |
| multiline |
| rows={isMobile ? 5 : 4} |
| fullWidth |
| variant="outlined" |
| placeholder={currentPlaceholder} |
| value={customChoice} |
| onChange={(e) => setCustomChoice(e.target.value)} |
| sx={{ |
| "& .MuiOutlinedInput-root": { |
| backgroundColor: "rgba(0, 0, 0, 0.5)", |
| border: "1px solid rgba(255, 255, 255, 0.1)", |
| "&:hover": { |
| backgroundColor: "rgba(0, 0, 0, 0.6)", |
| border: "1px solid rgba(255, 255, 255, 0.2)", |
| }, |
| "&.Mui-focused": { |
| backgroundColor: "rgba(0, 0, 0, 0.7)", |
| border: "1px solid rgba(255, 255, 255, 0.3)", |
| }, |
| }, |
| "& .MuiOutlinedInput-input": { |
| color: "text.primary", |
| fontSize: isMobile ? "0.9rem" : "1rem", |
| lineHeight: "1.5", |
| }, |
| }} |
| /> |
| <Box |
| sx={{ display: "flex", justifyContent: "flex-end", gap: 1, mt: 1 }} |
| > |
| <Button |
| onClick={() => { |
| const randomChoice = getRandomPlaceholder(); |
| setCustomChoice(randomChoice.slice(0, -3)); |
| playDiceSound(); |
| }} |
| variant="outlined" |
| sx={{ |
| minWidth: "48px", |
| width: "48px", |
| height: "48px", |
| p: 0, |
| borderColor: "rgba(255, 255, 255, 0.23)", |
| color: "white", |
| "&:hover": { |
| borderColor: "white", |
| backgroundColor: "rgba(255, 255, 255, 0.08)", |
| }, |
| }} |
| > |
| <CasinoOutlinedIcon /> |
| </Button> |
| <Button |
| onClick={() => { |
| if (customChoice.trim()) { |
| initAudioContext(); |
| playPageSound(); |
| stopNarration(); |
| onChoice("custom", customChoice); |
| setShowCustomDialog(false); |
| setCustomChoice(""); |
| } |
| }} |
| disabled={!customChoice.trim()} |
| variant="contained" |
| sx={{ |
| py: 1.5, |
| px: 4, |
| fontWeight: "bold", |
| }} |
| > |
| Validate |
| </Button> |
| </Box> |
| </DialogContent> |
| </Dialog> |
| </Box> |
| ); |
| } |
|
|