From 999fa4ea90f46fc955725c8509ad95ed6e53f939 Mon Sep 17 00:00:00 2001 From: Christophe Date: Sun, 12 Apr 2026 17:06:33 +0200 Subject: [PATCH] =?UTF-8?q?R=C3=A9organiser=20le=20projet=20et=20fiabilise?= =?UTF-8?q?r=20le=20chargement=20HTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + ChessClock.html | 580 +--- README.md | 29 + index.html | 1835 +++++++++++++ legacy/ChessClock.legacy.html | 2345 +++++++++++++++++ package.json | 19 + src/App.jsx | 5 + src/features/chess-clock/ChessClockApp.jsx | 1785 +++++++++++++ .../chess-clock/ChessClockStandalone.jsx | 1793 +++++++++++++ src/features/chess-clock/chessClock.css | 536 ++++ src/main.jsx | 10 + vite.config.js | 6 + 12 files changed, 8402 insertions(+), 545 deletions(-) create mode 100755 .gitignore mode change 100644 => 100755 ChessClock.html create mode 100755 README.md create mode 100755 index.html create mode 100755 legacy/ChessClock.legacy.html create mode 100755 package.json create mode 100755 src/App.jsx create mode 100755 src/features/chess-clock/ChessClockApp.jsx create mode 100755 src/features/chess-clock/ChessClockStandalone.jsx create mode 100755 src/features/chess-clock/chessClock.css create mode 100755 src/main.jsx create mode 100755 vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..a80fcf2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.DS_Store +.codex diff --git a/ChessClock.html b/ChessClock.html old mode 100644 new mode 100755 index 70a0d75..7f17039 --- a/ChessClock.html +++ b/ChessClock.html @@ -3,557 +3,42 @@ + Chess Clock + + +
Chargement de Chess Clock...
diff --git a/README.md b/README.md new file mode 100755 index 0000000..bf75099 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Chess Clock + +Projet React/Vite reconstruit a partir d'un ancien fichier HTML monolithique. + +## Scripts + +- `npm install` +- `npm run dev` +- `npm run build` +- `npm run preview` + +## Ouverture + +- Double-clic sur `index.html` : mode autonome direct dans le navigateur via CDN React/Babel +- Serveur Vite : le meme `index.html` bascule automatiquement sur l'entree `src/main.jsx` + +## Structure + +- `index.html` : shell HTML minimal +- `src/main.jsx` : point d'entree React +- `src/App.jsx` : wrapper d'application +- `src/features/chess-clock/ChessClockApp.jsx` : logique de l'horloge et ecrans associes +- `src/features/chess-clock/ChessClockStandalone.jsx` : entree autonome pour ouverture directe du fichier HTML +- `src/features/chess-clock/chessClock.css` : styles extraits du HTML initial +- `legacy/ChessClock.legacy.html` : archive du fichier HTML d'origine + +## Notes + +Le nettoyage a separe l'enveloppe HTML, les styles et la logique React sans changer volontairement le comportement metier. Le composant principal reste encore volumineux, mais il est maintenant place dans une structure de projet standard et pret a etre decoupe plus finement si besoin. diff --git a/index.html b/index.html new file mode 100755 index 0000000..7f17039 --- /dev/null +++ b/index.html @@ -0,0 +1,1835 @@ + + + + + + + Chess Clock + + + + + + + +
Chargement de Chess Clock...
+
+ + + diff --git a/legacy/ChessClock.legacy.html b/legacy/ChessClock.legacy.html new file mode 100755 index 0000000..70a0d75 --- /dev/null +++ b/legacy/ChessClock.legacy.html @@ -0,0 +1,2345 @@ + + + + + + Chess Clock + + + + + +
+ + + diff --git a/package.json b/package.json new file mode 100755 index 0000000..c8a06fc --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "chess-clock", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "19.1.1", + "react-dom": "19.1.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "5.0.2", + "vite": "7.1.5" + } +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100755 index 0000000..341bb5e --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,5 @@ +import ChessClockApp from "./features/chess-clock/ChessClockApp.jsx"; + +export default function App() { + return ; +} diff --git a/src/features/chess-clock/ChessClockApp.jsx b/src/features/chess-clock/ChessClockApp.jsx new file mode 100755 index 0000000..1d460a7 --- /dev/null +++ b/src/features/chess-clock/ChessClockApp.jsx @@ -0,0 +1,1785 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +import "./chessClock.css"; + +/* ══ HELPERS ══ */ +const fmt = s => Math.floor(Math.abs(s)/60).toString().padStart(2,"0")+":"+Math.floor(Math.abs(s)%60).toString().padStart(2,"0"); +const fmtMs = ms => Math.floor(ms/1000)+"."+(Math.floor((ms%1000)/10)).toString().padStart(2,"0"); +const fmtDate = ts => { const d=new Date(ts); return d.getDate()+"/"+(d.getMonth()+1)+"/"+d.getFullYear()+" "+d.getHours().toString().padStart(2,"0")+"h"+d.getMinutes().toString().padStart(2,"0"); }; +const buildPreview = (m,s,i) => { let t=m>0?m+" min":""; if(m>0&&s>0)t+=" "; if(s>0)t+=s+" sec"; if(!t)t="0 sec"; if(i>0)t+=" + "+i+"s"; return t; }; + +/* ══ SCROLL GUARD ══ + Returns pointer handlers that cancel the action if the finger moves > THRESHOLD px. + Usage: const {onPD, onPU, onPC, onPM} = useScrollGuard(onDown, onUp, player) +*/ +const SCROLL_THRESHOLD = 10; +function useScrollGuard(onDown, onUp, arg){ + const startY = useRef(null); + const startX = useRef(null); + const cancelled = useRef(false); + + const onPD = useCallback((e) => { + e.stopPropagation(); + startY.current = e.clientY; + startX.current = e.clientX; + cancelled.current = false; + if(arg !== undefined) onDown(arg); + else onDown(); + }, [onDown, arg]); + + const onPM = useCallback((e) => { + if(cancelled.current) return; + if(startY.current === null) return; + const dy = Math.abs(e.clientY - startY.current); + const dx = Math.abs(e.clientX - startX.current); + if(dy > SCROLL_THRESHOLD || dx > SCROLL_THRESHOLD){ + cancelled.current = true; + if(arg !== undefined) onUp(arg); + else onUp(); + } + }, [onUp, arg]); + + const onPU = useCallback((e) => { + e.stopPropagation(); + startY.current = null; + if(cancelled.current){ cancelled.current = false; return; } + if(arg !== undefined) onUp(arg); + else onUp(); + }, [onUp, arg]); + + const onPC = useCallback((e) => { + e.stopPropagation(); + startY.current = null; + cancelled.current = false; + if(arg !== undefined) onUp(arg); + else onUp(); + }, [onUp, arg]); + + return { onPD, onPM, onPU, onPC }; +} + +/* ══ HAPTIC ══ */ +const haptic = pat => { + try { + if(navigator.vibrate){ + navigator.vibrate(typeof pat==="number"?Math.max(50,pat):pat); + } + } catch(e){} +}; + +/* ══ AUDIO ══ */ +let _ac=null; +const getAC=()=>{ if(!_ac) _ac=new (window.AudioContext||window.webkitAudioContext)(); return _ac; }; +const beep=(freq,dur,type,vol,delay)=>{ try{ const c=getAC(),o=c.createOscillator(),g=c.createGain(); o.connect(g);g.connect(c.destination); o.type=type||"sine"; o.frequency.value=freq; const t=c.currentTime+(delay||0); g.gain.setValueAtTime(vol||0.14,t); g.gain.exponentialRampToValueAtTime(0.001,t+dur); o.start(t); o.stop(t+dur+0.05); }catch(e){} }; +const playTick=()=>beep(900,0.04,"square",0.12); +const playTock=()=>beep(600,0.04,"square",0.10); +const playSwoop=()=>{ try{ const c=getAC(),o=c.createOscillator(),g=c.createGain(); o.connect(g);g.connect(c.destination); o.frequency.setValueAtTime(520,c.currentTime); o.frequency.exponentialRampToValueAtTime(180,c.currentTime+0.2); g.gain.setValueAtTime(0.15,c.currentTime); g.gain.exponentialRampToValueAtTime(0.001,c.currentTime+0.2); o.start(); o.stop(c.currentTime+0.22); }catch(e){} }; +const playFanfare=()=>{ [523,659,784,1047].forEach((f,i)=>beep(f,0.22,"sine",0.16,i*0.11)); }; +const playWarn=()=>{ beep(220,0.07,"sawtooth",0.12,0); beep(220,0.07,"sawtooth",0.12,0.14); }; + +/* ══ ELO ══ */ +const calcElo=(rA,rB,won)=>{ const K=32,ea=1/(1+Math.pow(10,(rB-rA)/400)); return Math.round(K*(won-ea)); }; +const tryLoad=(key,def)=>{ try{ return JSON.parse(localStorage.getItem(key)||"null")||def; }catch(e){ return def; } }; +const trySave=(key,val)=>{ try{ localStorage.setItem(key,JSON.stringify(val)); }catch(e){} }; + +/* ══ SCRAMBLE ══ */ +const SFACES=["U","D","F","B","L","R"], SMODS=["","'","2"]; +const genScramble=(n)=>{ const m=[]; let last=-1; for(let i=0;i<(n||20);i++){ let f; do{f=Math.floor(Math.random()*6);}while(f===last); last=f; m.push(SFACES[f]+SMODS[Math.floor(Math.random()*3)]); } return m; }; + +/* ══ CONSTANTS ══ */ +const PRESETS = [ + {base:60, inc:0, tag:"BULLET", lbl:"1 min"}, + {base:60, inc:1, tag:"BULLET", lbl:"1 min + 1s"}, + {base:120, inc:1, tag:"BULLET", lbl:"2 min + 1s"}, + {base:180, inc:0, tag:"BLITZ", lbl:"3 min"}, + {base:180, inc:2, tag:"BLITZ", lbl:"3 min + 2s"}, + {base:300, inc:0, tag:"BLITZ", lbl:"5 min"}, + {base:300, inc:3, tag:"BLITZ", lbl:"5 min + 3s"}, + {base:300, inc:5, tag:"BLITZ", lbl:"5 min + 5s"}, + {base:600, inc:0, tag:"RAPID", lbl:"10 min"}, + {base:600, inc:5, tag:"RAPID", lbl:"10 min + 5s"}, + {base:900, inc:10,tag:"RAPID", lbl:"15 min + 10s"}, + {base:1200,inc:5, tag:"RAPID", lbl:"20 min + 5s"}, + {base:1800,inc:0, tag:"CLASSIC",lbl:"30 min"}, + {base:1800,inc:20,tag:"CLASSIC",lbl:"30 min + 20s"}, + {base:3600,inc:30,tag:"CLASSIC",lbl:"60 min + 30s"}, +]; +const TAG_COLORS = {BULLET:"#f04040",BLITZ:"#e8b84b",RAPID:"#38e878",CLASSIC:"#4b9ee8"}; +const P1C = ["p1c0","p1c1","p1c2","p1c3","p1c4","p1c5","p1c6","p1c7","p1c8"]; +const P2C = ["p2c0","p2c1","p2c2","p2c3","p2c4","p2c5","p2c6","p2c7","p2c8"]; +const AVATARS=["♟","♞","♜","♛","♔","🦁","🐯","⚡","🔥","💎","👑","🎯","🦊","🐉","🌙","⚔"]; +const genId=()=>"u"+Math.random().toString(36).slice(2,8); +const DEF_USERS=[ + {id:"u_default0",name:"Joueur 1",avatar:"♟",elo:1200,streak:0,wins:0,games:0}, + {id:"u_default1",name:"Joueur 2",avatar:"♞",elo:1200,streak:0,wins:0,games:0}, +]; + +/* ══ RUBIK'S CUBE ══ */ +function RubiksCube({player,size="md"}){ + const colors=player===0?P1C:P2C; + return
{colors.map((c,i)=>
)}
; +} + +/* ══ CUSTOM TIME MODAL ══ */ +function CustomModal({initMin,initSec,initInc,onConfirm,onCancel}){ + const [mins,setMins]=useState(initMin); + const [secs,setSecs]=useState(initSec); + const [inc,setInc]=useState(initInc); + const sp=(set,d,mn,mx)=>set(v=>Math.max(mn,Math.min(mx,v+d))); + const total=mins*60+secs; + return( +
+
e.stopPropagation()}> +
+
Temps Personnalisé
+
Définissez votre contrôle de temps
+
+ {[{lbl:"Minutes",val:mins,set:setMins,d:1,mn:0,mx:180,u:"min"}, + {lbl:"Secondes",val:secs,set:setSecs,d:5,mn:0,mx:55,u:"sec (x5)"}, + {lbl:"Incrément",val:inc,set:setInc,d:1,mn:0,mx:60,u:"+sec/coup"} + ].map(f=>( +
+
{f.lbl}
+
+
sp(f.set,-f.d,f.mn,f.mx)}>−
+
{f.val}
+
sp(f.set,+f.d,f.mn,f.mx)}>+
+
+
{f.u}
+
+ ))} +
+
+ Aperçu + {buildPreview(mins,secs,inc)} +
+
+ + +
+
+
+ ); +} + +/* ══ TIME CHART ══ */ +function TimeChart({snapshots,initTime}){ + if(!snapshots||snapshots.length<2) return null; + const W=300,H=65,P=4; + const maxM=snapshots[snapshots.length-1].move||1; + const pts0=snapshots.map(s=>(P+((s.move/maxM)*(W-P*2)))+","+(P+(1-(s.t0/initTime))*(H-P*2))); + const pts1=snapshots.map(s=>(P+((s.move/maxM)*(W-P*2)))+","+(P+(1-(s.t1/initTime))*(H-P*2))); + return( +
+
Temps restant par coup
+ + + + +
+
J1
+
J2
+
+
+ ); +} + +/* ══ USER MANAGER SCREEN ══ */ +function UserManagerScreen({users,onSave,onClose}){ + const [list,setList]=useState(users.map(u=>({...u}))); + const [editId,setEditId]=useState(null); + const [showAvFor,setShowAvFor]=useState(null); + const edit=(id,k,v)=>setList(prev=>prev.map(u=>u.id===id?{...u,[k]:v}:u)); + const addUser=()=>{ + const nu={id:genId(),name:"Nouveau",avatar:"♟",elo:1200,streak:0,wins:0,games:0}; + setList(prev=>[...prev,nu]); + setEditId(nu.id); + }; + const delUser=(id)=>{ + if(list.length<=2)return; + setList(prev=>prev.filter(u=>u.id!==id)); + if(editId===id)setEditId(null); + }; + const saveUser=()=>{ setEditId(null); setShowAvFor(null); }; + return( +
+
+
Joueurs
+ +
{onSave(list);onClose();}}>✕
+
+
+ {list.map(u=>{ + const isEdit=editId===u.id; + return( +
+
+
isEdit&&setShowAvFor(showAvFor===u.id?null:u.id)}> + {u.avatar} +
+
+ {isEdit + ?edit(u.id,"name",e.target.value||"Joueur")} + onPointerDown={e=>e.stopPropagation()} autoFocus/> + :
{u.name}
+ } +
+ {"ELO "+u.elo} + {u.streak>=2&&{"🔥 "+u.streak}} + {u.wins+" victoires"} +
+
+
+ {isEdit + ?(null) + :
{setEditId(u.id);setShowAvFor(null);}}>✏
+ } + {list.length>2&&
delUser(u.id)}>🗑
} +
+
+ {isEdit&&showAvFor===u.id&&( +
+ {AVATARS.map(av=>( +
{edit(u.id,"avatar",av);setShowAvFor(null);}}> + {av} +
+ ))} +
+ )} + {isEdit&&( +
+ + +
+ )} +
+ ); + })} +
+
+ ); +} + +/* ══ PLAYER SELECT SCREEN ══ */ +function PlayerSelectScreen({users,onConfirm,onClose}){ + const [sel0,setSel0]=useState(users[0]?.id||null); + const [sel1,setSel1]=useState(users[1]?.id||null); + const ready=sel0&&sel1&&sel0!==sel1; + const u0=users.find(u=>u.id===sel0); + const u1=users.find(u=>u.id===sel1); + return( +
+ {/* J2 panel — top, rotated */} +
+
Joueur 2 — Haut de l'écran
+
+ {u1 + ?<>
{u1.avatar}
{u1.name}
+ :
Choisissez un joueur
+ } +
+
+ {users.map(u=>{ + let _sy1=null; + return( +
{_sy1=e.clientY;}} + onPointerUp={e=>{if(_sy1!==null&&Math.abs(e.clientY-_sy1)<10){setSel1(u.id);}_sy1=null;}}> +
{u.avatar}
+
+
{u.name}
+
{"ELO "+u.elo}
+
+
+ ); + })} +
+
+ + {/* Divider */} +
+
VS
+
+ + {/* J1 panel — bottom */} +
+
Joueur 1 — Bas de l'écran
+
+ {u0 + ?<>
{u0.avatar}
{u0.name}
+ :
Choisissez un joueur
+ } +
+
+ {users.map(u=>{ + let _sy0=null; + return( +
{_sy0=e.clientY;}} + onPointerUp={e=>{if(_sy0!==null&&Math.abs(e.clientY-_sy0)<10){setSel0(u.id);}_sy0=null;}}> +
{u.avatar}
+
+
{u.name}
+
{"ELO "+u.elo}
+
+
+ ); + })} +
+
+ + {/* Footer buttons */} +
+ + +
+
+ ); +} + +/* ══ CUBE PREP SCREEN ══ */ +function CubePrepScreen({p0,p1,onReady}){ + const [countdown,setCountdown]=useState(30); + const [scrambles]=useState([genScramble(18),genScramble(18)]); + const timerRef=useRef(null); + useEffect(()=>{ + timerRef.current=setInterval(()=>{ + setCountdown(c=>{ if(c<=1){clearInterval(timerRef.current);onReady();return 0;} return c-1; }); + },1000); + return()=>clearInterval(timerRef.current); + },[]); + const go=()=>{ clearInterval(timerRef.current); onReady(); }; + return( +
+
Mémorisez !
+
Chaque joueur a son propre mélange
+
+ {[p0,p1].map((u,pi)=>( +
+
{u.avatar}
+
{u.name}
+ +
+ {scrambles[pi].map((m,i)=>
{m}
)} +
+
+ ))} +
+
{countdown}
+ +
Passer
+
+ ); +} + +/* ══ SOLO CS TIMER ══ */ +const HOLD_MS=900; +function SoloCsTimerPanel({player,phase,elapsed,progress,onDown,onUp,user,isDone,isWinner}){ + const {onPD,onPM,onPU,onPC}=useScrollGuard(onDown,onUp,player); + const CIRC=264; + const prog=Math.min(1,progress||0); + const offset=CIRC*(1-prog); + const ph=isDone?(isWinner?"win":"lose"):phase; + const bgCls="scs-panel"+(player===1?" flip":"")+" "+(ph==="ready"?"hold":ph)+"-bg"; + + let stateLbl,stateClr,timeLbl,hint,showRing=false; + if(ph==="win"){stateLbl="VICTOIRE !";stateClr="gold";timeLbl=fmtMs(elapsed);hint=""} + else if(ph==="lose"){stateLbl="PERDU";stateClr="dim";timeLbl=fmtMs(elapsed);hint=""} + else if(phase==="idle"){stateLbl="CS TIMER";stateClr="blu";timeLbl="00.00";hint="Maintenez appuyé"} + else if(phase==="hold"){stateLbl="CHARGEZ…";stateClr="grn";timeLbl=Math.round(prog*100)+"%";hint="Ne relâchez pas !";showRing=true;} + else if(phase==="ready"){stateLbl="RELÂCHEZ !";stateClr="grn";timeLbl="GO";hint="Relâchez pour démarrer";showRing=true;} + else if(phase==="run"){stateLbl="⏱ EN COURS";stateClr="red";timeLbl=fmtMs(elapsed);hint="Appuyez pour stopper";} + else{stateLbl="STOPPÉ";stateClr="dim";timeLbl=fmtMs(elapsed);hint="En attente…";} + + return( +
+
+
{user.avatar}
+
{user.name}
+
+
{stateLbl}
+ {showRing?( +
+ + + + +
{timeLbl}
+
+ ):(phase==="run"&&!isDone)?( +
+ + + + + +
{timeLbl}
+
+ ):( +
{timeLbl}
+ )} + {ph==="win"&&
♛ Gagné !
} + {hint&&
{hint}
} +
+ ); +} + +/* ══ SOLO CS TIMER — one player ══ */ +function SoloCsTimerSolo({user}){ + const [phase,setPhase]=useState("idle"); + const [elapsed,setElapsed]=useState(0); + const [progress,setProgress]=useState(0); + const [history,setHistory]=useState([]); + + const phaseRef=useRef("idle"); + const startRef=useRef(null); + const holdStartRef=useRef(null); + const holdFrameRef=useRef(null); + const frameRef=useRef(null); + + useEffect(()=>{ phaseRef.current=phase; },[phase]); + + // RAF for run + useEffect(()=>{ + if(phase!=="run"){cancelAnimationFrame(frameRef.current);return;} + const loop=()=>{ + setElapsed(performance.now()-startRef.current); + frameRef.current=requestAnimationFrame(loop); + }; + frameRef.current=requestAnimationFrame(loop); + return()=>cancelAnimationFrame(frameRef.current); + },[phase]); + + const onDown=useCallback(()=>{ + const ph=phaseRef.current; + if(ph==="idle"||ph==="stop"){ + // Reset elapsed if coming from stop + if(ph==="stop"){setElapsed(0);} + holdStartRef.current=performance.now(); + setPhase("hold"); + const loop=()=>{ + if(!holdStartRef.current)return; + const prog=Math.min(1,(performance.now()-holdStartRef.current)/HOLD_MS); + setProgress(prog); + if(prog<1){ + holdFrameRef.current=requestAnimationFrame(loop); + } else { + holdStartRef.current=null; + haptic(50); + setPhase("ready"); + } + }; + holdFrameRef.current=requestAnimationFrame(loop); + } else if(ph==="run"){ + // Stop the timer + const el=performance.now()-startRef.current; + cancelAnimationFrame(frameRef.current); + setElapsed(el); + setHistory(prev=>[el,...prev].slice(0,10)); + setPhase("stop"); + haptic([60,40,100]); + } + },[]); + + const onUp=useCallback(()=>{ + const ph=phaseRef.current; + if(ph==="ready"){ + startRef.current=performance.now(); + setPhase("run"); + } else if(ph==="hold"){ + cancelAnimationFrame(holdFrameRef.current); + holdStartRef.current=null; + setProgress(0); + setPhase("idle"); + } + },[]); + + const reset=()=>{ + cancelAnimationFrame(frameRef.current); + cancelAnimationFrame(holdFrameRef.current); + holdStartRef.current=null; + startRef.current=null; + phaseRef.current="idle"; + setPhase("idle"); + setElapsed(0); + setProgress(0); + }; + + const CIRC=264; + const prog=Math.min(1,progress); + const offset=CIRC*(1-prog); + const best=history.length>0?Math.min(...history):null; + + let stateLbl,stateClr,timeDisplay,hint; + if(phase==="idle"){stateLbl="CS TIMER";stateClr="blu";timeDisplay=user.avatar;hint="Maintenez appuyé";} + else if(phase==="hold"){stateLbl="CHARGEZ…";stateClr="grn";timeDisplay=Math.round(prog*100)+"%";hint="Ne relâchez pas !";} + else if(phase==="ready"){stateLbl="RELÂCHEZ !";stateClr="wht";timeDisplay="GO";hint="Relâchez pour démarrer";} + else if(phase==="run"){stateLbl="⏱ EN COURS";stateClr="red";timeDisplay=fmtMs(elapsed);hint="Appuyez pour stopper";} + else{stateLbl="✓ TEMPS";stateClr="gold";timeDisplay=fmtMs(elapsed);hint="Utilisez ▶ Rejouer en bas";} + + const bgStyle=phase==="idle"?"scs-solo-panel idle-bg":phase==="hold"||phase==="ready"?"scs-solo-panel hold-bg":phase==="run"?"scs-solo-panel run-bg":"scs-solo-panel stop-bg"; + + const {onPD,onPM,onPU,onPC}=useScrollGuard(onDown,onUp); + + const isStop = phase==="stop"; + + return( +
+ +
+
{user.avatar}
+
{user.name}
+
+ +
{stateLbl}
+ + {(phase==="hold"||phase==="ready")?( +
+ + + + +
+ {timeDisplay} +
+
+ ):( +
{timeDisplay}
+ )} + +
{hint}
+ + {best!==null&&phase!=="hold"&&phase!=="ready"&&( +
e.stopPropagation()}> +
Meilleur
+
{fmtMs(best)}
+ {history.length>1&&( + <> +
Moy.
+
+ {fmtMs(history.reduce((a,b)=>a+b,0)/history.length)} +
+ + )} +
+ )} + + {history.length>0&&phase==="stop"&&( +
e.stopPropagation()} + onPointerMove={e=>e.stopPropagation()} + onPointerUp={e=>e.stopPropagation()}> + {history.slice(0,10).map((t,i)=>( +
+
#{i+1}
+
{fmtMs(t)}
+ {t===best&&
BEST
} +
+ ))} +
+ )} + +
e.stopPropagation()}> + {phase==="stop"&&( + + )} + + {history.length>0&&} +
+
+ ); +} + +function SoloCsTimer({p0,p1,onClose}){ + const [mode,setMode]=useState("solo"); + const [phases,setPhases]=useState(["idle","idle"]); + const [elapsed,setElapsed]=useState([0,0]); + const [progress,setProgress]=useState([0,0]); + const [done,setDone]=useState(false); + const [winner,setWinner]=useState(null); + + const phaseRef=useRef(["idle","idle"]); + const startRef=useRef([null,null]); + const holdStartRef=useRef([null,null]); + const holdFrameRef=useRef([null,null]); + const elRef=useRef([0,0]); + const doneRef=useRef(false); + const frameRef=useRef(null); + + useEffect(()=>{ phaseRef.current=phases; },[phases]); + + useEffect(()=>{ + if(done){cancelAnimationFrame(frameRef.current);return;} + const loop=()=>{ + const now=performance.now(); + setElapsed(prev=>{ + const n=[...prev]; + for(let p=0;p<2;p++) + if(phaseRef.current[p]==="run"&&startRef.current[p]!==null) + n[p]=now-startRef.current[p]; + return n; + }); + frameRef.current=requestAnimationFrame(loop); + }; + frameRef.current=requestAnimationFrame(loop); + return()=>cancelAnimationFrame(frameRef.current); + },[done]); + + const finalize=useCallback((e0,e1)=>{ + if(doneRef.current)return; + doneRef.current=true; + cancelAnimationFrame(frameRef.current); + const w=e0<=e1?0:1; + setWinner(w);setDone(true); + setPhases(prev=>{const n=[...prev];n[w]="win";n[w===0?1:0]="lose";return n;}); + },[]); + + const onDown=useCallback((player)=>{ + if(doneRef.current)return; + const ph=phaseRef.current[player]; + if(ph==="idle"){ + holdStartRef.current[player]=performance.now(); + setPhases(prev=>{const n=[...prev];n[player]="hold";return n;}); + const loop=()=>{ + if(!holdStartRef.current[player])return; + const prog=Math.min(1,(performance.now()-holdStartRef.current[player])/HOLD_MS); + setProgress(prev=>{const n=[...prev];n[player]=prog;return n;}); + if(prog<1){ + holdFrameRef.current[player]=requestAnimationFrame(loop); + } else { + holdStartRef.current[player]=null; + haptic(50); + setPhases(prev=>{const n=[...prev];n[player]="ready";return n;}); + } + }; + holdFrameRef.current[player]=requestAnimationFrame(loop); + } else if(ph==="run"){ + const el=performance.now()-startRef.current[player]; + startRef.current[player]=null; + elRef.current[player]=el; + setElapsed(prev=>{const n=[...prev];n[player]=el;return n;}); + const other=player===0?1:0; + const otherPh=phaseRef.current[other]; + setPhases(prev=>{const n=[...prev];n[player]="stop";return n;}); + if(otherPh==="stop"){ + finalize(player===0?el:elRef.current[0],player===1?el:elRef.current[1]); + } + } + },[finalize]); + + const onUp=useCallback((player)=>{ + const ph=phaseRef.current[player]; + if(ph==="ready"){ + startRef.current[player]=performance.now(); + setPhases(prev=>{const n=[...prev];n[player]="run";return n;}); + } else if(ph==="hold"){ + cancelAnimationFrame(holdFrameRef.current[player]); + holdStartRef.current[player]=null; + setProgress(prev=>{const n=[...prev];n[player]=0;return n;}); + setPhases(prev=>{const n=[...prev];n[player]="idle";return n;}); + } + },[]); + + const reset=()=>{ + cancelAnimationFrame(frameRef.current); + [0,1].forEach(p=>{ + cancelAnimationFrame(holdFrameRef.current[p]); + holdStartRef.current[p]=null; + startRef.current[p]=null; + }); + doneRef.current=false; + phaseRef.current=["idle","idle"]; + elRef.current=[0,0]; + setPhases(["idle","idle"]); + setElapsed([0,0]); + setProgress([0,0]); + setDone(false); + setWinner(null); + }; + + return( +
+ {/* Mode toggle header */} +
+
e.stopPropagation()}> +
{setMode("solo");reset();}}>Solo
+
{setMode("duel");reset();}}>Duel
+
+
+ + {mode==="solo"?( + + ):( + <> + +
VS
+ +
e.stopPropagation()}> + + +
+ + )} + + {/* Close button always visible in solo mode */} + {mode==="solo"&&( +
e.stopPropagation()}> + +
+ )} +
+ ); +} + +/* ══ TOURNAMENT ══ */ +function buildRound(players){ + return players.reduce((acc,_,i)=>i%2===0?[...acc,{p1:players[i],p2:players[i+1]||null,winner:null}]:acc,[]); +} +function TournamentScreen({onClose,onStartMatch}){ + const [size,setSize]=useState(4); + const DEFAULT_NAMES=["Joueur 1","Joueur 2","Joueur 3","Joueur 4","Joueur 5","Joueur 6","Joueur 7","Joueur 8"]; + const [names,setNames]=useState(DEFAULT_NAMES); + const [editIdx,setEditIdx]=useState(null); + const [bracket,setBracket]=useState(null); + const [phase,setPhase]=useState("setup"); + const setName=(i,v)=>setNames(prev=>{const n=[...prev];n[i]=v||DEFAULT_NAMES[i];return n;}); + const start=()=>{ + setEditIdx(null); + const players=names.slice(0,size).map((n,i)=>({name:n||DEFAULT_NAMES[i],id:i})); + setBracket({rounds:[buildRound(players)],currentRound:0,champion:null}); + setPhase("bracket"); + }; + const recordWin=(ri,mi,bracketWinnerId)=>{ + setBracket(prev=>{ + const b=JSON.parse(JSON.stringify(prev)); + b.rounds[ri][mi].winner=bracketWinnerId; + const round=b.rounds[ri]; + if(round.every(m=>m.winner!==null)){ + const winners=round.map(m=>m.p1&&m.p1.id===m.winner?m.p1:m.p2); + if(winners.length>1){b.rounds.push(buildRound(winners));b.currentRound=ri+1;} + else{b.champion=winners[0];} + } + return b; + }); + }; + const champion=bracket&&bracket.champion; + const totalRounds=Math.log2(size); + const getRoundName=(ri)=>{ + const rem=totalRounds-ri; + if(rem===1)return"Finale"; + if(rem===2)return"Demi-finales"; + if(rem===3)return"Quarts de finale"; + return"Tour "+(ri+1); + }; + return( +
+
+
Tournoi
+
+
+
+ {phase==="setup"&&( +
+
+
Nombre de joueurs
+
+ {[4,8].map(s=>( +
setSize(s)}> + {s+" joueurs"} +
+ ))} +
+
+
+
Joueurs — appuyez pour renommer
+
+ {names.slice(0,size).map((n,i)=>( +
setEditIdx(i)}> +
{i+1}
+ + {editIdx===i + ?setName(i,e.target.value)} + onBlur={()=>setEditIdx(null)} + onPointerDown={e=>e.stopPropagation()}/> + :
{n}
+ } +
+ ))} +
+
+ +
+ )} + {phase==="bracket"&&bracket&&( +
+ {champion&&( +
+
🏆
+
{"Champion : "+champion.name}
+
Tournoi terminé !
+ +
+ )} + {bracket.rounds.map((round,ri)=>( +
+
{getRoundName(ri)}
+ {round.map((match,mi)=>{ + const isDone=match.winner!==null; + const isActive=ri===bracket.currentRound&&!isDone&&!champion; + return( +
+ {[match.p1,match.p2].map((pl,pi)=>pl?( +
+
{pl.name}
+ {match.winner===pl.id&&} +
{match.winner===pl.id?"1":isDone?"0":"-"}
+
+ ):( +
+
En attente…
+
-
+
+ ))} + {isActive&&match.p1&&match.p2&&( +
{ + onStartMatch(match,(localW)=>recordWin(ri,mi,localW===0?match.p1.id:match.p2.id)); + }}>▶ Jouer ce match
+ )} + {isActive&&(!match.p1||!match.p2)&&( +
En attente des matchs précédents
+ )} +
+ ); + })} +
+ ))} +
+ )} +
+
+ ); +} + +/* ══ HISTORY ══ */ +function HistoryScreen({history,onClose,onClear}){ + return( +
+
+
Historique
+
+
+
+ {history.length===0?( +
+
🕐
+
Aucune partie
+
+ ):( + <> + {[...history].reverse().map((g,idx)=>( +
+
+
{fmtDate(g.ts)}
+
{"J"+(g.winner+1)+" Gagne"}
+
+
+ {[0,1].map(p=>( +
+
+ +
{"J"+(p+1)}
+ {g.winner===p&&} +
+
{fmt(g.times[p])}
+
{"Cube x"+g.cubeWins[p]+(g.penaltyTotal&&g.penaltyTotal[p]>0?" • -"+g.penaltyTotal[p]+"s pénalité":"")}
+
+ ))} +
+
+
{g.moves+" coups • "+g.csCount+" CS"}
+
{g.timeLabel+" • "+g.why}
+
+
+ ))} +
+ +
+ + )} +
+
+ ); +} + +/* ══ CONFIG SCREEN ══ */ +function ConfigScreen({onStart,history,onShowHistory,onShowTournament,onShowUsers,onShowSoloCsTimer,rules,onRulesChange,users,selIds}){ + const [sel,setSel]=useState(2); + const [sct,setSct]=useState(7); + const [showModal,setShowModal]=useState(false); + const [cBase,setCBase]=useState(300); + const [cInc,setCInc]=useState(0); + const sp=(set,d,mn,mx)=>set(v=>Math.max(mn,Math.min(mx,v+d))); + const isCustom=sel===-1; + const activeBase=isCustom?cBase:PRESETS[sel].base; + const activeInc=isCustom?cInc:PRESETS[sel].inc; + const cMins=Math.floor(cBase/60); + const cSecs=cBase%60; + const tog=(k)=>onRulesChange({...rules,[k]:!rules[k]}); + const u0=users.find(u=>u.id===selIds[0])||users[0]; + const u1=users.find(u=>u.id===selIds[1])||users[1]; + return( +
+
+
CHESSCUBING CLOCK
+
logo
Speed Challenge Edition
+
+
+
{u0.avatar}
+ {u0.name} +
+
+ 👥 + Joueurs +
+
+ 📋 + {"("+history.length+")"} +
+
+ 🏆 + Tournoi +
+
+ + CS Timer +
+
+ {u1.name} +
{u1.avatar}
+
+
+
+
+
CS Timer — tous les
+
+
+
+
⚡ CS Timer activé
+
Résolvez un cube Rubik's tous les N coups pour gagner x2
+
+
tog("csEnabled")}>
+
+ {rules.csEnabled&&( +
+
Coups avant CS Timer
+
+
sp(setSct,-1,2,20)}>−
+
{sct}
+
sp(setSct,+1,2,20)}>+
+
+
+ )} +
+
Règles de gameplay
+
+
+
+
⚔ Coups interdits
+
Double coup : prise de dame interdite
+
+
tog("forbidCapture")}>
+
+
+
+
{"⏳ Malus de temps ("+rules.moveLimitSec+"s / -"+rules.penaltySec+"s)"}
+
Dépasser la limite par coup = pénalité
+
+
tog("penaltyEnabled")}>
+
+ {rules.penaltyEnabled&&( +
+
+
Limite/coup
+
+
onRulesChange({...rules,moveLimitSec:Math.max(5,rules.moveLimitSec-5)})}>−
+
{rules.moveLimitSec+"s"}
+
onRulesChange({...rules,moveLimitSec:Math.min(60,rules.moveLimitSec+5)})}>+
+
+
+
+
Pénalité
+
+
onRulesChange({...rules,penaltySec:Math.max(5,rules.penaltySec-5)})}>−
+
{rules.penaltySec+"s"}
+
onRulesChange({...rules,penaltySec:Math.min(60,rules.penaltySec+5)})}>+
+
+
+
+ )} + {rules.csEnabled&&( +
+
+
🎲 Mode Chaos
+
Seuil CS Timer aléatoire à chaque cycle
+
+
tog("chaosMode")}>
+
+ )} + {rules.csEnabled&&( +
+
+
🧩 Préparation Rubik's
+
Mélange officiel à mémoriser avant la partie
+
+
tog("cubePrepEnabled")}>
+
+ )} +
+
Expérience
+
+
+
+
🔊 Sons
+
Tic-tac, swoosh CS Timer, fanfare victoire
+
+
tog("soundEnabled")}>
+
+
+
+
📳 Vibrations
+
Retour haptique sur chaque action
+
+
tog("hapticEnabled")}>
+
+
+
Temps de jeu
+
setShowModal(true)}> +
+
{isCustom?buildPreview(cMins,cSecs,cInc):"✦ Temps personnalisé"}
+ {isCustom&&
{cInc>0?"+ "+cInc+" sec/coup":"Sans incrément"}{" · Perso"}
} +
+ {isCustom?"✏":"›"} +
+ {["BULLET","BLITZ","RAPID","CLASSIC"].map(tag=>( +
+
{tag}
+
+ {PRESETS.filter(p=>p.tag===tag).map(p=>{ + const i=PRESETS.indexOf(p); + let _sy=null; + return( +
{_sy=e.clientY;}} + onPointerUp={e=>{if(_sy!==null&&Math.abs(e.clientY-_sy)<10){setSel(i);}_sy=null;}}> +
+ {p.base<60?p.base+"s":Math.floor(p.base/60)+" min"} + {p.inc>0&&{"+ "+p.inc+" sec/coup"}} +
+ +
+ ); + })} +
+
+ ))} +
+ {"⚡ Après "+sct+" coups — CS Timer — le gagnant joue x2"} +
+
+
+ +
+ {showModal&&( + {setCBase(base);setCInc(inc);setSel(-1);setShowModal(false);}} + onCancel={()=>setShowModal(false)}/> + )} +
+ ); +} + +/* ══ MOVE DOTS ══ */ +function Dots({moves,sct}){ + const inC=moves===0?0:((moves-1)%sct)+1; + const onT=moves>0&&moves%sct===0; + return( +
+ {Array.from({length:sct},(_,i)=>{ + const filled=i<(onT?sct:inC); + return
; + })} +
+ ); +} + +/* ══ CS OVERLAY ══ */ +function CSOverlay({phase,elapsed,isDone,isWinner,onDown,onUp,player,holdProgress}){ + const {onPD,onPM,onPU,onPC}=useScrollGuard(onDown,onUp,player); + const CIRC=264; // circumference for r=42 circle + const prog=Math.min(1,holdProgress||0); + const offset=CIRC*(1-prog); + + let ts,tc,ls,lc,hint; + if(isDone){ts=fmtMs(elapsed);tc=isWinner?"win":"lose";ls=isWinner?"Victoire !":"Éliminé";lc=isWinner?"grn":"dim";hint=isWinner?"2 coups consécutifs !":"";} + else if(phase==="idle"){ts="0.00";tc="idle";ls="CS TIMER";lc="blu";hint="Maintenez appuyé";} + else if(phase==="hold"){ts="0.00";tc="hold";ls="CHARGEZ…";lc="grn";hint="Ne relâchez pas !";} + else if(phase==="ready"){ts="GO";tc="hold";ls="RELÂCHEZ !";lc="grn";hint="Relâchez pour démarrer";} + else if(phase==="run"){ts=fmtMs(elapsed);tc="run";ls="⏱";lc="red";hint="Appuyez pour stopper";} + else{ts=fmtMs(elapsed);tc="stop";ls="Stoppé";lc="dim";hint="En attente…";} + const bgc=isDone?(isWinner?"win":"lose"):phase==="ready"?"hold":phase; + + return( +
+
+
{ls}
+ + {(phase==="hold"||phase==="ready")&&!isDone&&( +
+ + + + +
+ {phase==="ready"?"GO":Math.round(prog*100)+"%"} +
+
+ )} + + {phase==="run"&&!isDone&&( +
+ + + + + +
{ts}
+
+ )} + + {phase!=="hold"&&phase!=="ready"&&phase!=="run"&&( +
{ts}
+ )} + + {isDone&&isWinner?
{hint}
:hint?
{hint}
:null} +
+ ); +} + +/* ══ DOUBLE MOVE OVERLAY ══ */ +function DblOverlay({isWinner,movesLeft,forbidCapture,showChallenge}){ + return( +
+ {isWinner?
:
} + {isWinner?( +
+
+
Double Coup !
+ {forbidCapture&&
⚔ Prise de dame interdite
} +
+ {[0,1].map(i=>
=movesLeft?" used":"")}/>)} +
+
Appuyez pour jouer
+
+ ):( +
+
+
Adversaire joue 2 coups…
+
Attendez votre tour
+ {forbidCapture&&showChallenge&&( +
⚔ Appuyez pour contester une prise
+ )} +
+ )} +
+ ); +} + +/* ══ GAME SCREEN ══ */ +function GameScreen({initTime,inc,sct,timeLabel,onBack,onGameEnd,rules,playerNames,tournamentCb,profiles}){ + const [times,setTimes]=useState([initTime,initTime]); + const [active,setActive]=useState(null); + const [moves,setMoves]=useState(0); + const [paused,setPaused]=useState(false); + const [scOn,setScOn]=useState(false); + const [csPhase,setCsPhase]=useState(["idle","idle"]); + const [csEl,setCsEl]=useState([0,0]); + const [scDone,setScDone]=useState(false); + const [scWin,setScWin]=useState(null); + const [dbl,setDbl]=useState(false); + const [dblP,setDblP]=useState(null); + const [dblL,setDblL]=useState(2); + const [over,setOver]=useState(false); + const [winner,setWinner]=useState(null); + const [why,setWhy]=useState(""); + const [ripple,setRipple]=useState([null,null]); + const [cubeWins,setCubeWins]=useState([0,0]); + const [pendingWin,setPendingWin]=useState(null); + const [penaltyFlash,setPenaltyFlash]=useState(null); + const [totalPenalty,setTotalPenalty]=useState([0,0]); + const [moveTimer,setMoveTimer]=useState(0); + const [showForbidden,setShowForbidden]=useState(null); + const [showChallenge,setShowChallenge]=useState(false); + const [snapshots,setSnapshots]=useState([]); + const [currentSct,setCurrentSct]=useState(sct); + const [eloDelta,setEloDelta]=useState([0,0]); + + const clockRef=useRef(null),frameRef=useRef(null),startRef=useRef([null,null]); + const doneRef=useRef(false),phaseRef=useRef(["idle","idle"]),elRef=useRef([0,0]); + const movesRef=useRef(0),cubeWinsRef=useRef([0,0]),csCountRef=useRef(0); + const penaltyRef=useRef([0,0]),pendingTimerRef=useRef(null); + const moveStartRef=useRef(null),moveFrameRef=useRef(null); + const timesRef=useRef([initTime,initTime]); + const currentSctRef=useRef(sct); + const holdStartRef=useRef([null,null]); + const holdFrameRef=useRef([null,null]); + + const HOLD_DURATION=900; // ms to fill the circle + + const [holdProgress,setHoldProgress]=useState([0,0]); + + useEffect(()=>{phaseRef.current=csPhase;},[csPhase]); + useEffect(()=>{elRef.current=csEl;},[csEl]); + useEffect(()=>{doneRef.current=scDone;},[scDone]); + useEffect(()=>{movesRef.current=moves;},[moves]); + useEffect(()=>{cubeWinsRef.current=cubeWins;},[cubeWins]); + useEffect(()=>{csCountRef.current=Math.floor(moves/currentSct);},[moves,currentSct]); + useEffect(()=>{penaltyRef.current=totalPenalty;},[totalPenalty]); + useEffect(()=>{timesRef.current=times;},[times]); + useEffect(()=>{currentSctRef.current=currentSct;},[currentSct]); + + // Challenge hint + useEffect(()=>{ + if(dbl&&rules.forbidCapture){ + const t=setTimeout(()=>setShowChallenge(true),400); + return()=>clearTimeout(t); + } else { setShowChallenge(false); } + },[dbl,rules.forbidCapture]); + + // Tick-tock sounds + useEffect(()=>{ + if(!rules.soundEnabled||active===null||paused||scOn||dbl||over)return; + const id=setInterval(()=>{ if(active===0)playTick(); else playTock(); },1000); + return()=>clearInterval(id); + },[active,paused,scOn,dbl,over,rules.soundEnabled]); + + // Chess clock + useEffect(()=>{ + if(active===null||paused||scOn||dbl||over){clearInterval(clockRef.current);return;} + clockRef.current=setInterval(()=>{ + setTimes(prev=>{ + const n=[...prev]; + n[active]=Math.max(0,n[active]-0.1); + if(n[active]<=0){ + const w=active===0?1:0; + const d=[calcElo(profiles.p0.elo,profiles.p1.elo,w===0?1:0),calcElo(profiles.p1.elo,profiles.p0.elo,w===1?1:0)]; + setEloDelta(d); + setOver(true);setWinner(w);setWhy("Temps écoulé"); + if(rules.soundEnabled)playFanfare(); + if(rules.hapticEnabled)haptic([50,50,150]); + if(onGameEnd)onGameEnd({winner:w,times:n,moves:movesRef.current,csCount:csCountRef.current,cubeWins:cubeWinsRef.current,timeLabel,ts:Date.now(),penaltyTotal:penaltyRef.current,why:"Temps écoulé",snapshots,eloDelta:d,names:[profiles.p0.name,profiles.p1.name],avatars:[profiles.p0.avatar,profiles.p1.avatar]}); + if(tournamentCb)tournamentCb(w); + } + return n; + }); + },100); + return()=>clearInterval(clockRef.current); + },[active,paused,scOn,dbl,over]); + + // Move timer bar + useEffect(()=>{ + if(!rules.penaltyEnabled||active===null||paused||scOn||dbl||over){cancelAnimationFrame(moveFrameRef.current);return;} + moveStartRef.current=performance.now(); + const loop=()=>{setMoveTimer((performance.now()-moveStartRef.current)/1000);moveFrameRef.current=requestAnimationFrame(loop);}; + moveFrameRef.current=requestAnimationFrame(loop); + return()=>cancelAnimationFrame(moveFrameRef.current); + },[active,paused,scOn,dbl,over,rules.penaltyEnabled]); + + // CS Timer RAF + useEffect(()=>{ + if(!scOn||scDone){cancelAnimationFrame(frameRef.current);return;} + const loop=()=>{ + const now=performance.now(); + setCsEl(prev=>{const n=[...prev];for(let p=0;p<2;p++)if(phaseRef.current[p]==="run"&&startRef.current[p]!==null)n[p]=now-startRef.current[p];return n;}); + frameRef.current=requestAnimationFrame(loop); + }; + frameRef.current=requestAnimationFrame(loop); + return()=>cancelAnimationFrame(frameRef.current); + },[scOn,scDone]); + + const finalize=useCallback((e0,e1)=>{ + if(doneRef.current)return; + doneRef.current=true; + const w=e0<=e1?0:1; + setScDone(true);setScWin(w); + cancelAnimationFrame(frameRef.current); + setCubeWins(prev=>{const n=[...prev];n[w]++;return n;}); + if(rules.soundEnabled)playSwoop(); + if(rules.hapticEnabled)haptic([30,20,80]); + const nextSct=rules.chaosMode?Math.max(2,sct+Math.floor(Math.random()*5)-1):sct; + setTimeout(()=>{ + setScOn(false);setCsPhase(["idle","idle"]);setCsEl([0,0]); + startRef.current=[null,null];setScDone(false);doneRef.current=false;setScWin(null); + setCurrentSct(nextSct); + setDbl(true);setDblP(w);setDblL(2);setActive(w); + },1200); + },[rules,sct]); + + const csDown=useCallback((player)=>{ + if(!scOn||doneRef.current)return; + const ph=phaseRef.current[player]; + if(ph==="idle"){ + holdStartRef.current[player]=performance.now(); + setCsPhase(prev=>{const n=[...prev];n[player]="hold";return n;}); + const loop=()=>{ + if(!holdStartRef.current[player])return; + const prog=Math.min(1,(performance.now()-holdStartRef.current[player])/900); + setHoldProgress(prev=>{const n=[...prev];n[player]=prog;return n;}); + if(prog<1){ + holdFrameRef.current[player]=requestAnimationFrame(loop); + } else { + // Circle full → ready to launch on release + holdStartRef.current[player]=null; + if(rules.hapticEnabled)haptic([60,40,60]); + setCsPhase(prev=>{const n=[...prev];n[player]="ready";return n;}); + } + }; + holdFrameRef.current[player]=requestAnimationFrame(loop); + } + else if(ph==="run"){ + const el=performance.now()-startRef.current[player]; + startRef.current[player]=null; + setCsEl(prev=>{const n=[...prev];n[player]=el;return n;}); + setCsPhase(prev=>{const n=[...prev];n[player]="stop";const ot=player===0?1:0;if(n[ot]==="stop")finalize(player===0?el:elRef.current[0],player===1?el:elRef.current[1]);return n;}); + } + },[scOn,finalize,rules.hapticEnabled]); + + const csUp=useCallback((player)=>{ + if(!scOn||doneRef.current)return; + const ph=phaseRef.current[player]; + if(ph==="ready"){ + // Released after full → start timer now + startRef.current[player]=performance.now(); + setCsPhase(prev=>{const n=[...prev];n[player]="run";return n;}); + } else if(ph==="hold"){ + // Released before full → cancel + cancelAnimationFrame(holdFrameRef.current[player]); + holdStartRef.current[player]=null; + setHoldProgress(prev=>{const n=[...prev];n[player]=0;return n;}); + setCsPhase(prev=>{const n=[...prev];n[player]="idle";return n;}); + } + },[scOn]); + + const fireRipple=(p)=>setRipple(prev=>{const n=[...prev];n[p]=Date.now();return n;}); + + const applyPenalty=useCallback((player)=>{ + const pen=rules.penaltySec; + setTimes(prev=>{const n=[...prev];n[player]=Math.max(0,n[player]-pen);return n;}); + setTotalPenalty(prev=>{const n=[...prev];n[player]+=pen;return n;}); + setPenaltyFlash(player); + if(rules.soundEnabled)playWarn(); + if(rules.hapticEnabled)haptic([50,30,50,30,80]); + setTimeout(()=>setPenaltyFlash(null),800); + },[rules]); + + const handlePress=useCallback((player)=>{ + if(over||scOn)return; + if(rules.hapticEnabled)haptic(50); + fireRipple(player); + if(active===null){setActive(player===0?1:0);setMoves(1);return;} + if(dbl&&dblP!==null){ + if(player!==dblP){ + if(rules.forbidCapture){ + setShowChallenge(false); + setShowForbidden(player); + if(rules.hapticEnabled)haptic([80,40,80]); + setTimeout(()=>setShowForbidden(null),2000); + setDbl(false);setDblP(null);setActive(player); + } + return; + } + if(active!==player)return; + const left=dblL-1;setDblL(left); + if(left===0){setDbl(false);setDblP(null);setActive(player===0?1:0);} + return; + } + if(active!==player)return; + if(rules.penaltyEnabled&&moveStartRef.current!==null){ + const elapsed=(performance.now()-moveStartRef.current)/1000; + if(elapsed>rules.moveLimitSec)applyPenalty(player); + } + cancelAnimationFrame(moveFrameRef.current);setMoveTimer(0); + const nm=moves+1;setMoves(nm); + setSnapshots(prev=>[...prev,{move:nm,t0:timesRef.current[0],t1:timesRef.current[1]}]); + if(inc>0)setTimes(prev=>{const n=[...prev];n[player]+=inc;return n;}); + if(rules.soundEnabled){if(player===0)playTick();else playTock();} + setActive(player===0?1:0); + if(rules.csEnabled&&nm%currentSctRef.current===0){ + doneRef.current=false;startRef.current=[null,null]; + setScOn(true);setCsPhase(["idle","idle"]);setCsEl([0,0]);setScDone(false);setScWin(null); + if(rules.soundEnabled)playSwoop(); + if(rules.hapticEnabled)haptic([50,40,50,40,50]); + } + },[active,over,scOn,dbl,dblP,dblL,moves,inc,rules,applyPenalty]); + + const declareWinner=useCallback((player)=>{ + if(over||scOn)return; + if(pendingWin===player){ + clearTimeout(pendingTimerRef.current);setPendingWin(null); + clearInterval(clockRef.current); + setOver(true);setWinner(player);setWhy("Échec et mat"); + const d=[calcElo(profiles.p0.elo,profiles.p1.elo,player===0?1:0),calcElo(profiles.p1.elo,profiles.p0.elo,player===1?1:0)]; + setEloDelta(d); + if(rules.soundEnabled)playFanfare(); + if(rules.hapticEnabled)haptic([50,50,150]); + if(onGameEnd)onGameEnd({winner:player,times,moves:movesRef.current,csCount:csCountRef.current,cubeWins:cubeWinsRef.current,timeLabel,ts:Date.now(),penaltyTotal:penaltyRef.current,why:"Échec et mat",snapshots,eloDelta:d,names:[profiles.p0.name,profiles.p1.name],avatars:[profiles.p0.avatar,profiles.p1.avatar]}); + if(tournamentCb)tournamentCb(player); + } else { + clearTimeout(pendingTimerRef.current);setPendingWin(player); + pendingTimerRef.current=setTimeout(()=>setPendingWin(null),3000); + } + },[over,scOn,pendingWin,times,timeLabel,onGameEnd,tournamentCb,rules,profiles,snapshots]); + + const reset=useCallback(()=>{ + clearInterval(clockRef.current);cancelAnimationFrame(frameRef.current); + cancelAnimationFrame(moveFrameRef.current);clearTimeout(pendingTimerRef.current); + cancelAnimationFrame(holdFrameRef.current[0]);cancelAnimationFrame(holdFrameRef.current[1]); + holdStartRef.current=[null,null];holdFrameRef.current=[null,null]; + startRef.current=[null,null];doneRef.current=false; + setTimes([initTime,initTime]);timesRef.current=[initTime,initTime]; + setActive(null);setMoves(0);setPaused(false); + setScOn(false);setCsPhase(["idle","idle"]);setCsEl([0,0]);setScDone(false);setScWin(null); + setDbl(false);setDblP(null);setDblL(2);setCurrentSct(sct); + setOver(false);setWinner(null);setWhy("");setRipple([null,null]); + setCubeWins([0,0]);setPendingWin(null);setPenaltyFlash(null); + setTotalPenalty([0,0]);setMoveTimer(0);setShowForbidden(null);setShowChallenge(false); + setSnapshots([]);setEloDelta([0,0]);setHoldProgress([0,0]); + },[initTime,sct]); + + const [t0,t1]=times; + const p0on=active===0&&!scOn&&!dbl; + const p1on=active===1&&!scOn&&!dbl; + const divLabel=over?"FIN":paused?"PAUSE":scOn?"CS TIMER":dbl?"x2":active===null?"APPUYEZ":"J"+(active+1)+" JOUE"; + const pName=playerNames||(profiles?[profiles.p0.name,profiles.p1.name]:["Joueur 1","Joueur 2"]); + const pAv=profiles?[profiles.p0.avatar,profiles.p1.avatar]:["♟","♞"]; + const pStr=profiles?[profiles.p0.streak||0,profiles.p1.streak||0]:[0,0]; + const mtPct=rules.penaltyEnabled&&active!==null&&!scOn&&!dbl?Math.min(100,(moveTimer/rules.moveLimitSec)*100):0; + const mtCls=mtPct>=100?"crit":mtPct>=75?"warn":""; + + return( +
+ {/* Player 2 — top, flipped */} +
{if(!scOn)handlePress(1);}}> {ripple[1]&&
} + {scOn&&} + {dbl&&} + {showForbidden===1&&( +
+
+
Contestation !
+
Double coup annulé — prise de dame
+
+ )} +
{fmt(t1)}
+ {rules.penaltyEnabled&&p1on&&
} + {rules.csEnabled&&!scOn&&!dbl&&} + {rules.chaosMode&&!scOn&&!dbl&&
🎲 Chaos
} + {p1on&&
} + {totalPenalty[1]>0&&
{"-"+totalPenalty[1]+"s"}
} +
+ +
{pAv[1]}
+
{pName[1]}
+ {pStr[1]>=2&&
{"🔥 "+pStr[1]}
} + {"x"+cubeWins[1]} +
+ {penaltyFlash===1&&
} +
+ + {/* Divider + Win buttons */} +
+ {!over&&( +
{e.stopPropagation();declareWinner(1);}}> +
+
+
{pendingWin===1?"Confirmer":pName[1]+" ♛"}
+ {pendingWin===1&&
Retaper ✓
} +
+
+ )} +
+
{moves===0?"PRÊT":"COUP "+moves}
+
{divLabel}
+
+ {!over&&( +
{e.stopPropagation();declareWinner(0);}}> +
+
+
{pendingWin===0?"Confirmer":pName[0]+" ♛"}
+ {pendingWin===0&&
Retaper ✓
} +
+
+ )} +
+ + {/* Player 1 — bottom */} +
{if(!scOn)handlePress(0);}}> + {ripple[0]&&
} + {scOn&&} + {dbl&&} + {showForbidden===0&&( +
+
+
Contestation !
+
Double coup annulé — prise de dame
+
+ )} + {p0on&&
} +
{fmt(t0)}
+ {rules.penaltyEnabled&&p0on&&
} + {rules.csEnabled&&!scOn&&!dbl&&} + {rules.chaosMode&&!scOn&&!dbl&&
🎲 Chaos
} + {totalPenalty[0]>0&&
{"-"+totalPenalty[0]+"s"}
} +
+ {"x"+cubeWins[0]} + {pStr[0]>=2&&
{"🔥 "+pStr[0]}
} +
{pName[0]}
+
{pAv[0]}
+ +
+ {penaltyFlash===0&&
} +
+ + {/* Controls */} + {!over&&( +
+ + + +
+ )} + + {/* End */} + {over&&( +
+
+
{pName[winner||0]+" Gagne !"}
+
+ {[0,1].map(p=>( +
+
{pAv[p]}
+ +
{p===winner?"♛ Victoire":pName[p]}
+
{"Cube x"+cubeWins[p]}
+ {totalPenalty[p]>0&&
{"−"+totalPenalty[p]+"s pén."}
} +
=0?"pos":"neg")}>{(eloDelta[p]>=0?"+":"")+eloDelta[p]+" ELO"}
+ {pStr[p]>=2&&
{"🔥 Streak "+pStr[p]}
} +
+ ))} +
+
{why}
+ +
+
{moves}
Coups
+
{Math.floor(moves/currentSct)||0}
CS Timer
+
{fmt(times[0])}
J1 Restant
+
{fmt(times[1])}
J2 Restant
+
+
+ + +
+
+ )} +
+ ); +} + +/* ══ ROOT ══ */ +export default function ChessClockApp(){ + + // Set favicon + useEffect(()=>{ + const link=document.querySelector("link[rel*='icon']")||document.createElement("link"); + link.type="image/png";link.rel="shortcut icon"; + link.href="data:image/png;base64,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"; + document.getElementsByTagName("head")[0].appendChild(link); + document.title="ChessCubing Clock"; + },[]); + + const [screen,setScreen]=useState("config"); + const [gTime,setGTime]=useState(180); + const [gInc,setGInc]=useState(2); + const [gSct,setGSct]=useState(7); + const [gLabel,setGLabel]=useState("3 min"); + const [showHist,setShowHist]=useState(false); + const [showTrn,setShowTrn]=useState(false); + const [showUsers,setShowUsers]=useState(false); + const [showSelect,setShowSelect]=useState(false); + const [showPrep,setShowPrep]=useState(false); + const [showSoloCs,setShowSoloCs]=useState(false); + const [history,setHistory]=useState([]); + const [trnCb,setTrnCb]=useState(null); + const [trnIds,setTrnIds]=useState(null); // [id0, id1] for tournament matches + const [users,setUsers]=useState(()=>tryLoad("chess_users",DEF_USERS)); + const [selIds,setSelIds]=useState(()=>{ + const u=tryLoad("chess_users",DEF_USERS); + return [u[0]?.id||DEF_USERS[0].id, u[1]?.id||DEF_USERS[1].id]; + }); + const [rules,setRules]=useState({ + forbidCapture:false,penaltyEnabled:false,moveLimitSec:20,penaltySec:10, + chaosMode:false,cubePrepEnabled:false,soundEnabled:true,hapticEnabled:true, + csEnabled:true + }); + + const saveUsers=(list)=>{ setUsers(list); trySave("chess_users",list); }; + + // Derive profile objects for game + const getUser=(id)=>users.find(u=>u.id===id)||users[0]; + const p0=getUser(selIds[0]); + const p1=getUser(selIds[1]); + const profiles={p0,p1}; + + const updateElo=(entry)=>{ + setUsers(prev=>{ + const next=prev.map(u=>{ + const idx=selIds.indexOf(u.id); // 0=J1, 1=J2, -1=not playing + if(idx===-1)return u; + const isWinner=entry.winner===idx; + const delta=entry.eloDelta?entry.eloDelta[idx]:0; + return{...u, + elo:Math.max(100,(u.elo||1200)+delta), + wins:(u.wins||0)+(isWinner?1:0), + games:(u.games||0)+1, + streak:isWinner?(u.streak||0)+1:0, + }; + }); + trySave("chess_users",next); + return next; + }); + }; + + const addGame=(entry)=>{ setHistory(prev=>[...prev,{id:Date.now(),...entry}]); updateElo(entry); }; + + const startTournamentMatch=(match,cb)=>{ + setTrnCb(()=>(lw)=>cb(lw)); + setTrnIds([match.p1.userId||selIds[0], match.p2.userId||selIds[1]]); + setScreen("game");setShowTrn(false); + }; + + // Config → PlayerSelect → (CubePrepScreen) → Game + const onConfigStart=(base,inc,sct,label)=>{ + setGTime(base);setGInc(inc);setGSct(sct);setGLabel(label); + setTrnCb(null);setTrnIds(null); + setShowSelect(true); + }; + + const onPlayersConfirmed=(id0,id1)=>{ + setSelIds([id0,id1]); + setShowSelect(false); + if(rules.cubePrepEnabled){ setShowPrep(true); } + else{ setScreen("game"); } + }; + + const activeSelIds=trnIds||selIds; + const ap0=getUser(activeSelIds[0]); + const ap1=getUser(activeSelIds[1]); + const activeProfiles={p0:ap0,p1:ap1}; + + return( +
+ {showUsers&&setShowUsers(false)}/>} + {showHist&&setShowHist(false)} onClear={()=>setHistory([])}/>} + {showTrn&&setShowTrn(false)} onStartMatch={startTournamentMatch}/>} + {showSelect&&setShowSelect(false)}/>} + {showPrep&&{ setShowPrep(false); setScreen("game"); }}/>} + {showSoloCs&&setShowSoloCs(false)}/>} + {screen==="config" + ?setShowHist(true)} + onShowTournament={()=>setShowTrn(true)} + onShowUsers={()=>setShowUsers(true)} + onShowSoloCsTimer={()=>setShowSoloCs(true)} + rules={rules} + onRulesChange={setRules} + onStart={onConfigStart}/> + :{setScreen("config");if(trnCb){setShowTrn(true);}setTrnCb(null);setTrnIds(null);}} + onGameEnd={addGame}/> + } +
+ ); +} diff --git a/src/features/chess-clock/ChessClockStandalone.jsx b/src/features/chess-clock/ChessClockStandalone.jsx new file mode 100755 index 0000000..a79d9dc --- /dev/null +++ b/src/features/chess-clock/ChessClockStandalone.jsx @@ -0,0 +1,1793 @@ +const { StrictMode, useCallback, useEffect, useRef, useState } = React; + + +/* ══ HELPERS ══ */ +const fmt = s => Math.floor(Math.abs(s)/60).toString().padStart(2,"0")+":"+Math.floor(Math.abs(s)%60).toString().padStart(2,"0"); +const fmtMs = ms => Math.floor(ms/1000)+"."+(Math.floor((ms%1000)/10)).toString().padStart(2,"0"); +const fmtDate = ts => { const d=new Date(ts); return d.getDate()+"/"+(d.getMonth()+1)+"/"+d.getFullYear()+" "+d.getHours().toString().padStart(2,"0")+"h"+d.getMinutes().toString().padStart(2,"0"); }; +const buildPreview = (m,s,i) => { let t=m>0?m+" min":""; if(m>0&&s>0)t+=" "; if(s>0)t+=s+" sec"; if(!t)t="0 sec"; if(i>0)t+=" + "+i+"s"; return t; }; + +/* ══ SCROLL GUARD ══ + Returns pointer handlers that cancel the action if the finger moves > THRESHOLD px. + Usage: const {onPD, onPU, onPC, onPM} = useScrollGuard(onDown, onUp, player) +*/ +const SCROLL_THRESHOLD = 10; +function useScrollGuard(onDown, onUp, arg){ + const startY = useRef(null); + const startX = useRef(null); + const cancelled = useRef(false); + + const onPD = useCallback((e) => { + e.stopPropagation(); + startY.current = e.clientY; + startX.current = e.clientX; + cancelled.current = false; + if(arg !== undefined) onDown(arg); + else onDown(); + }, [onDown, arg]); + + const onPM = useCallback((e) => { + if(cancelled.current) return; + if(startY.current === null) return; + const dy = Math.abs(e.clientY - startY.current); + const dx = Math.abs(e.clientX - startX.current); + if(dy > SCROLL_THRESHOLD || dx > SCROLL_THRESHOLD){ + cancelled.current = true; + if(arg !== undefined) onUp(arg); + else onUp(); + } + }, [onUp, arg]); + + const onPU = useCallback((e) => { + e.stopPropagation(); + startY.current = null; + if(cancelled.current){ cancelled.current = false; return; } + if(arg !== undefined) onUp(arg); + else onUp(); + }, [onUp, arg]); + + const onPC = useCallback((e) => { + e.stopPropagation(); + startY.current = null; + cancelled.current = false; + if(arg !== undefined) onUp(arg); + else onUp(); + }, [onUp, arg]); + + return { onPD, onPM, onPU, onPC }; +} + +/* ══ HAPTIC ══ */ +const haptic = pat => { + try { + if(navigator.vibrate){ + navigator.vibrate(typeof pat==="number"?Math.max(50,pat):pat); + } + } catch(e){} +}; + +/* ══ AUDIO ══ */ +let _ac=null; +const getAC=()=>{ if(!_ac) _ac=new (window.AudioContext||window.webkitAudioContext)(); return _ac; }; +const beep=(freq,dur,type,vol,delay)=>{ try{ const c=getAC(),o=c.createOscillator(),g=c.createGain(); o.connect(g);g.connect(c.destination); o.type=type||"sine"; o.frequency.value=freq; const t=c.currentTime+(delay||0); g.gain.setValueAtTime(vol||0.14,t); g.gain.exponentialRampToValueAtTime(0.001,t+dur); o.start(t); o.stop(t+dur+0.05); }catch(e){} }; +const playTick=()=>beep(900,0.04,"square",0.12); +const playTock=()=>beep(600,0.04,"square",0.10); +const playSwoop=()=>{ try{ const c=getAC(),o=c.createOscillator(),g=c.createGain(); o.connect(g);g.connect(c.destination); o.frequency.setValueAtTime(520,c.currentTime); o.frequency.exponentialRampToValueAtTime(180,c.currentTime+0.2); g.gain.setValueAtTime(0.15,c.currentTime); g.gain.exponentialRampToValueAtTime(0.001,c.currentTime+0.2); o.start(); o.stop(c.currentTime+0.22); }catch(e){} }; +const playFanfare=()=>{ [523,659,784,1047].forEach((f,i)=>beep(f,0.22,"sine",0.16,i*0.11)); }; +const playWarn=()=>{ beep(220,0.07,"sawtooth",0.12,0); beep(220,0.07,"sawtooth",0.12,0.14); }; + +/* ══ ELO ══ */ +const calcElo=(rA,rB,won)=>{ const K=32,ea=1/(1+Math.pow(10,(rB-rA)/400)); return Math.round(K*(won-ea)); }; +const tryLoad=(key,def)=>{ try{ return JSON.parse(localStorage.getItem(key)||"null")||def; }catch(e){ return def; } }; +const trySave=(key,val)=>{ try{ localStorage.setItem(key,JSON.stringify(val)); }catch(e){} }; + +/* ══ SCRAMBLE ══ */ +const SFACES=["U","D","F","B","L","R"], SMODS=["","'","2"]; +const genScramble=(n)=>{ const m=[]; let last=-1; for(let i=0;i<(n||20);i++){ let f; do{f=Math.floor(Math.random()*6);}while(f===last); last=f; m.push(SFACES[f]+SMODS[Math.floor(Math.random()*3)]); } return m; }; + +/* ══ CONSTANTS ══ */ +const PRESETS = [ + {base:60, inc:0, tag:"BULLET", lbl:"1 min"}, + {base:60, inc:1, tag:"BULLET", lbl:"1 min + 1s"}, + {base:120, inc:1, tag:"BULLET", lbl:"2 min + 1s"}, + {base:180, inc:0, tag:"BLITZ", lbl:"3 min"}, + {base:180, inc:2, tag:"BLITZ", lbl:"3 min + 2s"}, + {base:300, inc:0, tag:"BLITZ", lbl:"5 min"}, + {base:300, inc:3, tag:"BLITZ", lbl:"5 min + 3s"}, + {base:300, inc:5, tag:"BLITZ", lbl:"5 min + 5s"}, + {base:600, inc:0, tag:"RAPID", lbl:"10 min"}, + {base:600, inc:5, tag:"RAPID", lbl:"10 min + 5s"}, + {base:900, inc:10,tag:"RAPID", lbl:"15 min + 10s"}, + {base:1200,inc:5, tag:"RAPID", lbl:"20 min + 5s"}, + {base:1800,inc:0, tag:"CLASSIC",lbl:"30 min"}, + {base:1800,inc:20,tag:"CLASSIC",lbl:"30 min + 20s"}, + {base:3600,inc:30,tag:"CLASSIC",lbl:"60 min + 30s"}, +]; +const TAG_COLORS = {BULLET:"#f04040",BLITZ:"#e8b84b",RAPID:"#38e878",CLASSIC:"#4b9ee8"}; +const P1C = ["p1c0","p1c1","p1c2","p1c3","p1c4","p1c5","p1c6","p1c7","p1c8"]; +const P2C = ["p2c0","p2c1","p2c2","p2c3","p2c4","p2c5","p2c6","p2c7","p2c8"]; +const AVATARS=["♟","♞","♜","♛","♔","🦁","🐯","⚡","🔥","💎","👑","🎯","🦊","🐉","🌙","⚔"]; +const genId=()=>"u"+Math.random().toString(36).slice(2,8); +const DEF_USERS=[ + {id:"u_default0",name:"Joueur 1",avatar:"♟",elo:1200,streak:0,wins:0,games:0}, + {id:"u_default1",name:"Joueur 2",avatar:"♞",elo:1200,streak:0,wins:0,games:0}, +]; + +/* ══ RUBIK'S CUBE ══ */ +function RubiksCube({player,size="md"}){ + const colors=player===0?P1C:P2C; + return
{colors.map((c,i)=>
)}
; +} + +/* ══ CUSTOM TIME MODAL ══ */ +function CustomModal({initMin,initSec,initInc,onConfirm,onCancel}){ + const [mins,setMins]=useState(initMin); + const [secs,setSecs]=useState(initSec); + const [inc,setInc]=useState(initInc); + const sp=(set,d,mn,mx)=>set(v=>Math.max(mn,Math.min(mx,v+d))); + const total=mins*60+secs; + return( +
+
e.stopPropagation()}> +
+
Temps Personnalisé
+
Définissez votre contrôle de temps
+
+ {[{lbl:"Minutes",val:mins,set:setMins,d:1,mn:0,mx:180,u:"min"}, + {lbl:"Secondes",val:secs,set:setSecs,d:5,mn:0,mx:55,u:"sec (x5)"}, + {lbl:"Incrément",val:inc,set:setInc,d:1,mn:0,mx:60,u:"+sec/coup"} + ].map(f=>( +
+
{f.lbl}
+
+
sp(f.set,-f.d,f.mn,f.mx)}>−
+
{f.val}
+
sp(f.set,+f.d,f.mn,f.mx)}>+
+
+
{f.u}
+
+ ))} +
+
+ Aperçu + {buildPreview(mins,secs,inc)} +
+
+ + +
+
+
+ ); +} + +/* ══ TIME CHART ══ */ +function TimeChart({snapshots,initTime}){ + if(!snapshots||snapshots.length<2) return null; + const W=300,H=65,P=4; + const maxM=snapshots[snapshots.length-1].move||1; + const pts0=snapshots.map(s=>(P+((s.move/maxM)*(W-P*2)))+","+(P+(1-(s.t0/initTime))*(H-P*2))); + const pts1=snapshots.map(s=>(P+((s.move/maxM)*(W-P*2)))+","+(P+(1-(s.t1/initTime))*(H-P*2))); + return( +
+
Temps restant par coup
+ + + + +
+
J1
+
J2
+
+
+ ); +} + +/* ══ USER MANAGER SCREEN ══ */ +function UserManagerScreen({users,onSave,onClose}){ + const [list,setList]=useState(users.map(u=>({...u}))); + const [editId,setEditId]=useState(null); + const [showAvFor,setShowAvFor]=useState(null); + const edit=(id,k,v)=>setList(prev=>prev.map(u=>u.id===id?{...u,[k]:v}:u)); + const addUser=()=>{ + const nu={id:genId(),name:"Nouveau",avatar:"♟",elo:1200,streak:0,wins:0,games:0}; + setList(prev=>[...prev,nu]); + setEditId(nu.id); + }; + const delUser=(id)=>{ + if(list.length<=2)return; + setList(prev=>prev.filter(u=>u.id!==id)); + if(editId===id)setEditId(null); + }; + const saveUser=()=>{ setEditId(null); setShowAvFor(null); }; + return( +
+
+
Joueurs
+ +
{onSave(list);onClose();}}>✕
+
+
+ {list.map(u=>{ + const isEdit=editId===u.id; + return( +
+
+
isEdit&&setShowAvFor(showAvFor===u.id?null:u.id)}> + {u.avatar} +
+
+ {isEdit + ?edit(u.id,"name",e.target.value||"Joueur")} + onPointerDown={e=>e.stopPropagation()} autoFocus/> + :
{u.name}
+ } +
+ {"ELO "+u.elo} + {u.streak>=2&&{"🔥 "+u.streak}} + {u.wins+" victoires"} +
+
+
+ {isEdit + ?(null) + :
{setEditId(u.id);setShowAvFor(null);}}>✏
+ } + {list.length>2&&
delUser(u.id)}>🗑
} +
+
+ {isEdit&&showAvFor===u.id&&( +
+ {AVATARS.map(av=>( +
{edit(u.id,"avatar",av);setShowAvFor(null);}}> + {av} +
+ ))} +
+ )} + {isEdit&&( +
+ + +
+ )} +
+ ); + })} +
+
+ ); +} + +/* ══ PLAYER SELECT SCREEN ══ */ +function PlayerSelectScreen({users,onConfirm,onClose}){ + const [sel0,setSel0]=useState(users[0]?.id||null); + const [sel1,setSel1]=useState(users[1]?.id||null); + const ready=sel0&&sel1&&sel0!==sel1; + const u0=users.find(u=>u.id===sel0); + const u1=users.find(u=>u.id===sel1); + return( +
+ {/* J2 panel — top, rotated */} +
+
Joueur 2 — Haut de l'écran
+
+ {u1 + ?<>
{u1.avatar}
{u1.name}
+ :
Choisissez un joueur
+ } +
+
+ {users.map(u=>{ + let _sy1=null; + return( +
{_sy1=e.clientY;}} + onPointerUp={e=>{if(_sy1!==null&&Math.abs(e.clientY-_sy1)<10){setSel1(u.id);}_sy1=null;}}> +
{u.avatar}
+
+
{u.name}
+
{"ELO "+u.elo}
+
+
+ ); + })} +
+
+ + {/* Divider */} +
+
VS
+
+ + {/* J1 panel — bottom */} +
+
Joueur 1 — Bas de l'écran
+
+ {u0 + ?<>
{u0.avatar}
{u0.name}
+ :
Choisissez un joueur
+ } +
+
+ {users.map(u=>{ + let _sy0=null; + return( +
{_sy0=e.clientY;}} + onPointerUp={e=>{if(_sy0!==null&&Math.abs(e.clientY-_sy0)<10){setSel0(u.id);}_sy0=null;}}> +
{u.avatar}
+
+
{u.name}
+
{"ELO "+u.elo}
+
+
+ ); + })} +
+
+ + {/* Footer buttons */} +
+ + +
+
+ ); +} + +/* ══ CUBE PREP SCREEN ══ */ +function CubePrepScreen({p0,p1,onReady}){ + const [countdown,setCountdown]=useState(30); + const [scrambles]=useState([genScramble(18),genScramble(18)]); + const timerRef=useRef(null); + useEffect(()=>{ + timerRef.current=setInterval(()=>{ + setCountdown(c=>{ if(c<=1){clearInterval(timerRef.current);onReady();return 0;} return c-1; }); + },1000); + return()=>clearInterval(timerRef.current); + },[]); + const go=()=>{ clearInterval(timerRef.current); onReady(); }; + return( +
+
Mémorisez !
+
Chaque joueur a son propre mélange
+
+ {[p0,p1].map((u,pi)=>( +
+
{u.avatar}
+
{u.name}
+ +
+ {scrambles[pi].map((m,i)=>
{m}
)} +
+
+ ))} +
+
{countdown}
+ +
Passer
+
+ ); +} + +/* ══ SOLO CS TIMER ══ */ +const HOLD_MS=900; +function SoloCsTimerPanel({player,phase,elapsed,progress,onDown,onUp,user,isDone,isWinner}){ + const {onPD,onPM,onPU,onPC}=useScrollGuard(onDown,onUp,player); + const CIRC=264; + const prog=Math.min(1,progress||0); + const offset=CIRC*(1-prog); + const ph=isDone?(isWinner?"win":"lose"):phase; + const bgCls="scs-panel"+(player===1?" flip":"")+" "+(ph==="ready"?"hold":ph)+"-bg"; + + let stateLbl,stateClr,timeLbl,hint,showRing=false; + if(ph==="win"){stateLbl="VICTOIRE !";stateClr="gold";timeLbl=fmtMs(elapsed);hint=""} + else if(ph==="lose"){stateLbl="PERDU";stateClr="dim";timeLbl=fmtMs(elapsed);hint=""} + else if(phase==="idle"){stateLbl="CS TIMER";stateClr="blu";timeLbl="00.00";hint="Maintenez appuyé"} + else if(phase==="hold"){stateLbl="CHARGEZ…";stateClr="grn";timeLbl=Math.round(prog*100)+"%";hint="Ne relâchez pas !";showRing=true;} + else if(phase==="ready"){stateLbl="RELÂCHEZ !";stateClr="grn";timeLbl="GO";hint="Relâchez pour démarrer";showRing=true;} + else if(phase==="run"){stateLbl="⏱ EN COURS";stateClr="red";timeLbl=fmtMs(elapsed);hint="Appuyez pour stopper";} + else{stateLbl="STOPPÉ";stateClr="dim";timeLbl=fmtMs(elapsed);hint="En attente…";} + + return( +
+
+
{user.avatar}
+
{user.name}
+
+
{stateLbl}
+ {showRing?( +
+ + + + +
{timeLbl}
+
+ ):(phase==="run"&&!isDone)?( +
+ + + + + +
{timeLbl}
+
+ ):( +
{timeLbl}
+ )} + {ph==="win"&&
♛ Gagné !
} + {hint&&
{hint}
} +
+ ); +} + +/* ══ SOLO CS TIMER — one player ══ */ +function SoloCsTimerSolo({user}){ + const [phase,setPhase]=useState("idle"); + const [elapsed,setElapsed]=useState(0); + const [progress,setProgress]=useState(0); + const [history,setHistory]=useState([]); + + const phaseRef=useRef("idle"); + const startRef=useRef(null); + const holdStartRef=useRef(null); + const holdFrameRef=useRef(null); + const frameRef=useRef(null); + + useEffect(()=>{ phaseRef.current=phase; },[phase]); + + // RAF for run + useEffect(()=>{ + if(phase!=="run"){cancelAnimationFrame(frameRef.current);return;} + const loop=()=>{ + setElapsed(performance.now()-startRef.current); + frameRef.current=requestAnimationFrame(loop); + }; + frameRef.current=requestAnimationFrame(loop); + return()=>cancelAnimationFrame(frameRef.current); + },[phase]); + + const onDown=useCallback(()=>{ + const ph=phaseRef.current; + if(ph==="idle"||ph==="stop"){ + // Reset elapsed if coming from stop + if(ph==="stop"){setElapsed(0);} + holdStartRef.current=performance.now(); + setPhase("hold"); + const loop=()=>{ + if(!holdStartRef.current)return; + const prog=Math.min(1,(performance.now()-holdStartRef.current)/HOLD_MS); + setProgress(prog); + if(prog<1){ + holdFrameRef.current=requestAnimationFrame(loop); + } else { + holdStartRef.current=null; + haptic(50); + setPhase("ready"); + } + }; + holdFrameRef.current=requestAnimationFrame(loop); + } else if(ph==="run"){ + // Stop the timer + const el=performance.now()-startRef.current; + cancelAnimationFrame(frameRef.current); + setElapsed(el); + setHistory(prev=>[el,...prev].slice(0,10)); + setPhase("stop"); + haptic([60,40,100]); + } + },[]); + + const onUp=useCallback(()=>{ + const ph=phaseRef.current; + if(ph==="ready"){ + startRef.current=performance.now(); + setPhase("run"); + } else if(ph==="hold"){ + cancelAnimationFrame(holdFrameRef.current); + holdStartRef.current=null; + setProgress(0); + setPhase("idle"); + } + },[]); + + const reset=()=>{ + cancelAnimationFrame(frameRef.current); + cancelAnimationFrame(holdFrameRef.current); + holdStartRef.current=null; + startRef.current=null; + phaseRef.current="idle"; + setPhase("idle"); + setElapsed(0); + setProgress(0); + }; + + const CIRC=264; + const prog=Math.min(1,progress); + const offset=CIRC*(1-prog); + const best=history.length>0?Math.min(...history):null; + + let stateLbl,stateClr,timeDisplay,hint; + if(phase==="idle"){stateLbl="CS TIMER";stateClr="blu";timeDisplay=user.avatar;hint="Maintenez appuyé";} + else if(phase==="hold"){stateLbl="CHARGEZ…";stateClr="grn";timeDisplay=Math.round(prog*100)+"%";hint="Ne relâchez pas !";} + else if(phase==="ready"){stateLbl="RELÂCHEZ !";stateClr="wht";timeDisplay="GO";hint="Relâchez pour démarrer";} + else if(phase==="run"){stateLbl="⏱ EN COURS";stateClr="red";timeDisplay=fmtMs(elapsed);hint="Appuyez pour stopper";} + else{stateLbl="✓ TEMPS";stateClr="gold";timeDisplay=fmtMs(elapsed);hint="Utilisez ▶ Rejouer en bas";} + + const bgStyle=phase==="idle"?"scs-solo-panel idle-bg":phase==="hold"||phase==="ready"?"scs-solo-panel hold-bg":phase==="run"?"scs-solo-panel run-bg":"scs-solo-panel stop-bg"; + + const {onPD,onPM,onPU,onPC}=useScrollGuard(onDown,onUp); + + const isStop = phase==="stop"; + + return( +
+ +
+
{user.avatar}
+
{user.name}
+
+ +
{stateLbl}
+ + {(phase==="hold"||phase==="ready")?( +
+ + + + +
+ {timeDisplay} +
+
+ ):( +
{timeDisplay}
+ )} + +
{hint}
+ + {best!==null&&phase!=="hold"&&phase!=="ready"&&( +
e.stopPropagation()}> +
Meilleur
+
{fmtMs(best)}
+ {history.length>1&&( + <> +
Moy.
+
+ {fmtMs(history.reduce((a,b)=>a+b,0)/history.length)} +
+ + )} +
+ )} + + {history.length>0&&phase==="stop"&&( +
e.stopPropagation()} + onPointerMove={e=>e.stopPropagation()} + onPointerUp={e=>e.stopPropagation()}> + {history.slice(0,10).map((t,i)=>( +
+
#{i+1}
+
{fmtMs(t)}
+ {t===best&&
BEST
} +
+ ))} +
+ )} + +
e.stopPropagation()}> + {phase==="stop"&&( + + )} + + {history.length>0&&} +
+
+ ); +} + +function SoloCsTimer({p0,p1,onClose}){ + const [mode,setMode]=useState("solo"); + const [phases,setPhases]=useState(["idle","idle"]); + const [elapsed,setElapsed]=useState([0,0]); + const [progress,setProgress]=useState([0,0]); + const [done,setDone]=useState(false); + const [winner,setWinner]=useState(null); + + const phaseRef=useRef(["idle","idle"]); + const startRef=useRef([null,null]); + const holdStartRef=useRef([null,null]); + const holdFrameRef=useRef([null,null]); + const elRef=useRef([0,0]); + const doneRef=useRef(false); + const frameRef=useRef(null); + + useEffect(()=>{ phaseRef.current=phases; },[phases]); + + useEffect(()=>{ + if(done){cancelAnimationFrame(frameRef.current);return;} + const loop=()=>{ + const now=performance.now(); + setElapsed(prev=>{ + const n=[...prev]; + for(let p=0;p<2;p++) + if(phaseRef.current[p]==="run"&&startRef.current[p]!==null) + n[p]=now-startRef.current[p]; + return n; + }); + frameRef.current=requestAnimationFrame(loop); + }; + frameRef.current=requestAnimationFrame(loop); + return()=>cancelAnimationFrame(frameRef.current); + },[done]); + + const finalize=useCallback((e0,e1)=>{ + if(doneRef.current)return; + doneRef.current=true; + cancelAnimationFrame(frameRef.current); + const w=e0<=e1?0:1; + setWinner(w);setDone(true); + setPhases(prev=>{const n=[...prev];n[w]="win";n[w===0?1:0]="lose";return n;}); + },[]); + + const onDown=useCallback((player)=>{ + if(doneRef.current)return; + const ph=phaseRef.current[player]; + if(ph==="idle"){ + holdStartRef.current[player]=performance.now(); + setPhases(prev=>{const n=[...prev];n[player]="hold";return n;}); + const loop=()=>{ + if(!holdStartRef.current[player])return; + const prog=Math.min(1,(performance.now()-holdStartRef.current[player])/HOLD_MS); + setProgress(prev=>{const n=[...prev];n[player]=prog;return n;}); + if(prog<1){ + holdFrameRef.current[player]=requestAnimationFrame(loop); + } else { + holdStartRef.current[player]=null; + haptic(50); + setPhases(prev=>{const n=[...prev];n[player]="ready";return n;}); + } + }; + holdFrameRef.current[player]=requestAnimationFrame(loop); + } else if(ph==="run"){ + const el=performance.now()-startRef.current[player]; + startRef.current[player]=null; + elRef.current[player]=el; + setElapsed(prev=>{const n=[...prev];n[player]=el;return n;}); + const other=player===0?1:0; + const otherPh=phaseRef.current[other]; + setPhases(prev=>{const n=[...prev];n[player]="stop";return n;}); + if(otherPh==="stop"){ + finalize(player===0?el:elRef.current[0],player===1?el:elRef.current[1]); + } + } + },[finalize]); + + const onUp=useCallback((player)=>{ + const ph=phaseRef.current[player]; + if(ph==="ready"){ + startRef.current[player]=performance.now(); + setPhases(prev=>{const n=[...prev];n[player]="run";return n;}); + } else if(ph==="hold"){ + cancelAnimationFrame(holdFrameRef.current[player]); + holdStartRef.current[player]=null; + setProgress(prev=>{const n=[...prev];n[player]=0;return n;}); + setPhases(prev=>{const n=[...prev];n[player]="idle";return n;}); + } + },[]); + + const reset=()=>{ + cancelAnimationFrame(frameRef.current); + [0,1].forEach(p=>{ + cancelAnimationFrame(holdFrameRef.current[p]); + holdStartRef.current[p]=null; + startRef.current[p]=null; + }); + doneRef.current=false; + phaseRef.current=["idle","idle"]; + elRef.current=[0,0]; + setPhases(["idle","idle"]); + setElapsed([0,0]); + setProgress([0,0]); + setDone(false); + setWinner(null); + }; + + return( +
+ {/* Mode toggle header */} +
+
e.stopPropagation()}> +
{setMode("solo");reset();}}>Solo
+
{setMode("duel");reset();}}>Duel
+
+
+ + {mode==="solo"?( + + ):( + <> + +
VS
+ +
e.stopPropagation()}> + + +
+ + )} + + {/* Close button always visible in solo mode */} + {mode==="solo"&&( +
e.stopPropagation()}> + +
+ )} +
+ ); +} + +/* ══ TOURNAMENT ══ */ +function buildRound(players){ + return players.reduce((acc,_,i)=>i%2===0?[...acc,{p1:players[i],p2:players[i+1]||null,winner:null}]:acc,[]); +} +function TournamentScreen({onClose,onStartMatch}){ + const [size,setSize]=useState(4); + const DEFAULT_NAMES=["Joueur 1","Joueur 2","Joueur 3","Joueur 4","Joueur 5","Joueur 6","Joueur 7","Joueur 8"]; + const [names,setNames]=useState(DEFAULT_NAMES); + const [editIdx,setEditIdx]=useState(null); + const [bracket,setBracket]=useState(null); + const [phase,setPhase]=useState("setup"); + const setName=(i,v)=>setNames(prev=>{const n=[...prev];n[i]=v||DEFAULT_NAMES[i];return n;}); + const start=()=>{ + setEditIdx(null); + const players=names.slice(0,size).map((n,i)=>({name:n||DEFAULT_NAMES[i],id:i})); + setBracket({rounds:[buildRound(players)],currentRound:0,champion:null}); + setPhase("bracket"); + }; + const recordWin=(ri,mi,bracketWinnerId)=>{ + setBracket(prev=>{ + const b=JSON.parse(JSON.stringify(prev)); + b.rounds[ri][mi].winner=bracketWinnerId; + const round=b.rounds[ri]; + if(round.every(m=>m.winner!==null)){ + const winners=round.map(m=>m.p1&&m.p1.id===m.winner?m.p1:m.p2); + if(winners.length>1){b.rounds.push(buildRound(winners));b.currentRound=ri+1;} + else{b.champion=winners[0];} + } + return b; + }); + }; + const champion=bracket&&bracket.champion; + const totalRounds=Math.log2(size); + const getRoundName=(ri)=>{ + const rem=totalRounds-ri; + if(rem===1)return"Finale"; + if(rem===2)return"Demi-finales"; + if(rem===3)return"Quarts de finale"; + return"Tour "+(ri+1); + }; + return( +
+
+
Tournoi
+
+
+
+ {phase==="setup"&&( +
+
+
Nombre de joueurs
+
+ {[4,8].map(s=>( +
setSize(s)}> + {s+" joueurs"} +
+ ))} +
+
+
+
Joueurs — appuyez pour renommer
+
+ {names.slice(0,size).map((n,i)=>( +
setEditIdx(i)}> +
{i+1}
+ + {editIdx===i + ?setName(i,e.target.value)} + onBlur={()=>setEditIdx(null)} + onPointerDown={e=>e.stopPropagation()}/> + :
{n}
+ } +
+ ))} +
+
+ +
+ )} + {phase==="bracket"&&bracket&&( +
+ {champion&&( +
+
🏆
+
{"Champion : "+champion.name}
+
Tournoi terminé !
+ +
+ )} + {bracket.rounds.map((round,ri)=>( +
+
{getRoundName(ri)}
+ {round.map((match,mi)=>{ + const isDone=match.winner!==null; + const isActive=ri===bracket.currentRound&&!isDone&&!champion; + return( +
+ {[match.p1,match.p2].map((pl,pi)=>pl?( +
+
{pl.name}
+ {match.winner===pl.id&&} +
{match.winner===pl.id?"1":isDone?"0":"-"}
+
+ ):( +
+
En attente…
+
-
+
+ ))} + {isActive&&match.p1&&match.p2&&( +
{ + onStartMatch(match,(localW)=>recordWin(ri,mi,localW===0?match.p1.id:match.p2.id)); + }}>▶ Jouer ce match
+ )} + {isActive&&(!match.p1||!match.p2)&&( +
En attente des matchs précédents
+ )} +
+ ); + })} +
+ ))} +
+ )} +
+
+ ); +} + +/* ══ HISTORY ══ */ +function HistoryScreen({history,onClose,onClear}){ + return( +
+
+
Historique
+
+
+
+ {history.length===0?( +
+
🕐
+
Aucune partie
+
+ ):( + <> + {[...history].reverse().map((g,idx)=>( +
+
+
{fmtDate(g.ts)}
+
{"J"+(g.winner+1)+" Gagne"}
+
+
+ {[0,1].map(p=>( +
+
+ +
{"J"+(p+1)}
+ {g.winner===p&&} +
+
{fmt(g.times[p])}
+
{"Cube x"+g.cubeWins[p]+(g.penaltyTotal&&g.penaltyTotal[p]>0?" • -"+g.penaltyTotal[p]+"s pénalité":"")}
+
+ ))} +
+
+
{g.moves+" coups • "+g.csCount+" CS"}
+
{g.timeLabel+" • "+g.why}
+
+
+ ))} +
+ +
+ + )} +
+
+ ); +} + +/* ══ CONFIG SCREEN ══ */ +function ConfigScreen({onStart,history,onShowHistory,onShowTournament,onShowUsers,onShowSoloCsTimer,rules,onRulesChange,users,selIds}){ + const [sel,setSel]=useState(2); + const [sct,setSct]=useState(7); + const [showModal,setShowModal]=useState(false); + const [cBase,setCBase]=useState(300); + const [cInc,setCInc]=useState(0); + const sp=(set,d,mn,mx)=>set(v=>Math.max(mn,Math.min(mx,v+d))); + const isCustom=sel===-1; + const activeBase=isCustom?cBase:PRESETS[sel].base; + const activeInc=isCustom?cInc:PRESETS[sel].inc; + const cMins=Math.floor(cBase/60); + const cSecs=cBase%60; + const tog=(k)=>onRulesChange({...rules,[k]:!rules[k]}); + const u0=users.find(u=>u.id===selIds[0])||users[0]; + const u1=users.find(u=>u.id===selIds[1])||users[1]; + return( +
+
+
CHESSCUBING CLOCK
+
logo
Speed Challenge Edition
+
+
+
{u0.avatar}
+ {u0.name} +
+
+ 👥 + Joueurs +
+
+ 📋 + {"("+history.length+")"} +
+
+ 🏆 + Tournoi +
+
+ + CS Timer +
+
+ {u1.name} +
{u1.avatar}
+
+
+
+
+
CS Timer — tous les
+
+
+
+
⚡ CS Timer activé
+
Résolvez un cube Rubik's tous les N coups pour gagner x2
+
+
tog("csEnabled")}>
+
+ {rules.csEnabled&&( +
+
Coups avant CS Timer
+
+
sp(setSct,-1,2,20)}>−
+
{sct}
+
sp(setSct,+1,2,20)}>+
+
+
+ )} +
+
Règles de gameplay
+
+
+
+
⚔ Coups interdits
+
Double coup : prise de dame interdite
+
+
tog("forbidCapture")}>
+
+
+
+
{"⏳ Malus de temps ("+rules.moveLimitSec+"s / -"+rules.penaltySec+"s)"}
+
Dépasser la limite par coup = pénalité
+
+
tog("penaltyEnabled")}>
+
+ {rules.penaltyEnabled&&( +
+
+
Limite/coup
+
+
onRulesChange({...rules,moveLimitSec:Math.max(5,rules.moveLimitSec-5)})}>−
+
{rules.moveLimitSec+"s"}
+
onRulesChange({...rules,moveLimitSec:Math.min(60,rules.moveLimitSec+5)})}>+
+
+
+
+
Pénalité
+
+
onRulesChange({...rules,penaltySec:Math.max(5,rules.penaltySec-5)})}>−
+
{rules.penaltySec+"s"}
+
onRulesChange({...rules,penaltySec:Math.min(60,rules.penaltySec+5)})}>+
+
+
+
+ )} + {rules.csEnabled&&( +
+
+
🎲 Mode Chaos
+
Seuil CS Timer aléatoire à chaque cycle
+
+
tog("chaosMode")}>
+
+ )} + {rules.csEnabled&&( +
+
+
🧩 Préparation Rubik's
+
Mélange officiel à mémoriser avant la partie
+
+
tog("cubePrepEnabled")}>
+
+ )} +
+
Expérience
+
+
+
+
🔊 Sons
+
Tic-tac, swoosh CS Timer, fanfare victoire
+
+
tog("soundEnabled")}>
+
+
+
+
📳 Vibrations
+
Retour haptique sur chaque action
+
+
tog("hapticEnabled")}>
+
+
+
Temps de jeu
+
setShowModal(true)}> +
+
{isCustom?buildPreview(cMins,cSecs,cInc):"✦ Temps personnalisé"}
+ {isCustom&&
{cInc>0?"+ "+cInc+" sec/coup":"Sans incrément"}{" · Perso"}
} +
+ {isCustom?"✏":"›"} +
+ {["BULLET","BLITZ","RAPID","CLASSIC"].map(tag=>( +
+
{tag}
+
+ {PRESETS.filter(p=>p.tag===tag).map(p=>{ + const i=PRESETS.indexOf(p); + let _sy=null; + return( +
{_sy=e.clientY;}} + onPointerUp={e=>{if(_sy!==null&&Math.abs(e.clientY-_sy)<10){setSel(i);}_sy=null;}}> +
+ {p.base<60?p.base+"s":Math.floor(p.base/60)+" min"} + {p.inc>0&&{"+ "+p.inc+" sec/coup"}} +
+ +
+ ); + })} +
+
+ ))} +
+ {"⚡ Après "+sct+" coups — CS Timer — le gagnant joue x2"} +
+
+
+ +
+ {showModal&&( + {setCBase(base);setCInc(inc);setSel(-1);setShowModal(false);}} + onCancel={()=>setShowModal(false)}/> + )} +
+ ); +} + +/* ══ MOVE DOTS ══ */ +function Dots({moves,sct}){ + const inC=moves===0?0:((moves-1)%sct)+1; + const onT=moves>0&&moves%sct===0; + return( +
+ {Array.from({length:sct},(_,i)=>{ + const filled=i<(onT?sct:inC); + return
; + })} +
+ ); +} + +/* ══ CS OVERLAY ══ */ +function CSOverlay({phase,elapsed,isDone,isWinner,onDown,onUp,player,holdProgress}){ + const {onPD,onPM,onPU,onPC}=useScrollGuard(onDown,onUp,player); + const CIRC=264; // circumference for r=42 circle + const prog=Math.min(1,holdProgress||0); + const offset=CIRC*(1-prog); + + let ts,tc,ls,lc,hint; + if(isDone){ts=fmtMs(elapsed);tc=isWinner?"win":"lose";ls=isWinner?"Victoire !":"Éliminé";lc=isWinner?"grn":"dim";hint=isWinner?"2 coups consécutifs !":"";} + else if(phase==="idle"){ts="0.00";tc="idle";ls="CS TIMER";lc="blu";hint="Maintenez appuyé";} + else if(phase==="hold"){ts="0.00";tc="hold";ls="CHARGEZ…";lc="grn";hint="Ne relâchez pas !";} + else if(phase==="ready"){ts="GO";tc="hold";ls="RELÂCHEZ !";lc="grn";hint="Relâchez pour démarrer";} + else if(phase==="run"){ts=fmtMs(elapsed);tc="run";ls="⏱";lc="red";hint="Appuyez pour stopper";} + else{ts=fmtMs(elapsed);tc="stop";ls="Stoppé";lc="dim";hint="En attente…";} + const bgc=isDone?(isWinner?"win":"lose"):phase==="ready"?"hold":phase; + + return( +
+
+
{ls}
+ + {(phase==="hold"||phase==="ready")&&!isDone&&( +
+ + + + +
+ {phase==="ready"?"GO":Math.round(prog*100)+"%"} +
+
+ )} + + {phase==="run"&&!isDone&&( +
+ + + + + +
{ts}
+
+ )} + + {phase!=="hold"&&phase!=="ready"&&phase!=="run"&&( +
{ts}
+ )} + + {isDone&&isWinner?
{hint}
:hint?
{hint}
:null} +
+ ); +} + +/* ══ DOUBLE MOVE OVERLAY ══ */ +function DblOverlay({isWinner,movesLeft,forbidCapture,showChallenge}){ + return( +
+ {isWinner?
:
} + {isWinner?( +
+
+
Double Coup !
+ {forbidCapture&&
⚔ Prise de dame interdite
} +
+ {[0,1].map(i=>
=movesLeft?" used":"")}/>)} +
+
Appuyez pour jouer
+
+ ):( +
+
+
Adversaire joue 2 coups…
+
Attendez votre tour
+ {forbidCapture&&showChallenge&&( +
⚔ Appuyez pour contester une prise
+ )} +
+ )} +
+ ); +} + +/* ══ GAME SCREEN ══ */ +function GameScreen({initTime,inc,sct,timeLabel,onBack,onGameEnd,rules,playerNames,tournamentCb,profiles}){ + const [times,setTimes]=useState([initTime,initTime]); + const [active,setActive]=useState(null); + const [moves,setMoves]=useState(0); + const [paused,setPaused]=useState(false); + const [scOn,setScOn]=useState(false); + const [csPhase,setCsPhase]=useState(["idle","idle"]); + const [csEl,setCsEl]=useState([0,0]); + const [scDone,setScDone]=useState(false); + const [scWin,setScWin]=useState(null); + const [dbl,setDbl]=useState(false); + const [dblP,setDblP]=useState(null); + const [dblL,setDblL]=useState(2); + const [over,setOver]=useState(false); + const [winner,setWinner]=useState(null); + const [why,setWhy]=useState(""); + const [ripple,setRipple]=useState([null,null]); + const [cubeWins,setCubeWins]=useState([0,0]); + const [pendingWin,setPendingWin]=useState(null); + const [penaltyFlash,setPenaltyFlash]=useState(null); + const [totalPenalty,setTotalPenalty]=useState([0,0]); + const [moveTimer,setMoveTimer]=useState(0); + const [showForbidden,setShowForbidden]=useState(null); + const [showChallenge,setShowChallenge]=useState(false); + const [snapshots,setSnapshots]=useState([]); + const [currentSct,setCurrentSct]=useState(sct); + const [eloDelta,setEloDelta]=useState([0,0]); + + const clockRef=useRef(null),frameRef=useRef(null),startRef=useRef([null,null]); + const doneRef=useRef(false),phaseRef=useRef(["idle","idle"]),elRef=useRef([0,0]); + const movesRef=useRef(0),cubeWinsRef=useRef([0,0]),csCountRef=useRef(0); + const penaltyRef=useRef([0,0]),pendingTimerRef=useRef(null); + const moveStartRef=useRef(null),moveFrameRef=useRef(null); + const timesRef=useRef([initTime,initTime]); + const currentSctRef=useRef(sct); + const holdStartRef=useRef([null,null]); + const holdFrameRef=useRef([null,null]); + + const HOLD_DURATION=900; // ms to fill the circle + + const [holdProgress,setHoldProgress]=useState([0,0]); + + useEffect(()=>{phaseRef.current=csPhase;},[csPhase]); + useEffect(()=>{elRef.current=csEl;},[csEl]); + useEffect(()=>{doneRef.current=scDone;},[scDone]); + useEffect(()=>{movesRef.current=moves;},[moves]); + useEffect(()=>{cubeWinsRef.current=cubeWins;},[cubeWins]); + useEffect(()=>{csCountRef.current=Math.floor(moves/currentSct);},[moves,currentSct]); + useEffect(()=>{penaltyRef.current=totalPenalty;},[totalPenalty]); + useEffect(()=>{timesRef.current=times;},[times]); + useEffect(()=>{currentSctRef.current=currentSct;},[currentSct]); + + // Challenge hint + useEffect(()=>{ + if(dbl&&rules.forbidCapture){ + const t=setTimeout(()=>setShowChallenge(true),400); + return()=>clearTimeout(t); + } else { setShowChallenge(false); } + },[dbl,rules.forbidCapture]); + + // Tick-tock sounds + useEffect(()=>{ + if(!rules.soundEnabled||active===null||paused||scOn||dbl||over)return; + const id=setInterval(()=>{ if(active===0)playTick(); else playTock(); },1000); + return()=>clearInterval(id); + },[active,paused,scOn,dbl,over,rules.soundEnabled]); + + // Chess clock + useEffect(()=>{ + if(active===null||paused||scOn||dbl||over){clearInterval(clockRef.current);return;} + clockRef.current=setInterval(()=>{ + setTimes(prev=>{ + const n=[...prev]; + n[active]=Math.max(0,n[active]-0.1); + if(n[active]<=0){ + const w=active===0?1:0; + const d=[calcElo(profiles.p0.elo,profiles.p1.elo,w===0?1:0),calcElo(profiles.p1.elo,profiles.p0.elo,w===1?1:0)]; + setEloDelta(d); + setOver(true);setWinner(w);setWhy("Temps écoulé"); + if(rules.soundEnabled)playFanfare(); + if(rules.hapticEnabled)haptic([50,50,150]); + if(onGameEnd)onGameEnd({winner:w,times:n,moves:movesRef.current,csCount:csCountRef.current,cubeWins:cubeWinsRef.current,timeLabel,ts:Date.now(),penaltyTotal:penaltyRef.current,why:"Temps écoulé",snapshots,eloDelta:d,names:[profiles.p0.name,profiles.p1.name],avatars:[profiles.p0.avatar,profiles.p1.avatar]}); + if(tournamentCb)tournamentCb(w); + } + return n; + }); + },100); + return()=>clearInterval(clockRef.current); + },[active,paused,scOn,dbl,over]); + + // Move timer bar + useEffect(()=>{ + if(!rules.penaltyEnabled||active===null||paused||scOn||dbl||over){cancelAnimationFrame(moveFrameRef.current);return;} + moveStartRef.current=performance.now(); + const loop=()=>{setMoveTimer((performance.now()-moveStartRef.current)/1000);moveFrameRef.current=requestAnimationFrame(loop);}; + moveFrameRef.current=requestAnimationFrame(loop); + return()=>cancelAnimationFrame(moveFrameRef.current); + },[active,paused,scOn,dbl,over,rules.penaltyEnabled]); + + // CS Timer RAF + useEffect(()=>{ + if(!scOn||scDone){cancelAnimationFrame(frameRef.current);return;} + const loop=()=>{ + const now=performance.now(); + setCsEl(prev=>{const n=[...prev];for(let p=0;p<2;p++)if(phaseRef.current[p]==="run"&&startRef.current[p]!==null)n[p]=now-startRef.current[p];return n;}); + frameRef.current=requestAnimationFrame(loop); + }; + frameRef.current=requestAnimationFrame(loop); + return()=>cancelAnimationFrame(frameRef.current); + },[scOn,scDone]); + + const finalize=useCallback((e0,e1)=>{ + if(doneRef.current)return; + doneRef.current=true; + const w=e0<=e1?0:1; + setScDone(true);setScWin(w); + cancelAnimationFrame(frameRef.current); + setCubeWins(prev=>{const n=[...prev];n[w]++;return n;}); + if(rules.soundEnabled)playSwoop(); + if(rules.hapticEnabled)haptic([30,20,80]); + const nextSct=rules.chaosMode?Math.max(2,sct+Math.floor(Math.random()*5)-1):sct; + setTimeout(()=>{ + setScOn(false);setCsPhase(["idle","idle"]);setCsEl([0,0]); + startRef.current=[null,null];setScDone(false);doneRef.current=false;setScWin(null); + setCurrentSct(nextSct); + setDbl(true);setDblP(w);setDblL(2);setActive(w); + },1200); + },[rules,sct]); + + const csDown=useCallback((player)=>{ + if(!scOn||doneRef.current)return; + const ph=phaseRef.current[player]; + if(ph==="idle"){ + holdStartRef.current[player]=performance.now(); + setCsPhase(prev=>{const n=[...prev];n[player]="hold";return n;}); + const loop=()=>{ + if(!holdStartRef.current[player])return; + const prog=Math.min(1,(performance.now()-holdStartRef.current[player])/900); + setHoldProgress(prev=>{const n=[...prev];n[player]=prog;return n;}); + if(prog<1){ + holdFrameRef.current[player]=requestAnimationFrame(loop); + } else { + // Circle full → ready to launch on release + holdStartRef.current[player]=null; + if(rules.hapticEnabled)haptic([60,40,60]); + setCsPhase(prev=>{const n=[...prev];n[player]="ready";return n;}); + } + }; + holdFrameRef.current[player]=requestAnimationFrame(loop); + } + else if(ph==="run"){ + const el=performance.now()-startRef.current[player]; + startRef.current[player]=null; + setCsEl(prev=>{const n=[...prev];n[player]=el;return n;}); + setCsPhase(prev=>{const n=[...prev];n[player]="stop";const ot=player===0?1:0;if(n[ot]==="stop")finalize(player===0?el:elRef.current[0],player===1?el:elRef.current[1]);return n;}); + } + },[scOn,finalize,rules.hapticEnabled]); + + const csUp=useCallback((player)=>{ + if(!scOn||doneRef.current)return; + const ph=phaseRef.current[player]; + if(ph==="ready"){ + // Released after full → start timer now + startRef.current[player]=performance.now(); + setCsPhase(prev=>{const n=[...prev];n[player]="run";return n;}); + } else if(ph==="hold"){ + // Released before full → cancel + cancelAnimationFrame(holdFrameRef.current[player]); + holdStartRef.current[player]=null; + setHoldProgress(prev=>{const n=[...prev];n[player]=0;return n;}); + setCsPhase(prev=>{const n=[...prev];n[player]="idle";return n;}); + } + },[scOn]); + + const fireRipple=(p)=>setRipple(prev=>{const n=[...prev];n[p]=Date.now();return n;}); + + const applyPenalty=useCallback((player)=>{ + const pen=rules.penaltySec; + setTimes(prev=>{const n=[...prev];n[player]=Math.max(0,n[player]-pen);return n;}); + setTotalPenalty(prev=>{const n=[...prev];n[player]+=pen;return n;}); + setPenaltyFlash(player); + if(rules.soundEnabled)playWarn(); + if(rules.hapticEnabled)haptic([50,30,50,30,80]); + setTimeout(()=>setPenaltyFlash(null),800); + },[rules]); + + const handlePress=useCallback((player)=>{ + if(over||scOn)return; + if(rules.hapticEnabled)haptic(50); + fireRipple(player); + if(active===null){setActive(player===0?1:0);setMoves(1);return;} + if(dbl&&dblP!==null){ + if(player!==dblP){ + if(rules.forbidCapture){ + setShowChallenge(false); + setShowForbidden(player); + if(rules.hapticEnabled)haptic([80,40,80]); + setTimeout(()=>setShowForbidden(null),2000); + setDbl(false);setDblP(null);setActive(player); + } + return; + } + if(active!==player)return; + const left=dblL-1;setDblL(left); + if(left===0){setDbl(false);setDblP(null);setActive(player===0?1:0);} + return; + } + if(active!==player)return; + if(rules.penaltyEnabled&&moveStartRef.current!==null){ + const elapsed=(performance.now()-moveStartRef.current)/1000; + if(elapsed>rules.moveLimitSec)applyPenalty(player); + } + cancelAnimationFrame(moveFrameRef.current);setMoveTimer(0); + const nm=moves+1;setMoves(nm); + setSnapshots(prev=>[...prev,{move:nm,t0:timesRef.current[0],t1:timesRef.current[1]}]); + if(inc>0)setTimes(prev=>{const n=[...prev];n[player]+=inc;return n;}); + if(rules.soundEnabled){if(player===0)playTick();else playTock();} + setActive(player===0?1:0); + if(rules.csEnabled&&nm%currentSctRef.current===0){ + doneRef.current=false;startRef.current=[null,null]; + setScOn(true);setCsPhase(["idle","idle"]);setCsEl([0,0]);setScDone(false);setScWin(null); + if(rules.soundEnabled)playSwoop(); + if(rules.hapticEnabled)haptic([50,40,50,40,50]); + } + },[active,over,scOn,dbl,dblP,dblL,moves,inc,rules,applyPenalty]); + + const declareWinner=useCallback((player)=>{ + if(over||scOn)return; + if(pendingWin===player){ + clearTimeout(pendingTimerRef.current);setPendingWin(null); + clearInterval(clockRef.current); + setOver(true);setWinner(player);setWhy("Échec et mat"); + const d=[calcElo(profiles.p0.elo,profiles.p1.elo,player===0?1:0),calcElo(profiles.p1.elo,profiles.p0.elo,player===1?1:0)]; + setEloDelta(d); + if(rules.soundEnabled)playFanfare(); + if(rules.hapticEnabled)haptic([50,50,150]); + if(onGameEnd)onGameEnd({winner:player,times,moves:movesRef.current,csCount:csCountRef.current,cubeWins:cubeWinsRef.current,timeLabel,ts:Date.now(),penaltyTotal:penaltyRef.current,why:"Échec et mat",snapshots,eloDelta:d,names:[profiles.p0.name,profiles.p1.name],avatars:[profiles.p0.avatar,profiles.p1.avatar]}); + if(tournamentCb)tournamentCb(player); + } else { + clearTimeout(pendingTimerRef.current);setPendingWin(player); + pendingTimerRef.current=setTimeout(()=>setPendingWin(null),3000); + } + },[over,scOn,pendingWin,times,timeLabel,onGameEnd,tournamentCb,rules,profiles,snapshots]); + + const reset=useCallback(()=>{ + clearInterval(clockRef.current);cancelAnimationFrame(frameRef.current); + cancelAnimationFrame(moveFrameRef.current);clearTimeout(pendingTimerRef.current); + cancelAnimationFrame(holdFrameRef.current[0]);cancelAnimationFrame(holdFrameRef.current[1]); + holdStartRef.current=[null,null];holdFrameRef.current=[null,null]; + startRef.current=[null,null];doneRef.current=false; + setTimes([initTime,initTime]);timesRef.current=[initTime,initTime]; + setActive(null);setMoves(0);setPaused(false); + setScOn(false);setCsPhase(["idle","idle"]);setCsEl([0,0]);setScDone(false);setScWin(null); + setDbl(false);setDblP(null);setDblL(2);setCurrentSct(sct); + setOver(false);setWinner(null);setWhy("");setRipple([null,null]); + setCubeWins([0,0]);setPendingWin(null);setPenaltyFlash(null); + setTotalPenalty([0,0]);setMoveTimer(0);setShowForbidden(null);setShowChallenge(false); + setSnapshots([]);setEloDelta([0,0]);setHoldProgress([0,0]); + },[initTime,sct]); + + const [t0,t1]=times; + const p0on=active===0&&!scOn&&!dbl; + const p1on=active===1&&!scOn&&!dbl; + const divLabel=over?"FIN":paused?"PAUSE":scOn?"CS TIMER":dbl?"x2":active===null?"APPUYEZ":"J"+(active+1)+" JOUE"; + const pName=playerNames||(profiles?[profiles.p0.name,profiles.p1.name]:["Joueur 1","Joueur 2"]); + const pAv=profiles?[profiles.p0.avatar,profiles.p1.avatar]:["♟","♞"]; + const pStr=profiles?[profiles.p0.streak||0,profiles.p1.streak||0]:[0,0]; + const mtPct=rules.penaltyEnabled&&active!==null&&!scOn&&!dbl?Math.min(100,(moveTimer/rules.moveLimitSec)*100):0; + const mtCls=mtPct>=100?"crit":mtPct>=75?"warn":""; + + return( +
+ {/* Player 2 — top, flipped */} +
{if(!scOn)handlePress(1);}}> {ripple[1]&&
} + {scOn&&} + {dbl&&} + {showForbidden===1&&( +
+
+
Contestation !
+
Double coup annulé — prise de dame
+
+ )} +
{fmt(t1)}
+ {rules.penaltyEnabled&&p1on&&
} + {rules.csEnabled&&!scOn&&!dbl&&} + {rules.chaosMode&&!scOn&&!dbl&&
🎲 Chaos
} + {p1on&&
} + {totalPenalty[1]>0&&
{"-"+totalPenalty[1]+"s"}
} +
+ +
{pAv[1]}
+
{pName[1]}
+ {pStr[1]>=2&&
{"🔥 "+pStr[1]}
} + {"x"+cubeWins[1]} +
+ {penaltyFlash===1&&
} +
+ + {/* Divider + Win buttons */} +
+ {!over&&( +
{e.stopPropagation();declareWinner(1);}}> +
+
+
{pendingWin===1?"Confirmer":pName[1]+" ♛"}
+ {pendingWin===1&&
Retaper ✓
} +
+
+ )} +
+
{moves===0?"PRÊT":"COUP "+moves}
+
{divLabel}
+
+ {!over&&( +
{e.stopPropagation();declareWinner(0);}}> +
+
+
{pendingWin===0?"Confirmer":pName[0]+" ♛"}
+ {pendingWin===0&&
Retaper ✓
} +
+
+ )} +
+ + {/* Player 1 — bottom */} +
{if(!scOn)handlePress(0);}}> + {ripple[0]&&
} + {scOn&&} + {dbl&&} + {showForbidden===0&&( +
+
+
Contestation !
+
Double coup annulé — prise de dame
+
+ )} + {p0on&&
} +
{fmt(t0)}
+ {rules.penaltyEnabled&&p0on&&
} + {rules.csEnabled&&!scOn&&!dbl&&} + {rules.chaosMode&&!scOn&&!dbl&&
🎲 Chaos
} + {totalPenalty[0]>0&&
{"-"+totalPenalty[0]+"s"}
} +
+ {"x"+cubeWins[0]} + {pStr[0]>=2&&
{"🔥 "+pStr[0]}
} +
{pName[0]}
+
{pAv[0]}
+ +
+ {penaltyFlash===0&&
} +
+ + {/* Controls */} + {!over&&( +
+ + + +
+ )} + + {/* End */} + {over&&( +
+
+
{pName[winner||0]+" Gagne !"}
+
+ {[0,1].map(p=>( +
+
{pAv[p]}
+ +
{p===winner?"♛ Victoire":pName[p]}
+
{"Cube x"+cubeWins[p]}
+ {totalPenalty[p]>0&&
{"−"+totalPenalty[p]+"s pén."}
} +
=0?"pos":"neg")}>{(eloDelta[p]>=0?"+":"")+eloDelta[p]+" ELO"}
+ {pStr[p]>=2&&
{"🔥 Streak "+pStr[p]}
} +
+ ))} +
+
{why}
+ +
+
{moves}
Coups
+
{Math.floor(moves/currentSct)||0}
CS Timer
+
{fmt(times[0])}
J1 Restant
+
{fmt(times[1])}
J2 Restant
+
+
+ + +
+
+ )} +
+ ); +} + +/* ══ ROOT ══ */ +function ChessClockApp(){ + + // Set favicon + useEffect(()=>{ + const link=document.querySelector("link[rel*='icon']")||document.createElement("link"); + link.type="image/png";link.rel="shortcut icon"; + link.href="data:image/png;base64,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"; + document.getElementsByTagName("head")[0].appendChild(link); + document.title="ChessCubing Clock"; + },[]); + + const [screen,setScreen]=useState("config"); + const [gTime,setGTime]=useState(180); + const [gInc,setGInc]=useState(2); + const [gSct,setGSct]=useState(7); + const [gLabel,setGLabel]=useState("3 min"); + const [showHist,setShowHist]=useState(false); + const [showTrn,setShowTrn]=useState(false); + const [showUsers,setShowUsers]=useState(false); + const [showSelect,setShowSelect]=useState(false); + const [showPrep,setShowPrep]=useState(false); + const [showSoloCs,setShowSoloCs]=useState(false); + const [history,setHistory]=useState([]); + const [trnCb,setTrnCb]=useState(null); + const [trnIds,setTrnIds]=useState(null); // [id0, id1] for tournament matches + const [users,setUsers]=useState(()=>tryLoad("chess_users",DEF_USERS)); + const [selIds,setSelIds]=useState(()=>{ + const u=tryLoad("chess_users",DEF_USERS); + return [u[0]?.id||DEF_USERS[0].id, u[1]?.id||DEF_USERS[1].id]; + }); + const [rules,setRules]=useState({ + forbidCapture:false,penaltyEnabled:false,moveLimitSec:20,penaltySec:10, + chaosMode:false,cubePrepEnabled:false,soundEnabled:true,hapticEnabled:true, + csEnabled:true + }); + + const saveUsers=(list)=>{ setUsers(list); trySave("chess_users",list); }; + + // Derive profile objects for game + const getUser=(id)=>users.find(u=>u.id===id)||users[0]; + const p0=getUser(selIds[0]); + const p1=getUser(selIds[1]); + const profiles={p0,p1}; + + const updateElo=(entry)=>{ + setUsers(prev=>{ + const next=prev.map(u=>{ + const idx=selIds.indexOf(u.id); // 0=J1, 1=J2, -1=not playing + if(idx===-1)return u; + const isWinner=entry.winner===idx; + const delta=entry.eloDelta?entry.eloDelta[idx]:0; + return{...u, + elo:Math.max(100,(u.elo||1200)+delta), + wins:(u.wins||0)+(isWinner?1:0), + games:(u.games||0)+1, + streak:isWinner?(u.streak||0)+1:0, + }; + }); + trySave("chess_users",next); + return next; + }); + }; + + const addGame=(entry)=>{ setHistory(prev=>[...prev,{id:Date.now(),...entry}]); updateElo(entry); }; + + const startTournamentMatch=(match,cb)=>{ + setTrnCb(()=>(lw)=>cb(lw)); + setTrnIds([match.p1.userId||selIds[0], match.p2.userId||selIds[1]]); + setScreen("game");setShowTrn(false); + }; + + // Config → PlayerSelect → (CubePrepScreen) → Game + const onConfigStart=(base,inc,sct,label)=>{ + setGTime(base);setGInc(inc);setGSct(sct);setGLabel(label); + setTrnCb(null);setTrnIds(null); + setShowSelect(true); + }; + + const onPlayersConfirmed=(id0,id1)=>{ + setSelIds([id0,id1]); + setShowSelect(false); + if(rules.cubePrepEnabled){ setShowPrep(true); } + else{ setScreen("game"); } + }; + + const activeSelIds=trnIds||selIds; + const ap0=getUser(activeSelIds[0]); + const ap1=getUser(activeSelIds[1]); + const activeProfiles={p0:ap0,p1:ap1}; + + return( +
+ {showUsers&&setShowUsers(false)}/>} + {showHist&&setShowHist(false)} onClear={()=>setHistory([])}/>} + {showTrn&&setShowTrn(false)} onStartMatch={startTournamentMatch}/>} + {showSelect&&setShowSelect(false)}/>} + {showPrep&&{ setShowPrep(false); setScreen("game"); }}/>} + {showSoloCs&&setShowSoloCs(false)}/>} + {screen==="config" + ?setShowHist(true)} + onShowTournament={()=>setShowTrn(true)} + onShowUsers={()=>setShowUsers(true)} + onShowSoloCsTimer={()=>setShowSoloCs(true)} + rules={rules} + onRulesChange={setRules} + onStart={onConfigStart}/> + :{setScreen("config");if(trnCb){setShowTrn(true);}setTrnCb(null);setTrnIds(null);}} + onGameEnd={addGame}/> + } +
+ ); +} + +document.getElementById("boot-status")?.remove(); + +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render( + + + , +); diff --git a/src/features/chess-clock/chessClock.css b/src/features/chess-clock/chessClock.css new file mode 100755 index 0000000..7813396 --- /dev/null +++ b/src/features/chess-clock/chessClock.css @@ -0,0 +1,536 @@ +@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Oxanium:wght@400;600;700;800&display=swap'); +*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} +:root{ + --bg:#060a0e;--s1:#0d1218;--s2:#131b24;--s3:#1c2632;--bd:#263040; + --gold:#e8b84b;--g2:#ffd97a;--g3:#7a5c1e; + --red:#f04040;--grn:#38e878;--blu:#4b9ee8;--pur:#a855f7; + --txt:#c8d8e8;--dim:#3e5060; +} +html,body,#root{width:100%;height:100%;overflow:hidden;background:var(--bg); + -webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none} +.app{width:100vw;height:100dvh;display:flex;flex-direction:column; + font-family:'Oxanium',sans-serif;background:var(--bg);position:relative;overflow:hidden; + animation:app-in .5s cubic-bezier(.16,1,.3,1) both} +@keyframes app-in{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}} +.app::before{content:'';position:fixed;inset:-20%;width:140%;height:140%; + background:radial-gradient(ellipse 60% 40% at 30% 10%,rgba(232,184,75,.07) 0%,transparent 70%), + radial-gradient(ellipse 50% 50% at 70% 90%,rgba(75,158,232,.05) 0%,transparent 70%); + pointer-events:none;z-index:0;animation:mesh 14s ease-in-out infinite alternate} +@keyframes mesh{from{transform:translate(0,0)}to{transform:translate(2%,1%)}} +.app::after{content:'';position:fixed;inset:0; + background:repeating-linear-gradient(0deg,transparent,transparent 3px,rgba(0,0,0,.06) 3px,rgba(0,0,0,.06) 4px); + pointer-events:none;z-index:999;opacity:.35} +@keyframes shimmer{0%,100%{background-position:0%}50%{background-position:100%}} +@keyframes cfg-in{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}} +@keyframes fade-in{from{opacity:0}to{opacity:1}} +@keyframes pop{0%{transform:scale(0);opacity:0}60%{transform:scale(1.2)}100%{transform:scale(1);opacity:1}} + +/* CONFIG */ +.cfg{width:100%;height:100%;display:flex;flex-direction:column;position:relative;z-index:1;overflow:hidden;animation:cfg-in .45s cubic-bezier(.16,1,.3,1) both} +.cfg-hd{padding:clamp(14px,3.5vh,24px) 20px 8px;text-align:center;flex-shrink:0} +.cfg-logo{font-family:'Bebas Neue',sans-serif;font-size:clamp(28px,7.5vw,44px);letter-spacing:8px; + background:linear-gradient(135deg,var(--g2) 0%,var(--gold) 50%,#b8882b 100%); + -webkit-background-clip:text;background-clip:text;color:transparent;line-height:1; + background-size:200%;animation:shimmer 4s ease-in-out infinite} +.cfg-sub{font-size:9px;letter-spacing:6px;color:var(--dim);text-transform:uppercase;margin-top:3px} +.cfg-hd-row{display:flex;align-items:center;justify-content:center;gap:10px;margin-top:8px;flex-wrap:wrap} +.icon-btn{display:flex;align-items:center;gap:5px;background:var(--s1);border:1px solid var(--bd); + border-radius:10px;padding:5px 10px;cursor:pointer;transition:all .15s} +.icon-btn:active{background:var(--s2);border-color:var(--gold)} +.icon-btn-lbl{font-size:9px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;font-weight:700} +.cfg-body{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 14px 0;-webkit-overflow-scrolling:touch} +.cfg-body::-webkit-scrollbar{display:none} +.cfg-section{font-size:9px;letter-spacing:4px;color:var(--dim);text-transform:uppercase;margin:12px 0 6px 2px} +.cfg-card{background:var(--s1);border:1px solid var(--bd);border-radius:14px;overflow:hidden;margin-bottom:8px;box-shadow:0 4px 24px rgba(0,0,0,.3)} +.cfg-row{display:flex;align-items:center;justify-content:space-between; + padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.04);cursor:pointer; + position:relative;overflow:hidden;transition:background .2s} +.cfg-row:last-child{border-bottom:none} +.cfg-row:active{background:rgba(232,184,75,.04)} +.cfg-row.sel{background:linear-gradient(90deg,rgba(232,184,75,.09),rgba(232,184,75,.03));border-left:2px solid var(--gold)} +.cfg-row-lbl{font-weight:600;font-size:clamp(12px,3.5vw,15px);color:var(--txt);transition:color .2s} +.cfg-row.sel .cfg-row-lbl{color:var(--g2)} +.cfg-chk{color:var(--gold);font-size:15px;opacity:0;transform:scale(0) rotate(-45deg);transition:opacity .25s,transform .35s cubic-bezier(.34,1.56,.64,1)} +.cfg-row.sel .cfg-chk{opacity:1;transform:scale(1) rotate(0deg)} +.cfg-stepper{display:flex;align-items:center;justify-content:space-between;padding:10px 14px} +.cfg-s-lbl{font-weight:600;font-size:clamp(12px,3.3vw,14px);color:var(--txt)} +.cfg-s-val{font-family:'Bebas Neue',sans-serif;font-size:clamp(18px,5vw,26px);color:var(--gold);letter-spacing:2px;min-width:36px;text-align:center} +.cfg-btns{display:flex;gap:5px} +.cfg-btn{width:32px;height:32px;border-radius:9px;background:var(--s2);border:1px solid var(--bd);color:var(--gold);font-size:18px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s,transform .15s} +.cfg-btn:active{background:var(--s3);transform:scale(.88)} +.cfg-ft{padding:8px 14px clamp(14px,3.5vh,24px);flex-shrink:0} +.start-btn{width:100%;padding:clamp(14px,3.8vw,18px);border:none;border-radius:14px;font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,20px);letter-spacing:7px;color:var(--bg);background:linear-gradient(110deg,var(--g3),var(--g2),var(--gold),var(--g2),var(--g3));background-size:250%;cursor:pointer;box-shadow:0 4px 28px rgba(232,184,75,.5),inset 0 1px 0 rgba(255,255,255,.2);text-transform:uppercase;transition:transform .18s cubic-bezier(.34,1.56,.64,1);animation:shimmer 3s ease-in-out infinite} +.start-btn:active{transform:scale(.96)} +.cfg-custom{display:flex;align-items:center;justify-content:space-between;background:var(--s1);border:1px dashed var(--g3);border-radius:12px;padding:11px 16px;margin-bottom:8px;cursor:pointer;transition:background .15s,border-color .15s} +.cfg-custom:active{background:var(--s2);border-color:var(--gold)} +.cfg-custom.c-on{border-style:solid;border-color:var(--gold);background:rgba(232,184,75,.06)} +.cfg-c-main{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,3.8vw,18px);letter-spacing:3px;color:var(--gold)} +.cfg-c-detail{font-size:9px;letter-spacing:1px;color:var(--dim);margin-top:2px} +.cfg-c-arrow{font-size:17px;color:var(--dim)} +.toggle-row{display:flex;align-items:center;justify-content:space-between;padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.04)} +.toggle-row:last-child{border-bottom:none} +.toggle-lbl{display:flex;flex-direction:column;gap:2px} +.toggle-name{font-weight:600;font-size:clamp(11px,3.2vw,13px);color:var(--txt)} +.toggle-desc{font-size:9px;letter-spacing:1px;color:var(--dim)} +.toggle{width:40px;height:22px;border-radius:11px;background:var(--s3);border:1px solid var(--bd);position:relative;cursor:pointer;transition:background .25s,border-color .25s;flex-shrink:0} +.toggle.on{background:var(--grn);border-color:var(--grn)} +.toggle-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dim);transition:all .25s cubic-bezier(.34,1.56,.64,1)} +.toggle.on .toggle-knob{left:20px;background:#fff;box-shadow:0 0 8px rgba(56,232,120,.6)} + +/* MODAL */ +.modal-bg{position:fixed;inset:0;z-index:300;background:rgba(0,0,0,.75);backdrop-filter:blur(6px);display:flex;align-items:flex-end;justify-content:center;animation:fade-in .2s ease-out} +.modal{width:100%;max-width:480px;background:var(--s1);border-radius:22px 22px 0 0;border-top:1px solid var(--bd);padding:0 0 clamp(18px,4.5vh,32px);animation:modal-in .28s cubic-bezier(.34,1.4,.64,1)} +@keyframes modal-in{from{transform:translateY(100%)}to{transform:translateY(0)}} +.modal-handle{width:40px;height:4px;background:var(--bd);border-radius:2px;margin:12px auto 0} +.modal-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,20px);letter-spacing:5px;color:var(--gold);text-align:center;padding:12px 18px 3px} +.modal-sub{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase;text-align:center;margin-bottom:14px} +.modal-fields{display:flex;gap:8px;padding:0 14px;margin-bottom:14px} +.modal-field{flex:1;display:flex;flex-direction:column;gap:5px} +.modal-f-lbl{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase;padding-left:2px} +.modal-spin{display:flex;align-items:center;background:var(--s2);border:1px solid var(--bd);border-radius:11px;overflow:hidden} +.modal-sb{width:38px;height:46px;display:flex;align-items:center;justify-content:center;font-size:19px;color:var(--gold);cursor:pointer;flex-shrink:0;transition:background .12s} +.modal-sb:active{background:var(--s3)} +.modal-sv{flex:1;text-align:center;font-family:'Bebas Neue',sans-serif;font-size:clamp(19px,5vw,24px);color:var(--txt);letter-spacing:2px} +.modal-su{font-size:8px;letter-spacing:2px;color:var(--dim);text-align:center;margin-top:2px} +.modal-prev{margin:0 14px 12px;background:var(--s2);border:1px solid var(--bd);border-radius:11px;padding:9px 13px;display:flex;align-items:center;justify-content:space-between} +.modal-prev-lbl{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase} +.modal-prev-val{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,3.8vw,18px);color:var(--gold);letter-spacing:2px} +.modal-actions{display:flex;flex-direction:column;gap:7px;padding:0 14px} +.modal-ok{width:100%;padding:12px;border:none;border-radius:11px;font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:4px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));cursor:pointer;box-shadow:0 4px 20px rgba(232,184,75,.4);transition:filter .15s} +.modal-ok:active{filter:brightness(.88)} +.modal-cancel{width:100%;padding:9px;border:none;background:transparent;font-size:10px;letter-spacing:3px;color:var(--dim);cursor:pointer;text-transform:uppercase} + +/* GAME */ +.game{width:100%;height:100%;display:flex;flex-direction:column;position:relative;z-index:1} +.panel{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;cursor:pointer;gap:clamp(5px,1.5vh,10px);overflow:hidden;transition:opacity .3s,filter .3s} +.panel.flip{transform:rotate(180deg)} +.panel.on{opacity:1} +.panel.on::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,rgba(232,184,75,.10) 0%,rgba(232,184,75,.02) 40%,transparent 70%);pointer-events:none;animation:sweep 2.4s ease-in-out infinite alternate} +@keyframes sweep{from{opacity:.6}to{opacity:1}} +.panel.off{opacity:.38;filter:brightness(.7)} +.panel.dng{animation:dng .55s ease-in-out infinite} +@keyframes dng{0%,100%{background:transparent}30%{background:rgba(240,64,64,.16)}60%{background:rgba(240,64,64,.06)}} +.panel.pf{animation:pfx .25s ease-in-out 3} +@keyframes pfx{0%,100%{background:transparent}50%{background:rgba(168,85,247,.22)}} +.p-ripple{position:absolute;width:200px;height:200px;border-radius:50%;background:radial-gradient(circle,rgba(232,184,75,.22) 0%,transparent 70%);transform:scale(0);pointer-events:none;animation:ripple .55s cubic-bezier(.2,.8,.3,1) forwards} +@keyframes ripple{from{transform:scale(0);opacity:1}to{transform:scale(4);opacity:0}} +.p-label{font-size:9px;letter-spacing:6px;color:var(--dim);text-transform:uppercase;font-weight:700} +.clock{font-family:'Bebas Neue',sans-serif;font-size:clamp(58px,16vw,106px);letter-spacing:-3px;color:var(--gold);line-height:1;text-shadow:0 0 30px rgba(232,184,75,.45),0 0 70px rgba(232,184,75,.15);position:relative;z-index:1;transition:color .3s,text-shadow .3s} +.clock.dng{color:var(--red);text-shadow:0 0 20px rgba(240,64,64,.8),0 0 50px rgba(240,64,64,.4);animation:glitch 1.4s steps(1) infinite} +@keyframes glitch{0%,89%,100%{transform:translate(0,0)}90%{transform:translate(3px,0)}92%{transform:translate(-3px,0)}94%{transform:translate(0,0)}} +.sonar{width:9px;height:9px;border-radius:50%;background:var(--gold);box-shadow:0 0 0 0 rgba(232,184,75,.7);animation:sonar 1.4s ease-out infinite} +@keyframes sonar{0%{box-shadow:0 0 0 0 rgba(232,184,75,.8);transform:scale(1)}50%{box-shadow:0 0 0 10px rgba(232,184,75,0);transform:scale(1.15)}100%{box-shadow:0 0 0 0 rgba(232,184,75,0);transform:scale(1)}} +.dots{display:flex;gap:5px;flex-wrap:wrap;justify-content:center;max-width:190px} +.dot{width:6px;height:6px;border-radius:50%;border:1px solid rgba(122,92,30,.6);background:transparent;transition:transform .3s cubic-bezier(.34,1.56,.64,1),background .25s,box-shadow .25s} +.dot.f{background:var(--gold);border-color:var(--gold);box-shadow:0 0 6px rgba(232,184,75,.7);transform:scale(1.15);animation:dpop .35s cubic-bezier(.34,1.56,.64,1)} +@keyframes dpop{0%{transform:scale(0);opacity:0}60%{transform:scale(1.4)}100%{transform:scale(1.15);opacity:1}} +.dot.t{border-color:rgba(240,64,64,.5)} +.dot.f.t{background:var(--red);border-color:var(--red);animation:tstrobe .45s ease-in-out infinite alternate} +@keyframes tstrobe{from{box-shadow:0 0 4px rgba(240,64,64,.5);transform:scale(1.1)}to{box-shadow:0 0 14px rgba(240,64,64,1);transform:scale(1.3)}} +.pen-badge{background:rgba(168,85,247,.18);border:1px solid rgba(168,85,247,.45);border-radius:18px;padding:2px 9px;font-family:'Bebas Neue',sans-serif;font-size:10px;letter-spacing:2px;color:var(--pur)} +.mt-wrap{width:75%;max-width:160px;height:3px;background:var(--bd);border-radius:2px;overflow:hidden} +.mt-bar{height:100%;border-radius:2px;transition:width .1s linear;background:linear-gradient(90deg,var(--grn),var(--gold))} +.mt-bar.warn{background:linear-gradient(90deg,var(--gold),var(--red))} +.mt-bar.crit{background:var(--red);animation:crit .3s steps(1) infinite} +@keyframes crit{0%,100%{opacity:1}50%{opacity:.45}} +.pen-bar{position:absolute;bottom:0;left:0;right:0;height:3px;background:linear-gradient(90deg,transparent,var(--pur),transparent);animation:pb-fl .3s ease-in-out 3} +@keyframes pb-fl{0%,100%{opacity:0}50%{opacity:1}} +.div{height:3px;background:linear-gradient(90deg,transparent 0%,rgba(122,92,30,.4) 10%,var(--gold) 30%,var(--g2) 50%,var(--gold) 70%,rgba(122,92,30,.4) 90%,transparent 100%);background-size:200%;position:relative;z-index:10;flex-shrink:0;box-shadow:0 0 18px rgba(232,184,75,.4);animation:dflow 3s linear infinite} +@keyframes dflow{from{background-position:0%}to{background-position:200%}} +.div-c{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:3px;z-index:20;pointer-events:none} +.div-badge{background:var(--bg);border:1.5px solid var(--gold);border-radius:20px;padding:3px 13px;font-family:'Bebas Neue',sans-serif;font-size:12px;letter-spacing:3px;color:var(--gold);white-space:nowrap;box-shadow:0 0 12px rgba(232,184,75,.25)} +.div-sub{background:var(--bg);border:1px solid var(--bd);border-radius:10px;padding:1px 7px;font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;white-space:nowrap} +.win-btn{position:absolute;top:50%;z-index:30;cursor:pointer;transform:translateY(-50%);transition:all .2s cubic-bezier(.34,1.56,.64,1)} +.win-btn.wl{left:8px} +.win-btn.wr{right:8px} +.win-btn-inner{background:var(--s2);border:1.5px solid var(--bd);border-radius:10px;padding:5px 8px;display:flex;flex-direction:column;align-items:center;gap:2px;transition:all .2s;box-shadow:0 2px 10px rgba(0,0,0,.3)} +.win-btn:active .win-btn-inner{transform:scale(.9)} +.win-btn.pnd .win-btn-inner{background:rgba(232,184,75,.12);border-color:var(--gold);animation:wbp .5s ease-in-out infinite alternate} +@keyframes wbp{from{box-shadow:0 0 8px rgba(232,184,75,.3)}to{box-shadow:0 0 22px rgba(232,184,75,.7)}} +.win-btn-ico{font-size:12px;line-height:1} +.win-btn-lbl{font-family:'Bebas Neue',sans-serif;font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;white-space:nowrap} +.win-btn.pnd .win-btn-lbl{color:var(--gold)} +.win-btn.flip2{transform:translateY(-50%) rotate(180deg)} +.ctrls{position:fixed;bottom:0;left:0;right:0;display:flex;justify-content:center;gap:10px;padding:7px 14px clamp(10px,2.5vh,20px);z-index:100;background:linear-gradient(to top,rgba(6,10,14,.98) 55%,transparent)} +.cbtn{font-family:'Oxanium',sans-serif;font-size:10px;font-weight:700;letter-spacing:2px;color:var(--dim);background:var(--s1);border:1px solid var(--bd);border-radius:10px;padding:8px 13px;cursor:pointer;text-transform:uppercase;transition:all .2s cubic-bezier(.34,1.56,.64,1)} +.cbtn:active{color:var(--gold);border-color:var(--g3);transform:scale(.92)} + +/* CS OVERLAY */ +.cso{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(5px,1.3vh,9px);z-index:50;overflow:hidden;animation:cso-in .35s cubic-bezier(.16,1,.3,1) both} +@keyframes cso-in{from{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}} +.cso-bg{position:absolute;inset:0;transition:background .4s} +.cso-bg.idle{background:radial-gradient(ellipse 70% 70% at center,rgba(20,55,130,.98) 0%,rgba(6,10,14,.99) 70%)} +.cso-bg.hold{animation:hbg .9s ease-in-out infinite alternate} +@keyframes hbg{from{background:radial-gradient(ellipse 65% 65% at center,rgba(10,110,44,.98) 0%,rgba(6,10,14,.99) 68%)}to{background:radial-gradient(ellipse 88% 88% at center,rgba(14,140,56,.98) 0%,rgba(6,10,14,.99) 68%)}} +.cso-bg.run{animation:rbg .4s ease-in-out infinite alternate} +@keyframes rbg{from{background:radial-gradient(ellipse 70% 70% at center,rgba(140,20,20,.98) 0%,rgba(6,10,14,.99) 68%)}to{background:radial-gradient(ellipse 90% 90% at center,rgba(180,28,28,.98) 0%,rgba(6,10,14,.99) 68%)}} +.cso-bg.stop{background:rgba(6,10,14,.98)} +.cso-bg.win{animation:wbg .6s ease-in-out infinite alternate} +@keyframes wbg{from{background:radial-gradient(ellipse 70% 70% at center,rgba(10,120,48,.98) 0%,rgba(6,10,14,.99) 68%)}to{background:radial-gradient(ellipse 95% 95% at center,rgba(16,160,64,.98) 0%,rgba(6,10,14,.99) 68%)}} +.cso-bg.lose{background:rgba(6,10,14,.98)} +.cso-lbl{font-family:'Bebas Neue',sans-serif;font-size:clamp(12px,3.2vw,15px);letter-spacing:6px;text-transform:uppercase;position:relative;z-index:1;font-weight:700} +.cso-lbl.blu{color:#7ec8ff;text-shadow:0 0 24px rgba(100,180,255,1),0 0 48px rgba(75,158,232,.6)} +.cso-lbl.grn{color:#7affc0;text-shadow:0 0 24px rgba(80,255,160,1),0 0 48px rgba(56,232,120,.6)} +.cso-lbl.red{color:#ff8080;text-shadow:0 0 24px rgba(255,100,100,1),0 0 48px rgba(240,64,64,.7)} +.cso-lbl.dim{color:rgba(200,216,232,.6)} +.cso-t{font-family:'Bebas Neue',sans-serif;font-size:clamp(38px,10.5vw,70px);line-height:1;letter-spacing:-1px;position:relative;z-index:1;transition:color .25s,text-shadow .25s} +.cso-t.idle{color:#90d4ff;text-shadow:0 0 40px rgba(100,190,255,1),0 0 80px rgba(75,158,232,.5)} +.cso-t.hold{color:#80ffbe;text-shadow:0 0 50px rgba(80,255,160,1),0 0 100px rgba(56,232,120,.5);animation:hbreathe .5s ease-in-out infinite alternate} +@keyframes hbreathe{from{transform:scale(.97);filter:brightness(.9)}to{transform:scale(1.05);filter:brightness(1.2)}} +.cso-t.run{color:#ff9090;text-shadow:0 0 40px rgba(255,100,100,1),0 0 80px rgba(240,64,64,.6);animation:runpulse .35s ease-in-out infinite alternate} +@keyframes runpulse{from{filter:brightness(1)}to{filter:brightness(1.3)}} +.cso-t.stop{color:rgba(200,216,232,.5)} +.cso-t.win{color:#80ffbe;text-shadow:0 0 60px rgba(80,255,160,1),0 0 120px rgba(56,232,120,.7);animation:wbreathe .5s ease-in-out infinite alternate} +.cso-t.lose{color:rgba(200,216,232,.3)} +@keyframes wbreathe{from{transform:scale(1);filter:brightness(1)}to{transform:scale(1.07);filter:brightness(1.25)}} +.cso-hint{font-size:clamp(9px,2.2vw,11px);letter-spacing:3px;color:rgba(200,216,232,.75);text-transform:uppercase;position:relative;z-index:1;text-align:center;padding:0 10px;font-weight:600} +.cso-win{font-family:'Bebas Neue',sans-serif;font-size:clamp(11px,3.2vw,15px);letter-spacing:6px;color:#80ffbe;text-shadow:0 0 30px rgba(80,255,160,1);position:relative;z-index:1;text-align:center} +.cso-ring{position:relative;z-index:1;display:flex;align-items:center;justify-content:center} +.cso-ring svg{position:absolute;top:0;left:0;overflow:visible} +.rb{fill:none;stroke:rgba(255,255,255,.08);stroke-linecap:round} +.rf{fill:none;stroke:#80ffbe;stroke-linecap:round;filter:drop-shadow(0 0 10px rgba(80,255,160,.9));stroke-dasharray:289;animation:rspin .7s linear infinite} +@keyframes rspin{from{stroke-dashoffset:289}to{stroke-dashoffset:0}} +.rf2{fill:none;stroke:rgba(80,255,160,.3);stroke-linecap:round;stroke-dasharray:60 229;animation:rspin2 1.4s linear infinite reverse} +@keyframes rspin2{from{stroke-dashoffset:0}to{stroke-dashoffset:289}} + +/* DOUBLE MOVE */ +.dbl{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(8px,2vh,14px);z-index:50;pointer-events:none;animation:dbl-in .4s cubic-bezier(.16,1,.3,1) both} +@keyframes dbl-in{from{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}} +.dbl-bw{position:absolute;inset:0;background:rgba(3,18,10,.97)} +.dbl-bl{position:absolute;inset:0;background:rgba(4,8,14,.97)} +.dbl-card{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;gap:clamp(8px,2vh,13px);border-radius:20px;padding:clamp(16px,4vh,24px) clamp(20px,6vw,34px)} +.dbl-card.win-card{background:rgba(56,232,120,.12);border:1.5px solid rgba(56,232,120,.5);box-shadow:0 0 40px rgba(56,232,120,.15),inset 0 1px 0 rgba(56,232,120,.2)} +.dbl-card.lose-card{background:rgba(255,255,255,.06);border:1.5px solid rgba(255,255,255,.15);box-shadow:0 4px 24px rgba(0,0,0,.4)} +.dbl-icon{font-size:clamp(28px,8vw,40px);line-height:1;filter:drop-shadow(0 0 16px rgba(56,232,120,.8))} +.dbl-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(24px,6.5vw,34px);letter-spacing:6px;color:#ffffff;text-transform:uppercase;text-align:center;text-shadow:0 0 30px rgba(56,232,120,.8),0 2px 0 rgba(0,0,0,.6)} +.dbl-sub{font-size:11px;letter-spacing:2px;color:rgba(56,232,120,1);text-transform:uppercase;text-align:center;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,.7);background:rgba(56,232,120,.15);border:1px solid rgba(56,232,120,.4);border-radius:8px;padding:4px 10px} +.dbl-pips{display:flex;gap:16px} +.dbl-pip{width:20px;height:20px;border-radius:50%;background:var(--grn);box-shadow:0 0 16px rgba(56,232,120,.9),0 2px 6px rgba(0,0,0,.5);animation:psonar 1s ease-out infinite} +.dbl-pip:nth-child(2){animation-delay:.5s} +@keyframes psonar{0%{box-shadow:0 0 0 0 rgba(56,232,120,.9),0 2px 6px rgba(0,0,0,.5)}70%{box-shadow:0 0 0 14px rgba(56,232,120,0),0 2px 6px rgba(0,0,0,.5)}100%{box-shadow:0 0 0 0 rgba(56,232,120,0),0 2px 6px rgba(0,0,0,.5)}} +.dbl-pip.used{background:rgba(255,255,255,.2);box-shadow:none;animation:none;transform:scale(.65);opacity:.35;transition:all .35s cubic-bezier(.34,1.56,.64,1)} +.dbl-hint{font-size:12px;letter-spacing:3px;color:rgba(255,255,255,.85);text-transform:uppercase;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,.7)} +.dbl-wait-ico{font-size:clamp(22px,6.5vw,32px);line-height:1;opacity:.6} +.dbl-wait-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(18px,5vw,26px);letter-spacing:5px;color:#ffffff;text-transform:uppercase;text-align:center;text-shadow:0 2px 10px rgba(0,0,0,.8);opacity:.9} +.dbl-wait-sub{font-size:11px;letter-spacing:2px;color:rgba(200,216,232,.65);text-transform:uppercase;text-align:center;font-weight:600} +.dbl-challenge{font-size:11px;letter-spacing:2px;color:#ffffff;text-transform:uppercase;text-align:center;padding:8px 16px;background:rgba(168,85,247,.4);border:1.5px solid rgba(168,85,247,.8);border-radius:11px;font-weight:700;text-shadow:0 1px 4px rgba(0,0,0,.6);box-shadow:0 0 20px rgba(168,85,247,.5);animation:wfade 1s ease-in-out infinite alternate} +@keyframes wfade{from{opacity:.8}to{opacity:1}} +/* FORBIDDEN MOVE */ +.forb{position:absolute;inset:0;z-index:60;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:rgba(240,64,64,.08);animation:fade-in .2s ease-out both} +.forb-ico{font-size:clamp(28px,8vw,46px);animation:fshake .4s ease-in-out} +@keyframes fshake{0%,100%{transform:rotate(0)}25%{transform:rotate(-9deg)}75%{transform:rotate(9deg)}} +.forb-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,4vw,20px);letter-spacing:5px;color:var(--red);text-align:center} +.forb-sub{font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;text-align:center;padding:0 14px} + +/* TOURNAMENT */ +.trn{position:fixed;inset:0;z-index:250;display:flex;flex-direction:column;background:var(--bg);animation:cfg-in .35s cubic-bezier(.16,1,.3,1) both} +.trn-hd{display:flex;align-items:center;justify-content:space-between;padding:clamp(14px,3.5vh,24px) 18px 10px;border-bottom:1px solid var(--bd);flex-shrink:0} +.trn-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,22px);letter-spacing:5px;color:var(--gold)} +.trn-close{width:34px;height:34px;border-radius:50%;background:var(--s2);border:1px solid var(--bd);color:var(--dim);font-size:15px;display:flex;align-items:center;justify-content:center;cursor:pointer} +.trn-close:active{background:var(--s3)} +.trn-body{flex:1;overflow-y:auto;padding:12px 14px 36px;-webkit-overflow-scrolling:touch} +.trn-body::-webkit-scrollbar{display:none} +.trn-setup{display:flex;flex-direction:column;gap:14px} +.trn-players-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px} +.trn-player-slot{background:var(--s2);border:1px solid var(--bd);border-radius:11px;padding:9px 11px;display:flex;align-items:center;gap:7px} +.trn-p-num{font-family:'Bebas Neue',sans-serif;font-size:17px;color:var(--gold);width:20px;text-align:center} +.trn-p-name{font-size:11px;letter-spacing:1px;color:var(--txt);font-weight:600} +.trn-size-row{display:flex;gap:7px} +.trn-size-btn{flex:1;padding:11px;background:var(--s2);border:1.5px solid var(--bd);border-radius:11px;font-family:'Bebas Neue',sans-serif;font-size:15px;letter-spacing:3px;color:var(--dim);cursor:pointer;text-align:center;transition:all .2s} +.trn-size-btn.sel{background:rgba(232,184,75,.08);border-color:var(--gold);color:var(--gold)} +.trn-start-btn{padding:13px;border:none;border-radius:11px;font-family:'Bebas Neue',sans-serif;font-size:15px;letter-spacing:5px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));cursor:pointer;box-shadow:0 4px 20px rgba(232,184,75,.4)} +.trn-bracket{display:flex;flex-direction:column;gap:10px} +.trn-round-title{font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:4px;color:var(--gold);text-align:center;margin-bottom:3px} +.trn-match{background:var(--s1);border:1px solid var(--bd);border-radius:11px;overflow:hidden} +.trn-match.active{border-color:var(--gold);box-shadow:0 0 14px rgba(232,184,75,.2)} +.trn-match.done{opacity:.6} +.trn-m-row{display:flex;align-items:center;padding:9px 13px;border-bottom:1px solid rgba(255,255,255,.04)} +.trn-m-row:last-child{border-bottom:none} +.trn-m-row.won{background:rgba(232,184,75,.07)} +.trn-m-name{flex:1;font-size:12px;font-weight:600;color:var(--txt);letter-spacing:.5px} +.trn-m-row.won .trn-m-name{color:var(--g2)} +.trn-m-score{font-family:'Bebas Neue',sans-serif;font-size:15px;color:var(--dim);letter-spacing:2px;width:22px;text-align:center} +.trn-m-row.won .trn-m-score{color:var(--gold)} +.trn-m-play{padding:7px 0;background:rgba(232,184,75,.06);border-top:1px solid var(--bd);text-align:center;font-family:'Bebas Neue',sans-serif;font-size:11px;letter-spacing:3px;color:var(--gold);cursor:pointer} +.trn-m-play:active{background:rgba(232,184,75,.12)} +.trn-m-tbd{padding:9px 13px;font-size:10px;letter-spacing:2px;color:var(--dim);text-align:center} +.trn-winner-banner{background:linear-gradient(135deg,rgba(232,184,75,.12),rgba(232,184,75,.04));border:1px solid var(--gold);border-radius:13px;padding:18px;text-align:center;margin-bottom:14px} +.trn-w-ico{font-size:36px;animation:efloat 2s ease-in-out infinite alternate} +@keyframes efloat{from{transform:translateY(0)}to{transform:translateY(-8px)}} +.trn-w-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(20px,5.5vw,30px);letter-spacing:5px;background:linear-gradient(135deg,var(--g2),var(--gold));-webkit-background-clip:text;background-clip:text;color:transparent;margin-top:5px} +.trn-w-sub{font-size:9px;letter-spacing:3px;color:var(--dim);margin-top:3px} +.trn-new-btn{width:100%;padding:12px;border:none;border-radius:11px;font-family:'Bebas Neue',sans-serif;font-size:14px;letter-spacing:4px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));cursor:pointer;margin-top:7px} +.trn-p-input{flex:1;background:transparent;border:none;outline:none;font-family:'Oxanium',sans-serif;font-size:11px;font-weight:600;color:var(--gold);letter-spacing:.5px;width:0;min-width:0;caret-color:var(--gold)} +.trn-p-slot-edit{background:rgba(232,184,75,.07);border-color:var(--g3)} +.trn-challenge{position:absolute;inset:0;z-index:55;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;background:rgba(168,85,247,.1);animation:fade-in .2s ease-out both} +.trn-ch-ico{font-size:clamp(24px,7vw,38px);animation:fshake .4s ease-in-out} +.trn-ch-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(12px,3.5vw,18px);letter-spacing:5px;color:var(--pur);text-align:center} +.trn-ch-sub{font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;text-align:center;padding:0 14px} +.dbl-challenge{font-family:'Bebas Neue',sans-serif;font-size:clamp(9px,2.5vw,12px);letter-spacing:3px;color:var(--pur);position:relative;z-index:1;text-transform:uppercase;text-align:center;padding:2px 10px;background:rgba(168,85,247,.12);border:1px solid rgba(168,85,247,.35);border-radius:8px;animation:wfade 1.2s ease-in-out infinite alternate} + +/* HISTORY */ +.hist{position:fixed;inset:0;z-index:250;display:flex;flex-direction:column;background:var(--bg);animation:cfg-in .35s cubic-bezier(.16,1,.3,1) both} +.hist-hd{display:flex;align-items:center;justify-content:space-between;padding:clamp(14px,3.5vh,24px) 18px 10px;border-bottom:1px solid var(--bd);flex-shrink:0} +.hist-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,22px);letter-spacing:5px;color:var(--gold)} +.hist-close{width:34px;height:34px;border-radius:50%;background:var(--s2);border:1px solid var(--bd);color:var(--dim);font-size:15px;display:flex;align-items:center;justify-content:center;cursor:pointer} +.hist-body{flex:1;overflow-y:auto;padding:12px 14px 36px;-webkit-overflow-scrolling:touch} +.hist-body::-webkit-scrollbar{display:none} +.hist-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:10px;opacity:.4} +.hist-empty-ico{font-size:36px} +.hist-empty-lbl{font-family:'Bebas Neue',sans-serif;font-size:15px;letter-spacing:4px;color:var(--dim)} +.hist-entry{background:var(--s1);border:1px solid var(--bd);border-radius:13px;padding:11px 13px;margin-bottom:9px;animation:cfg-in .3s cubic-bezier(.16,1,.3,1) both} +.hist-entry.w0{border-left:3px solid var(--gold)} +.hist-entry.w1{border-left:3px solid var(--blu)} +.hist-row1{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px} +.hist-date{font-size:9px;letter-spacing:2px;color:var(--dim);text-transform:uppercase} +.hist-badge{font-family:'Bebas Neue',sans-serif;font-size:10px;letter-spacing:3px;padding:2px 9px;border-radius:18px} +.hist-badge.b0{background:rgba(232,184,75,.15);color:var(--gold);border:1px solid var(--g3)} +.hist-badge.b1{background:rgba(75,158,232,.15);color:var(--blu);border:1px solid rgba(75,158,232,.4)} +.hist-players{display:flex;gap:7px;margin-bottom:7px} +.hist-player{flex:1;background:var(--s2);border-radius:9px;padding:8px 10px;display:flex;flex-direction:column;gap:4px} +.hist-p-top{display:flex;align-items:center;gap:5px} +.hist-p-name{font-family:'Bebas Neue',sans-serif;font-size:11px;letter-spacing:3px;color:var(--txt);flex:1} +.hist-p-time{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,4vw,20px);color:var(--gold);letter-spacing:2px;line-height:1} +.hist-player.winner .hist-p-time{color:var(--g2)} +.hist-p-meta{font-size:8px;letter-spacing:1px;color:var(--dim)} +.hist-footer{display:flex;align-items:center;justify-content:space-between;margin-top:3px} +.hist-meta{font-size:8px;letter-spacing:2px;color:var(--dim)} +.hist-clear{font-size:9px;letter-spacing:2px;color:var(--red);opacity:.6;background:none;border:none;cursor:pointer;font-family:'Oxanium',sans-serif;text-transform:uppercase} + +/* END */ +.end{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(9px,2.2vh,16px);background:rgba(6,10,14,.97);overflow:hidden} +.end::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 80% 50% at 50% 50%,rgba(232,184,75,.07) 0%,transparent 70%);animation:eburst 2s ease-in-out infinite alternate} +@keyframes eburst{from{transform:scale(1);opacity:.7}to{transform:scale(1.15);opacity:1}} +.end > *{animation:estag .55s cubic-bezier(.16,1,.3,1) both;position:relative;z-index:1} +.end > *:nth-child(1){animation-delay:.0s}.end > *:nth-child(2){animation-delay:.1s}.end > *:nth-child(3){animation-delay:.18s}.end > *:nth-child(4){animation-delay:.26s}.end > *:nth-child(5){animation-delay:.34s}.end > *:nth-child(6){animation-delay:.4s} +@keyframes estag{from{opacity:0;transform:translateY(22px) scale(.92)}to{opacity:1;transform:translateY(0) scale(1)}} +.end-ico{font-size:clamp(38px,10vw,54px);filter:drop-shadow(0 0 20px rgba(232,184,75,.5));animation:efloat 2s ease-in-out infinite alternate} +.end-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(24px,7vw,40px);letter-spacing:7px;background:linear-gradient(135deg,var(--g2),var(--gold),#b8882b);background-size:200%;-webkit-background-clip:text;background-clip:text;color:transparent;text-align:center;animation:shimmer 3s ease-in-out infinite} +.end-reason{font-size:10px;letter-spacing:5px;color:var(--dim);text-transform:uppercase} +.end-cubes{display:flex;align-items:center;gap:18px} +.end-cube-card{display:flex;flex-direction:column;align-items:center;gap:4px;opacity:.45;transition:opacity .3s} +.end-cube-card.winner{opacity:1} +.end-cube-lbl{font-family:'Bebas Neue',sans-serif;font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase} +.end-cube-card.winner .end-cube-lbl{color:var(--gold)} +.end-cube-sub{font-size:8px;letter-spacing:1px;color:var(--dim)} +.end-stats{display:flex;gap:clamp(14px,4.5vw,24px);flex-wrap:wrap;justify-content:center;background:var(--s1);border:1px solid var(--bd);border-radius:14px;padding:clamp(9px,2.2vw,14px) clamp(14px,3.5vw,22px);box-shadow:0 4px 30px rgba(0,0,0,.4)} +.stat{display:flex;flex-direction:column;align-items:center;gap:2px} +.stat-v{font-family:'Bebas Neue',sans-serif;font-size:clamp(18px,5vw,26px);color:var(--gold);letter-spacing:2px} +.stat-l{font-size:7px;letter-spacing:3px;color:var(--dim);text-transform:uppercase} +.end-btns{display:flex;gap:9px;flex-wrap:wrap;justify-content:center} +.btn1{font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:5px;color:var(--bg);background:linear-gradient(110deg,var(--g3),var(--g2),var(--gold));background-size:200%;border:none;border-radius:11px;padding:13px 26px;cursor:pointer;box-shadow:0 0 26px rgba(232,184,75,.5);animation:shimmer 3s ease-in-out infinite;transition:transform .18s} +.btn1:active{transform:scale(.94)} +.btn2{font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:5px;color:var(--txt);background:var(--s2);border:1px solid var(--bd);border-radius:11px;padding:13px 26px;cursor:pointer;transition:transform .18s,border-color .2s} +.btn2:active{transform:scale(.94);border-color:var(--g3)} + +/* RUBIK'S CUBE */ +.rcube{display:grid;grid-template-columns:repeat(3,1fr);gap:1.5px;padding:2px;background:var(--s3);border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.4)} +.rcube.sm{width:22px;height:22px;gap:1px;padding:1.5px} +.rcube.md{width:30px;height:30px;gap:1.5px;padding:2px} +.rcube.lg{width:42px;height:42px;gap:2px;padding:3px} +.rc{border-radius:1.5px} +.p1c0{background:#e84b4b}.p1c1{background:#e8b84b}.p1c2{background:#38e878} +.p1c3{background:#4b9ee8}.p1c4{background:#e8b84b}.p1c5{background:#e84b4b} +.p1c6{background:#38e878}.p1c7{background:#4b9ee8}.p1c8{background:#e8b84b} +.p2c0{background:#4b9ee8}.p2c1{background:#38e878}.p2c2{background:#e8b84b} +.p2c3{background:#e84b4b}.p2c4{background:#4b9ee8}.p2c5{background:#38e878} +.p2c6{background:#e8b84b}.p2c7{background:#e84b4b}.p2c8{background:#4b9ee8} + +/* USER MANAGER */ +.um{position:fixed;inset:0;z-index:260;display:flex;flex-direction:column;background:var(--bg);animation:cfg-in .35s cubic-bezier(.16,1,.3,1) both} +.um-hd{display:flex;align-items:center;gap:8px;padding:clamp(14px,3.5vh,24px) 18px 10px;border-bottom:1px solid var(--bd);flex-shrink:0} +.um-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,22px);letter-spacing:5px;color:var(--gold);flex:1} +.um-add-btn{font-family:'Bebas Neue',sans-serif;font-size:12px;letter-spacing:3px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));border:none;border-radius:9px;padding:7px 13px;cursor:pointer;white-space:nowrap} +.um-close{width:34px;height:34px;border-radius:50%;background:var(--s2);border:1px solid var(--bd);color:var(--dim);font-size:15px;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0} +.um-body{flex:1;overflow-y:auto;padding:12px 14px 36px;-webkit-overflow-scrolling:touch} +.um-body::-webkit-scrollbar{display:none} +.um-card{background:var(--s1);border:1px solid var(--bd);border-radius:13px;padding:12px 13px;margin-bottom:9px;display:flex;flex-direction:column;gap:9px;transition:border-color .2s} +.um-card.editing{border-color:var(--gold);background:rgba(232,184,75,.04)} +.um-row{display:flex;align-items:center;gap:10px} +.um-av{font-size:24px;line-height:1;width:44px;height:44px;background:var(--s2);border:1.5px solid var(--bd);border-radius:10px;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;transition:border-color .2s} +.um-av:active{transform:scale(.9)} +.um-av.open{border-color:var(--gold)} +.um-info{flex:1;display:flex;flex-direction:column;gap:3px;min-width:0} +.um-name{font-weight:700;font-size:clamp(13px,3.8vw,15px);color:var(--txt);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.um-meta{display:flex;gap:8px;align-items:center} +.um-elo{font-family:'Bebas Neue',sans-serif;font-size:13px;color:var(--grn);letter-spacing:1px} +.um-streak{font-size:10px;color:var(--red)} +.um-wins{font-size:10px;color:var(--dim)} +.um-actions{display:flex;gap:6px;flex-shrink:0} +.um-btn{width:32px;height:32px;border-radius:9px;background:var(--s2);border:1px solid var(--bd);font-size:14px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s} +.um-btn.edit:active{background:var(--s3);border-color:var(--gold)} +.um-btn.del:active{background:rgba(240,64,64,.1);border-color:var(--red)} +.um-name-input{background:var(--s2);border:1px solid var(--gold);border-radius:9px;padding:7px 11px;font-family:'Oxanium',sans-serif;font-size:clamp(13px,3.8vw,15px);font-weight:700;color:var(--gold);caret-color:var(--gold);outline:none;width:100%} +.um-av-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:5px;background:var(--s2);border-radius:10px;padding:8px} +.um-av-item{aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:17px;border-radius:7px;cursor:pointer;border:1.5px solid transparent;transition:all .15s} +.um-av-item.sel{border-color:var(--gold);background:rgba(232,184,75,.12)} +.um-av-item:active{transform:scale(.88)} +.um-save-row{display:flex;gap:7px} +.um-save-btn{flex:1;padding:8px;border:none;border-radius:9px;font-family:'Bebas Neue',sans-serif;font-size:12px;letter-spacing:3px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));cursor:pointer} +.um-cancel-btn{padding:8px 14px;border:1px solid var(--bd);background:transparent;border-radius:9px;font-size:10px;letter-spacing:2px;color:var(--dim);cursor:pointer;text-transform:uppercase} + +/* PLAYER SELECT */ +.ps{position:fixed;inset:0;z-index:170;display:flex;flex-direction:column;background:var(--bg);animation:cfg-in .35s cubic-bezier(.16,1,.3,1) both} +.ps-half{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:14px 14px 20px;gap:10px;overflow:hidden} +.ps-half.flip{transform:rotate(180deg)} +.ps-half.on0{background:radial-gradient(ellipse 80% 70% at center,rgba(232,184,75,.07) 0%,transparent 70%)} +.ps-half.on1{background:radial-gradient(ellipse 80% 70% at center,rgba(75,158,232,.07) 0%,transparent 70%)} +.ps-label{font-size:9px;letter-spacing:5px;color:var(--dim);text-transform:uppercase;font-weight:700} +.ps-sel-display{display:flex;align-items:center;gap:8px;height:32px} +.ps-sel-av{font-size:22px;line-height:1} +.ps-sel-name{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,4vw,20px);letter-spacing:3px;color:var(--gold)} +.ps-sel-name.blue{color:var(--blu)} +.ps-sel-empty{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase} +.ps-grid{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;max-width:360px} +.ps-chip{display:flex;align-items:center;gap:7px;padding:7px 11px;background:var(--s1);border:1.5px solid var(--bd);border-radius:11px;cursor:pointer;transition:all .18s cubic-bezier(.34,1.56,.64,1)} +.ps-chip:active{transform:scale(.91)} +.ps-chip.sel0{background:rgba(232,184,75,.1);border-color:var(--gold);box-shadow:0 0 12px rgba(232,184,75,.3)} +.ps-chip.sel1{background:rgba(75,158,232,.1);border-color:var(--blu);box-shadow:0 0 12px rgba(75,158,232,.3)} +.ps-chip.other-taken{opacity:.3;pointer-events:none} +.ps-chip-av{font-size:16px;line-height:1} +.ps-chip-name{font-size:11px;font-weight:700;color:var(--txt);white-space:nowrap} +.ps-chip-elo{font-family:'Bebas Neue',sans-serif;font-size:10px;color:var(--grn);letter-spacing:1px} +.ps-div{height:3px;flex-shrink:0;background:linear-gradient(90deg,transparent,var(--gold),var(--g2),var(--gold),transparent);background-size:200%;animation:dflow 3s linear infinite;position:relative} +.ps-vs{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:var(--bg);border:1.5px solid var(--gold);border-radius:20px;padding:3px 13px;font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:4px;color:var(--gold)} +.ps-footer{position:absolute;bottom:0;left:0;right:0;padding:7px 14px clamp(10px,2.5vh,20px);background:linear-gradient(to top,rgba(6,10,14,.98) 55%,transparent);display:flex;gap:8px;z-index:20} +.ps-go{flex:1;padding:13px;border:none;border-radius:13px;font-family:'Bebas Neue',sans-serif;font-size:15px;letter-spacing:6px;cursor:pointer;transition:all .2s;text-transform:uppercase} +.ps-go.ready{color:var(--bg);background:linear-gradient(110deg,var(--g3),var(--g2),var(--gold));background-size:250%;box-shadow:0 4px 28px rgba(232,184,75,.5);animation:shimmer 3s ease-in-out infinite} +.ps-go.wait{color:var(--dim);background:var(--s2)} +.ps-back{padding:13px 16px;border:1px solid var(--bd);background:transparent;border-radius:13px;font-size:11px;letter-spacing:2px;color:var(--dim);cursor:pointer;text-transform:uppercase} + +/* STREAK */ +.streak-badge{display:inline-flex;align-items:center;gap:3px;background:rgba(240,64,64,.1);border:1px solid rgba(240,64,64,.3);border-radius:10px;padding:2px 7px;font-family:'Bebas Neue',sans-serif;font-size:10px;letter-spacing:2px;color:var(--red);animation:str-pulse 1.8s ease-in-out infinite alternate} +@keyframes str-pulse{from{box-shadow:0 0 3px rgba(240,64,64,.2)}to{box-shadow:0 0 10px rgba(240,64,64,.5)}} +.chaos-lbl{font-size:8px;letter-spacing:3px;color:var(--pur);text-transform:uppercase;animation:cpulse .8s ease-in-out infinite alternate} +@keyframes cpulse{from{opacity:.4}to{opacity:1}} +.p-av{font-size:clamp(16px,4.5vw,22px);line-height:1} + +/* TOGGLE CHAOS */ +.toggle.chaos.on{background:var(--pur);border-color:var(--pur)} +.toggle.chaos.on .toggle-knob{box-shadow:0 0 8px rgba(168,85,247,.8)} + +/* TIME CHART */ +.chart-wrap{width:100%;max-width:320px;background:var(--s1);border:1px solid var(--bd);border-radius:12px;padding:9px 12px} +.chart-ttl{font-size:8px;letter-spacing:3px;color:var(--dim);text-transform:uppercase;margin-bottom:6px;text-align:center} +.chart-leg{display:flex;gap:14px;justify-content:center;margin-top:5px} +.chart-li{display:flex;align-items:center;gap:4px;font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase} +.chart-ld{width:8px;height:8px;border-radius:50%} +.end-elo{font-size:9px;letter-spacing:2px} +.end-elo.pos{color:var(--grn)} +.end-elo.neg{color:var(--red)} + +/* PROFILE SCREEN */ +.prf{position:fixed;inset:0;z-index:260;display:flex;flex-direction:column;background:var(--bg);animation:cfg-in .35s cubic-bezier(.16,1,.3,1) both} +.prf-hd{display:flex;align-items:center;justify-content:space-between;padding:clamp(14px,3.5vh,24px) 18px 10px;border-bottom:1px solid var(--bd);flex-shrink:0} +.prf-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,22px);letter-spacing:5px;color:var(--gold)} +.prf-save-btn{font-family:'Bebas Neue',sans-serif;font-size:12px;letter-spacing:3px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));border:none;border-radius:9px;padding:7px 14px;cursor:pointer} +.prf-body{flex:1;overflow-y:auto;padding:14px 14px 36px;-webkit-overflow-scrolling:touch} +.prf-body::-webkit-scrollbar{display:none} +.prf-card{background:var(--s1);border:1px solid var(--bd);border-radius:14px;padding:14px;margin-bottom:12px} +.prf-card-title{font-size:9px;letter-spacing:4px;color:var(--dim);text-transform:uppercase;margin-bottom:10px} +.prf-top-row{display:flex;align-items:flex-start;gap:10px;margin-bottom:12px} +.prf-av-btn{font-size:26px;line-height:1;width:50px;height:50px;border-radius:12px;background:var(--s2);border:1.5px solid var(--bd);display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;transition:border-color .2s} +.prf-av-btn.open{border-color:var(--gold)} +.prf-name-col{flex:1;display:flex;flex-direction:column;gap:5px} +.prf-name-input{background:var(--s2);border:1px solid var(--bd);border-radius:9px;padding:8px 11px;font-family:'Oxanium',sans-serif;font-size:clamp(13px,3.8vw,16px);font-weight:700;color:var(--gold);caret-color:var(--gold);outline:none;width:100%;transition:border-color .2s} +.prf-name-input:focus{border-color:var(--gold)} +.prf-name-hint{font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase} +.prf-stats-row{display:flex;gap:7px;margin-bottom:10px} +.prf-stat-box{flex:1;background:var(--s2);border:1px solid var(--bd);border-radius:10px;padding:7px 8px;display:flex;flex-direction:column;align-items:center;gap:2px} +.prf-stat-val{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,22px);color:var(--gold);letter-spacing:2px} +.prf-stat-val.green{color:var(--grn)} +.prf-stat-val.red{color:var(--red)} +.prf-stat-lbl{font-size:7px;letter-spacing:2px;color:var(--dim);text-transform:uppercase} +.prf-av-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:5px;background:var(--s2);border-radius:10px;padding:8px;margin-bottom:8px} +.prf-av-item{aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:17px;border-radius:7px;cursor:pointer;border:1.5px solid transparent;transition:all .15s} +.prf-av-item.sel{border-color:var(--gold);background:rgba(232,184,75,.12)} +.prf-av-item:active{transform:scale(.88)} +.prf-reset-btn{width:100%;padding:8px;border:1px solid rgba(240,64,64,.3);background:transparent;border-radius:9px;font-size:10px;letter-spacing:2px;color:rgba(240,64,64,.6);cursor:pointer;text-transform:uppercase;transition:all .2s} +.prf-reset-btn:active{background:rgba(240,64,64,.1);border-color:var(--red)} + +/* CUBE PREP SCREEN */ +.prep{position:fixed;inset:0;z-index:180;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(12px,3vh,22px);padding:20px;animation:cfg-in .4s cubic-bezier(.16,1,.3,1) both} +.prep::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 70% 50% at 50% 40%,rgba(75,158,232,.05) 0%,transparent 70%);pointer-events:none} +.prep-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(20px,6vw,34px);letter-spacing:7px;background:linear-gradient(135deg,var(--g2),var(--gold));-webkit-background-clip:text;background-clip:text;color:transparent;text-align:center;background-size:200%;animation:shimmer 3s ease-in-out infinite} +.prep-sub{font-size:9px;letter-spacing:4px;color:var(--dim);text-transform:uppercase;text-align:center} +.prep-players{display:flex;gap:14px;align-items:flex-start;width:100%} +.prep-p-card{background:var(--s1);border:1px solid var(--bd);border-radius:14px;padding:10px 8px;display:flex;flex-direction:column;align-items:center;gap:7px;flex:1} +.prep-p-name{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase;text-align:center} +.prep-scramble{display:flex;flex-wrap:wrap;gap:3px;justify-content:center} +.prep-move{font-family:'Bebas Neue',sans-serif;font-size:clamp(10px,2.8vw,13px);color:var(--gold);background:var(--s2);border:1px solid var(--bd);border-radius:5px;padding:2px 5px;min-width:22px;text-align:center} +.prep-countdown{font-family:'Bebas Neue',sans-serif;font-size:clamp(48px,14vw,86px);color:var(--blu);text-shadow:0 0 40px rgba(75,158,232,.6);letter-spacing:-2px;animation:hbreathe .5s ease-in-out infinite alternate} +.prep-ready-btn{padding:13px 34px;border:none;border-radius:13px;font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,4vw,18px);letter-spacing:6px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));cursor:pointer;box-shadow:0 4px 28px rgba(232,184,75,.5);transition:transform .18s} +.prep-ready-btn:active{transform:scale(.95)} +.prep-skip{font-size:9px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;cursor:pointer;text-decoration:underline;text-underline-offset:3px} + +/* SOLO CS TIMER */ +.scs{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;background:var(--bg)} +.scs-panel{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(8px,2vh,14px);position:relative;overflow:hidden;cursor:pointer;transition:background .3s} +.scs-panel.flip{transform:rotate(180deg)} +.scs-panel.idle-bg{background:radial-gradient(ellipse 70% 70% at center,rgba(20,55,130,.7) 0%,rgba(6,10,14,.98) 70%)} +.scs-panel.hold-bg{animation:scs-hbg .8s ease-in-out infinite alternate} +@keyframes scs-hbg{from{background:radial-gradient(ellipse 65% 65% at center,rgba(10,110,44,.85) 0%,rgba(6,10,14,.98) 68%)}to{background:radial-gradient(ellipse 88% 88% at center,rgba(14,150,58,.9) 0%,rgba(6,10,14,.98) 68%)}} +.scs-panel.run-bg{animation:scs-rbg .4s ease-in-out infinite alternate} +@keyframes scs-rbg{from{background:radial-gradient(ellipse 70% 70% at center,rgba(140,20,20,.9) 0%,rgba(6,10,14,.98) 68%)}to{background:radial-gradient(ellipse 90% 90% at center,rgba(180,28,28,.95) 0%,rgba(6,10,14,.98) 68%)}} +.scs-panel.stop-bg{background:rgba(6,10,14,.98)} +.scs-panel.win-bg{animation:scs-wbg .6s ease-in-out infinite alternate} +@keyframes scs-wbg{from{background:radial-gradient(ellipse 70% 70% at center,rgba(10,120,48,.9) 0%,rgba(6,10,14,.98) 68%)}to{background:radial-gradient(ellipse 95% 95% at center,rgba(16,160,64,.95) 0%,rgba(6,10,14,.98) 68%)}} +.scs-panel.lose-bg{background:rgba(6,10,14,.98)} +.scs-state{font-family:'Bebas Neue',sans-serif;font-size:clamp(11px,3vw,14px);letter-spacing:6px;text-transform:uppercase;position:relative;z-index:1} +.scs-state.blu{color:#7ec8ff;text-shadow:0 0 20px rgba(100,180,255,1)} +.scs-state.grn{color:#7affc0;text-shadow:0 0 20px rgba(80,255,160,1)} +.scs-state.red{color:#ff8080;text-shadow:0 0 20px rgba(255,100,100,1)} +.scs-state.gold{color:var(--g2);text-shadow:0 0 20px rgba(255,217,122,.9)} +.scs-state.dim{color:rgba(200,216,232,.5)} +.scs-time{font-family:'Bebas Neue',sans-serif;font-size:clamp(52px,14vw,90px);letter-spacing:-2px;line-height:1;position:relative;z-index:1;transition:color .2s,text-shadow .2s} +.scs-time.idle{color:#90d4ff;text-shadow:0 0 40px rgba(100,190,255,.9)} +.scs-time.hold{color:#80ffbe;text-shadow:0 0 50px rgba(80,255,160,1);animation:hbreathe .5s ease-in-out infinite alternate} +.scs-time.run{color:#ff9090;text-shadow:0 0 40px rgba(255,100,100,1);animation:runpulse .35s ease-in-out infinite alternate} +.scs-time.stop{color:rgba(200,216,232,.5)} +.scs-time.win{color:#80ffbe;text-shadow:0 0 60px rgba(80,255,160,1);animation:wbreathe .5s ease-in-out infinite alternate} +.scs-time.lose{color:rgba(200,216,232,.25)} +.scs-hint{font-size:clamp(9px,2.2vw,11px);letter-spacing:3px;color:rgba(200,216,232,.7);text-transform:uppercase;position:relative;z-index:1;font-weight:600} +.scs-ring{position:relative;z-index:1;display:flex;align-items:center;justify-content:center} +.scs-name{display:flex;align-items:center;gap:7px;position:relative;z-index:1} +.scs-av{font-size:clamp(16px,4.5vw,22px);line-height:1} +.scs-plbl{font-size:9px;letter-spacing:5px;color:var(--dim);text-transform:uppercase;font-weight:700} +.scs-win-lbl{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,4vw,22px);letter-spacing:5px;color:#80ffbe;text-shadow:0 0 24px rgba(80,255,160,.9);position:relative;z-index:1} +.scs-div{height:3px;flex-shrink:0;background:linear-gradient(90deg,transparent,var(--gold),var(--g2),var(--gold),transparent);background-size:200%;animation:dflow 3s linear infinite;position:relative} +.scs-vs{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:var(--bg);border:1.5px solid var(--gold);border-radius:20px;padding:3px 13px;font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:4px;color:var(--gold)} +.scs-footer{position:absolute;bottom:0;left:0;right:0;padding:6px 14px clamp(8px,2vh,18px);background:linear-gradient(to top,rgba(6,10,14,.98) 55%,transparent);display:flex;justify-content:center;gap:10px;z-index:30} +.scs-reset{font-family:'Oxanium',sans-serif;font-size:10px;font-weight:700;letter-spacing:2px;color:var(--dim);background:var(--s1);border:1px solid var(--bd);border-radius:10px;padding:8px 13px;cursor:pointer;text-transform:uppercase;transition:all .2s} +.scs-reset:active{color:var(--gold);border-color:var(--g3);transform:scale(.92)} +.scs-close{font-family:'Oxanium',sans-serif;font-size:10px;font-weight:700;letter-spacing:2px;color:var(--dim);background:var(--s1);border:1px solid var(--bd);border-radius:10px;padding:8px 13px;cursor:pointer;text-transform:uppercase;transition:all .2s} +.scs-close:active{color:var(--red);border-color:var(--red);transform:scale(.92)} + +/* SOLO ONE-PLAYER MODE */ +.scs-solo{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;background:var(--bg)} +.scs-solo-panel{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(10px,2.5vh,18px);position:relative;overflow:hidden;cursor:pointer;transition:background .3s} +.scs-solo-panel.idle-bg{background:radial-gradient(ellipse 70% 70% at center,rgba(20,55,130,.7) 0%,rgba(6,10,14,.98) 70%)} +.scs-solo-panel.hold-bg,.scs-solo-panel.ready-bg{animation:scs-hbg .8s ease-in-out infinite alternate} +.scs-solo-panel.run-bg{animation:scs-rbg .4s ease-in-out infinite alternate} +.scs-solo-panel.stop-bg{background:rgba(6,10,14,.98)} +.scs-solo-time{font-family:'Bebas Neue',sans-serif;font-size:clamp(72px,20vw,130px);letter-spacing:-4px;line-height:1;position:relative;z-index:1;transition:color .2s,text-shadow .2s} +.scs-solo-time.idle{color:#90d4ff;text-shadow:0 0 40px rgba(100,190,255,.9)} +.scs-solo-time.hold{color:#80ffbe;text-shadow:0 0 50px rgba(80,255,160,1);animation:hbreathe .5s ease-in-out infinite alternate} +.scs-solo-time.ready{color:#ffffff;text-shadow:0 0 60px rgba(80,255,160,1);animation:wbreathe .4s ease-in-out infinite alternate} +.scs-solo-time.run{color:#ff9090;text-shadow:0 0 40px rgba(255,100,100,1)} +.scs-solo-time.stop{color:#80ffbe;text-shadow:0 0 50px rgba(80,255,160,.8)} +.scs-solo-state{font-family:'Bebas Neue',sans-serif;font-size:clamp(12px,3.5vw,16px);letter-spacing:6px;text-transform:uppercase;position:relative;z-index:1} +.scs-solo-state.blu{color:#7ec8ff;text-shadow:0 0 20px rgba(100,180,255,1)} +.scs-solo-state.grn{color:#7affc0;text-shadow:0 0 20px rgba(80,255,160,1)} +.scs-solo-state.wht{color:#ffffff;text-shadow:0 0 20px rgba(255,255,255,.8)} +.scs-solo-state.red{color:#ff8080;text-shadow:0 0 20px rgba(255,100,100,1)} +.scs-solo-state.gold{color:var(--g2);text-shadow:0 0 20px rgba(255,217,122,.9)} +.scs-solo-hint{font-size:clamp(10px,2.5vw,12px);letter-spacing:3px;color:rgba(200,216,232,.65);text-transform:uppercase;position:relative;z-index:1;font-weight:600} +.scs-solo-history{position:relative;z-index:1;display:flex;flex-direction:column;gap:5px;align-items:center;width:100%;max-width:280px} +.scs-solo-hist-row{display:flex;align-items:center;justify-content:space-between;width:100%;padding:3px 10px;background:rgba(255,255,255,.05);border-radius:8px;border:1px solid rgba(255,255,255,.07)} +.scs-solo-hist-n{font-size:9px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;width:30px} +.scs-solo-hist-t{font-family:'Bebas Neue',sans-serif;font-size:15px;letter-spacing:1px;color:var(--gold)} +.scs-solo-hist-t.best{color:#80ffbe} +.scs-solo-best{display:flex;gap:10px;align-items:center;position:relative;z-index:1} +.scs-solo-best-lbl{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase} +.scs-solo-best-val{font-family:'Bebas Neue',sans-serif;font-size:14px;letter-spacing:2px;color:#80ffbe} +.scs-mode-toggle{display:flex;background:var(--s2);border:1px solid var(--bd);border-radius:10px;overflow:hidden} +.scs-mode-btn{padding:7px 14px;font-family:'Bebas Neue',sans-serif;font-size:11px;letter-spacing:3px;color:var(--dim);cursor:pointer;transition:all .2s} +.scs-mode-btn.active{background:var(--s3);color:var(--gold)} diff --git a/src/main.jsx b/src/main.jsx new file mode 100755 index 0000000..76b4a7a --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import App from "./App.jsx"; + +createRoot(document.getElementById("root")).render( + + + , +); diff --git a/vite.config.js b/vite.config.js new file mode 100755 index 0000000..081c8d9 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], +});