feat(sprint1-step3b): backend save system + anti-cheat + données rattrapées
- game_saves table + migration 002 (JSON state, anti-cheat metadata) - saveControllers.js : load/save avec validation delta ressources (750k/s × 1.1) - GameSaveManager : upsert MySQL ON DUPLICATE KEY UPDATE - useSaveSync hook : auto-save 30s + keepalive beforeunload + guest fallback - save-validation.test.ts : 8 tests anti-cheat - economy.ts : arbre d'évolution 5 nœuds + prestige ADN (rattrapage step 2) - economy.test.ts : +40 tests (évolution tree, multipliers, start bonus) - GDD + SPRINT1.md : docs sprint complètes - Rethème data : shop.json, Achievements.json, Cookie, Legal (rattrapage step 1)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<!--Head-->
|
||||
|
||||
<h3>Xmass Clicker</h3>
|
||||
<h3>Clickerz — Tetard Universe</h3>
|
||||
<br>
|
||||
|
||||
### 📄 About :
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Xmass Click votre nouveau Clicker préféré !"
|
||||
content="Clickerz — Clicker idle dans le Tetard Universe. Fais éclore des têtards, évolue et domine le marais !"
|
||||
/>
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta
|
||||
@@ -21,42 +21,25 @@
|
||||
name="bingbot"
|
||||
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
||||
/>
|
||||
<link rel="canonical" href="https://xmass.click" />
|
||||
<meta property="og:url" content="https://xmass.click" />
|
||||
<meta property="og:site_name" content="Xmass Click" />
|
||||
<link rel="canonical" href="https://clickerz.tetardtek.com" />
|
||||
<meta property="og:url" content="https://clickerz.tetardtek.com" />
|
||||
<meta property="og:site_name" content="Clickerz" />
|
||||
<meta property="og:locale" content="fr_FR" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Xmass Click" />
|
||||
<meta property="og:title" content="Clickerz — Tetard Universe" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Xmass Click votre nouveau Clicker préféré !"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://xmass.click/webp/share-cover.webp"
|
||||
/>
|
||||
<meta
|
||||
property="og:image:secure_url"
|
||||
content="https://xmass.click/webp/share-cover.webp"
|
||||
content="Clickerz — Clicker idle dans le Tetard Universe. Fais éclore des têtards, évolue et domine le marais !"
|
||||
/>
|
||||
<meta property="og:image:width" content="584" />
|
||||
<meta property="og:image:height" content="384" />
|
||||
<meta property="fb:pages" content />
|
||||
<meta property="fb:admins" content />
|
||||
<meta property="fb:app_id" content />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content />
|
||||
<meta name="twitter:creator" content />
|
||||
<meta name="twitter:title" content="Xmass Click" />
|
||||
<meta name="twitter:title" content="Clickerz — Tetard Universe" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Xmass Click votre nouveau Clicker préféré !"
|
||||
content="Clickerz — Clicker idle dans le Tetard Universe. Fais éclore des têtards, évolue et domine le marais !"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://xmass.click/webp/share-cover.webp"
|
||||
/>
|
||||
<title>Name</title>
|
||||
<title>Clickerz — Tetard Universe</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Sitemap: https://xmass.click/sitemap.xml
|
||||
Sitemap: https://clickerz.tetardtek.com/sitemap.xml
|
||||
|
||||
User-agent: AlphaSeoBot
|
||||
User-agent: AlphaSeoBot-SA
|
||||
|
||||
@@ -4,22 +4,20 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
|
||||
|
||||
|
||||
<url>
|
||||
<loc>https://www.xmass.click/</loc>
|
||||
<lastmod>2023-08-15T19:22:16+00:00</lastmod>
|
||||
<loc>https://clickerz.tetardtek.com/</loc>
|
||||
<lastmod>2026-03-20T12:00:00+00:00</lastmod>
|
||||
<priority>1.00</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.xmass.click/boutique</loc>
|
||||
<lastmod>2023-08-15T19:22:16+00:00</lastmod>
|
||||
<loc>https://clickerz.tetardtek.com/boutique</loc>
|
||||
<lastmod>2026-03-20T12:00:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.xmass.click/achievements</loc>
|
||||
<lastmod>2023-08-15T19:22:16+00:00</lastmod>
|
||||
<loc>https://clickerz.tetardtek.com/achievements</loc>
|
||||
<lastmod>2026-03-20T12:00:00+00:00</lastmod>
|
||||
<priority>0.80</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
</urlset>
|
||||
|
||||
@@ -7,13 +7,19 @@ import {
|
||||
buyGenerator,
|
||||
applyPrestige,
|
||||
canPrestige,
|
||||
computePrestigeDna,
|
||||
canBuyEvolutionNode,
|
||||
buyEvolutionNode,
|
||||
getClickMultiplierFromTree,
|
||||
getProductionMultiplierFromTree,
|
||||
getStartBonusFromTree,
|
||||
DEFAULT_STATE,
|
||||
DEFAULT_GENERATORS,
|
||||
DEFAULT_EVOLUTION_TREE,
|
||||
} from "../core/economy";
|
||||
|
||||
// PrestigePanel visibility guard — canPrestige drives render condition
|
||||
// Ces tests valident l'invariant : le panneau prestige ne doit jamais être
|
||||
// visible (canPrestige = false) si les ressources sont inférieures au seuil.
|
||||
// --- PrestigePanel visibility ---
|
||||
|
||||
describe("PrestigePanel visibility (canPrestige guard)", () => {
|
||||
it("canPrestige = false pour resources = 0 → panneau non visible", () => {
|
||||
expect(canPrestige({ ...DEFAULT_STATE, resources: 0 })).toBe(false);
|
||||
@@ -28,6 +34,8 @@ describe("PrestigePanel visibility (canPrestige guard)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Prestige reset ---
|
||||
|
||||
describe("applyPrestige — post-prestige state", () => {
|
||||
const prestigeState = {
|
||||
...DEFAULT_STATE,
|
||||
@@ -35,9 +43,10 @@ describe("applyPrestige — post-prestige state", () => {
|
||||
generators: DEFAULT_STATE.generators.map((g) => ({ ...g, owned: 3 })),
|
||||
prestigeCount: 0,
|
||||
prestigeMultiplier: 1,
|
||||
lifetimeTadpoles: 2_000_000_000,
|
||||
};
|
||||
|
||||
it("ressources = 0 après prestige", () => {
|
||||
it("ressources = startBonus (0 sans nœud Mémoire Génétique) après prestige", () => {
|
||||
expect(applyPrestige(prestigeState).resources).toBe(0);
|
||||
});
|
||||
|
||||
@@ -53,8 +62,43 @@ describe("applyPrestige — post-prestige state", () => {
|
||||
it("prestigeCount incrémenté à 1 après premier prestige", () => {
|
||||
expect(applyPrestige(prestigeState).prestigeCount).toBe(1);
|
||||
});
|
||||
|
||||
it("gagne de l'ADN Ancestral au prestige", () => {
|
||||
const result = applyPrestige(prestigeState);
|
||||
const expectedDna = computePrestigeDna(2_000_000_000);
|
||||
expect(result.ancestralDna).toBe(expectedDna);
|
||||
expect(expectedDna).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("lifetimeTadpoles reset à 0 après prestige", () => {
|
||||
expect(applyPrestige(prestigeState).lifetimeTadpoles).toBe(0);
|
||||
});
|
||||
|
||||
it("arbre d'évolution persiste après prestige", () => {
|
||||
const stateWithNode = {
|
||||
...prestigeState,
|
||||
evolutionTree: prestigeState.evolutionTree.map((n) =>
|
||||
n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n
|
||||
),
|
||||
};
|
||||
const result = applyPrestige(stateWithNode);
|
||||
expect(result.evolutionTree.find((n) => n.id === "ponte_amelioree")!.unlocked).toBe(true);
|
||||
});
|
||||
|
||||
it("startBonus appliqué si Mémoire Génétique débloquée", () => {
|
||||
const stateWithMemory = {
|
||||
...prestigeState,
|
||||
evolutionTree: prestigeState.evolutionTree.map((n) =>
|
||||
n.id === "memoire_genetique" ? { ...n, unlocked: true } : n
|
||||
),
|
||||
};
|
||||
const result = applyPrestige(stateWithMemory);
|
||||
expect(result.resources).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Generator cost ---
|
||||
|
||||
describe("generatorCost", () => {
|
||||
it("retourne baseCost quand owned = 0", () => {
|
||||
const gen = { ...DEFAULT_GENERATORS[0], owned: 0 };
|
||||
@@ -67,6 +111,8 @@ describe("generatorCost", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Production ---
|
||||
|
||||
describe("totalProductionPerSecond", () => {
|
||||
it("retourne 0 si aucun générateur acheté", () => {
|
||||
expect(totalProductionPerSecond(DEFAULT_STATE)).toBe(0);
|
||||
@@ -87,8 +133,21 @@ describe("totalProductionPerSecond", () => {
|
||||
const state = { ...DEFAULT_STATE, generators: DEFAULT_STATE.generators.map((g, i) => i === 0 ? { ...g, owned: 1 } : g), prestigeMultiplier: 1.5 };
|
||||
expect(totalProductionPerSecond(state)).toBeCloseTo(DEFAULT_GENERATORS[0].baseProduction * 1.5);
|
||||
});
|
||||
|
||||
it("applique le multiplicateur arbre d'évolution", () => {
|
||||
const state = {
|
||||
...DEFAULT_STATE,
|
||||
generators: DEFAULT_STATE.generators.map((g, i) => i === 0 ? { ...g, owned: 1 } : g),
|
||||
evolutionTree: DEFAULT_STATE.evolutionTree.map((n) =>
|
||||
n.id === "instinct_gregaire" ? { ...n, unlocked: true } : n
|
||||
),
|
||||
};
|
||||
expect(totalProductionPerSecond(state)).toBeCloseTo(DEFAULT_GENERATORS[0].baseProduction * 1.5);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Idle gains ---
|
||||
|
||||
describe("computeIdleGains (lazy calculation)", () => {
|
||||
it("calcule les gains proportionnellement au temps écoulé", () => {
|
||||
const state = {
|
||||
@@ -108,29 +167,54 @@ describe("computeIdleGains (lazy calculation)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Click ---
|
||||
|
||||
describe("applyClick", () => {
|
||||
it("augmente les ressources du clickMultiplier × prestigeMultiplier", () => {
|
||||
const state = { ...DEFAULT_STATE, clickMultiplier: 3, prestigeMultiplier: 2 };
|
||||
const result = applyClick(state);
|
||||
expect(result.resources).toBe(6);
|
||||
});
|
||||
|
||||
it("applique le multiplicateur click de l'arbre", () => {
|
||||
const state = {
|
||||
...DEFAULT_STATE,
|
||||
clickMultiplier: 1,
|
||||
prestigeMultiplier: 1,
|
||||
evolutionTree: DEFAULT_STATE.evolutionTree.map((n) =>
|
||||
n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n
|
||||
),
|
||||
};
|
||||
const result = applyClick(state);
|
||||
expect(result.resources).toBe(2); // ×2 depuis Ponte Améliorée
|
||||
});
|
||||
|
||||
it("incrémente lifetimeTadpoles", () => {
|
||||
const state = { ...DEFAULT_STATE, clickMultiplier: 5, prestigeMultiplier: 1 };
|
||||
const result = applyClick(state);
|
||||
expect(result.lifetimeTadpoles).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Buy generator ---
|
||||
|
||||
describe("buyGenerator", () => {
|
||||
it("retourne null si fonds insuffisants", () => {
|
||||
const result = buyGenerator(DEFAULT_STATE, "manic");
|
||||
expect(result).toBeNull(); // 0 ressources, coût = 15
|
||||
const result = buyGenerator(DEFAULT_STATE, "nid");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("achète correctement et déduit le coût", () => {
|
||||
const state = { ...DEFAULT_STATE, resources: 100 };
|
||||
const result = buyGenerator(state, "manic");
|
||||
const result = buyGenerator(state, "nid");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.generators.find((g) => g.id === "manic")!.owned).toBe(1);
|
||||
expect(result!.generators.find((g) => g.id === "nid")!.owned).toBe(1);
|
||||
expect(result!.resources).toBe(100 - DEFAULT_GENERATORS[0].baseCost);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Prestige legacy ---
|
||||
|
||||
describe("prestige", () => {
|
||||
it("canPrestige retourne false si < 1 000 000 ressources", () => {
|
||||
expect(canPrestige({ ...DEFAULT_STATE, resources: 999_999 })).toBe(false);
|
||||
@@ -154,3 +238,129 @@ describe("prestige", () => {
|
||||
expect(result.generators.every((g) => g.owned === 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// --- ADN Ancestral ---
|
||||
|
||||
describe("computePrestigeDna", () => {
|
||||
it("retourne 0 pour 0 têtards", () => {
|
||||
expect(computePrestigeDna(0)).toBe(0);
|
||||
});
|
||||
|
||||
it("retourne 150 pour 1e9 têtards (sqrt(1) = 1)", () => {
|
||||
expect(computePrestigeDna(1e9)).toBe(150);
|
||||
});
|
||||
|
||||
it("retourne 212 pour 2e9 têtards (sqrt(2) ≈ 1.414)", () => {
|
||||
expect(computePrestigeDna(2e9)).toBe(Math.floor(150 * Math.sqrt(2)));
|
||||
});
|
||||
|
||||
it("scaling sub-linéaire — 10× têtards ≠ 10× ADN", () => {
|
||||
const dna1 = computePrestigeDna(1e9);
|
||||
const dna10 = computePrestigeDna(10e9);
|
||||
expect(dna10 / dna1).toBeCloseTo(Math.sqrt(10), 1);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Arbre d'Évolution ---
|
||||
|
||||
describe("Evolution Tree", () => {
|
||||
describe("canBuyEvolutionNode", () => {
|
||||
it("peut acheter le premier nœud (pas de prérequis) avec assez d'ADN", () => {
|
||||
const state = { ...DEFAULT_STATE, ancestralDna: 5 };
|
||||
expect(canBuyEvolutionNode(state, "ponte_amelioree")).toBe(true);
|
||||
});
|
||||
|
||||
it("ne peut pas acheter sans assez d'ADN", () => {
|
||||
const state = { ...DEFAULT_STATE, ancestralDna: 0 };
|
||||
expect(canBuyEvolutionNode(state, "ponte_amelioree")).toBe(false);
|
||||
});
|
||||
|
||||
it("ne peut pas acheter un nœud déjà débloqué", () => {
|
||||
const state = {
|
||||
...DEFAULT_STATE,
|
||||
ancestralDna: 100,
|
||||
evolutionTree: DEFAULT_STATE.evolutionTree.map((n) =>
|
||||
n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n
|
||||
),
|
||||
};
|
||||
expect(canBuyEvolutionNode(state, "ponte_amelioree")).toBe(false);
|
||||
});
|
||||
|
||||
it("ne peut pas acheter un nœud dont le prérequis n'est pas débloqué", () => {
|
||||
const state = { ...DEFAULT_STATE, ancestralDna: 100 };
|
||||
expect(canBuyEvolutionNode(state, "instinct_gregaire")).toBe(false);
|
||||
});
|
||||
|
||||
it("peut acheter un nœud si le prérequis est débloqué", () => {
|
||||
const state = {
|
||||
...DEFAULT_STATE,
|
||||
ancestralDna: 100,
|
||||
evolutionTree: DEFAULT_STATE.evolutionTree.map((n) =>
|
||||
n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n
|
||||
),
|
||||
};
|
||||
expect(canBuyEvolutionNode(state, "instinct_gregaire")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buyEvolutionNode", () => {
|
||||
it("débloque le nœud et déduit l'ADN", () => {
|
||||
const state = { ...DEFAULT_STATE, ancestralDna: 5 };
|
||||
const result = buyEvolutionNode(state, "ponte_amelioree");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.ancestralDna).toBe(4);
|
||||
expect(result!.evolutionTree.find((n) => n.id === "ponte_amelioree")!.unlocked).toBe(true);
|
||||
});
|
||||
|
||||
it("retourne null si impossible", () => {
|
||||
const result = buyEvolutionNode(DEFAULT_STATE, "ponte_amelioree");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("ne modifie pas les autres nœuds", () => {
|
||||
const state = { ...DEFAULT_STATE, ancestralDna: 5 };
|
||||
const result = buyEvolutionNode(state, "ponte_amelioree")!;
|
||||
const otherNodes = result.evolutionTree.filter((n) => n.id !== "ponte_amelioree");
|
||||
expect(otherNodes.every((n) => n.unlocked === false)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getClickMultiplierFromTree", () => {
|
||||
it("retourne 1 si aucun nœud click débloqué", () => {
|
||||
expect(getClickMultiplierFromTree(DEFAULT_EVOLUTION_TREE)).toBe(1);
|
||||
});
|
||||
|
||||
it("retourne 2 si Ponte Améliorée débloquée", () => {
|
||||
const tree = DEFAULT_EVOLUTION_TREE.map((n) =>
|
||||
n.id === "ponte_amelioree" ? { ...n, unlocked: true } : n
|
||||
);
|
||||
expect(getClickMultiplierFromTree(tree)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProductionMultiplierFromTree", () => {
|
||||
it("retourne 1 si aucun nœud production débloqué", () => {
|
||||
expect(getProductionMultiplierFromTree(DEFAULT_EVOLUTION_TREE)).toBe(1);
|
||||
});
|
||||
|
||||
it("retourne 1.5 si Instinct Grégaire débloqué", () => {
|
||||
const tree = DEFAULT_EVOLUTION_TREE.map((n) =>
|
||||
n.id === "instinct_gregaire" ? { ...n, unlocked: true } : n
|
||||
);
|
||||
expect(getProductionMultiplierFromTree(tree)).toBe(1.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStartBonusFromTree", () => {
|
||||
it("retourne 0 si aucun nœud start débloqué", () => {
|
||||
expect(getStartBonusFromTree(DEFAULT_EVOLUTION_TREE)).toBe(0);
|
||||
});
|
||||
|
||||
it("retourne 100 si Mémoire Génétique débloquée", () => {
|
||||
const tree = DEFAULT_EVOLUTION_TREE.map((n) =>
|
||||
n.id === "memoire_genetique" ? { ...n, unlocked: true } : n
|
||||
);
|
||||
expect(getStartBonusFromTree(tree)).toBe(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
123
Frontend/src/__tests__/save-validation.test.ts
Normal file
123
Frontend/src/__tests__/save-validation.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// save-validation.test.ts — Tests anti-cheat validation logic
|
||||
// Ported from backend saveControllers.validateGameState for unit testing
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { DEFAULT_STATE } from "../core/economy";
|
||||
|
||||
// Reproduce the validation logic client-side for testing
|
||||
const MAX_PRODUCTION_PER_SECOND = 750_000;
|
||||
const CHEAT_MARGIN = 1.1;
|
||||
|
||||
interface PreviousSave {
|
||||
last_save: string;
|
||||
game_state: { resources: number };
|
||||
}
|
||||
|
||||
function validateGameState(
|
||||
gameState: { resources: number; generators?: unknown[] },
|
||||
previousSave: PreviousSave | null
|
||||
): { valid: boolean; reason?: string } {
|
||||
if (!gameState || typeof gameState !== "object") {
|
||||
return { valid: false, reason: "Invalid game state format" };
|
||||
}
|
||||
|
||||
if (typeof gameState.resources !== "number" || gameState.resources < 0) {
|
||||
return { valid: false, reason: "Invalid resources value" };
|
||||
}
|
||||
|
||||
if (!Array.isArray(gameState.generators)) {
|
||||
return { valid: false, reason: "Invalid generators" };
|
||||
}
|
||||
|
||||
if (!previousSave) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
const lastSaveTime = new Date(previousSave.last_save).getTime();
|
||||
const now = Date.now();
|
||||
const elapsedSeconds = Math.max((now - lastSaveTime) / 1000, 0);
|
||||
|
||||
const maxPossibleResources =
|
||||
MAX_PRODUCTION_PER_SECOND * elapsedSeconds * CHEAT_MARGIN;
|
||||
|
||||
const previousResources = previousSave.game_state?.resources ?? 0;
|
||||
const resourceDelta = gameState.resources - previousResources;
|
||||
|
||||
if (resourceDelta > maxPossibleResources && resourceDelta > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Resource gain exceeds maximum possible`,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
describe("Anti-cheat validation", () => {
|
||||
it("accepts first save (no previous)", () => {
|
||||
const result = validateGameState(
|
||||
{ ...DEFAULT_STATE, resources: 1000 },
|
||||
null
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts legitimate resource gain", () => {
|
||||
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
||||
const result = validateGameState(
|
||||
{ ...DEFAULT_STATE, resources: 100_000 },
|
||||
{ last_save: fiveMinutesAgo, game_state: { resources: 50_000 } }
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects impossibly large resource gain", () => {
|
||||
const oneSecondAgo = new Date(Date.now() - 1000).toISOString();
|
||||
const result = validateGameState(
|
||||
{ ...DEFAULT_STATE, resources: 999_999_999_999 },
|
||||
{ last_save: oneSecondAgo, game_state: { resources: 0 } }
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.reason).toContain("exceeds maximum");
|
||||
});
|
||||
|
||||
it("rejects negative resources", () => {
|
||||
const result = validateGameState(
|
||||
{ ...DEFAULT_STATE, resources: -100 },
|
||||
null
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects invalid game state format", () => {
|
||||
const result = validateGameState(null as any, null);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects missing generators array", () => {
|
||||
const result = validateGameState(
|
||||
{ resources: 100, generators: "not an array" } as any,
|
||||
null
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts resource loss (spending on generators)", () => {
|
||||
const oneMinuteAgo = new Date(Date.now() - 60 * 1000).toISOString();
|
||||
const result = validateGameState(
|
||||
{ ...DEFAULT_STATE, resources: 1000 },
|
||||
{ last_save: oneMinuteAgo, game_state: { resources: 5000 } }
|
||||
);
|
||||
expect(result.valid).toBe(true); // delta < 0, so always OK
|
||||
});
|
||||
|
||||
it("accepts gains within max production window", () => {
|
||||
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000).toISOString();
|
||||
// 10 min × 750,000/s × 1.1 = 495,000,000
|
||||
const result = validateGameState(
|
||||
{ ...DEFAULT_STATE, resources: 400_000_000 },
|
||||
{ last_save: tenMinutesAgo, game_state: { resources: 0 } }
|
||||
);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -9,15 +9,94 @@ export interface Generator {
|
||||
owned: number;
|
||||
}
|
||||
|
||||
export type EffectType = "click_multiplier" | "production_multiplier" | "start_bonus" | "unlock_generator" | "achievement_scaling";
|
||||
|
||||
export interface EvolutionNode {
|
||||
id: string;
|
||||
name: string;
|
||||
cost: number; // en ADN Ancestral
|
||||
effect: EffectType;
|
||||
value: number;
|
||||
unlocked: boolean;
|
||||
requires: string | null; // id du nœud prérequis (null = racine)
|
||||
}
|
||||
|
||||
export interface GameState {
|
||||
resources: number;
|
||||
clickMultiplier: number;
|
||||
generators: Generator[];
|
||||
lastTick: number; // timestamp ms — lazy calc reference
|
||||
prestigeCount: number;
|
||||
prestigeMultiplier: number; // 1 + prestigeCount * 0.1
|
||||
prestigeMultiplier: number; // legacy — conservé pour compat, remplacé par arbre
|
||||
ancestralDna: number;
|
||||
evolutionTree: EvolutionNode[];
|
||||
lifetimeTadpoles: number; // total cumulé de la run (pour calcul ADN)
|
||||
}
|
||||
|
||||
// --- Arbre d'Évolution ---
|
||||
|
||||
export const DEFAULT_EVOLUTION_TREE: EvolutionNode[] = [
|
||||
{ id: "ponte_amelioree", name: "Ponte Améliorée", cost: 1, effect: "click_multiplier", value: 2, unlocked: false, requires: null },
|
||||
{ id: "instinct_gregaire", name: "Instinct Grégaire", cost: 3, effect: "production_multiplier", value: 1.5, unlocked: false, requires: "ponte_amelioree" },
|
||||
{ id: "memoire_genetique", name: "Mémoire Génétique", cost: 10, effect: "start_bonus", value: 100, unlocked: false, requires: "instinct_gregaire" },
|
||||
{ id: "mutation_alpha", name: "Mutation Alpha", cost: 25, effect: "unlock_generator", value: 0, unlocked: false, requires: "memoire_genetique" },
|
||||
{ id: "symbiose", name: "Symbiose", cost: 50, effect: "achievement_scaling", value: 0.01, unlocked: false, requires: "mutation_alpha" },
|
||||
];
|
||||
|
||||
// Calcule l'ADN gagné lors d'un prestige : floor(150 × sqrt(lifetime / 1e9))
|
||||
export function computePrestigeDna(lifetimeTadpoles: number): number {
|
||||
return Math.floor(150 * Math.sqrt(lifetimeTadpoles / 1e9));
|
||||
}
|
||||
|
||||
// Vérifie si un nœud peut être acheté
|
||||
export function canBuyEvolutionNode(state: GameState, nodeId: string): boolean {
|
||||
const node = state.evolutionTree.find((n) => n.id === nodeId);
|
||||
if (!node || node.unlocked) return false;
|
||||
if (state.ancestralDna < node.cost) return false;
|
||||
if (node.requires) {
|
||||
const prereq = state.evolutionTree.find((n) => n.id === node.requires);
|
||||
if (!prereq || !prereq.unlocked) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Achète un nœud d'évolution (retourne null si impossible)
|
||||
export function buyEvolutionNode(state: GameState, nodeId: string): GameState | null {
|
||||
if (!canBuyEvolutionNode(state, nodeId)) return null;
|
||||
|
||||
const node = state.evolutionTree.find((n) => n.id === nodeId)!;
|
||||
return {
|
||||
...state,
|
||||
ancestralDna: state.ancestralDna - node.cost,
|
||||
evolutionTree: state.evolutionTree.map((n) =>
|
||||
n.id === nodeId ? { ...n, unlocked: true } : n
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Calcule le multiplicateur click total depuis l'arbre
|
||||
export function getClickMultiplierFromTree(tree: EvolutionNode[]): number {
|
||||
return tree
|
||||
.filter((n) => n.unlocked && n.effect === "click_multiplier")
|
||||
.reduce((mult, n) => mult * n.value, 1);
|
||||
}
|
||||
|
||||
// Calcule le multiplicateur production total depuis l'arbre
|
||||
export function getProductionMultiplierFromTree(tree: EvolutionNode[]): number {
|
||||
return tree
|
||||
.filter((n) => n.unlocked && n.effect === "production_multiplier")
|
||||
.reduce((mult, n) => mult * n.value, 1);
|
||||
}
|
||||
|
||||
// Bonus de départ (têtards offerts au début de chaque run)
|
||||
export function getStartBonusFromTree(tree: EvolutionNode[]): number {
|
||||
return tree
|
||||
.filter((n) => n.unlocked && n.effect === "start_bonus")
|
||||
.reduce((sum, n) => sum + n.value, 0);
|
||||
}
|
||||
|
||||
// --- Core economy (mis à jour pour intégrer l'arbre) ---
|
||||
|
||||
// Coût d'achat du N-ième générateur : baseCost × 1.15^owned
|
||||
export function generatorCost(gen: Generator): number {
|
||||
return Math.floor(gen.baseCost * Math.pow(1.15, gen.owned));
|
||||
@@ -29,7 +108,8 @@ export function totalProductionPerSecond(state: GameState): number {
|
||||
(sum, gen) => sum + gen.baseProduction * gen.owned,
|
||||
0
|
||||
);
|
||||
return base * state.prestigeMultiplier;
|
||||
const treeMultiplier = getProductionMultiplierFromTree(state.evolutionTree);
|
||||
return base * state.prestigeMultiplier * treeMultiplier;
|
||||
}
|
||||
|
||||
// Lazy calculation : ressources accumulées depuis lastTick
|
||||
@@ -44,15 +124,19 @@ export function applyIdleGains(state: GameState, now: number): GameState {
|
||||
return {
|
||||
...state,
|
||||
resources: state.resources + gains,
|
||||
lifetimeTadpoles: state.lifetimeTadpoles + gains,
|
||||
lastTick: now,
|
||||
};
|
||||
}
|
||||
|
||||
// Clic manuel
|
||||
export function applyClick(state: GameState): GameState {
|
||||
const treeClickMult = getClickMultiplierFromTree(state.evolutionTree);
|
||||
const gain = state.clickMultiplier * state.prestigeMultiplier * treeClickMult;
|
||||
return {
|
||||
...state,
|
||||
resources: state.resources + state.clickMultiplier * state.prestigeMultiplier,
|
||||
resources: state.resources + gain,
|
||||
lifetimeTadpoles: state.lifetimeTadpoles + gain,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,30 +159,36 @@ export function buyGenerator(state: GameState, genId: string): GameState | null
|
||||
};
|
||||
}
|
||||
|
||||
// Prestige : reset ressources + générateurs, +0.1× multiplicateur permanent
|
||||
// Prestige : reset run, gain ADN, arbre persiste
|
||||
export function canPrestige(state: GameState): boolean {
|
||||
return state.resources >= 1_000_000;
|
||||
}
|
||||
|
||||
export function applyPrestige(state: GameState): GameState {
|
||||
const newPrestigeCount = state.prestigeCount + 1;
|
||||
const dnaGained = computePrestigeDna(state.lifetimeTadpoles);
|
||||
const startBonus = getStartBonusFromTree(state.evolutionTree);
|
||||
|
||||
return {
|
||||
...state,
|
||||
resources: 0,
|
||||
resources: startBonus,
|
||||
generators: state.generators.map((g) => ({ ...g, owned: 0 })),
|
||||
prestigeCount: newPrestigeCount,
|
||||
prestigeMultiplier: 1 + newPrestigeCount * 0.1,
|
||||
ancestralDna: state.ancestralDna + dnaGained,
|
||||
lifetimeTadpoles: 0,
|
||||
lastTick: Date.now(),
|
||||
// evolutionTree persiste — jamais reset
|
||||
};
|
||||
}
|
||||
|
||||
// Valeurs par défaut — 5 tiers alignés GDD (x10 coût / tier, x5 production)
|
||||
// Valeurs par défaut — 5 tiers alignés GDD Tetard Universe (x10 coût / tier)
|
||||
export const DEFAULT_GENERATORS: Generator[] = [
|
||||
{ id: "manic", name: "Manic", baseCost: 10, baseProduction: 0.1, owned: 0 },
|
||||
{ id: "coffee", name: "Tasse à café", baseCost: 100, baseProduction: 0.5, owned: 0 },
|
||||
{ id: "sugar", name: "Sucre", baseCost: 1_000, baseProduction: 3, owned: 0 },
|
||||
{ id: "factory", name: "Usine", baseCost: 10_000, baseProduction: 20, owned: 0 },
|
||||
{ id: "portal", name: "Portail", baseCost: 100_000, baseProduction: 150, owned: 0 },
|
||||
{ id: "nid", name: "Nid", baseCost: 10, baseProduction: 0.1, owned: 0 },
|
||||
{ id: "mare", name: "Mare", baseCost: 100, baseProduction: 0.5, owned: 0 },
|
||||
{ id: "marecage", name: "Marécage", baseCost: 1_000, baseProduction: 3, owned: 0 },
|
||||
{ id: "etang", name: "Étang Ancien", baseCost: 10_000, baseProduction: 20, owned: 0 },
|
||||
{ id: "lac", name: "Lac Mystique", baseCost: 100_000, baseProduction: 150, owned: 0 },
|
||||
];
|
||||
|
||||
export const DEFAULT_STATE: GameState = {
|
||||
@@ -108,4 +198,7 @@ export const DEFAULT_STATE: GameState = {
|
||||
lastTick: Date.now(),
|
||||
prestigeCount: 0,
|
||||
prestigeMultiplier: 1,
|
||||
ancestralDna: 0,
|
||||
evolutionTree: DEFAULT_EVOLUTION_TREE,
|
||||
lifetimeTadpoles: 0,
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "un pull de noël",
|
||||
"name": "Peau de grenouille rare",
|
||||
"founded": false,
|
||||
"image": "https://i.goopics.net/uwjwn1.jpg"
|
||||
},
|
||||
@@ -189,7 +189,7 @@
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "Calendrier de l'Avent avec des chocolats au wasabi et moutarde forte",
|
||||
"name": "Kit de survie du marais au wasabi et moutarde forte",
|
||||
"founded": false,
|
||||
"image": "https://i.goopics.net/dakyj9.png"
|
||||
},
|
||||
@@ -228,7 +228,7 @@
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "Chapeau de Noël clignotant",
|
||||
"name": "Couronne de nénuphars lumineuse",
|
||||
"founded": false,
|
||||
"image": "https://i.goopics.net/d4su7e.png"
|
||||
},
|
||||
|
||||
@@ -1,89 +1,89 @@
|
||||
[
|
||||
{
|
||||
"name": "Manic",
|
||||
"name": "Griffes de Grenouille",
|
||||
"price": 15,
|
||||
"incrementValue": 1,
|
||||
"description": "Evite de vous bruler quand vous sortez les cookies du four, vous gagnez 5 CPS",
|
||||
"description": "Des griffes acérées pour une ponte plus efficace. +1 par clic.",
|
||||
"link": "/",
|
||||
"image": "./svg/Hand.svg",
|
||||
"buyed": false,
|
||||
"type": "actif"
|
||||
},
|
||||
{
|
||||
"name": "Tasse à café",
|
||||
"name": "Algues Nutritives",
|
||||
"price": 15,
|
||||
"incrementValue": 1,
|
||||
"description": "Bien chaud vous permet de tenir sur la durée",
|
||||
"description": "Les algues nourrissent le marais en continu. +1 têtard/s.",
|
||||
"link": "/",
|
||||
"image": "./svg/Tasse.svg",
|
||||
"buyed": false,
|
||||
"type": "passif"
|
||||
},
|
||||
{
|
||||
"name": "Mr Bonhomme",
|
||||
"name": "Crapaud Gardien",
|
||||
"price": 150,
|
||||
"incrementValue": 10,
|
||||
"description": "Un assistant idéal pour le click",
|
||||
"description": "Un ancien du marais qui veille sur les pontes. +10 par clic.",
|
||||
"link": "/",
|
||||
"image": "./svg/Bonhome.svg",
|
||||
"buyed": false,
|
||||
"type": "actif"
|
||||
},
|
||||
{
|
||||
"name": "Bonnet",
|
||||
"name": "Nénuphar Géant",
|
||||
"price": 150,
|
||||
"incrementValue": 10,
|
||||
"description": "Garder vos oreilles bien à l'abri du froid et click !",
|
||||
"description": "Un nénuphar massif qui attire les têtards. +10 têtards/s.",
|
||||
"link": "/",
|
||||
"image": "./svg/Bonnet.svg",
|
||||
"buyed": false,
|
||||
"type": "passif"
|
||||
},
|
||||
{
|
||||
"name": "Cookie",
|
||||
"name": "Oeuf Doré",
|
||||
"price": 1500,
|
||||
"incrementValue": 100,
|
||||
"description": "Fait avec amour",
|
||||
"description": "Un oeuf rare qui éclot en masse. +100 par clic.",
|
||||
"link": "/",
|
||||
"image": "./svg/Cookie.svg",
|
||||
"buyed": false,
|
||||
"type": "actif"
|
||||
},
|
||||
{
|
||||
"name": "Canne en sucre",
|
||||
"name": "Mousse Lumineuse",
|
||||
"price": 1500,
|
||||
"incrementValue": 100,
|
||||
"description": "Le sucre c'est connu, ca reboost",
|
||||
"description": "La mousse phosphorescente accélère la croissance. +100 têtards/s.",
|
||||
"link": "/",
|
||||
"image": "./svg/Canne.svg",
|
||||
"buyed": false,
|
||||
"type": "passif"
|
||||
},
|
||||
{
|
||||
"name": "Couronne d'hiver",
|
||||
"name": "Couronne de Roseaux",
|
||||
"price": 15000,
|
||||
"incrementValue": 1000,
|
||||
"description": "Un bisous ou rien du tout !",
|
||||
"description": "Le symbole du Gardien suprême du Marais. +1000 par clic.",
|
||||
"link": "/",
|
||||
"image": "./svg/Courone.svg",
|
||||
"buyed": false,
|
||||
"type": "actif"
|
||||
},
|
||||
{
|
||||
"name": "Mr pain d'épice",
|
||||
"name": "Esprit du Marais",
|
||||
"price": 15000,
|
||||
"incrementValue": 1000,
|
||||
"description": "Le meilleur c'est la tête",
|
||||
"description": "L'esprit ancestral bénit les eaux. +1000 têtards/s.",
|
||||
"link": "/",
|
||||
"image": "./svg/PainDep.svg",
|
||||
"buyed": false,
|
||||
"type": "passif"
|
||||
},
|
||||
{
|
||||
"name": "Bière",
|
||||
"name": "Nectar de Lotus",
|
||||
"price": 8000,
|
||||
"incrementValue": 1000,
|
||||
"description": "Boisson de qualité, double tout les CPS, attention à ne pas trop en abuser",
|
||||
"description": "Un nectar enivrant qui trouble les eaux... mais booste la ponte. Attention aux effets secondaires.",
|
||||
"link": "/",
|
||||
"image": "./svg/Beer.svg",
|
||||
"buyed": false,
|
||||
|
||||
115
Frontend/src/hooks/useSaveSync.ts
Normal file
115
Frontend/src/hooks/useSaveSync.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
// useSaveSync.ts — Auto-save game state to backend every 30s
|
||||
// Requires JWT token in localStorage (set by auth flow)
|
||||
// Falls back silently if no token (guest mode)
|
||||
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
import type { GameState } from "../core/economy";
|
||||
|
||||
const SAVE_INTERVAL_MS = 30_000; // 30 seconds
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:3310";
|
||||
|
||||
interface SaveSyncOptions {
|
||||
getGameState: () => GameState;
|
||||
onLoad: (state: GameState) => void;
|
||||
playTimeSeconds: number;
|
||||
}
|
||||
|
||||
async function apiRequest(path: string, options: RequestInit = {}) {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return null;
|
||||
|
||||
const res = await fetch(`${BACKEND_URL}/api${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-auth-token": token,
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
console.warn(`[SaveSync] ${path} failed:`, res.status, body);
|
||||
return null;
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncOptions) {
|
||||
const lastSaveRef = useRef<string | null>(null);
|
||||
const loadedRef = useRef(false);
|
||||
|
||||
// Load save on mount (once)
|
||||
useEffect(() => {
|
||||
if (loadedRef.current) return;
|
||||
loadedRef.current = true;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
apiRequest("/save").then((data) => {
|
||||
if (data?.gameState) {
|
||||
onLoad(data.gameState);
|
||||
lastSaveRef.current = data.lastSave;
|
||||
console.info("[SaveSync] Loaded save from server");
|
||||
}
|
||||
});
|
||||
}, [onLoad]);
|
||||
|
||||
// Save function
|
||||
const saveToServer = useCallback(async () => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
const gameState = getGameState();
|
||||
const result = await apiRequest("/save", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ gameState, playTimeSeconds }),
|
||||
});
|
||||
|
||||
if (result?.lastSave) {
|
||||
lastSaveRef.current = result.lastSave;
|
||||
}
|
||||
}, [getGameState, playTimeSeconds]);
|
||||
|
||||
// Auto-save interval
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return undefined;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
saveToServer();
|
||||
}, SAVE_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [saveToServer]);
|
||||
|
||||
// Save on page unload
|
||||
useEffect(() => {
|
||||
const handleUnload = () => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
const gameState = getGameState();
|
||||
const payload = JSON.stringify({ gameState, playTimeSeconds });
|
||||
|
||||
// Use fetch with keepalive for reliable save on tab close
|
||||
// (sendBeacon doesn't support custom headers)
|
||||
fetch(`${BACKEND_URL}/api/save`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-auth-token": token,
|
||||
},
|
||||
body: payload,
|
||||
keepalive: true,
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", handleUnload);
|
||||
return () => window.removeEventListener("beforeunload", handleUnload);
|
||||
}, [getGameState, playTimeSeconds]);
|
||||
|
||||
return { saveToServer, lastSave: lastSaveRef.current };
|
||||
}
|
||||
@@ -3,32 +3,32 @@ function Cookie() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="item">
|
||||
<h2>Qu’est-ce qu’un cookie ?</h2>
|
||||
<h2>Qu'est-ce qu'un cookie ?</h2>
|
||||
<p>
|
||||
Un cookie est un petit fichier texte sauvegardé sur votre ordinateur
|
||||
lorsque vous visitez un site web. Ce fichier texte enregistre des
|
||||
informations qui peuvent être lues par un site web lorsque vous le
|
||||
visitez de nouveau plus tard. Certains de ces cookies sont nécessaires
|
||||
pour accéder à certaines fonctionnalités d’un site. D’autres cookies
|
||||
sont d’utilité pratique pour le visiteur : ils sauvegardent de manière
|
||||
sécurisée votre nom d’utilisateur ou vos préférences linguistiques par
|
||||
exemple. Les cookies signifient tout simplement qu’à chaque fois que
|
||||
vous visitez un site web, vous n’avez pas besoin de saisir à nouveau
|
||||
pour accéder à certaines fonctionnalités d'un site. D'autres cookies
|
||||
sont d'utilité pratique pour le visiteur : ils sauvegardent de manière
|
||||
sécurisée votre nom d'utilisateur ou vos préférences linguistiques par
|
||||
exemple. Les cookies signifient tout simplement qu'à chaque fois que
|
||||
vous visitez un site web, vous n'avez pas besoin de saisir à nouveau
|
||||
les mêmes informations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="item">
|
||||
<h2>Pourquoi Xmass Clicker utilise des cookies ?</h2>
|
||||
<h2>Pourquoi Clickerz utilise des cookies ?</h2>
|
||||
<p>
|
||||
Nous utilisons des cookies pour vous fournir une expérience
|
||||
utilisateur optimale et adaptée à vos préférences personnelles. En
|
||||
utilisant les cookies, Les cookies sont également utilisés pour
|
||||
optimiser la performance du site. Xmass Clicker a pris toutes les
|
||||
mesures organisationnelles et techniques pour protéger vos données
|
||||
personnelles ainsi que d’une éventuelle perte d’informations ou de
|
||||
toute forme de traitement illicite. Pour davantage d’informations,
|
||||
consultez notre Politique de confidentialité.
|
||||
utilisateur optimale et adaptée à vos préférences personnelles.
|
||||
Les cookies sont également utilisés pour optimiser la performance
|
||||
du site. Clickerz a pris toutes les mesures organisationnelles et
|
||||
techniques pour protéger vos données personnelles ainsi que d'une
|
||||
éventuelle perte d'informations ou de toute forme de traitement
|
||||
illicite. Pour davantage d'informations, consultez notre Politique
|
||||
de confidentialité.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ function Cookie() {
|
||||
<p>
|
||||
Vous pouvez paramétrer votre navigateur Internet pour désactiver les
|
||||
cookies. Notez toutefois que si vous désactivez les cookies, votre nom
|
||||
d’utilisateur ainsi que votre mot de passe ne seront plus sauvegardés
|
||||
d'utilisateur ainsi que votre mot de passe ne seront plus sauvegardés
|
||||
sur aucun site web.
|
||||
</p>
|
||||
</div>
|
||||
@@ -49,9 +49,9 @@ function Cookie() {
|
||||
2. Appuyez sur la touche « Alt » <br />
|
||||
3. Dans le menu en haut de la page cliquez sur « Outils » puis «
|
||||
Options » <br />
|
||||
4. Sélectionnez l’onglet « Vie privée » <br />
|
||||
4. Sélectionnez l'onglet « Vie privée » <br />
|
||||
5. Dans le menu déroulant à droite de « Règles de conservation »,
|
||||
cliquez sur « utiliser les paramètres personnalisés pour l’historique
|
||||
cliquez sur « utiliser les paramètres personnalisés pour l'historique
|
||||
» <br />
|
||||
6. Un peu plus bas, décochez « Accepter les cookies » <br />
|
||||
7. Sauvegardez vos préférences en cliquant sur « OK »
|
||||
@@ -63,7 +63,7 @@ function Cookie() {
|
||||
<p>
|
||||
1. Ouvrez Internet Explorer <br />
|
||||
2. Dans le menu « Outils », sélectionnez « Options Internet » <br />
|
||||
3. Cliquez sur l’onglet « Confidentialité » <br />
|
||||
3. Cliquez sur l'onglet « Confidentialité » <br />
|
||||
4. Cliquez sur « Avancé » et décochez « Accepter » <br />
|
||||
5. Sauvegardez vos préférences en cliquant sur « OK »
|
||||
</p>
|
||||
@@ -75,7 +75,7 @@ function Cookie() {
|
||||
1. Ouvrez Safari <br />
|
||||
2. Dans la barre de menu en haut, cliquez sur « Safari », puis «
|
||||
Préférences » <br />
|
||||
3. Sélectionnez l’icône « Sécurité » <br />
|
||||
3. Sélectionnez l'icône « Sécurité » <br />
|
||||
4. À côté de « Accepter les cookies », cochez « Jamais » <br />
|
||||
5. Si vous souhaitez voir les cookies qui sont déjà sauvegardés sur
|
||||
votre ordinateur, cliquez sur « Afficher les cookies »
|
||||
@@ -86,9 +86,9 @@ function Cookie() {
|
||||
<h2>Google Chrome :</h2>
|
||||
<p>
|
||||
1. Ouvrez Google Chrome <br />
|
||||
2. Cliquez sur l’icône d’outils dans la barre de menu <br />
|
||||
2. Cliquez sur l'icône d'outils dans la barre de menu <br />
|
||||
3. Sélectionnez « Options » <br />
|
||||
4. Cliquez sur l’onglet « Options avancées » <br />
|
||||
4. Cliquez sur l'onglet « Options avancées » <br />
|
||||
5. Dans le menu déroulant « Paramètres des cookies », sélectionnez «
|
||||
Bloquer tous les cookies »
|
||||
</p>
|
||||
|
||||
@@ -4,20 +4,18 @@ function Legal() {
|
||||
<div className="mentionslegales">
|
||||
<h2>Éditeur :</h2>
|
||||
<p>
|
||||
Xmass'Click est un projet réalisé dans le cadre d'un hackathon sur 2
|
||||
jours.
|
||||
Clickerz est un projet indépendant faisant partie du Tetard Universe.
|
||||
</p>
|
||||
|
||||
<h2>Coordonnées :</h2>
|
||||
<p>
|
||||
Téléphone : 04 22 52 10 10 <br />
|
||||
E-mail : pere-noel@laposte.net <br />
|
||||
Adresse : 250 avenue des Nuages, 1000 Pôle Nord <br />
|
||||
E-mail : contact@tetardtek.com <br />
|
||||
Site : https://tetardtek.com <br />
|
||||
</p>
|
||||
|
||||
<h2>Responsabilité :</h2>
|
||||
<p>
|
||||
Xmass'Click décline toute responsabilité quant à l'utilisation du site.
|
||||
Clickerz décline toute responsabilité quant à l'utilisation du site.
|
||||
Les informations fournies sont à titre informatif et peuvent être
|
||||
sujettes à des erreurs.
|
||||
</p>
|
||||
@@ -25,26 +23,26 @@ function Legal() {
|
||||
<h2>Propriété Intellectuelle :</h2>
|
||||
<p>
|
||||
Tout le contenu du site (textes, images, etc.) reste la propriété de
|
||||
Xmass'Click. Toute reproduction est interdite sans autorisation
|
||||
Tetardtek. Toute reproduction est interdite sans autorisation
|
||||
préalable.
|
||||
</p>
|
||||
|
||||
<h2>Protection des Données Personnelles :</h2>
|
||||
<p>
|
||||
Xmass'Click ne collecte pas de données personnelles. Aucune information
|
||||
personnelle n'est stockée lors de l'utilisation du site.
|
||||
Clickerz utilise un système d'authentification via SuperOAuth.
|
||||
Les données de jeu sont sauvegardées côté serveur.
|
||||
Aucune donnée personnelle n'est partagée avec des tiers.
|
||||
</p>
|
||||
|
||||
<h2>Conditions Générales d'Utilisation :</h2>
|
||||
<p>
|
||||
Aucune condition générale d'utilisation n'est applicable. L'utilisation
|
||||
du site Xmass'Click se fait à titre gratuit et sans engagement.
|
||||
L'utilisation du site Clickerz se fait à titre gratuit et sans engagement.
|
||||
</p>
|
||||
|
||||
<h2>Loi Applicable :</h2>
|
||||
<p>
|
||||
Le présent site est régi par la loi du Pôle Nord. En cas de litige, les
|
||||
tribunaux du Père Noël seront compétents.
|
||||
Le présent site est régi par la loi française. En cas de litige, les
|
||||
tribunaux compétents seront ceux du ressort du siège social de l'éditeur.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user