tfrere commited on
Commit
ad593d1
·
1 Parent(s): ad2a30e

update sound / add random feature

Browse files
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
client/public/sounds/dice-1.mp3 ADDED
Binary file (31.8 kB). View file
 
client/public/sounds/dice-2.mp3 ADDED
Binary file (31.8 kB). View file
 
client/public/sounds/dice-3.mp3 ADDED
Binary file (31.8 kB). View file
 
client/public/sounds/lock-1.mp3 ADDED
Binary file (5.42 kB). View file
 
client/public/sounds/tick-1.mp3 ADDED
Binary file (2.93 kB). View file
 
client/public/sounds/tick-2.mp3 ADDED
Binary file (3.41 kB). View file
 
client/public/sounds/tick-3.mp3 ADDED
Binary file (4.37 kB). View file
 
client/public/sounds/tick-4.mp3 ADDED
Binary file (4.37 kB). View file
 
client/src/components/StoryChoices.jsx CHANGED
@@ -13,6 +13,7 @@ import {
13
  useMediaQuery,
14
  useTheme,
15
  IconButton,
 
16
  } from "@mui/material";
17
  import { useNavigate } from "react-router-dom";
18
  import { TalkWithSarah } from "./TalkWithSarah";
@@ -21,9 +22,24 @@ import { useGame } from "../contexts/GameContext";
21
  import { storyApi } from "../utils/api";
22
  import { useSoundEffect } from "../hooks/useSoundEffect";
23
  import CloseIcon from "@mui/icons-material/Close";
 
24
 
