feat: toast notifications + guide du gardien
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 19s
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:
@@ -6,6 +6,7 @@ import Footer from "./components/footer";
|
|||||||
import { GameTick } from "./components/GameTick";
|
import { GameTick } from "./components/GameTick";
|
||||||
import { GameSync } from "./components/GameSync";
|
import { GameSync } from "./components/GameSync";
|
||||||
import { OfflineReport } from "./components/OfflineReport";
|
import { OfflineReport } from "./components/OfflineReport";
|
||||||
|
import { ToastContainer } from "./components/ToastContainer";
|
||||||
|
|
||||||
import navData from "./data/NavBarData.json";
|
import navData from "./data/NavBarData.json";
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ function App() {
|
|||||||
<GameTick />
|
<GameTick />
|
||||||
<GameSync />
|
<GameSync />
|
||||||
<OfflineReport />
|
<OfflineReport />
|
||||||
|
<ToastContainer />
|
||||||
<Navbar
|
<Navbar
|
||||||
navData={navData}
|
navData={navData}
|
||||||
toggleRain={toggleRain}
|
toggleRain={toggleRain}
|
||||||
|
|||||||
56
Frontend/src/components/ToastContainer.tsx
Normal file
56
Frontend/src/components/ToastContainer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,5 +10,11 @@
|
|||||||
"linkname": "Succès",
|
"linkname": "Succès",
|
||||||
"linkurl": "/achievements",
|
"linkurl": "/achievements",
|
||||||
"btn": false
|
"btn": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"linkname": "Guide",
|
||||||
|
"linkurl": "/guide",
|
||||||
|
"btn": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 {
|
@keyframes float-up {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Achievements from "./pages/Achievements";
|
|||||||
import Settings from "./pages/Settings";
|
import Settings from "./pages/Settings";
|
||||||
import Legal from "./pages/Legal";
|
import Legal from "./pages/Legal";
|
||||||
import Cookie from "./pages/Cookie";
|
import Cookie from "./pages/Cookie";
|
||||||
|
import Guide from "./pages/Guide";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -38,6 +39,10 @@ const router = createBrowserRouter([
|
|||||||
path: "/cookies",
|
path: "/cookies",
|
||||||
element: <Cookie />,
|
element: <Cookie />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/guide",
|
||||||
|
element: <Guide />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
element: <Settings />,
|
element: <Settings />,
|
||||||
|
|||||||
105
Frontend/src/pages/Guide.tsx
Normal file
105
Frontend/src/pages/Guide.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -160,7 +160,10 @@ export default function Home() {
|
|||||||
<EvolutionTree />
|
<EvolutionTree />
|
||||||
<CosmeticsPanel />
|
<CosmeticsPanel />
|
||||||
<a href="/achievements" className="achieve-badge">
|
<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>
|
</a>
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
offlineEfficiency,
|
offlineEfficiency,
|
||||||
} from "../core/economy";
|
} from "../core/economy";
|
||||||
import { migrateSave } from "../core/migrateSave";
|
import { migrateSave } from "../core/migrateSave";
|
||||||
|
import { toast } from "./useToastStore";
|
||||||
import {
|
import {
|
||||||
computeNewUnlocks,
|
computeNewUnlocks,
|
||||||
equipCosmetic as equipCosmeticFn,
|
equipCosmetic as equipCosmeticFn,
|
||||||
@@ -180,6 +181,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
|||||||
if (newUnlocks.length > 0) {
|
if (newUnlocks.length > 0) {
|
||||||
const newCos = addToInventory(cosState, newUnlocks);
|
const newCos = addToInventory(cosState, newUnlocks);
|
||||||
updated.cosmeticInventory = newCos.inventory;
|
updated.cosmeticInventory = newCos.inventory;
|
||||||
|
newUnlocks.forEach(() => toast("Nouveau cosmetique debloque !", "reward"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +230,11 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
|||||||
set((s) => {
|
set((s) => {
|
||||||
const updated = buyEvolutionNode(s.state, nodeId);
|
const updated = buyEvolutionNode(s.state, nodeId);
|
||||||
if (!updated) return s;
|
if (!updated) return s;
|
||||||
|
const node = updated.evolutionTree.find((n) => n.id === nodeId);
|
||||||
saveLocal(updated);
|
saveLocal(updated);
|
||||||
|
if (node?.capstone) {
|
||||||
|
toast(`Capstone debloque : ${node.name} !`, "reward", 5000);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
state: updated,
|
state: updated,
|
||||||
productionPerSecond: totalProductionPerSecond(updated),
|
productionPerSecond: totalProductionPerSecond(updated),
|
||||||
@@ -242,6 +248,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
|||||||
if (!canPrestigeCheck(s.state)) return s;
|
if (!canPrestigeCheck(s.state)) return s;
|
||||||
const updated = applyPrestige(s.state);
|
const updated = applyPrestige(s.state);
|
||||||
saveLocal(updated);
|
saveLocal(updated);
|
||||||
|
toast(`Generation #${updated.prestigeCount} — Nouvelle vie !`, "success", 4000);
|
||||||
return {
|
return {
|
||||||
state: updated,
|
state: updated,
|
||||||
canPrestige: canPrestigeCheck(updated),
|
canPrestige: canPrestigeCheck(updated),
|
||||||
@@ -305,6 +312,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
|||||||
const updated = claimMilestoneFn(s.state, milestoneId);
|
const updated = claimMilestoneFn(s.state, milestoneId);
|
||||||
if (!updated) return s;
|
if (!updated) return s;
|
||||||
saveLocal(updated);
|
saveLocal(updated);
|
||||||
|
toast("Milestone debloque !", "reward", 4000);
|
||||||
return { state: updated };
|
return { state: updated };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
48
Frontend/src/store/useToastStore.ts
Normal file
48
Frontend/src/store/useToastStore.ts
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user