Skip to content

Commit eccc216

Browse files
committed
fix(anim): fixes #38 - animations et gameplay
1 parent 8717273 commit eccc216

3 files changed

Lines changed: 295 additions & 71 deletions

File tree

lucky7-app/frontend/src/app/components/GameScreen.tsx

Lines changed: 173 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ function useSlotMachine(rolling: boolean, direction: 'up' | 'down' = 'up'): numb
138138

139139
const 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

Comments
 (0)