feat: toast notifications + guide du gardien
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 19s

- Toast system: store Zustand + ToastContainer (slide-in, auto-dismiss)
- Toasts on: prestige, milestone claim, capstone unlock, cosmetic unlock
- Guide in-game: /guide route, toutes les mecaniques expliquees
- Lien navbar + sidebar
This commit is contained in:
2026-03-28 18:47:41 +01:00
parent 450d559216
commit a665fdf2f4
9 changed files with 245 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ import Footer from "./components/footer";
import { GameTick } from "./components/GameTick";
import { GameSync } from "./components/GameSync";
import { OfflineReport } from "./components/OfflineReport";
import { ToastContainer } from "./components/ToastContainer";
import navData from "./data/NavBarData.json";
@@ -17,6 +18,7 @@ function App() {
<GameTick />
<GameSync />
<OfflineReport />
<ToastContainer />
<Navbar
navData={navData}
toggleRain={toggleRain}

View File

@@ -0,0 +1,56 @@
// ToastContainer.tsx — Stack de toasts en bas à droite
import { useToastStore } from "../store/useToastStore";
import type { ToastVariant } from "../store/useToastStore";
const VARIANT_STYLES: Record<ToastVariant, string> = {
success: "border-emerald-500/40 bg-emerald-500/10",
info: "border-blue-400/40 bg-blue-400/10",
reward: "border-amber-400/40 bg-amber-400/10",
warning: "border-red-400/40 bg-red-400/10",
};
const VARIANT_ICONS: Record<ToastVariant, string> = {
success: "✓",
info: "",
reward: "★",
warning: "⚠",
};
const VARIANT_ICON_COLORS: Record<ToastVariant, string> = {
success: "text-emerald-400",
info: "text-blue-400",
reward: "text-amber-400",
warning: "text-red-400",
};
export function ToastContainer() {
const toasts = useToastStore((s) => s.toasts);
const remove = useToastStore((s) => s.removeToast);
if (toasts.length === 0) return null;
return (
<div className="fixed bottom-4 right-4 z-[60] flex flex-col gap-2 max-w-sm">
{toasts.map((t) => (
<div
key={t.id}
onClick={() => remove(t.id)}
className={`
gp cursor-pointer border
${VARIANT_STYLES[t.variant]}
animate-[slide-in_0.3s_ease-out]
`}
style={{ backdropFilter: "blur(12px)" }}
>
<div className="flex items-center gap-2">
<span className={`text-sm font-bold ${VARIANT_ICON_COLORS[t.variant]}`}>
{VARIANT_ICONS[t.variant]}
</span>
<span className="gp-value text-[0.75rem]!">{t.message}</span>
</div>
</div>
))}
</div>
);
}

View File

@@ -10,5 +10,11 @@
"linkname": "Succès",
"linkurl": "/achievements",
"btn": false
},
{
"id": "4",
"linkname": "Guide",
"linkurl": "/guide",
"btn": false
}
]

View File

