feat(sprint1-step5): migration Tailwind v4 + Zustand — suppression WildCoinContext

- Install tailwindcss @tailwindcss/vite zustand
- useGameStore.ts : Zustand store wrappant economy.ts (tick, click, buy, prestige, buyNode, loadFromServer)
- GameTick.tsx : composant timer 1s
- GeneratorShop.tsx : boutique générateurs Tailwind (remplace Amelioration.jsx)
- EvolutionTree, PrestigePanel, MilestoneBar : rewrite Zustand + Tailwind
- Hud.jsx : rewrite Zustand + Tailwind (suppression Hud.scss)
- BoutiqueCard, Achievements : migrés vers Zustand
- Supprimé : WildCoin/ (4 fichiers), timer/Timer.jsx, useEconomy.ts, Hud.scss
- WildCoinProvider retiré de main.jsx
This commit is contained in:
2026-03-20 13:40:51 +01:00
parent d215e9a33e
commit 307feb711f
20 changed files with 783 additions and 877 deletions

View File

@@ -1,7 +1,9 @@
import { useWildCoin } from "./WildCoin/WildCoinContext";
// BoutiqueCard.jsx — Legacy shop card (shop.json boosters)
// TODO: Migrate to economy.ts generator system in a future step
import "../scss/components/boutiquecard.scss";
import "../scss/components/buttons.scss";
import PropTypes from "prop-types";
import { useGameStore } from "../store/useGameStore";
export default function BoutiqueCard({
name,
@@ -9,89 +11,13 @@ export default function BoutiqueCard({
incrementValue,
description,
image,
link,
type,
buyed,
}) {
BoutiqueCard.propTypes = {
name: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
incrementValue: PropTypes.number.isRequired,
description: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
buyed: PropTypes.bool.isRequired,
};
const resources = useGameStore((s) => s.state.resources);
const {
wildCoin,
incrementClick,
setWildCoin,
setIncrementClick,
incrementPerSecond,
setIncrementPerSecond,
setCoffee,
setSantaDrunk,
setManic,
setSnowman,
setBonnet,
setSugar,
setCookie,
setCouronne,
setEpice,
setBiere,
} = useWildCoin();
// Legacy shop — disabled for now, generators are in GeneratorShop
const canAfford = resources >= price;
const acheterAmelioration = (type, price, name) => {
const prices = price;
const value = prices;
if (wildCoin >= value) {
if (type === "actif") {
setIncrementClick(incrementClick + incrementValue);
} else if (type === "passif") {
setIncrementPerSecond(incrementPerSecond + incrementValue);
}
setWildCoin(wildCoin - value);
switch (name) {
case "Tasse à café":
setCoffee((prevCoffee) => [true, prevCoffee[1] + 1]);
break;
case "Manic":
setManic((prevManic) => [true, prevManic[1] + 1]);
break;
case "Bonnet":
setBonnet((prevBonnet) => [true, prevBonnet[1] + 1]);
break;
case "Mr Bonhomme":
setSnowman((prevSnowman) => [true, prevSnowman[1] + 1]);
break;
case "Canne en sucre":
setSugar((prevSugar) => [true, prevSugar[1] + 1]);
break;
case "Cookie":
setCookie((prevCookie) => [true, prevCookie[1] + 1]);
break;
case "Couronne d'hiver":
setCouronne((prevCouronne) => [true, prevCouronne[1] + 1]);
break;
case "Mr pain d'épice":
setEpice((prevEpice) => [true, prevEpice[1] + 1]);
break;
case "Bière":
setBiere((prevBiere) => [true, prevBiere[1] + 1]);
setSantaDrunk(true);
break;
default:
break;
}
} else {
console.log("Pas assez de WildCoin pour acheter cette amélioration.");
}
};
return (
<div className="shopcardcontainer">
<div className="shopcontainer">
@@ -105,7 +31,6 @@ export default function BoutiqueCard({
<p className="itemname">{name}</p>
<div className="price">
<p className="itemprice">{price}</p>
<div className="priceicon" />
</div>
</div>
@@ -119,12 +44,22 @@ export default function BoutiqueCard({
</div>
</div>
<button
onClick={() => acheterAmelioration(type, price, name)}
disabled={!canAfford}
className="primary-button"
style={{ opacity: canAfford ? 1 : 0.5 }}
>
Acheter
Bientôt
</button>
</div>
</div>
);
}
BoutiqueCard.propTypes = {
name: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
incrementValue: PropTypes.number.isRequired,
description: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};

View File

@@ -0,0 +1,99 @@
// EvolutionTree.tsx — Arbre d'Évolution permanent (jamais reset)
// Visible après le premier prestige (prestigeCount >= 1)
import React from "react";
import { useGameStore } from "../store/useGameStore";
import { canBuyEvolutionNode } from "../core/economy";
import type { EvolutionNode } from "../core/economy";
const EFFECT_DESCRIPTIONS: Record<string, (value: number) => string> = {
click_multiplier: (v) => `x${v} puissance de Ponte`,
production_multiplier: (v) => `x${v} production tous générateurs`,
start_bonus: (v) => `+${v} têtards au début de chaque run`,
unlock_generator: () => `Débloque le Lac Mystique dès le début`,
achievement_scaling: (v) => `+${(v * 100).toFixed(0)}% production par succès`,
};
function NodeCard({
node,
canBuy,
onBuy,
}: {
node: EvolutionNode;
canBuy: boolean;
onBuy: () => void;
}) {
return (
<div
className={`flex flex-col gap-2 p-3 rounded-lg border text-sm transition-colors ${
node.unlocked
? "border-emerald-500/50 bg-emerald-950/30"
: canBuy
? "border-amber-500/50 bg-amber-950/20"
: "border-gray-700/50 bg-gray-800/30 opacity-50"
}`}
>
<div className="flex justify-between items-center">
<span className="text-white font-semibold">{node.name}</span>
<span className="text-xs text-gray-400">
{node.unlocked ? "Débloqué" : `${node.cost} ADN`}
</span>
</div>
<p className="text-xs text-gray-300">
{EFFECT_DESCRIPTIONS[node.effect](node.value)}
</p>
{!node.unlocked && (
<button
disabled={!canBuy}
onClick={onBuy}
className={`px-3 py-1 rounded text-xs font-medium transition-colors cursor-pointer ${
canBuy
? "bg-amber-600 hover:bg-amber-500 text-white"
: "bg-gray-700 text-gray-500 cursor-not-allowed"
}`}
>
{canBuy ? "Débloquer" : "Verrouillé"}
</button>
)}
</div>
);
}
export function EvolutionTree() {
const state = useGameStore((s) => s.state);
const buyNode = useGameStore((s) => s.buyNode);
const { evolutionTree, prestigeCount } = state;
if (prestigeCount < 1) return null;
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 justify-between items-center">
<h3 className="text-lg font-bold text-white">Arbre d'Évolution</h3>
<span className="text-sm text-amber-300">{state.ancestralDna} ADN</span>
</div>
<div className="flex flex-col gap-2">
{evolutionTree.map((node, index) => (
<React.Fragment key={node.id}>
{index > 0 && (
<div
className={`text-center text-xs ${
evolutionTree[index - 1].unlocked
? "text-emerald-400"
: "text-gray-600"
}`}
>
|
</div>
)}
<NodeCard
node={node}
canBuy={canBuyEvolutionNode(state, node.id)}
onBuy={() => buyNode(node.id)}
/>
</React.Fragment>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
// GameTick.tsx — Lance le tick Zustand toutes les secondes
// À monter une seule fois dans l'arbre React (dans App)
import { useEffect } from "react";
import { useGameStore } from "../store/useGameStore";
export function GameTick() {
const tick = useGameStore((s) => s.tick);
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [tick]);
return null;
}

View File

@@ -0,0 +1,51 @@
// GeneratorShop.tsx — Boutique de générateurs (economy.ts)
// Remplace Amelioration.jsx (legacy WildCoinContext)
import { useGameStore } from "../store/useGameStore";
import { formatNumber } from "../utils/formatNumber";
export function GeneratorShop() {
const generators = useGameStore((s) => s.state.generators);
const resources = useGameStore((s) => s.state.resources);
const buy = useGameStore((s) => s.buy);
const generatorCost = useGameStore((s) => s.generatorCost);
return (
<div className="flex flex-col gap-3 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>
{generators.map((gen) => {
const cost = generatorCost(gen);
const canAfford = resources >= cost;
return (
<div
key={gen.id}
className={`flex items-center justify-between gap-3 p-3 rounded-lg border transition-colors ${
canAfford
? "border-emerald-500/50 bg-emerald-950/30 hover:bg-emerald-950/50"
: "border-gray-700/50 bg-gray-800/30 opacity-60"
}`}
>
<div className="flex flex-col min-w-0">
<span className="text-white font-semibold text-sm">{gen.name}</span>
<span className="text-gray-400 text-xs">
+{gen.baseProduction}/s &middot; x{gen.owned}
</span>
</div>
<button
onClick={() => buy(gen.id)}
disabled={!canAfford}
className={`shrink-0 px-3 py-1.5 rounded-md text-sm font-medium transition-colors cursor-pointer ${
canAfford
? "bg-emerald-600 hover:bg-emerald-500 text-white"
: "bg-gray-700 text-gray-500 cursor-not-allowed"
}`}
>
{formatNumber(cost)}
</button>
</div>
);
})}
</div>
);
}

View File

@@ -1,155 +1,41 @@
import "../../scss/components/Hud.scss";
import { useWildCoin } from "../WildCoin/WildCoinContext";
import Timer from "../timer/Timer";
import propTypes from "prop-types";
// Hud.jsx — Stats HUD (Zustand)
import { useGameStore } from "../../store/useGameStore";
import { formatNumber } from "../../utils/formatNumber";
const formatTime = (time) => {
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
const secs = time % 60;
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
};
function Hud({ isVisible }) {
Hud.propTypes = {
isVisible: propTypes.bool.isRequired,
};
const resources = useGameStore((s) => s.state.resources);
const clickMultiplier = useGameStore((s) => s.state.clickMultiplier);
const productionPerSecond = useGameStore((s) => s.productionPerSecond);
const playSeconds = useGameStore((s) => s.playSeconds);
const {
manic,
snowman,
bonnet,
sugar,
cookie,
couronne,
epice,
biere,
coffee,
} = useWildCoin();
const { incrementClick, incrementPerSecond } = useWildCoin();
const hiddenDiv = isVisible ? "none" : null;
if (isVisible) return null;
return (
<div className="hudContainer">
<div style={{ display: hiddenDiv }} className="hudStats">
<div className="time section">
<p>Temps de jeu</p>
<p><Timer /></p>
<div className="fixed top-20 left-1/2 -translate-x-1/2 z-10 flex flex-col items-center gap-2 px-6 py-3 rounded-xl bg-gray-900/90 backdrop-blur-sm text-white font-[var(--font)]">
<div className="flex gap-6 text-sm">
<div className="flex flex-col items-center">
<span className="text-gray-400 text-xs">Temps</span>
<span>{formatTime(playSeconds)}</span>
</div>
<div className="auto section">
<p>Auto CPS</p>
<p>{incrementPerSecond}</p>
<div className="flex flex-col items-center">
<span className="text-gray-400 text-xs">Têtards/s</span>
<span>{formatNumber(productionPerSecond)}</span>
</div>
<div className="player section">
<p>Player Click</p>
<p>{incrementClick}</p>
<div className="flex flex-col items-center">
<span className="text-gray-400 text-xs">Ponte</span>
<span>{clickMultiplier}</span>
</div>
</div>
<div className="hudBooster">
{coffee[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Tasse.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{coffee[1]}</p>
</div>
</div>
) : null}
{manic[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Hand.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{manic[1]}</p>
</div>
</div>
) : null}
{snowman[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Bonhome.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{snowman[1]}</p>
</div>
</div>
) : null}
{bonnet[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Bonnet.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{bonnet[1]}</p>
</div>
</div>
) : null}
{sugar[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Canne.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{sugar[1]}</p>
</div>
</div>
) : null}
{cookie[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Cookie.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{cookie[1]}</p>
</div>
</div>
) : null}
{couronne[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Courone.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{couronne[1]}</p>
</div>
</div>
) : null}
{epice[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/PainDep.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{epice[1]}</p>
</div>
</div>
) : null}
{biere[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Beer.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{biere[1]}</p>
</div>
</div>
) : null}
</div>
<div className="text-lg font-bold text-emerald-400">
{formatNumber(resources)}
</div>
</div>
);
}

View File

@@ -1,50 +1,37 @@
// MilestoneBar.tsx — Progression vers le prochain prestige
// Barre visuelle ressources / 1 000 000 + indicateur restant
// Barre visuelle ressources / 1 000 000
import React from "react";
import { useGameStore } from "../store/useGameStore";
import { formatNumber } from "../utils/formatNumber";
const PRESTIGE_THRESHOLD = 1_000_000;
interface MilestoneBarProps {
resources: number;
}
export function MilestoneBar() {
const resources = useGameStore((s) => s.state.resources);
export function MilestoneBar({ resources }: MilestoneBarProps) {
const progress = Math.min(resources / PRESTIGE_THRESHOLD, 1);
const progressPercent = (progress * 100).toFixed(1);
const remaining = Math.max(PRESTIGE_THRESHOLD - resources, 0);
const formatNumber = (n: number): string => {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
return Math.floor(n).toString();
};
return (
<div className="milestone-bar" aria-label="Progression vers le prestige">
<div className="milestone-label">
Prochain prestige : {formatNumber(resources)} / {formatNumber(PRESTIGE_THRESHOLD)}
<div className="flex flex-col gap-1 max-w-md w-full">
<div className="text-xs text-gray-300 flex justify-between">
<span>Prochaine Génération</span>
<span>
{formatNumber(resources)} / {formatNumber(PRESTIGE_THRESHOLD)}
</span>
</div>
<div
className="milestone-track"
role="progressbar"
aria-valuenow={Math.floor(progress * 100)}
aria-valuemin={0}
aria-valuemax={100}
>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className="milestone-fill"
className="h-full bg-gradient-to-r from-purple-600 to-purple-400 transition-all duration-500 rounded-full"
style={{ width: `${progressPercent}%` }}
/>
</div>
{remaining > 0 && (
<div className="milestone-remaining">
{formatNumber(remaining)} ressources restantes
</div>
)}
{remaining === 0 && (
<div className="milestone-ready">Prestige disponible !</div>
)}
<div className="text-xs text-gray-400 text-right">
{remaining > 0
? `${formatNumber(remaining)} têtards restants`
: "Nouvelle Génération disponible !"}
</div>
</div>
);
}

View File

@@ -1,54 +1,50 @@
// PrestigePanel.tsx — Boucle de prestige long terme
// Visible uniquement quand canPrestige = true (ressources 1 000 000)
// PrestigePanel.tsx — Nouvelle Génération (prestige)
// Visible uniquement quand canPrestige = true (ressources >= 1 000 000)
import React from "react";
import { useGameStore } from "../store/useGameStore";
import { computePrestigeDna } from "../core/economy";
interface PrestigePanelProps {
prestigeCount: number;
prestigeMultiplier: number;
canPrestige: boolean;
onPrestige: () => void;
}
export function PrestigePanel() {
const { prestigeCount, prestigeMultiplier, ancestralDna, lifetimeTadpoles } =
useGameStore((s) => s.state);
const canPrestige = useGameStore((s) => s.canPrestige);
const prestige = useGameStore((s) => s.prestige);
const dnaPreview = computePrestigeDna(lifetimeTadpoles);
export function PrestigePanel({
prestigeCount,
prestigeMultiplier,
canPrestige,
onPrestige,
}: PrestigePanelProps) {
const handlePrestige = () => {
const confirmed = window.confirm(
`Prestige — Reset total : ressources et générateurs à zéro.\n` +
`Récompense : +0.1× multiplicateur permanent.\n\n` +
`Multiplicateur actuel : ×${prestigeMultiplier.toFixed(1)}\n` +
`Multiplicateur après : ×${(prestigeMultiplier + 0.1).toFixed(1)}\n\n` +
`Confirmer le prestige ?`
`Nouvelle Génération\n\n` +
`Reset : têtards et générateurs à zéro.\n` +
`Récompense : +${dnaPreview} ADN Ancestral\n` +
` +0.1x multiplicateur permanent\n\n` +
`ADN actuel : ${ancestralDna}\n` +
`ADN après : ${ancestralDna + dnaPreview}\n` +
`Multiplicateur : x${prestigeMultiplier.toFixed(1)} → x${(prestigeMultiplier + 0.1).toFixed(1)}\n\n` +
`L'Arbre d'Évolution persiste.\n\n` +
`Confirmer la Nouvelle Génération ?`
);
if (confirmed) {
onPrestige();
}
if (confirmed) prestige();
};
return (
<div className="prestige-panel">
<div className="prestige-stats">
<span className="prestige-count">Prestiges : {prestigeCount}</span>
<span className="prestige-multiplier">
Multiplicateur : ×{prestigeMultiplier.toFixed(1)}
</span>
<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">
<span>Générations : {prestigeCount}</span>
<span>Mult : x{prestigeMultiplier.toFixed(1)}</span>
<span>ADN : {ancestralDna}</span>
</div>
{canPrestige && (
<div className="prestige-action">
<div className="prestige-reward">
Récompense disponible : <strong>+0.1× multiplicateur permanent</strong>
</div>
<div className="flex flex-col gap-2 mt-2">
<p className="text-sm text-purple-100">
Nouvelle Génération : <strong>+{dnaPreview} ADN</strong> + <strong>+0.1x mult</strong>
</p>
<button
className="prestige-button"
onClick={handlePrestige}
aria-label="Déclencher le prestige"
className="px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-white font-semibold text-sm transition-colors cursor-pointer"
>
Prestige
Nouvelle Génération
</button>
</div>
)}

View File

@@ -1,75 +0,0 @@
import { useWildCoin } from "./WildCoinContext";
function Ameliorations() {
const {
wildCoin,
incrementClick,
setWildCoin,
setIncrementClick,
incrementPerSecond,
setIncrementPerSecond,
} = useWildCoin();
const activePrices = [5, 15, 50, 500]; // prix
const passivePrices = [5, 15, 50, 500];
const activeIncrementValues = [1, 3, 10, 100]; // boost = incrementValue
const passiveIncrementValues = [1, 3, 10, 100]; // = incrementValue
const acheterAmelioration = (type, amount) => {
const prices = type === "actif" ? activePrices : passivePrices;
const incrementValues =
type === "actif" ? activeIncrementValues : passiveIncrementValues;
const price = prices[amount - 1];
const incrementValue = incrementValues[amount - 1];
if (wildCoin >= price) {
if (type === "actif") {
setIncrementClick(incrementClick + incrementValue);
} else if (type === "passif") {
setIncrementPerSecond(incrementPerSecond + incrementValue);
}
setWildCoin(wildCoin - price);
} else {
console.log("Pas assez de WildCoin pour acheter cette amélioration.");
}
};
return (
<div className="divMagasinAmelio">
<h2>Magasin d'Améliorations</h2>
<div className="divAmelioActives">
<p>Améliorations Actives :</p>
{[1, 2, 3, 4].map((amount) => (
<div key={amount}>
Price: {activePrices[amount - 1]} - (+
{activeIncrementValues[amount - 1]})
<button
className="amelioActives"
onClick={() => acheterAmelioration("actif", amount)}
>
Acheter
</button>
</div>
))}
</div>
<div className="divAmelioPassives">
<p>Améliorations Passives :</p>
{[1, 2, 3, 4].map((amount) => (
<div key={amount}>
Price: {passivePrices[amount - 1]} - (+
{passiveIncrementValues[amount - 1]})
<button
className="amelioPassives"
onClick={() => acheterAmelioration("passif", amount)}
>
Acheter
</button>
</div>
))}
</div>
</div>
);
}
export default Ameliorations;

View File

@@ -1,43 +0,0 @@
import { createContext, useContext, useState, useEffect } from "react";
export const WildCoinContext = createContext();
export const useWildCoin = () => {
return useContext(WildCoinContext);
};
export function WildCoinProvider({ children }) {
// Value of coin
const [wildCoin, setWildCoin] = useState(0);
// increment by click state
const [incrementClick, setIncrementClick] = useState(1);
// increment inner useEffect state
const [incrementPerSecond, setIncrementPerSecond] = useState(1);
const incrementWildCoin = (amount) => {
setWildCoin((prevWildCoin) => prevWildCoin + amount);
};
/**
* @passiveGenerationInterval incre per sec wild coin in wildCoin
* */
useEffect(() => {
const passiveGenerationInterval = setInterval(() => {
incrementWildCoin(incrementPerSecond);
}, 1000);
return () => clearInterval(passiveGenerationInterval);
}, [incrementPerSecond]);
const value = {
wildCoin,
setWildCoin,
incrementClick,
incrementWildCoin,
};
return (
<WildCoinContext.Provider value={value}>
{children}
</WildCoinContext.Provider>
);
}

View File

@@ -1,138 +0,0 @@
import { createContext, useContext, useState, useEffect } from "react";
export const WildCoinContext = createContext();
export const useWildCoin = () => {
return useContext(WildCoinContext);
};
export function WildCoinProvider({ children }) {
const initialState = {
wildCoin: 0,
incrementClick: 1,
incrementPerSecond: 0,
};
const [state, setState] = useState(() => {
const storedContext = JSON.parse(localStorage.getItem("wildCoinContext"));
return {
...initialState,
...(storedContext || {}),
};
});
const [coffee, setCoffee] = useState([false, 0]);
const [manic, setManic] = useState([false, 0]);
const [snowman, setSnowman] = useState([false, 0]);
const [bonnet, setBonnet] = useState([false, 0]);
const [sugar, setSugar] = useState([false, 0]);
const [cookie, setCookie] = useState([false, 0]);
const [couronne, setCouronne] = useState([false, 0]);
const [epice, setEpice] = useState([false, 0]);
const [biere, setBiere] = useState([false, 0]);
const [santaDrunk, setSantaDrunk] = useState(false);
const updateWildCoin = (amount) => {
setState((prev) => ({
...prev,
wildCoin: prev.wildCoin + amount,
}));
};
const incrementWildCoin = (amount) => {
updateWildCoin(amount);
};
const setIncrementClick = (amount) => {
setState((prev) => ({
...prev,
incrementClick: amount,
}));
};
const setIncrementPerSecond = (amount) => {
setState((prev) => ({
...prev,
incrementPerSecond: amount,
}));
};
const setWildCoin = (amount) => {
setState((prev) => ({
...prev,
wildCoin: amount,
}));
};
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds((prevSeconds) => prevSeconds + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);
const formatTime = (time) => {
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
const seconds = time % 60;
const formattedTime = `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
return formattedTime;
};
useEffect(() => {
localStorage.setItem("wildCoinContext", JSON.stringify(state));
}, [state]);
useEffect(() => {
const passiveGenerationInterval = setInterval(() => {
updateWildCoin(state.incrementPerSecond);
}, 1000);
return () => clearInterval(passiveGenerationInterval);
}, [state.incrementPerSecond]);
const contextValue = {
...state,
incrementWildCoin,
setIncrementClick,
setIncrementPerSecond,
setWildCoin,
coffee,
setCoffee,
manic,
setManic,
snowman,
setSnowman,
bonnet,
setBonnet,
sugar,
setSugar,
cookie,
setCookie,
couronne,
setCouronne,
epice,
setEpice,
biere,
setBiere,
setSantaDrunk,
santaDrunk,
seconds,
setSeconds,
formatTime,
};
return (
<WildCoinContext.Provider value={contextValue}>
{children}
</WildCoinContext.Provider>
);
}

View File

@@ -1,16 +0,0 @@
import { useWildCoin } from "./WildCoinContext";
import WildCoinS from "../../../public/WildCoin.svg";
function WildCoinIncrementAction() {
const { incrementClick, incrementWildCoin } = useWildCoin();
const handleIncrement = () => {
incrementWildCoin(incrementClick);
};
return (
<img src={WildCoinS} className="wildCoinBtn" style={{width:"40px", height:"40px"}} alt="Clique pour augmenter le score" aria-label="Clique pour augmenter le score" onClick={handleIncrement} />
);
}
export default WildCoinIncrementAction;

View File

@@ -1,13 +0,0 @@
import { useWildCoin } from "../WildCoin/WildCoinContext";
function Timer() {
const { formatTime, seconds } = useWildCoin();
return (
<div>
<p>{formatTime(seconds)}</p>
</div>
);
}
export default Timer;

View File

@@ -1,93 +0,0 @@
// useEconomy.ts — Hook React avec lazy calculation + localStorage
// Pas de setInterval pour les gains passifs — tout est calculé au read
import { useState, useCallback, useEffect } from "react";
import {
GameState,
DEFAULT_STATE,
applyIdleGains,
applyClick,
buyGenerator,
applyPrestige,
canPrestige,
totalProductionPerSecond,
generatorCost,
} from "../core/economy";
const SAVE_KEY = "clickerz_state";
function loadState(): GameState {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return { ...DEFAULT_STATE, lastTick: Date.now() };
const saved = JSON.parse(raw) as GameState;
// Appliquer les gains idle accumulés pendant l'absence
return applyIdleGains(saved, Date.now());
} catch {
return { ...DEFAULT_STATE, lastTick: Date.now() };
}
}
function saveState(state: GameState): void {
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
}
export function useEconomy() {
const [state, setState] = useState<GameState>(loadState);
// Auto-save + tick UI toutes les secondes (pour rafraîchir l'affichage uniquement)
// La vraie valeur est calculée lazily dans totalProductionPerSecond
useEffect(() => {
const id = setInterval(() => {
setState((prev) => {
const updated = applyIdleGains(prev, Date.now());
saveState(updated);
return updated;
});
}, 1000);
return () => clearInterval(id);
}, []);
const click = useCallback(() => {
setState((prev) => {
const updated = applyClick(applyIdleGains(prev, Date.now()));
saveState(updated);
return updated;
});
}, []);
const buy = useCallback((genId: string) => {
setState((prev) => {
const withIdle = applyIdleGains(prev, Date.now());
const updated = buyGenerator(withIdle, genId);
if (!updated) return prev;
saveState(updated);
return updated;
});
}, []);
const prestige = useCallback(() => {
setState((prev) => {
if (!canPrestige(prev)) return prev;
const updated = applyPrestige(prev);
saveState(updated);
return updated;
});
}, []);
const reset = useCallback(() => {
const fresh = { ...DEFAULT_STATE, lastTick: Date.now() };
saveState(fresh);
setState(fresh);
}, []);
return {
state,
click,
buy,
prestige,
canPrestige: canPrestige(state),
productionPerSecond: totalProductionPerSecond(state),
generatorCost,
};
}

View File

@@ -1,7 +1,9 @@
@import "tailwindcss";
:root {
margin: 0;
padding: 0;
}
::-webkit-scrollbar {
width: 1px;

View File

@@ -1,14 +1,14 @@
import { useState } from "react";
import AchievementsCard from "../components/AchievementsCard";
import "../scss/achievements.scss";
import { useWildCoin } from "../components/WildCoin/WildCoinContext";
import { useGameStore } from "../store/useGameStore";
import achievements from "../data/Achievements.json";
function Achievements() {
const { wildCoin } = useWildCoin();
const resources = useGameStore((s) => s.state.resources);
let score = 1;
if (wildCoin >= 25) {
score = Math.floor((wildCoin - 25) / 400) + 1;
if (resources >= 25) {
score = Math.floor((resources - 25) / 400) + 1;
} else {
score = 0;
}

View File

@@ -1,102 +0,0 @@
.hudContainer {
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-items: center;
justify-content: space-around;
min-width: 260px;
width: fit-content;
max-width: 1280px;
height: fit-content;
gap: 1rem;
padding: 1rem;
border-radius: 8px;
box-sizing: border-box;
background-color: var(--color-grey);
color: var(--color-white);
font-family: var(--font);
color: aliceblue;
font-size: 1rem;
text-align: center;
position: absolute;
left: 50%;
transform: translate(-50%);
z-index: 2;
.hudStats {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
.section {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
}
.hudBooster {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
align-items: center;
padding: 1rem;
gap: 1rem;
min-width: 280px;
width: auto;
max-width: 1280px;
height: fit-content;
color: var(--color-white);
font-family: var(--font);
color: aliceblue;
font-size: 1rem;
text-align: center;
border-radius: 8px;
box-sizing: border-box;
.boosterItem {
display: flex;
flex-wrap: wrap;
width: 30px;
height: 30px;
gap: 0.6rem;
.boosterIcon {
width: 100%;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.countbox {
position: absolute;
margin-top: 20px;
margin-left: 15px;
border: solid 1px var(--color-white);
background-color: var(--color-white);
border-radius: 20%;
padding: 0.1rem;
color: var(--color-grey);
min-width: 20px;
width: fit-content;
height: 20px;
box-shadow: -1px -1px 7px 0px var(--color-grey);
}
.boosterCount {
font-family: var(--font);
font-size: 0.7rem;
text-align: center;
font-weight: 900;
}
}
}
}

View File

@@ -0,0 +1,146 @@
// useGameStore.ts — Zustand store, source unique de l'état game
// Lazy calculation pattern : gains passifs calculés au read depuis lastTick
import { create } from "zustand";
import {
GameState,
DEFAULT_STATE,
applyIdleGains,
applyClick,
buyGenerator,
buyEvolutionNode,
applyPrestige,
canPrestige as canPrestigeCheck,
totalProductionPerSecond,
generatorCost as genCost,
} from "../core/economy";
const SAVE_KEY = "clickerz_state";
function loadState(): GameState {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return { ...DEFAULT_STATE, lastTick: Date.now() };
const saved = JSON.parse(raw) as GameState;
return applyIdleGains(saved, Date.now());
} catch {
return { ...DEFAULT_STATE, lastTick: Date.now() };
}
}
function saveState(state: GameState): void {
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
}
interface GameStore {
// State
state: GameState;
playSeconds: number;
// Derived (recalculated on tick)
canPrestige: boolean;
productionPerSecond: number;
// Actions
tick: () => void;
click: () => void;
buy: (genId: string) => void;
buyNode: (nodeId: string) => void;
prestige: () => void;
reset: () => void;
loadFromServer: (serverState: GameState) => void;
generatorCost: typeof genCost;
}
export const useGameStore = create<GameStore>((set, get) => ({
state: loadState(),
playSeconds: 0,
canPrestige: canPrestigeCheck(loadState()),
productionPerSecond: totalProductionPerSecond(loadState()),
tick: () => {
set((s) => {
const updated = applyIdleGains(s.state, Date.now());
saveState(updated);
return {
state: updated,
playSeconds: s.playSeconds + 1,
canPrestige: canPrestigeCheck(updated),
productionPerSecond: totalProductionPerSecond(updated),
};
});
},
click: () => {
set((s) => {
const updated = applyClick(applyIdleGains(s.state, Date.now()));
saveState(updated);
return {
state: updated,
canPrestige: canPrestigeCheck(updated),
productionPerSecond: totalProductionPerSecond(updated),
};
});
},
buy: (genId: string) => {
set((s) => {
const withIdle = applyIdleGains(s.state, Date.now());
const updated = buyGenerator(withIdle, genId);
if (!updated) return s;
saveState(updated);
return {
state: updated,
productionPerSecond: totalProductionPerSecond(updated),
};
});
},
buyNode: (nodeId: string) => {
set((s) => {
const updated = buyEvolutionNode(s.state, nodeId);
if (!updated) return s;
saveState(updated);
return {
state: updated,
productionPerSecond: totalProductionPerSecond(updated),
};
});
},
prestige: () => {
set((s) => {
if (!canPrestigeCheck(s.state)) return s;
const updated = applyPrestige(s.state);
saveState(updated);
return {
state: updated,
canPrestige: canPrestigeCheck(updated),
productionPerSecond: totalProductionPerSecond(updated),
};
});
},
reset: () => {
const fresh = { ...DEFAULT_STATE, lastTick: Date.now() };
saveState(fresh);
set({
state: fresh,
playSeconds: 0,
canPrestige: false,
productionPerSecond: 0,
});
},
loadFromServer: (serverState: GameState) => {
const hydrated = applyIdleGains(serverState, Date.now());
saveState(hydrated);
set({
state: hydrated,
canPrestige: canPrestigeCheck(hydrated),
productionPerSecond: totalProductionPerSecond(hydrated),
});
},
generatorCost: genCost,
}));