const SHOWCASE = [
{ id: "espadas-1", suit: "espadas", rank: 1 },
{ id: "bastos-1", suit: "bastos", rank: 1 },
{ id: "espadas-7", suit: "espadas", rank: 7 },
{ id: "oros-7", suit: "oros", rank: 7 },
];
function TrucoApp() {
const [save, setSave] = useState(null);
const [screen, setScreen] = useState("title");
const [match, setMatch] = useState(null);
const [helpOpen, setHelpOpen] = useState(false);
const [result, setResult] = useState(null);
const [lang, setLang] = useState(getLanguage());
useEffect(() => { const fn = (e) => setLang(e.detail.lang); window.addEventListener("languagechange", fn); return () => window.removeEventListener("languagechange", fn); }, []);
const recorded = useRef(false);
useEffect(() => {
const loaded = loadSave();
setSave(loaded);
setSoundEnabled(loaded.sound);
}, []);
useEffect(() => {
if (!match)
return;
if (waitingOn(match) !== "ai")
return;
const rival = rivalById(match.rivalId);
const t = window.setTimeout(() => {
const action = chooseAiAction(match, rival.profile);
playSfx(action);
setMatch((m) => (m ? applyAction(m, action) : m));
}, thinkDelay(rival.profile));
return () => window.clearTimeout(t);
}, [match]);
useEffect(() => {
if (!match || !save)
return;
if (match.phase !== "matchOver" || !match.matchWinner)
return;
if (recorded.current)
return;
recorded.current = true;
const rival = rivalById(match.rivalId);
const won = match.matchWinner === "player";
const { save: next, delta } = recordMatch(save, {
ranked: match.ranked,
won,
rivalId: rival.id,
rivalPower: rival.difficulty,
matchId: match.id,
});
writeSave(next);
setSave(next);
setResult({
won,
delta,
ranked: match.ranked,
rival,
before: save.lp,
after: next.lp,
scores: match.scores,
});
if (won)
sfx.win();
else
sfx.lose();
setScreen("result");
}, [match, save]);
function persist(partial) {
if (!save)
return;
const next = { ...save, ...partial };
setSave(next);
writeSave(next);
}
function begin(rival, ranked) {
unlockAudio();
recorded.current = false;
setResult(null);
setMatch(startMatch(rival.id, ranked));
setScreen("play");
sfx.deal();
}
function dispatch(action) {
playSfx(action);
setMatch((m) => (m ? applyAction(m, action) : m));
}
function playSfx(action) {
if (action.type === "PLAY")
sfx.play();
else if (action.type === "CALL")
sfx.call();
else if (action.type === "RESPOND" && action.response === "quiero")
sfx.want();
else if (action.type === "RESPOND" && action.response === "no-quiero")
sfx.fold();
else if (action.type === "MAZO")
sfx.fold();
else if (action.type === "ACK")
sfx.click();
}
const rank = lpToRank(save?.lp ?? 0);
const rival = match ? rivalById(match.rivalId) : null;
return (
{screen === "title" ? (
{
unlockAudio();
setScreen("rivals");
}} onRanked={() => {
unlockAudio();
setScreen("ranked");
}} onRules={() => setScreen("rules")} onHelp={() => setHelpOpen(true)} onToggleSound={() => {
const next = !(save?.sound ?? true);
persist({ sound: next });
setSoundEnabled(next);
unlockAudio();
}}/>) : null}
{screen === "rivals" ? ( setScreen("title")} onPick={(r) => begin(r, false)}/>) : null}
{screen === "ranked" && save ? ( setScreen("title")} onQueue={() => begin(rivalForLp(save.lp), true)}/>) : null}
{screen === "rules" ? setScreen("title")}/> : null}
{screen === "play" && match && rival && save ? ( setHelpOpen(true)} onExit={() => {
setMatch(null);
setScreen("title");
}} onToggleSound={() => {
persist({ sound: !save.sound });
setSoundEnabled(!save.sound);
}}/>) : null}
{screen === "result" && result && save ? ( {
setMatch(null);
setScreen("title");
}} onRematch={() => begin(result.rival, result.ranked)} onRanked={() => {
setMatch(null);
setScreen("ranked");
}}/>) : null}
setHelpOpen(false)} match={match}/>
);
}
function LanguageSelector(){ return ; }
function TitleScreen({ rank, sound, onPlay, onRanked, onRules, onHelp, onToggleSound, }) {
return (
{t("deck")}
TRUCO URUGUAYO
{t("tagline")}
{t("update")} {t("newTable")}
{SHOWCASE.map((card, i) => (
))}
);
}
function RivalSelect({ onBack, onPick }) {
return (
{t("rivals")}
{t("rivalsDesc")}
{RIVALS.map((r) => (
))}
);
}
function RankedHub({ save, onBack, onQueue, }) {
const rank = lpToRank(save.lp);
const peak = lpToRank(save.peakLp);
const rival = rivalForLp(save.lp);
const games = save.rankedWins + save.rankedLosses;
const wr = games ? Math.round((save.rankedWins / games) * 100) : 0;
return (
{t("ranked")}
{t("rankDesc")}
- {t("wins")}
- {save.rankedWins}
- {t("losses")}
- {save.rankedLosses}
- WR
- {wr}%
{t("peak")}: {rankLabel(peak)} · {t("streak")} {save.streak} ({t("best")} {save.bestStreak})
{t("rankDesc")}
{["madera","hierro","bronce","plata","oro","platino","esmeralda","diamante","maestro","gran_maestro","challenger","retador","retador_dorado"].map((tier)=>{ const rr=lpToRank(TIER_META?.[tier]?.min ?? 0); return
{trTier(tier)}{["madera","hierro","bronce","plata","oro","platino","esmeralda","diamante"].includes(tier)?" IV–I":""}
;})}
{t("nextRival")}
{rival.name}
{rival.difficultyLabel}
{save.history.length ? (
{save.history.slice(0, 8).map((h, i) => (-
{h.won ? t("victory") : t("defeat")} {t("versus")} {rivalById(h.rival).name}
= 0 ? "text-cream" : "text-crimson")}>
{h.lp >= 0 ? `+${h.lp}` : h.lp} LP
))}
) : null}
);
}
function RulesScreen({ onBack }) {
return (
{t("rules")}
{RULE_SECTIONS.map((s) => (
))}
{t("hierarchy")}
{POWER_LADDER.slice(0, 4).map((row) => (
))}
);
}
function ResultScreen({ result, save, onMenu, onRematch, onRanked, }) {
const before = lpToRank(result.before);
const after = lpToRank(result.after);
const promoted = rankLabel(before) !== rankLabel(after) && result.won;
return (
{result.ranked ? t("rankedShort") : t("free")}
{result.won ? t("win") : t("lose")}
{result.scores.player} a {result.scores.ai} {t("versus")} {result.rival.name}
“{trRivalField(result.rival.id, "quote", result.rival.quote)}”
{result.ranked ? (
{result.delta >= 0 ? "+" : ""}
{result.delta} LP
{promoted ? (
{t("promotion", {rank: rankLabel(after)})}
) : null}
{t("streak")} {save.streak}
) : null}
{result.ranked ? () : null}
);
}