@@ -366,6 +366,17 @@
}
}
@keyframes slide-in {
from {
opacity: 0;
transform: translateX(100%) scale(0.95);
}
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
@keyframes float-up {
0% {
opacity: 1;

View File

@@ -12,6 +12,7 @@ import Achievements from "./pages/Achievements";
import Settings from "./pages/Settings";
import Legal from "./pages/Legal";
import Cookie from "./pages/Cookie";
import Guide from "./pages/Guide";
const router = createBrowserRouter([
{
@@ -38,6 +39,10 @@ const router = createBrowserRouter([
path: "/cookies",
element: <Cookie />,
},
{
path: "/guide",
element: <Guide />,
},
{
path: "/settings",
element: <Settings />,

View File

@@ -0,0 +1,105 @@
// Guide.tsx — Guide joueur in-game
export default function Guide() {
return (
<div className="container" style={{ color: "var(--color-grey)" }}>
<h1>Guide du Gardien</h1>
<div className="content">
<h2 className="subtitle">Le Marais</h2>
<p className="paragraphe">
Tu es le <strong>Gardien du Marais</strong>. Les tetards naissent sous tes clics,
grandissent grace a tes generateurs, et evoluent a chaque nouvelle generation.
</p>
</div>
<div className="content">
<h2 className="subtitle">Boucle de jeu</h2>
<p className="paragraphe">
<strong>1. Clique</strong> pour pondre des tetards. Achete des <strong>generateurs</strong> (Nid, Mare, Marecage...)
qui produisent des tetards automatiquement.
</p>
<p className="paragraphe">
<strong>2. Prestige</strong> quand tu atteins 1M de tetards. Tu perds tes tetards et generateurs,
mais tu gagnes de l'<strong>ADN Ancestral</strong> et un multiplicateur permanent.
Chaque generation est plus rapide que la precedente.
</p>
<p className="paragraphe">
<strong>3. Arbre d'Evolution</strong> depense ton ADN dans 3 branches :
</p>
<ul className="paragraphe" style={{ marginLeft: "1.5rem" }}>
<li><strong>Ponte</strong> booste tes clics, double ponte, critiques</li>
<li><strong>Marais</strong> booste la production des generateurs</li>
<li><strong>Adaptation</strong> bonus offline, ADN bonus, seuil prestige reduit</li>
</ul>
<p className="paragraphe">
Chaque branche a un <strong>capstone</strong> (noeud final puissant) et des
<strong> post-capstones</strong> achetables a l'infini pour une progression endless.
</p>
</div>
<div className="content">
<h2 className="subtitle">Capstones</h2>
<ul className="paragraphe" style={{ marginLeft: "1.5rem" }}>
<li><strong>Ponte Automatique</strong> — auto-click 1/s qui scale avec les upgrades</li>
<li><strong>Symbiose Totale</strong> — chaque type de generateur booste les autres</li>
<li><strong>Memoire du Marais</strong> — offline cap passe a 75%, duree 8h</li>
</ul>
</div>
<div className="content">
<h2 className="subtitle">Convergence</h2>
<p className="paragraphe">
Quand tu as debloque un capstone + des noeuds d'une 2e branche, tu peux acheter
<strong> Convergence Alpha</strong> (+10% a tous les effets).
Avec 2 capstones, elle evolue en <strong>Convergence Omega</strong> (-20% cout post-capstones).
</p>
</div>
<div className="content">
<h2 className="subtitle">Reset d'arbre</h2>
<p className="paragraphe">
Tu peux reinitialiser ton arbre pour tester d'autres builds.
<strong> 1 reset gratuit par prestige</strong>, puis 5 ADN par reset supplementaire.
L'ADN investi est entierement rembourse.
</p>
</div>
<div className="content">
<h2 className="subtitle">Milestones</h2>
<p className="paragraphe">
8 paliers de prestige (de 1 a 100) qui debloquent des <strong>cosmetiques exclusifs</strong> et
des <strong>bonus gameplay legers</strong> :
</p>
<ul className="paragraphe" style={{ marginLeft: "1.5rem" }}>
<li>1 prestige — Ruban queue</li>
<li>3 prestiges — Titre "Gardien Recurrent"</li>
<li>5 prestiges — 1 Nid gratuit au depart</li>
<li>10 prestiges — Couronne doree</li>
<li>15 prestiges — +5% offline permanent</li>
<li>25 prestiges — Cape d'algues ancestrales</li>
<li>50 prestiges Queue enflamee + particules</li>
<li>100 prestiges Skin Tetard Primordial</li>
</ul>
</div>
<div className="content">
<h2 className="subtitle">Cosmetiques</h2>
<p className="paragraphe">
Les cosmetiques sont purement visuels <strong>zero pay-to-win</strong>.
Debloque-les via les achievements et les milestones prestige.
5 slots : chapeau, yeux, corps, queue, accessoire.
</p>
</div>
<div className="content">
<h2 className="subtitle">Offline</h2>
<p className="paragraphe">
Quand tu fermes le jeu, le marais continue de produire.
Efficacite : 100% les 15 premieres minutes, puis degressive jusqu'a 0% a 2h.
Les noeuds d'arbre et milestones peuvent augmenter le cap offline.
</p>
</div>
</div>
);
}

View File

@@ -160,7 +160,10 @@ export default function Home() {
<EvolutionTree />
<CosmeticsPanel />
<a href="/achievements" className="achieve-badge">
{ACHIEVEMENTS.filter((a) => a.check(useGameStore.getState().state)).length}/{ACHIEVEMENTS.length} succès
{ACHIEVEMENTS.filter((a) => a.check(useGameStore.getState().state)).length}/{ACHIEVEMENTS.length} succes
</a>
<a href="/guide" className="achieve-badge" style={{ borderColor: "rgba(139, 92, 246, 0.2)", background: "rgba(139, 92, 246, 0.08)", color: "#a78bfa" }}>
Guide du Gardien
</a>
</aside>
</main>

View File

@@ -25,6 +25,7 @@ import {
offlineEfficiency,
} from "../core/economy";
import { migrateSave } from "../core/migrateSave";
import { toast } from "./useToastStore";
import {
computeNewUnlocks,
equipCosmetic as equipCosmeticFn,
@@ -180,6 +181,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
if (newUnlocks.length > 0) {
const newCos = addToInventory(cosState, newUnlocks);
updated.cosmeticInventory = newCos.inventory;
newUnlocks.forEach(() => toast("Nouveau cosmetique debloque !", "reward"));
}
}
@@ -228,7 +230,11 @@ export const useGameStore = create<GameStore>((set, get) => ({
set((s) => {
const updated = buyEvolutionNode(s.state, nodeId);
if (!updated) return s;
const node = updated.evolutionTree.find((n) => n.id === nodeId);
saveLocal(updated);
if (node?.capstone) {
toast(`Capstone debloque : ${node.name} !`, "reward", 5000);
}
return {
state: updated,
productionPerSecond: totalProductionPerSecond(updated),
@@ -242,6 +248,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
if (!canPrestigeCheck(s.state)) return s;
const updated = applyPrestige(s.state);
saveLocal(updated);
toast(`Generation #${updated.prestigeCount} — Nouvelle vie !`, "success", 4000);
return {
state: updated,
canPrestige: canPrestigeCheck(updated),
@@ -305,6 +312,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
const updated = claimMilestoneFn(s.state, milestoneId);
if (!updated) return s;
saveLocal(updated);
toast("Milestone debloque !", "reward", 4000);
return { state: updated };
});
},

View File

@@ -0,0 +1,48 @@
// useToastStore.ts — Toast notification system
// Stack de notifications auto-dismiss, utilisable partout via toast()
import { create } from "zustand";
export type ToastVariant = "success" | "info" | "reward" | "warning";
export interface Toast {
id: number;
message: string;
variant: ToastVariant;
duration: number; // ms
}
let nextId = 0;
interface ToastStore {
toasts: Toast[];
addToast: (message: string, variant?: ToastVariant, duration?: number) => void;
removeToast: (id: number) => void;
}
export const useToastStore = create<ToastStore>((set) => ({
toasts: [],
addToast: (message, variant = "info", duration = 3000) => {
const id = nextId++;
set((s) => ({
toasts: [...s.toasts, { id, message, variant, duration }],
}));
setTimeout(() => {
set((s) => ({
toasts: s.toasts.filter((t) => t.id !== id),
}));
}, duration);
},
removeToast: (id) => {
set((s) => ({
toasts: s.toasts.filter((t) => t.id !== id),
}));
},
}));
// Shorthand pour utiliser depuis n'importe où (pas besoin du hook)
export function toast(message: string, variant?: ToastVariant, duration?: number) {
useToastStore.getState().addToast(message, variant, duration);
}