feat: UX game — stats live, production détaillée, prestige visible, badge achievements
- Click-zone : production/s, puissance ponte, multiplicateur toujours visibles - GeneratorShop : production totale en header, prod individuelle par générateur - PrestigePanel : toujours affiché (hint "Atteins 1M" si pas encore dispo) - Badge achievements X/27 dans la sidebar avec lien vers /achievements - Landing : tadpole sprite animé en accroche visuelle
This commit is contained in:
@@ -6,35 +6,48 @@ import { formatNumber } from "../utils/formatNumber";
|
|||||||
export function GeneratorShop() {
|
export function GeneratorShop() {
|
||||||
const generators = useGameStore((s) => s.state.generators);
|
const generators = useGameStore((s) => s.state.generators);
|
||||||
const resources = useGameStore((s) => s.state.resources);
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
|
const productionPerSecond = useGameStore((s) => s.productionPerSecond);
|
||||||
const buy = useGameStore((s) => s.buy);
|
const buy = useGameStore((s) => s.buy);
|
||||||
const generatorCost = useGameStore((s) => s.generatorCost);
|
const generatorCost = useGameStore((s) => s.generatorCost);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-4 rounded-xl bg-gray-900/80 backdrop-blur-sm max-w-md w-full">
|
<div className="flex flex-col gap-2 p-4 rounded-xl bg-gray-900/80 backdrop-blur-sm max-w-md w-full">
|
||||||
<h2 className="text-lg font-bold text-white">Générateurs</h2>
|
<div className="flex justify-between items-center">
|
||||||
|
<h2 className="text-sm font-bold text-white">Générateurs</h2>
|
||||||
|
<span className="text-xs text-emerald-400 font-medium">
|
||||||
|
{formatNumber(productionPerSecond)}/s
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{generators.map((gen) => {
|
{generators.map((gen) => {
|
||||||
const cost = generatorCost(gen);
|
const cost = generatorCost(gen);
|
||||||
const canAfford = resources >= cost;
|
const canAfford = resources >= cost;
|
||||||
|
const currentProd = gen.baseProduction * gen.owned;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={gen.id}
|
key={gen.id}
|
||||||
className={`flex items-center justify-between gap-3 p-3 rounded-lg border transition-colors ${
|
className={`flex items-center justify-between gap-2 p-2.5 rounded-lg border transition-colors ${
|
||||||
canAfford
|
canAfford
|
||||||
? "border-emerald-500/50 bg-emerald-950/30 hover:bg-emerald-950/50"
|
? "border-emerald-500/50 bg-emerald-950/30 hover:bg-emerald-950/50"
|
||||||
: "border-gray-700/50 bg-gray-800/30 opacity-60"
|
: "border-gray-700/50 bg-gray-800/30 opacity-60"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col min-w-0">
|
<div className="flex flex-col min-w-0">
|
||||||
<span className="text-white font-semibold text-sm">{gen.name}</span>
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-white font-semibold text-sm">{gen.name}</span>
|
||||||
|
{gen.owned > 0 && (
|
||||||
|
<span className="text-emerald-400 text-xs font-medium">x{gen.owned}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span className="text-gray-400 text-xs">
|
<span className="text-gray-400 text-xs">
|
||||||
+{gen.baseProduction}/s · x{gen.owned}
|
+{gen.baseProduction}/s chacun
|
||||||
|
{gen.owned > 0 && ` · ${formatNumber(currentProd)}/s total`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => buy(gen.id)}
|
onClick={() => buy(gen.id)}
|
||||||
disabled={!canAfford}
|
disabled={!canAfford}
|
||||||
className={`shrink-0 px-3 py-1.5 rounded-md text-sm font-medium transition-colors cursor-pointer ${
|
className={`shrink-0 px-3 py-1.5 rounded-md text-xs font-medium transition-colors cursor-pointer ${
|
||||||
canAfford
|
canAfford
|
||||||
? "bg-emerald-600 hover:bg-emerald-500 text-white"
|
? "bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||||
: "bg-gray-700 text-gray-500 cursor-not-allowed"
|
: "bg-gray-700 text-gray-500 cursor-not-allowed"
|
||||||
|
|||||||
@@ -29,24 +29,29 @@ export function PrestigePanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 p-4 rounded-xl bg-purple-900/60 backdrop-blur-sm max-w-md w-full">
|
<div className="flex flex-col gap-2 p-4 rounded-xl bg-purple-900/60 backdrop-blur-sm max-w-md w-full">
|
||||||
<div className="flex flex-wrap gap-4 text-sm text-purple-200">
|
<h2 className="text-sm font-bold text-purple-100">Prestige</h2>
|
||||||
<span>Générations : {prestigeCount}</span>
|
<div className="flex flex-wrap gap-3 text-xs text-purple-200">
|
||||||
<span>Mult : x{prestigeMultiplier.toFixed(1)}</span>
|
<span>Gén. {prestigeCount}</span>
|
||||||
<span>ADN : {ancestralDna}</span>
|
<span>Mult x{prestigeMultiplier.toFixed(1)}</span>
|
||||||
|
<span>ADN {ancestralDna}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canPrestige && (
|
{canPrestige ? (
|
||||||
<div className="flex flex-col gap-2 mt-2">
|
<div className="flex flex-col gap-2 mt-1">
|
||||||
<p className="text-sm text-purple-100">
|
<p className="text-sm text-purple-100">
|
||||||
Nouvelle Génération : <strong>+{dnaPreview} ADN</strong> + <strong>+0.1x mult</strong>
|
+{dnaPreview} ADN · +0.1x mult
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={handlePrestige}
|
onClick={handlePrestige}
|
||||||
className="px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-white font-semibold text-sm transition-colors cursor-pointer"
|
className="px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-white font-semibold text-sm transition-colors cursor-pointer animate-pulse"
|
||||||
>
|
>
|
||||||
Nouvelle Génération
|
Nouvelle Génération
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-purple-300/60">
|
||||||
|
Atteins 1M têtards pour prestige
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { GeneratorShop } from "../components/GeneratorShop";
|
|||||||
import { PrestigePanel } from "../components/PrestigePanel";
|
import { PrestigePanel } from "../components/PrestigePanel";
|
||||||
import { EvolutionTree } from "../components/EvolutionTree";
|
import { EvolutionTree } from "../components/EvolutionTree";
|
||||||
import { MilestoneBar } from "../components/MilestoneBar";
|
import { MilestoneBar } from "../components/MilestoneBar";
|
||||||
|
import { ACHIEVEMENTS } from "../data/achievements";
|
||||||
import "../scss/home.scss";
|
import "../scss/home.scss";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@@ -15,6 +16,9 @@ export default function Home() {
|
|||||||
const click = useGameStore((s) => s.click);
|
const click = useGameStore((s) => s.click);
|
||||||
const resources = useGameStore((s) => s.state.resources);
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
const clickMultiplier = useGameStore((s) => s.state.clickMultiplier);
|
const clickMultiplier = useGameStore((s) => s.state.clickMultiplier);
|
||||||
|
const productionPerSecond = useGameStore((s) => s.productionPerSecond);
|
||||||
|
const state = useGameStore((s) => s.state);
|
||||||
|
const prestigeMultiplier = state.prestigeMultiplier;
|
||||||
|
|
||||||
const createParticle = useCallback((clientX, clientY) => {
|
const createParticle = useCallback((clientX, clientY) => {
|
||||||
const particle = document.createElement("span");
|
const particle = document.createElement("span");
|
||||||
@@ -114,8 +118,17 @@ export default function Home() {
|
|||||||
{/* Clicker area — centre */}
|
{/* Clicker area — centre */}
|
||||||
<div className="click-zone" onClick={handleIncrement}>
|
<div className="click-zone" onClick={handleIncrement}>
|
||||||
<div className="tadpole-sprite" />
|
<div className="tadpole-sprite" />
|
||||||
<div className="text-center text-3xl md:text-4xl font-bold text-white drop-shadow-lg font-[var(--font)] select-none pointer-events-none">
|
<div className="click-zone-stats">
|
||||||
{formatNumber(resources)}
|
<div className="text-center text-3xl md:text-4xl font-bold text-white drop-shadow-lg font-[var(--font)] select-none pointer-events-none">
|
||||||
|
{formatNumber(resources)}
|
||||||
|
</div>
|
||||||
|
<div className="stats-bar">
|
||||||
|
<span>{formatNumber(productionPerSecond)}/s</span>
|
||||||
|
<span className="stats-sep">·</span>
|
||||||
|
<span>x{clickMultiplier} ponte</span>
|
||||||
|
<span className="stats-sep">·</span>
|
||||||
|
<span>x{prestigeMultiplier.toFixed(1)} mult</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -125,6 +138,9 @@ export default function Home() {
|
|||||||
<GeneratorShop />
|
<GeneratorShop />
|
||||||
<PrestigePanel />
|
<PrestigePanel />
|
||||||
<EvolutionTree />
|
<EvolutionTree />
|
||||||
|
<a href="/achievements" className="achieve-badge">
|
||||||
|
{ACHIEVEMENTS.filter((a) => a.check(state)).length}/{ACHIEVEMENTS.length} succès
|
||||||
|
</a>
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,8 +11,14 @@ export default function Landing() {
|
|||||||
content="Clickerz — Clicker idle dans le Tetard Universe. Fais éclore des têtards, évolue et domine le marais !"
|
content="Clickerz — Clicker idle dans le Tetard Universe. Fais éclore des têtards, évolue et domine le marais !"
|
||||||
/>
|
/>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<main className="min-h-[92vh] mt-20 flex flex-col items-center justify-center gap-8 bg-[var(--bg-color)]">
|
<main className="min-h-[92vh] mt-20 flex flex-col items-center justify-center gap-6 bg-[var(--bg-color)]">
|
||||||
<div className="flex flex-col items-center gap-4 text-center px-4">
|
<img
|
||||||
|
src="/svg/tadpole.svg"
|
||||||
|
alt="Têtard Clickerz"
|
||||||
|
className="w-40 h-40 md:w-52 md:h-52 drop-shadow-lg animate-bounce"
|
||||||
|
style={{ animationDuration: "3s" }}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center gap-3 text-center px-4">
|
||||||
<h1 className="text-4xl md:text-6xl font-bold text-gray-800 font-[var(--font)]">
|
<h1 className="text-4xl md:text-6xl font-bold text-gray-800 font-[var(--font)]">
|
||||||
Clickerz
|
Clickerz
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -47,6 +47,53 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Stats bar sous le compteur ---
|
||||||
|
|
||||||
|
.click-zone-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-sep {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Badge achievements sidebar ---
|
||||||
|
|
||||||
|
.achieve-badge {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6ee7b7;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(16, 185, 129, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Click feedback particle ---
|
// --- Click feedback particle ---
|
||||||
|
|
||||||
.click-particle {
|
.click-particle {
|
||||||
|
|||||||
Reference in New Issue
Block a user