@@ -138,7 +138,7 @@ function useSlotMachine(rolling: boolean, direction: 'up' | 'down' = 'up'): numb
138138
139139const SLOT_STYLES = {
140140 jackpot : {
141- active : 'border-yellow -500/70 bg-yellow -500/10 text-yellow -500 dark:text-yellow -400' ,
141+ active : 'border-green -500/70 bg-green -500/10 text-green -500 dark:text-green -400' ,
142142 idle : 'border-border text-muted-foreground' ,
143143 } ,
144144 demon : {
@@ -172,32 +172,151 @@ function SlotMachineDisplay({
172172 ) ;
173173}
174174
175- // Animation d'un jeton qui monte et disparaît (+1 au pot)
176- function CoinAnimation ( ) {
177- const [ stage , setStage ] = useState ( 0 ) ;
175+ const CONFETTI_COLORS = [ '#f59e0b' , '#ef4444' , '#3b82f6' , '#10b981' , '#8b5cf6' , '#f97316' , '#ec4899' , '#14b8a6' ] ;
176+
177+ function TrophyConfetti ( ) {
178+ return (
179+ < span className = "relative inline-block" >
180+ 🏆
181+ { CONFETTI_COLORS . map ( ( color , i ) => (
182+ < span
183+ key = { i }
184+ className = "anim-confetti-particle"
185+ style = { { backgroundColor : color , '--a' : `${ i * 45 } deg` , animationDelay : `${ ( i * 0.14 ) % 1.1 } s` } as React . CSSProperties }
186+ />
187+ ) ) }
188+ </ span >
189+ ) ;
190+ }
191+
192+ // ── Jackpot badge coordinator ────────────────────────────────────────────────
193+ // Assigns one of 3 durations (2s/4s/5s) to each badge by registration order.
194+ // When all badges finish, reshuffles and restarts everyone simultaneously.
195+ const JACKPOT_TIERS = [ 1200 , 2500 , 4000 ] ;
196+
197+ function jShuffle ( arr : number [ ] ) : number [ ] {
198+ const a = [ ...arr ] ;
199+ for ( let i = a . length - 1 ; i > 0 ; i -- ) {
200+ const j = Math . floor ( Math . random ( ) * ( i + 1 ) ) ;
201+ [ a [ i ] , a [ j ] ] = [ a [ j ] , a [ i ] ] ;
202+ }
203+ return a ;
204+ }
205+
206+ const jackpotCoord = ( ( ) => {
207+ let assignments = jShuffle ( JACKPOT_TIERS ) ;
208+ let slots : Array < ( d : number ) => void > = [ ] ;
209+ let done = 0 ;
210+ return {
211+ register ( cb : ( d : number ) => void ) : number {
212+ const idx = slots . length ;
213+ slots . push ( cb ) ;
214+ return assignments [ idx ] ?? JACKPOT_TIERS [ 0 ] ;
215+ } ,
216+ unregister ( cb : ( d : number ) => void ) {
217+ slots = slots . filter ( s => s !== cb ) ;
218+ if ( slots . length === 0 ) { assignments = jShuffle ( JACKPOT_TIERS ) ; done = 0 ; }
219+ } ,
220+ reportDone ( ) {
221+ done ++ ;
222+ if ( done >= slots . length && slots . length > 0 ) {
223+ setTimeout ( ( ) => {
224+ done = 0 ;
225+ assignments = jShuffle ( JACKPOT_TIERS ) ;
226+ slots . forEach ( ( cb , i ) => cb ( assignments [ i ] ) ) ;
227+ } , 1000 ) ;
228+ }
229+ } ,
230+ } ;
231+ } ) ( ) ;
232+
233+ function JackpotBadge ( ) {
234+ const [ reels , setReels ] = useState ( [ 7 , 7 , 7 ] ) ;
235+ const [ locked , setLocked ] = useState ( [ false , false , false ] ) ;
236+ const r = useRef < { timers : ReturnType < typeof setTimeout > [ ] ; duration : number ; spin : ( ) => void } > ( {
237+ timers : [ ] , duration : 2000 , spin : ( ) => { } ,
238+ } ) ;
239+
178240 useEffect ( ( ) => {
179- const t1 = setTimeout ( ( ) => setStage ( 1 ) , 80 ) ;
180- const t2 = setTimeout ( ( ) => setStage ( 2 ) , 680 ) ;
181- return ( ) => {
182- clearTimeout ( t1 ) ;
183- clearTimeout ( t2 ) ;
241+ function clearAll ( ) {
242+ r . current . timers . forEach ( t => { clearTimeout ( t ) ; clearInterval ( t as unknown as ReturnType < typeof setInterval > ) ; } ) ;
243+ r . current . timers = [ ] ;
244+ }
245+
246+ r . current . spin = function spin ( ) {
247+ clearAll ( ) ;
248+ setLocked ( [ false , false , false ] ) ;
249+
250+ const base = r . current . duration ;
251+ const ivals : ReturnType < typeof setInterval > [ ] = [ ] ;
252+
253+ [ 0 , 1 , 2 ] . forEach ( i => {
254+ const id = setInterval ( ( ) => {
255+ setReels ( prev => { const n = [ ...prev ] ; n [ i ] = Math . floor ( Math . random ( ) * 9 ) + 1 ; return n ; } ) ;
256+ } , 80 + i * 15 ) ;
257+ r . current . timers . push ( id as unknown as ReturnType < typeof setTimeout > ) ;
258+ ivals . push ( id ) ;
259+ } ) ;
260+
261+ [ base , base + 400 , base + 800 ] . forEach ( ( delay , i ) => {
262+ const t = setTimeout ( ( ) => {
263+ clearInterval ( ivals [ i ] ) ;
264+ setReels ( prev => { const n = [ ...prev ] ; n [ i ] = 7 ; return n ; } ) ;
265+ setLocked ( prev => { const n = [ ...prev ] ; n [ i ] = true ; return n ; } ) ;
266+ // Last reel done — signal coordinator; wait for all badges before restart
267+ if ( i === 2 ) jackpotCoord . reportDone ( ) ;
268+ } , delay ) ;
269+ r . current . timers . push ( t ) ;
270+ } ) ;
184271 } ;
272+
273+ const cb = ( d : number ) => { r . current . duration = d ; r . current . spin ( ) ; } ;
274+ r . current . duration = jackpotCoord . register ( cb ) ;
275+ r . current . spin ( ) ;
276+
277+ return ( ) => { jackpotCoord . unregister ( cb ) ; clearAll ( ) ; } ;
185278 } , [ ] ) ;
279+
280+ const allLocked = locked . every ( Boolean ) ;
281+ const [ rainKey , setRainKey ] = useState ( 0 ) ;
282+ const wasLocked = useRef ( false ) ;
283+ if ( allLocked && ! wasLocked . current ) { wasLocked . current = true ; setRainKey ( k => k + 1 ) ; }
284+ if ( ! allLocked ) wasLocked . current = false ;
285+
186286 return (
187- < span
188- className = "absolute right-3 top-1 text-lg pointer-events-none select-none z-20"
189- style = { {
190- transform :
191- stage === 2
192- ? 'translateY(-22px) scale(0.4)'
193- : stage === 1
194- ? 'translateY(-10px)'
195- : 'translateY(0)' ,
196- opacity : stage === 2 ? 0 : 1 ,
197- transition : 'transform 0.55s ease-out, opacity 0.28s ease-in 0.42s' ,
198- } }
199- >
200- 🪙
287+ < span className = "relative inline-flex items-center gap-px bg-green-500/15 border border-green-500/40 rounded px-1 py-px font-black tabular-nums leading-none text-[11px]" >
288+ { reels . map ( ( n , i ) => (
289+ < span key = { i } className = { `w-[9px] text-center transition-colors duration-150 ${ locked [ i ] ? 'text-yellow-400' : 'text-green-400' } ` } > { n } </ span >
290+ ) ) }
291+ { allLocked && [ 0 , 1 , 2 ] . map ( i => (
292+ < span
293+ key = { `${ rainKey } -${ i } ` }
294+ className = "anim-coin-rain"
295+ style = { { right : `${ i * 10 } px` , animationDelay : `${ i * 0.12 } s` , zIndex : 50 } }
296+ > 🪙</ span >
297+ ) ) }
298+ </ span >
299+ ) ;
300+ }
301+
302+ function DoubleBadge ( { value } : { value : number } ) {
303+ const [ display , setDisplay ] = useState ( 1 ) ;
304+
305+ useEffect ( ( ) => {
306+ const timers : ReturnType < typeof setTimeout > [ ] = [ ] ;
307+ let cumulative = 0 ;
308+ for ( let target = 2 ; target <= value ; target ++ ) {
309+ // x1=1.2s, x2=1s, x3=0.8s, x4=0.6s, x5+=0.3s min
310+ cumulative += Math . max ( 300 , 1200 - ( target - 2 ) * 200 ) ;
311+ const t = setTimeout ( ( ) => setDisplay ( target ) , cumulative ) ;
312+ timers . push ( t ) ;
313+ }
314+ return ( ) => timers . forEach ( clearTimeout ) ;
315+ } , [ value ] ) ;
316+
317+ return (
318+ < span className = "inline-flex items-center bg-blue-500/15 border border-blue-500/40 rounded px-1 py-px font-black tabular-nums leading-none text-[11px] text-blue-400" >
319+ < span key = { display } className = "anim-badge-slam" > x{ display } </ span >
201320 </ span >
202321 ) ;
203322}
@@ -1162,7 +1281,7 @@ export default function GameScreen({ players: initialPlayers, rules, onEnd }: Ga
11621281 const demonVisible =
11631282 allRolled && rules . demon && rolledDice . filter ( r => r . score === 6 ) . length === 3 ;
11641283 const legendeVisible =
1165- allRolled && rules . legende && rolledDice . some ( r => r . score === 11 ) ;
1284+ rules . legende && rolledDice . some ( r => r . score === 11 ) ;
11661285
11671286 return players . map ( ( player , index ) => {
11681287 const roll = rolledDice . find ( r => r . playerId === player . id ) ;
@@ -1185,6 +1304,8 @@ export default function GameScreen({ players: initialPlayers, rules, onEnd }: Ga
11851304 ! isDemonPlayer &&
11861305 legendeVisible &&
11871306 ( roll . dice1 >= 5 || roll . dice2 >= 5 ) ;
1307+ const isLegendeTrigger =
1308+ revealed && ! isJackpotPlayer && ! isDemonPlayer && legendeVisible && roll . score === 11 ;
11881309 const isJeton =
11891310 revealed &&
11901311 ! isJackpotPlayer &&
@@ -1201,20 +1322,22 @@ export default function GameScreen({ players: initialPlayers, rules, onEnd }: Ga
12011322 const isRelanced = relancedPlayerIds . has ( player . id ) ;
12021323
12031324 const containerClass = [
1204- 'relative overflow-hidden flex items-center justify-between rounded-lg border px-3 py-2 transition-colors ' ,
1325+ 'relative overflow-hidden flex items-center justify-between rounded-lg border px-3 py-2' ,
12051326 isRelanceRow
12061327 ? 'border-yellow-500/40 bg-yellow-500/10'
12071328 : isJackpotPlayer
1208- ? 'bg-emerald-900/25 border-emerald-500/50 '
1329+ ? 'anim-jackpot '
12091330 : isDemonPlayer
1210- ? 'bg-red-950/40 border-red-700/60 '
1331+ ? 'anim-demon '
12111332 : isMarchand
1212- ? 'bg-amber-800/15 border-amber-500/40 '
1333+ ? 'anim-marchand '
12131334 : isDouble
1214- ? 'bg-blue-500/10 border-blue-500/40'
1215- : isCurrent && ! roll
1216- ? 'border-primary/40 bg-primary/5'
1217- : 'border-border' ,
1335+ ? 'anim-double'
1336+ : isLegendeTrigger
1337+ ? 'bg-purple-900/30 border-purple-400/60'
1338+ : isCurrent && ! roll
1339+ ? 'border-primary/40 bg-primary/5'
1340+ : 'border-border' ,
12181341 ] . join ( ' ' ) ;
12191342
12201343 return (
@@ -1223,10 +1346,10 @@ export default function GameScreen({ players: initialPlayers, rules, onEnd }: Ga
12231346 { isJackpotPlayer && (
12241347 < >
12251348 < span className = "absolute right-1 top-0 text-xl opacity-[0.12] select-none pointer-events-none leading-tight" >
1226- ♠♥
1349+ 🍀
12271350 </ span >
12281351 < span className = "absolute right-7 bottom-0 text-lg opacity-[0.10] select-none pointer-events-none leading-tight" >
1229- ♦♣
1352+ 🍀
12301353 </ span >
12311354 </ >
12321355 ) }
@@ -1235,40 +1358,30 @@ export default function GameScreen({ players: initialPlayers, rules, onEnd }: Ga
12351358 🔥
12361359 </ span >
12371360 ) }
1238- { isMarchand && (
1239- < span className = "absolute right-1 top-0 text-xl opacity-[0.18] select-none pointer-events-none leading-tight" >
1240- ☀️🏜️
1241- </ span >
1242- ) }
1243- { isLegende && (
1244- < span className = "absolute right-1 top-0 text-xl opacity-[0.18] select-none pointer-events-none leading-tight" >
1245- ⚔️🛡️
1246- </ span >
1247- ) }
1361+
12481362 { isDouble && (
12491363 < span className = "absolute right-1 top-0 text-5xl font-black opacity-[0.18] select-none pointer-events-none leading-tight text-blue-300" >
12501364 { roll . dice1 }
12511365 </ span >
12521366 ) }
1253- { isJeton && < CoinAnimation /> }
1254-
12551367 { /* Gauche : nom */ }
12561368 < div className = "flex items-center gap-2 min-w-0 relative z-10" >
12571369 { isCurrent && ! roll && ! rolling && (
12581370 < span className = "text-xs text-primary shrink-0" > ▶</ span >
12591371 ) }
1260- { isMarchand && < span > 🌙</ span > }
12611372 < span
12621373 className = { `text-sm font-medium truncate ${
12631374 ! roll && ! isCurrent ? 'text-muted-foreground' : ''
12641375 } `}
12651376 >
12661377 { player . name }
12671378 </ span >
1268- { isJackpotPlayer && < span > 🎰</ span > }
1269- { isDemonPlayer && < span > 😈</ span > }
1270- { isLegende && < span > ⭐</ span > }
1271- { isJeton && < span > 🪙</ span > }
1379+ { isMarchand && < span > 🌙</ span > }
1380+ { isJackpotPlayer && < JackpotBadge /> }
1381+ { isDemonPlayer && < span className = "anim-demon-laugh" > 😈</ span > }
1382+ { isDouble && < DoubleBadge value = { roll . dice1 } /> }
1383+ { isLegende && < span className = "anim-badge-pop" > ⭐</ span > }
1384+ { isJeton && < span className = "anim-badge-pop" > 🪙</ span > }
12721385 { isRelanced && (
12731386 < Badge variant = "outline" className = "text-xs shrink-0" >
12741387 Relancé
@@ -1790,10 +1903,10 @@ export default function GameScreen({ players: initialPlayers, rules, onEnd }: Ga
17901903 { isJackpotPlayer && (
17911904 < >
17921905 < span className = "absolute right-1 top-0 text-xl opacity-[0.12] select-none pointer-events-none leading-tight" >
1793- ♠♥
1906+ 🍀
17941907 </ span >
17951908 < span className = "absolute right-7 bottom-0 text-lg opacity-[0.10] select-none pointer-events-none leading-tight" >
1796- ♦♣
1909+ 🍀
17971910 </ span >
17981911 </ >
17991912 ) }
@@ -1802,31 +1915,21 @@ export default function GameScreen({ players: initialPlayers, rules, onEnd }: Ga
18021915 🔥
18031916 </ span >
18041917 ) }
1805- { isMarchand && (
1806- < span className = "absolute right-1 top-0 text-xl opacity-[0.18] select-none pointer-events-none leading-tight" >
1807- ☀️🏜️
1808- </ span >
1809- ) }
1810- { isLegende && (
1811- < span className = "absolute right-1 top-0 text-xl opacity-[0.18] select-none pointer-events-none leading-tight" >
1812- ⚔️🛡️
1813- </ span >
1814- ) }
1918+
18151919 { isDouble && (
18161920 < span className = "absolute right-1 top-0 text-5xl font-black opacity-[0.18] select-none pointer-events-none leading-tight text-blue-300" >
18171921 { s . dice1 }
18181922 </ span >
18191923 ) }
1820- { isJeton && < CoinAnimation /> }
1821-
18221924 { /* ── Contenu principal ── */ }
18231925 < div className = "flex items-center gap-2 flex-wrap relative z-10" >
1824- { isLucky && < span > 🏆</ span > }
1825- { isLooser && ! isLucky && < span > 💀</ span > }
1826- { isMarchand && < span > 🌙</ span > }
1926+ { isLucky && < TrophyConfetti /> }
1927+ { isLooser && ! isLucky && < span className = "anim-skull-decompose" > 💀</ span > }
18271928 < span className = "text-sm font-medium" > { s . player . name } </ span >
1828- { isJackpotPlayer && < span > 🎰</ span > }
1829- { isDemonPlayer && < span > 😈</ span > }
1929+ { isMarchand && < span > 🌙</ span > }
1930+ { isJackpotPlayer && < JackpotBadge /> }
1931+ { isDemonPlayer && < span className = "anim-demon-laugh" > 😈</ span > }
1932+ { isDouble && < DoubleBadge value = { s . dice1 } /> }
18301933 { isLegende && < span > ⭐</ span > }
18311934 { isJeton && < span > 🪙</ span > }
18321935 { isRelanced && (
0 commit comments