25
  const { initAudioContext } = storyApi;
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  // Function to convert text with ** to Chip elements
28
  const formatTextWithBold = (text) => {
29
  if (!text) return "";
@@ -54,6 +70,12 @@ export function StoryChoices() {
54
  const [sarahRecommendation, setSarahRecommendation] = useState(null);
55
  const [showCustomDialog, setShowCustomDialog] = useState(false);
56
  const [customChoice, setCustomChoice] = useState("");
 
 
 
 
 
 
57
  const {
58
  choices,
59
  onChoice,
@@ -76,12 +98,26 @@ export function StoryChoices() {
76
  volume: 0.5,
77
  });
78
 
 
 
 
 
 
 
 
 
79
  const lastSegment = getLastSegment();
80
  const isLastStep = lastSegment?.is_last_step;
81
  const isDeath = lastSegment?.isDeath;
82
  const isVictory = lastSegment?.isVictory;
83
  const storyText = lastSegment?.rawText || "";
84
 
 
 
 
 
 
 
85
  if (isGameOver()) {
86
  return (
87
  <Box
@@ -193,13 +229,26 @@ export function StoryChoices() {
193
  </Button>
194
  </Box>
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  <Box
197
  sx={{
198
  display: "flex",
199
  flexDirection: "column",
200
  alignItems: "center",
201
  gap: 1,
202
- ml: isMobile ? 0 : 4,
203
  minWidth: "fit-content",
204
  maxWidth: "30%",
205
  }}
@@ -216,7 +265,7 @@ export function StoryChoices() {
216
  textTransform: "none",
217
  }}
218
  >
219
- Write your own path
220
  </Button>
221
  </Box>
222
  </>
@@ -279,7 +328,7 @@ export function StoryChoices() {
279
  rows={isMobile ? 5 : 4}
280
  fullWidth
281
  variant="outlined"
282
- placeholder="A dragon appears right above the hero...."
283
  value={customChoice}
284
  onChange={(e) => setCustomChoice(e.target.value)}
285
  sx={{
@@ -302,7 +351,31 @@ export function StoryChoices() {
302
  },
303
  }}
304
  />
305
- <Box sx={{ display: "flex", justifyContent: "flex-end" }}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  <Button
307
  onClick={() => {
308
  if (customChoice.trim()) {
@@ -317,7 +390,6 @@ export function StoryChoices() {
317
  disabled={!customChoice.trim()}
318
  variant="contained"
319
  sx={{
320
- mt: 1,
321
  py: 1.5,
322
  px: 4,
323
  fontWeight: "bold",
 
13
  useMediaQuery,
14
  useTheme,
15
  IconButton,
16
+ Tooltip,
17
  } from "@mui/material";
18
  import { useNavigate } from "react-router-dom";
19
  import { TalkWithSarah } from "./TalkWithSarah";
 
22
  import { storyApi } from "../utils/api";
23
  import { useSoundEffect } from "../hooks/useSoundEffect";
24
  import CloseIcon from "@mui/icons-material/Close";
25
+ import CasinoOutlinedIcon from "@mui/icons-material/CasinoOutlined";
26
 
27
  const { initAudioContext } = storyApi;
28
 
29
+ // Phrases aléatoires WTF pour le placeholder
30
+ const RANDOM_PLACEHOLDERS = [
31
+ "A dragon appears right above the hero...",
32
+ "Suddenly, all the trees start dancing the macarena...",
33
+ "A time-traveling pizza delivery guy shows up with a mysterious package...",
34
+ "The ground turns into jello and starts wobbling menacingly...",
35
+ "A choir of singing cats descends from the sky...",
36
+ "The hero's shadow detaches itself and starts doing stand-up comedy...",
37
+ "All the nearby rocks transform into vintage toasters...",
38
+ "A portal opens, and out steps the hero's evil twin made entirely of cheese...",
39
+ "The moon starts beatboxing an ominous rhythm...",
40
+ "Every nearby plant suddenly develops a British accent and starts having tea...",
41
+ ];
42
+
43
  // Function to convert text with ** to Chip elements
44
  const formatTextWithBold = (text) => {
45
  if (!text) return "";
 
70
  const [sarahRecommendation, setSarahRecommendation] = useState(null);
71
  const [showCustomDialog, setShowCustomDialog] = useState(false);
72
  const [customChoice, setCustomChoice] = useState("");
73
+ const [currentPlaceholder] = useState(
74
+ () =>
75
+ RANDOM_PLACEHOLDERS[
76
+ Math.floor(Math.random() * RANDOM_PLACEHOLDERS.length)
77
+ ]
78
+ );
79
  const {
80
  choices,
81
  onChoice,
 
98
  volume: 0.5,
99
  });
100
 
101
+ // Son de dé
102
+ const playDiceSound = useSoundEffect({
103
+ basePath: "/sounds/dice-",
104
+ numSounds: 3,
105
+ volume: 0.1,
106
+ enabled: true,
107
+ });
108
+
109
  const lastSegment = getLastSegment();
110
  const isLastStep = lastSegment?.is_last_step;
111
  const isDeath = lastSegment?.isDeath;
112
  const isVictory = lastSegment?.isVictory;
113
  const storyText = lastSegment?.rawText || "";
114
 
115
+ const getRandomPlaceholder = () => {
116
+ return RANDOM_PLACEHOLDERS[
117
+ Math.floor(Math.random() * RANDOM_PLACEHOLDERS.length)
118
+ ];
119
+ };
120
+
121
  if (isGameOver()) {
122
  return (
123
  <Box
 
229
  </Button>
230
  </Box>
231
 
232
+ <Typography
233
+ variant="h6"
234
+ sx={{
235
+ display: { xs: "none", sm: "block" },
236
+ color: "rgba(255,255,255,0.5)",
237
+ fontWeight: "bold",
238
+ fontSize: "1.2rem",
239
+ mx: 2,
240
+ }}
241
+ >
242
+ OR
243
+ </Typography>
244
+
245
  <Box
246
  sx={{
247
  display: "flex",
248
  flexDirection: "column",
249
  alignItems: "center",
250
  gap: 1,
251
+ // ml: isMobile ? 0 : 4,
252
  minWidth: "fit-content",
253
  maxWidth: "30%",
254
  }}
 
265
  textTransform: "none",
266
  }}
267
  >
268
+ Write your own choice...
269
  </Button>
270
  </Box>
271
  </>
 
328
  rows={isMobile ? 5 : 4}
329
  fullWidth
330
  variant="outlined"
331
+ placeholder={currentPlaceholder}
332
  value={customChoice}
333
  onChange={(e) => setCustomChoice(e.target.value)}
334
  sx={{
 
351
  },
352
  }}
353
  />
354
+ <Box
355
+ sx={{ display: "flex", justifyContent: "flex-end", gap: 1, mt: 1 }}
356
+ >
357
+ <Button
358
+ onClick={() => {
359
+ const randomChoice = getRandomPlaceholder();
360
+ setCustomChoice(randomChoice.slice(0, -3));
361
+ playDiceSound();
362
+ }}
363
+ variant="outlined"
364
+ sx={{
365
+ minWidth: "48px",
366
+ width: "48px",
367
+ height: "48px",
368
+ p: 0,
369
+ borderColor: "rgba(255, 255, 255, 0.23)",
370
+ color: "white",
371
+ "&:hover": {
372
+ borderColor: "white",
373
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
374
+ },
375
+ }}
376
+ >
377
+ <CasinoOutlinedIcon />
378
+ </Button>
379
  <Button
380
  onClick={() => {
381
  if (customChoice.trim()) {
 
390
  disabled={!customChoice.trim()}
391
  variant="contained"
392
  sx={{
 
393
  py: 1.5,
394
  px: 4,
395
  fontWeight: "bold",
client/src/components/UniverseSlotMachine.jsx CHANGED
@@ -1,10 +1,11 @@
1
  import React, { useEffect, useRef, useState } from "react";
2
  import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
3
  import { motion, useAnimation } from "framer-motion";
 
4
 
5
  // Animation timing configuration
6
  const SLOT_ANIMATION_DURATION = 2; // Duration of each slot animation
7
- const SLOT_SPEED = 0.5; // Base speed of the slot animation (higher = faster)
8
  const TOTAL_ANIMATION_DURATION = 1; // Total duration for each slot reel in seconds
9
  const SLOT_START_DELAY = 2; // Delay between each slot start in seconds
10
 
@@ -26,6 +27,9 @@ const SlotReel = ({ words, isActive, finalValue, onComplete, delay = 0 }) => {
26
  const [isVisible, setIsVisible] = useState(false);
27
  const theme = useTheme();
28
  const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
 
 
 
29
 
30
  useEffect(() => {
31
  if (isActive) {
@@ -38,6 +42,7 @@ const SlotReel = ({ words, isActive, finalValue, onComplete, delay = 0 }) => {
38
  setReelItems(repeatedWords);
39
 
40
  const itemHeight = isMobile ? 60 : 80;
 
41
  const totalHeight = repeatedWords.length * itemHeight;
42
 
43
  setTimeout(() => {
@@ -49,6 +54,28 @@ const SlotReel = ({ words, isActive, finalValue, onComplete, delay = 0 }) => {
49
  duration: TOTAL_ANIMATION_DURATION / SLOT_SPEED,
50
  ease: [0.25, 0.1, 0.25, 1.0],
51
  times: [0, 1],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  },
53
  })
54
  .then(() => {
@@ -56,7 +83,11 @@ const SlotReel = ({ words, isActive, finalValue, onComplete, delay = 0 }) => {
56
  });
57
  }, delay * SLOT_START_DELAY * 1000);
58
  }
59
- }, [isActive, finalValue, words, delay, isMobile]);
 
 
 
 
60
 
61
  return (
62
  <Box
 
1
  import React, { useEffect, useRef, useState } from "react";
2
  import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
3
  import { motion, useAnimation } from "framer-motion";
4
+ import { useSoundSystem } from "../contexts/SoundContext";
5
 
6
  // Animation timing configuration
7
  const SLOT_ANIMATION_DURATION = 2; // Duration of each slot animation
8
+ const SLOT_SPEED = 1; // Base speed of the slot animation (higher = faster)
9
  const TOTAL_ANIMATION_DURATION = 1; // Total duration for each slot reel in seconds
10
  const SLOT_START_DELAY = 2; // Delay between each slot start in seconds
11
 
 
27
  const [isVisible, setIsVisible] = useState(false);
28
  const theme = useTheme();
29
  const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
30
+ const { playSound } = useSoundSystem();
31
+ const lastPositionRef = useRef(0);
32
+ const itemHeightRef = useRef(0);
33
 
34
  useEffect(() => {
35
  if (isActive) {
 
42
  setReelItems(repeatedWords);
43
 
44
  const itemHeight = isMobile ? 60 : 80;
45
+ itemHeightRef.current = itemHeight;
46
  const totalHeight = repeatedWords.length * itemHeight;
47
 
48
  setTimeout(() => {
 
54
  duration: TOTAL_ANIMATION_DURATION / SLOT_SPEED,
55
  ease: [0.25, 0.1, 0.25, 1.0],
56
  times: [0, 1],
57
+ onUpdate: (latest) => {
58
+ // Calculer l'index du mot actuel basé sur la position
59
+ const currentPosition = Math.abs(latest);
60
+ const currentIndex = Math.floor(currentPosition / itemHeight);
61
+
62
+ // Si on a changé de mot, jouer le son
63
+ if (
64
+ Math.floor(lastPositionRef.current / itemHeight) !==
65
+ currentIndex
66
+ ) {
67
+ // Vérifier si c'est le dernier mot (final)
68
+ const isFinalWord = currentIndex === repeatedWords.length - 1;
69
+ // Jouer le son approprié
70
+ if (isFinalWord) {
71
+ playSound("lock");
72
+ } else {
73
+ playSound("tick", "normal");
74
+ }
75
+ }
76
+
77
+ lastPositionRef.current = currentPosition;
78
+ },
79
  },
80
  })
81
  .then(() => {
 
83
  });
84
  }, delay * SLOT_START_DELAY * 1000);
85
  }
86
+
87
+ return () => {
88
+ lastPositionRef.current = 0;
89
+ };
90
+ }, [isActive, finalValue, words, delay, isMobile, playSound]);
91
 
92
  return (
93
  <Box
client/src/contexts/SoundContext.jsx CHANGED
@@ -35,6 +35,17 @@ const SOUNDS = {
35
  off: "/sounds/talky-walky-off.mp3",
36
  volume: 0.5,
37
  },
 
 
 
 
 
 
 
 
 
 
 
38
  };
39
 
40
  const SoundContext = createContext(null);
@@ -69,7 +80,19 @@ export function SoundProvider({ children }) {
69
  // Initialiser tous les sons
70
  const soundInstances = {};
71
  Object.entries(SOUNDS).forEach(([category, config]) => {
72
- if (Array.isArray(config.files)) {
 
 
 
 
 
 
 
 
 
 
 
 
73
  // Pour les sons avec plusieurs variations
74
  soundInstances[category] = config.files.map((file) => {
75
  const [play] = useSound(file, { volume: config.volume });
@@ -102,7 +125,13 @@ export function SoundProvider({ children }) {
102
  if (!isSoundEnabled) return;
103
 
104
  try {
105
- if (subCategory) {
 
 
 
 
 
 
106
  // Pour les sons avec sous-catégories (comme talkySarah.on)
107
  soundInstances[category][subCategory]?.();
108
  } else if (Array.isArray(soundInstances[category])) {
 
35
  off: "/sounds/talky-walky-off.mp3",
36
  volume: 0.5,
37
  },
38
+ tick: {
39
+ files: Array.from({ length: 4 }, (_, i) => `/sounds/tick-${i + 1}.mp3`),
40
+ volume: {
41
+ normal: 0.05, // Volume normal à 50% du volume final
42
+ final: 0.4, // Volume final (comme avant)
43
+ },
44
+ },
45
+ lock: {
46
+ files: ["/sounds/lock-1.mp3"],
47
+ volume: 0.025,
48
+ },
49
  };
50
 
51
  const SoundContext = createContext(null);
 
80
  // Initialiser tous les sons
81
  const soundInstances = {};
82
  Object.entries(SOUNDS).forEach(([category, config]) => {
83
+ if (category === "tick") {
84
+ // Initialisation spéciale pour les ticks avec volumes différents
85
+ soundInstances[category] = {
86
+ normal: config.files.map((file) => {
87
+ const [play] = useSound(file, { volume: config.volume.normal });
88
+ return play;
89
+ }),
90
+ final: config.files.map((file) => {
91
+ const [play] = useSound(file, { volume: config.volume.final });
92
+ return play;
93
+ }),
94
+ };
95
+ } else if (Array.isArray(config.files)) {
96
  // Pour les sons avec plusieurs variations
97
  soundInstances[category] = config.files.map((file) => {
98
  const [play] = useSound(file, { volume: config.volume });
 
125
  if (!isSoundEnabled) return;
126
 
127
  try {
128
+ if (category === "tick") {
129
+ // Pour les ticks avec volumes différents
130
+ const type = subCategory || "normal";
131
+ const sounds = soundInstances[category][type];
132
+ const randomIndex = Math.floor(Math.random() * sounds.length);
133
+ sounds[randomIndex]?.();
134
+ } else if (subCategory) {
135
  // Pour les sons avec sous-catégories (comme talkySarah.on)
136
  soundInstances[category][subCategory]?.();
137
  } else if (Array.isArray(soundInstances[category])) {
client/src/pages/Tutorial.jsx CHANGED
@@ -93,17 +93,20 @@ export function Tutorial() {
93
  <Box
94
  sx={{
95
  display: "flex",
96
- gap: 4,
97
  justifyContent: "center",
98
- mb: 2,
99
  alignItems: "center",
 
 
100
  }}
101
  >
102
  <Box
103
  sx={{
104
  position: "relative",
105
- flex: 1,
106
- maxWidth: "200px",
 
107
  "&::before": {
108
  content: '""',
109
  position: "absolute",
@@ -113,32 +116,39 @@ export function Tutorial() {
113
  bottom: 0,
114
  background: "rgba(255, 255, 255, 0.05)",
115
  backdropFilter: "blur(10px)",
116
- WebkitBackdropFilter: "blur(10px)", // Pour Safari
117
  borderRadius: "8px",
118
- border: "1px solid rgba(255,255,255,0.3)",
 
119
  },
120
  }}
121
  >
122
  <Box
123
  sx={{
124
  position: "relative",
125
- p: 2,
126
  display: "flex",
127
  flexDirection: "column",
128
  alignItems: "center",
129
- gap: 1,
130
  zIndex: 1,
131
  }}
132
  >
133
  <CallSplitOutlinedIcon
134
  sx={{
135
- fontSize: 40,
136
- color: "primary.text",
137
- mb: 1,
138
- transform: "rotate(90deg)", // Pour que la bifurcation soit horizontale
139
  }}
140
  />
141
- <Typography variant="subtitle1" sx={{ color: "primary.text" }}>
 
 
 
 
 
 
142
  Make a choice
143
  </Typography>
144
  </Box>
@@ -149,6 +159,8 @@ export function Tutorial() {
149
  sx={{
150
  color: "rgba(255,255,255,0.5)",
151
  fontWeight: "bold",
 
 
152
  }}
153
  >
154
  OR
@@ -157,8 +169,9 @@ export function Tutorial() {
157
  <Box
158
  sx={{
159
  position: "relative",
160
- flex: 1,
161
- maxWidth: "200px",
 
162
  "&::before": {
163
  content: '""',
164
  position: "absolute",
@@ -168,27 +181,38 @@ export function Tutorial() {
168
  bottom: 0,
169
  background: "rgba(255, 255, 255, 0.05)",
170
  backdropFilter: "blur(10px)",
171
- WebkitBackdropFilter: "blur(10px)", // Pour Safari
172
  borderRadius: "8px",
173
- border: "1px solid rgba(255,255,255,0.3)",
 
174
  },
175
  }}
176
  >
177
  <Box
178
  sx={{
179
  position: "relative",
180
- p: 2,
181
  display: "flex",
182
  flexDirection: "column",
183
  alignItems: "center",
184
- gap: 1,
185
  zIndex: 1,
186
  }}
187
  >
188
  <CreateOutlinedIcon
189
- sx={{ fontSize: 40, color: "primary.text", mb: 1 }}
 
 
 
 
190
  />
191
- <Typography variant="subtitle1" sx={{ color: "primary.text" }}>
 
 
 
 
 
 
192
  Write your own
193
  </Typography>
194
  </Box>
 
93
  <Box
94
  sx={{
95
  display: "flex",
96
+ gap: { xs: 1, sm: 4 },
97
  justifyContent: "center",
98
+ mb: { xs: 1, sm: 2 },
99
  alignItems: "center",
100
+ flexDirection: { xs: "column", sm: "row" },
101
+ width: "100%",
102
  }}
103
  >
104
  <Box
105
  sx={{
106
  position: "relative",
107
+ flex: { xs: "none", sm: 1 },
108
+ width: { xs: "50%", sm: "auto" },
109
+ maxWidth: { xs: "160px", sm: "200px" },
110
  "&::before": {
111
  content: '""',
112
  position: "absolute",
 
116
  bottom: 0,
117
  background: "rgba(255, 255, 255, 0.05)",
118
  backdropFilter: "blur(10px)",
119
+ WebkitBackdropFilter: "blur(10px)",
120
  borderRadius: "8px",
121
+ border: "1px solid",
122
+ borderColor: "primary.main",
123
  },
124
  }}
125
  >
126
  <Box
127
  sx={{
128
  position: "relative",
129
+ p: { xs: 1.5, sm: 2 },
130
  display: "flex",
131
  flexDirection: "column",
132
  alignItems: "center",
133
+ gap: 0.5,
134
  zIndex: 1,
135
  }}
136
  >
137
  <CallSplitOutlinedIcon
138
  sx={{
139
+ fontSize: { xs: 28, sm: 40 },
140
+ color: "primary.main",
141
+ mb: 0.5,
142
+ transform: "rotate(90deg)",
143
  }}
144
  />
145
+ <Typography
146
+ variant="subtitle1"
147
+ sx={{
148
+ color: "primary.main",
149
+ fontSize: { xs: "0.875rem", sm: "1rem" },
150
+ }}
151
+ >
152
  Make a choice
153
  </Typography>
154
  </Box>
 
159
  sx={{
160
  color: "rgba(255,255,255,0.5)",
161
  fontWeight: "bold",
162
+ fontSize: { xs: "1rem", sm: "1.2rem" },
163
+ my: { xs: 0.25, sm: 0 },
164
  }}
165
  >
166
  OR
 
169
  <Box
170
  sx={{
171
  position: "relative",
172
+ flex: { xs: "none", sm: 1 },
173
+ width: { xs: "50%", sm: "auto" },
174
+ maxWidth: { xs: "160px", sm: "200px" },
175
  "&::before": {
176
  content: '""',
177
  position: "absolute",
 
181
  bottom: 0,
182
  background: "rgba(255, 255, 255, 0.05)",
183
  backdropFilter: "blur(10px)",
184
+ WebkitBackdropFilter: "blur(10px)",
185
  borderRadius: "8px",
186
+ border: "1px solid",
187
+ borderColor: "secondary.main",
188
  },
189
  }}
190
  >
191
  <Box
192
  sx={{
193
  position: "relative",
194
+ p: { xs: 1.5, sm: 2 },
195
  display: "flex",
196
  flexDirection: "column",
197
  alignItems: "center",
198
+ gap: 0.5,
199
  zIndex: 1,
200
  }}
201
  >
202
  <CreateOutlinedIcon
203
+ sx={{
204
+ fontSize: { xs: 28, sm: 40 },
205
+ color: "secondary.main",
206
+ mb: 0.5,
207
+ }}
208
  />
209
+ <Typography
210
+ variant="subtitle1"
211
+ sx={{
212
+ color: "secondary.main",
213
+ fontSize: { xs: "0.875rem", sm: "1rem" },
214
+ }}
215
+ >
216
  Write your own
217
  </Typography>
218
  </Box>