Compare commits
4 Commits
9f0ccda99b
...
95dca420a5
| Author | SHA1 | Date | |
|---|---|---|---|
| 95dca420a5 | |||
| 307feb711f | |||
| d215e9a33e | |||
| a52746ed0c |
14
Backend/database/migrations/002_game_saves.sql
Normal file
14
Backend/database/migrations/002_game_saves.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
-- Migration 002 — Game saves with anti-cheat metadata
|
||||||
|
-- Safe: CREATE TABLE IF NOT EXISTS, no data loss
|
||||||
|
-- Run: mysql -u <user> -p clickerz < migrations/002_game_saves.sql
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS game_saves (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL UNIQUE,
|
||||||
|
game_state JSON NOT NULL,
|
||||||
|
last_save TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
lifetime_tadpoles BIGINT DEFAULT 0,
|
||||||
|
prestige_count INT DEFAULT 0,
|
||||||
|
play_time_seconds INT DEFAULT 0,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
DROP TABLE IF EXISTS game_saves;
|
||||||
DROP TABLE IF EXISTS users;
|
DROP TABLE IF EXISTS users;
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
@@ -10,3 +11,14 @@ CREATE TABLE users (
|
|||||||
lastname VARCHAR(50) NULL,
|
lastname VARCHAR(50) NULL,
|
||||||
super_oauth_id VARCHAR(36) NULL UNIQUE
|
super_oauth_id VARCHAR(36) NULL UNIQUE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE game_saves (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL UNIQUE,
|
||||||
|
game_state JSON NOT NULL,
|
||||||
|
last_save TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
lifetime_tadpoles BIGINT DEFAULT 0,
|
||||||
|
prestige_count INT DEFAULT 0,
|
||||||
|
play_time_seconds INT DEFAULT 0,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|||||||
141
Backend/src/controllers/saveControllers.js
Normal file
141
Backend/src/controllers/saveControllers.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
const tables = require("../tables");
|
||||||
|
|
||||||
|
// --- Anti-cheat validation ---
|
||||||
|
|
||||||
|
// Max production théorique par seconde (5 generators maxés + click humain réaliste)
|
||||||
|
// Tier 5 = 150/s × 200 owned = 30 000/s (extreme case)
|
||||||
|
// Tous tiers cumulés extreme ≈ 50 000/s
|
||||||
|
// × prestige multiplier max réaliste (×10) = 500 000/s
|
||||||
|
// × evolution tree multiplier (×1.5) = 750 000/s
|
||||||
|
// Marge ×1.1 appliquée dans la validation
|
||||||
|
const MAX_PRODUCTION_PER_SECOND = 750_000;
|
||||||
|
const CHEAT_MARGIN = 1.1;
|
||||||
|
|
||||||
|
function validateGameState(gameState, previousSave) {
|
||||||
|
// Vérification structurelle basique
|
||||||
|
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" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si pas de save précédente, c'est la première — accepter
|
||||||
|
if (!previousSave) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcul du temps écoulé depuis le dernier save
|
||||||
|
const lastSaveTime = new Date(previousSave.last_save).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsedSeconds = Math.max((now - lastSaveTime) / 1000, 0);
|
||||||
|
|
||||||
|
// Ressources max possibles = production max × temps × marge
|
||||||
|
const maxPossibleResources =
|
||||||
|
MAX_PRODUCTION_PER_SECOND * elapsedSeconds * CHEAT_MARGIN;
|
||||||
|
|
||||||
|
// Comparer les ressources actuelles avec le maximum théorique
|
||||||
|
// On compare le delta (gain depuis dernier save)
|
||||||
|
const previousState = previousSave.game_state;
|
||||||
|
const previousResources =
|
||||||
|
typeof previousState === "object"
|
||||||
|
? previousState.resources ?? 0
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const resourceDelta = gameState.resources - previousResources;
|
||||||
|
|
||||||
|
if (resourceDelta > maxPossibleResources && resourceDelta > 0) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: `Resource gain (${Math.floor(resourceDelta)}) exceeds maximum possible (${Math.floor(maxPossibleResources)}) in ${Math.floor(elapsedSeconds)}s`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Controllers ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/save
|
||||||
|
* Charge la save du joueur connecté.
|
||||||
|
*/
|
||||||
|
const load = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user;
|
||||||
|
const save = await tables.game_saves.getByUserId(userId);
|
||||||
|
|
||||||
|
if (!save) {
|
||||||
|
return res.status(200).json({ gameState: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
// game_state est stocké en JSON — MySQL le retourne comme objet si type JSON
|
||||||
|
const gameState =
|
||||||
|
typeof save.game_state === "string"
|
||||||
|
? JSON.parse(save.game_state)
|
||||||
|
: save.game_state;
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
gameState,
|
||||||
|
lastSave: save.last_save,
|
||||||
|
lifetimeTadpoles: save.lifetime_tadpoles,
|
||||||
|
prestigeCount: save.prestige_count,
|
||||||
|
playTimeSeconds: save.play_time_seconds,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("save/load error:", err);
|
||||||
|
return res.status(500).json({ message: "Failed to load save." });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/save
|
||||||
|
* Sauvegarde le game state du joueur connecté.
|
||||||
|
* Anti-cheat : valide le delta de ressources vs temps écoulé.
|
||||||
|
*/
|
||||||
|
const save = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user;
|
||||||
|
const { gameState, playTimeSeconds } = req.body;
|
||||||
|
|
||||||
|
if (!gameState) {
|
||||||
|
return res.status(400).json({ message: "gameState required." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer la save précédente pour validation
|
||||||
|
const previousSave = await tables.game_saves.getByUserId(userId);
|
||||||
|
|
||||||
|
// Valider l'état
|
||||||
|
const validation = validateGameState(gameState, previousSave);
|
||||||
|
if (!validation.valid) {
|
||||||
|
console.warn(
|
||||||
|
`Anti-cheat: user ${userId} rejected — ${validation.reason}`
|
||||||
|
);
|
||||||
|
return res.status(422).json({
|
||||||
|
message: "Save rejected: anomaly detected.",
|
||||||
|
reason: validation.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraire les métadonnées pour la table
|
||||||
|
const metadata = {
|
||||||
|
lifetimeTadpoles: gameState.lifetimeTadpoles ?? 0,
|
||||||
|
prestigeCount: gameState.prestigeCount ?? 0,
|
||||||
|
playTimeSeconds: playTimeSeconds ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
await tables.game_saves.upsert(userId, gameState, metadata);
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Save successful.", lastSave: new Date().toISOString() });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("save/save error:", err);
|
||||||
|
return res.status(500).json({ message: "Failed to save." });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { load, save };
|
||||||
43
Backend/src/models/GameSaveManager.js
Normal file
43
Backend/src/models/GameSaveManager.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const AbstractManager = require("./AbstractManager");
|
||||||
|
|
||||||
|
class GameSaveManager extends AbstractManager {
|
||||||
|
constructor() {
|
||||||
|
super({ table: "game_saves" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByUserId(userId) {
|
||||||
|
const [rows] = await this.database.query(
|
||||||
|
`SELECT * FROM ${this.table} WHERE user_id = ?`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsert(userId, gameState, metadata) {
|
||||||
|
const { lifetimeTadpoles, prestigeCount, playTimeSeconds } = metadata;
|
||||||
|
const gameStateJson = JSON.stringify(gameState);
|
||||||
|
|
||||||
|
const [result] = await this.database.query(
|
||||||
|
`INSERT INTO ${this.table} (user_id, game_state, lifetime_tadpoles, prestige_count, play_time_seconds)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
game_state = VALUES(game_state),
|
||||||
|
lifetime_tadpoles = VALUES(lifetime_tadpoles),
|
||||||
|
prestige_count = VALUES(prestige_count),
|
||||||
|
play_time_seconds = VALUES(play_time_seconds),
|
||||||
|
last_save = CURRENT_TIMESTAMP`,
|
||||||
|
[userId, gameStateJson, lifetimeTadpoles, prestigeCount, playTimeSeconds]
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.affectedRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(userId) {
|
||||||
|
await this.database.query(
|
||||||
|
`DELETE FROM ${this.table} WHERE user_id = ?`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GameSaveManager;
|
||||||
@@ -9,6 +9,7 @@ const router = express.Router();
|
|||||||
// Import Controllers
|
// Import Controllers
|
||||||
const userControllers = require("./controllers/userControllers");
|
const userControllers = require("./controllers/userControllers");
|
||||||
const authControllers = require("./controllers/authControllers");
|
const authControllers = require("./controllers/authControllers");
|
||||||
|
const saveControllers = require("./controllers/saveControllers");
|
||||||
const verifyToken = require("./middlewares/verifyToken");
|
const verifyToken = require("./middlewares/verifyToken");
|
||||||
const verifyOAuth = require("./middlewares/verifyOAuth");
|
const verifyOAuth = require("./middlewares/verifyOAuth");
|
||||||
|
|
||||||
@@ -36,6 +37,10 @@ router.post("/login", userControllers.login);
|
|||||||
// Sync game state — SuperOAuth uniquement
|
// Sync game state — SuperOAuth uniquement
|
||||||
router.patch("/users/:id/coins", verifyOAuth, verifySelf, userControllers.updateCoins);
|
router.patch("/users/:id/coins", verifyOAuth, verifySelf, userControllers.updateCoins);
|
||||||
|
|
||||||
|
// Game saves — JWT required
|
||||||
|
router.get("/save", verifyToken, saveControllers.load);
|
||||||
|
router.post("/save", verifyToken, saveControllers.save);
|
||||||
|
|
||||||
|
|
||||||
/* ************************************************************************* */
|
/* ************************************************************************* */
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
|
|
||||||
// Import the manager modules responsible for handling data operations on the tables
|
// Import the manager modules responsible for handling data operations on the tables
|
||||||
const UserManager = require("./models/UserManager");
|
const UserManager = require("./models/UserManager");
|
||||||
|
const GameSaveManager = require("./models/GameSaveManager");
|
||||||
|
|
||||||
const managers = [
|
const managers = [
|
||||||
UserManager,
|
UserManager,
|
||||||
// Add other managers here
|
GameSaveManager,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Create an empty object to hold data managers for different tables
|
// Create an empty object to hold data managers for different tables
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
# Backend API URL (call it in React with import.meta.env.VITE_BACKEND_URL)
|
# Backend API URL (call it in React with import.meta.env.VITE_BACKEND_URL)
|
||||||
VITE_BACKEND_URL=http://localhost:3310
|
VITE_BACKEND_URL=http://localhost:3310
|
||||||
|
|
||||||
# Other Environment Variables (if needed)
|
# SuperOAuth URL (OAuth login provider)
|
||||||
# VITE_OTHER_VARIABLE=value
|
VITE_SUPEROAUTH_URL=https://superoauth.tetardtek.com
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!--Head-->
|
<!--Head-->
|
||||||
|
|
||||||
<h3>Xmass Clicker</h3>
|
<h3>Clickerz — Tetard Universe</h3>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
### 📄 About :
|
### 📄 About :
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
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 name="robots" content="index, follow" />
|
||||||
<meta
|
<meta
|
||||||
@@ -21,42 +21,25 @@
|
|||||||
name="bingbot"
|
name="bingbot"
|
||||||
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
||||||
/>
|
/>
|
||||||
<link rel="canonical" href="https://xmass.click" />
|
<link rel="canonical" href="https://clickerz.tetardtek.com" />
|
||||||
<meta property="og:url" content="https://xmass.click" />
|
<meta property="og:url" content="https://clickerz.tetardtek.com" />
|
||||||
<meta property="og:site_name" content="Xmass Click" />
|
<meta property="og:site_name" content="Clickerz" />
|
||||||
<meta property="og:locale" content="fr_FR" />
|
<meta property="og:locale" content="fr_FR" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:title" content="Xmass Click" />
|
<meta property="og:title" content="Clickerz — Tetard Universe" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og: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
|
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
<meta property="og:image:width" content="584" />
|
<meta property="og:image:width" content="584" />
|
||||||
<meta property="og:image:height" content="384" />
|
<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:card" content="summary" />
|
||||||
<meta name="twitter:site" content />
|
<meta name="twitter:title" content="Clickerz — Tetard Universe" />
|
||||||
<meta name="twitter:creator" content />
|
|
||||||
<meta name="twitter:title" content="Xmass Click" />
|
|
||||||
<meta
|
<meta
|
||||||
name="twitter:description"
|
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
|
<title>Clickerz — Tetard Universe</title>
|
||||||
name="twitter:image"
|
|
||||||
content="https://xmass.click/webp/share-cover.webp"
|
|
||||||
/>
|
|
||||||
<title>Name</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
456
Frontend/package-lock.json
generated
456
Frontend/package-lock.json
generated
@@ -8,12 +8,15 @@
|
|||||||
"name": "template",
|
"name": "template",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-lottie-player": "^1.5.5",
|
"react-lottie-player": "^1.5.5",
|
||||||
"react-router-dom": "^6.19.0",
|
"react-router-dom": "^6.19.0",
|
||||||
"sass": "^1.69.5"
|
"sass": "^1.69.5",
|
||||||
|
"tailwindcss": "^4.2.2",
|
||||||
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.28",
|
"@types/react": "^18.3.28",
|
||||||
@@ -381,7 +384,6 @@
|
|||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -393,7 +395,6 @@
|
|||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -404,7 +405,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -418,7 +418,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"aix"
|
"aix"
|
||||||
@@ -434,7 +433,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -450,7 +448,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -466,7 +463,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -482,7 +478,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -498,7 +493,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -514,7 +508,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -530,7 +523,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -546,7 +538,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -562,7 +553,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -578,7 +568,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -594,7 +583,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -610,7 +598,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -626,7 +613,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -642,7 +628,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -658,7 +643,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -674,7 +658,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -708,7 +691,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"netbsd"
|
"netbsd"
|
||||||
@@ -742,7 +724,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"openbsd"
|
"openbsd"
|
||||||
@@ -776,7 +757,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"sunos"
|
"sunos"
|
||||||
@@ -792,7 +772,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -808,7 +787,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -824,7 +802,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -938,33 +915,29 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
"dev": true,
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/set-array": "^1.0.1",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
"@jridgewell/trace-mapping": "^0.3.9"
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"node_modules/@jridgewell/remapping": {
|
||||||
"node": ">=6.0.0"
|
"version": "2.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/set-array": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
@@ -973,14 +946,13 @@
|
|||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.20",
|
"version": "0.3.31",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
"dev": true,
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
@@ -990,7 +962,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1631,7 +1602,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -1644,7 +1614,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -1657,7 +1626,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -1670,7 +1638,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -1683,7 +1650,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1696,7 +1662,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1709,7 +1674,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1722,7 +1686,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1735,7 +1698,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1748,7 +1710,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1761,7 +1722,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1774,7 +1734,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1787,7 +1746,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -1800,7 +1758,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -1813,7 +1770,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -1826,7 +1782,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -1839,11 +1794,267 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/node": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
|
"enhanced-resolve": "^5.19.0",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
|
"lightningcss": "1.32.0",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"source-map-js": "^1.2.1",
|
||||||
|
"tailwindcss": "4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tailwindcss/oxide-android-arm64": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-darwin-x64": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
|
||||||
|
"bundleDependencies": [
|
||||||
|
"@napi-rs/wasm-runtime",
|
||||||
|
"@emnapi/core",
|
||||||
|
"@emnapi/runtime",
|
||||||
|
"@tybys/wasm-util",
|
||||||
|
"@emnapi/wasi-threads",
|
||||||
|
"tslib"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "^1.8.1",
|
||||||
|
"@emnapi/runtime": "^1.8.1",
|
||||||
|
"@emnapi/wasi-threads": "^1.1.0",
|
||||||
|
"@napi-rs/wasm-runtime": "^1.1.1",
|
||||||
|
"@tybys/wasm-util": "^0.10.1",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/vite": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/node": "4.2.2",
|
||||||
|
"@tailwindcss/oxide": "4.2.2",
|
||||||
|
"tailwindcss": "4.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1912,20 +2123,19 @@
|
|||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.11",
|
"version": "15.7.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
|
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.28",
|
"version": "18.3.28",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
||||||
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@@ -2424,7 +2634,7 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@@ -2485,7 +2695,6 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -2509,6 +2718,19 @@
|
|||||||
"integrity": "sha512-hohItzsQcG7/FBsviCYMtQwUSWvVF7NVqPOnJCErWsAshsP/CR2LAXdmq276RbESNdhxiAq5/vRo1g2pxGXVww==",
|
"integrity": "sha512-hohItzsQcG7/FBsviCYMtQwUSWvVF7NVqPOnJCErWsAshsP/CR2LAXdmq276RbESNdhxiAq5/vRo1g2pxGXVww==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/enhanced-resolve": {
|
||||||
|
"version": "5.20.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
|
||||||
|
"integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.4",
|
||||||
|
"tapable": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-abstract": {
|
"node_modules/es-abstract": {
|
||||||
"version": "1.22.3",
|
"version": "1.22.3",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
|
||||||
@@ -2635,7 +2857,6 @@
|
|||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
@@ -3120,7 +3341,6 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3274,6 +3494,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/graphemer": {
|
"node_modules/graphemer": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||||
@@ -3761,6 +3987,15 @@
|
|||||||
"set-function-name": "^2.0.1"
|
"set-function-name": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jiti": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||||
|
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -3861,7 +4096,6 @@
|
|||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
@@ -3894,7 +4128,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3915,7 +4148,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3936,7 +4168,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3957,7 +4188,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3978,7 +4208,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3999,7 +4228,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4020,7 +4248,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4041,7 +4268,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4062,7 +4288,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4083,7 +4308,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4104,7 +4328,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4168,7 +4391,6 @@
|
|||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
@@ -4196,7 +4418,6 @@
|
|||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4458,7 +4679,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
@@ -4478,7 +4698,6 @@
|
|||||||
"version": "8.5.8",
|
"version": "8.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -4805,7 +5024,6 @@
|
|||||||
"version": "4.17.2",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz",
|
||||||
"integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==",
|
"integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.5"
|
"@types/estree": "1.0.5"
|
||||||
},
|
},
|
||||||
@@ -5135,6 +5353,25 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwindcss": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tapable": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/webpack"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -5198,7 +5435,6 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
@@ -5363,7 +5599,6 @@
|
|||||||
"version": "5.2.11",
|
"version": "5.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
|
||||||
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
|
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.20.1",
|
"esbuild": "^0.20.1",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
@@ -6191,6 +6426,35 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
|
||||||
|
"integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,15 @@
|
|||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-lottie-player": "^1.5.5",
|
"react-lottie-player": "^1.5.5",
|
||||||
"react-router-dom": "^6.19.0",
|
"react-router-dom": "^6.19.0",
|
||||||
"sass": "^1.69.5"
|
"sass": "^1.69.5",
|
||||||
|
"tailwindcss": "^4.2.2",
|
||||||
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.28",
|
"@types/react": "^18.3.28",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Sitemap: https://xmass.click/sitemap.xml
|
Sitemap: https://clickerz.tetardtek.com/sitemap.xml
|
||||||
|
|
||||||
User-agent: AlphaSeoBot
|
User-agent: AlphaSeoBot
|
||||||
User-agent: AlphaSeoBot-SA
|
User-agent: AlphaSeoBot-SA
|
||||||
|
|||||||
@@ -4,22 +4,20 @@
|
|||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||||
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
|
|
||||||
|
|
||||||
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://www.xmass.click/</loc>
|
<loc>https://clickerz.tetardtek.com/</loc>
|
||||||
<lastmod>2023-08-15T19:22:16+00:00</lastmod>
|
<lastmod>2026-03-20T12:00:00+00:00</lastmod>
|
||||||
<priority>1.00</priority>
|
<priority>1.00</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://www.xmass.click/boutique</loc>
|
<loc>https://clickerz.tetardtek.com/boutique</loc>
|
||||||
<lastmod>2023-08-15T19:22:16+00:00</lastmod>
|
<lastmod>2026-03-20T12:00:00+00:00</lastmod>
|
||||||
<priority>0.80</priority>
|
<priority>0.80</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://www.xmass.click/achievements</loc>
|
<loc>https://clickerz.tetardtek.com/achievements</loc>
|
||||||
<lastmod>2023-08-15T19:22:16+00:00</lastmod>
|
<lastmod>2026-03-20T12:00:00+00:00</lastmod>
|
||||||
<priority>0.80</priority>
|
<priority>0.80</priority>
|
||||||
</url>
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
2254
Frontend/public/svg/tadpole.svg
Executable file
2254
Frontend/public/svg/tadpole.svg
Executable file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 127 KiB |
@@ -4,6 +4,8 @@ import { Outlet } from "react-router-dom";
|
|||||||
import Navbar from "./components/navbar";
|
import Navbar from "./components/navbar";
|
||||||
import Footer from "./components/footer";
|
import Footer from "./components/footer";
|
||||||
import Hud from "./components/Hud/Hud";
|
import Hud from "./components/Hud/Hud";
|
||||||
|
import { GameTick } from "./components/GameTick";
|
||||||
|
import { GameSync } from "./components/GameSync";
|
||||||
|
|
||||||
import "./scss/root.scss";
|
import "./scss/root.scss";
|
||||||
import "./scss/components/footer.scss";
|
import "./scss/components/footer.scss";
|
||||||
@@ -12,20 +14,22 @@ import navData from "./data/NavBarData.json";
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [toggleSnow, setToggleSnow] = useState(false);
|
const [toggleRain, setToggleRain] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<GameTick />
|
||||||
|
<GameSync />
|
||||||
<Navbar
|
<Navbar
|
||||||
navData={navData}
|
navData={navData}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
setIsVisible={setIsVisible}
|
setIsVisible={setIsVisible}
|
||||||
toggleSnow={toggleSnow}
|
toggleRain={toggleRain}
|
||||||
setToggleSnow={setToggleSnow}
|
setToggleRain={setToggleRain}
|
||||||
/>
|
/>
|
||||||
<Hud isVisible={isVisible} setIsVisible={setIsVisible} />
|
<Hud isVisible={isVisible} setIsVisible={setIsVisible} />
|
||||||
<main>
|
<main>
|
||||||
<Outlet context={[toggleSnow, setToggleSnow]} />
|
<Outlet context={[toggleRain, setToggleRain]} />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -7,13 +7,19 @@ import {
|
|||||||
buyGenerator,
|
buyGenerator,
|
||||||
applyPrestige,
|
applyPrestige,
|
||||||
canPrestige,
|
canPrestige,
|
||||||
|
computePrestigeDna,
|
||||||
|
canBuyEvolutionNode,
|
||||||
|
buyEvolutionNode,
|
||||||
|
getClickMultiplierFromTree,
|
||||||
|
getProductionMultiplierFromTree,
|
||||||
|
getStartBonusFromTree,
|
||||||
DEFAULT_STATE,
|
DEFAULT_STATE,
|
||||||
DEFAULT_GENERATORS,
|
DEFAULT_GENERATORS,
|
||||||
|
DEFAULT_EVOLUTION_TREE,
|
||||||
} from "../core/economy";
|
} from "../core/economy";
|
||||||
|
|
||||||
// PrestigePanel visibility guard — canPrestige drives render condition
|
// --- PrestigePanel visibility ---
|
||||||
// Ces tests valident l'invariant : le panneau prestige ne doit jamais être
|
|
||||||
// visible (canPrestige = false) si les ressources sont inférieures au seuil.
|
|
||||||
describe("PrestigePanel visibility (canPrestige guard)", () => {
|
describe("PrestigePanel visibility (canPrestige guard)", () => {
|
||||||
it("canPrestige = false pour resources = 0 → panneau non visible", () => {
|
it("canPrestige = false pour resources = 0 → panneau non visible", () => {
|
||||||
expect(canPrestige({ ...DEFAULT_STATE, resources: 0 })).toBe(false);
|
expect(canPrestige({ ...DEFAULT_STATE, resources: 0 })).toBe(false);
|
||||||
@@ -28,6 +34,8 @@ describe("PrestigePanel visibility (canPrestige guard)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Prestige reset ---
|
||||||
|
|
||||||
describe("applyPrestige — post-prestige state", () => {
|
describe("applyPrestige — post-prestige state", () => {
|
||||||
const prestigeState = {
|
const prestigeState = {
|
||||||
...DEFAULT_STATE,
|
...DEFAULT_STATE,
|
||||||
@@ -35,9 +43,10 @@ describe("applyPrestige — post-prestige state", () => {
|
|||||||
generators: DEFAULT_STATE.generators.map((g) => ({ ...g, owned: 3 })),
|
generators: DEFAULT_STATE.generators.map((g) => ({ ...g, owned: 3 })),
|
||||||
prestigeCount: 0,
|
prestigeCount: 0,
|
||||||
prestigeMultiplier: 1,
|
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);
|
expect(applyPrestige(prestigeState).resources).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,8 +62,43 @@ describe("applyPrestige — post-prestige state", () => {
|
|||||||
it("prestigeCount incrémenté à 1 après premier prestige", () => {
|
it("prestigeCount incrémenté à 1 après premier prestige", () => {
|
||||||
expect(applyPrestige(prestigeState).prestigeCount).toBe(1);
|
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", () => {
|
describe("generatorCost", () => {
|
||||||
it("retourne baseCost quand owned = 0", () => {
|
it("retourne baseCost quand owned = 0", () => {
|
||||||
const gen = { ...DEFAULT_GENERATORS[0], owned: 0 };
|
const gen = { ...DEFAULT_GENERATORS[0], owned: 0 };
|
||||||
@@ -67,6 +111,8 @@ describe("generatorCost", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Production ---
|
||||||
|
|
||||||
describe("totalProductionPerSecond", () => {
|
describe("totalProductionPerSecond", () => {
|
||||||
it("retourne 0 si aucun générateur acheté", () => {
|
it("retourne 0 si aucun générateur acheté", () => {
|
||||||
expect(totalProductionPerSecond(DEFAULT_STATE)).toBe(0);
|
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 };
|
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);
|
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)", () => {
|
describe("computeIdleGains (lazy calculation)", () => {
|
||||||
it("calcule les gains proportionnellement au temps écoulé", () => {
|
it("calcule les gains proportionnellement au temps écoulé", () => {
|
||||||
const state = {
|
const state = {
|
||||||
@@ -108,29 +167,54 @@ describe("computeIdleGains (lazy calculation)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Click ---
|
||||||
|
|
||||||
describe("applyClick", () => {
|
describe("applyClick", () => {
|
||||||
it("augmente les ressources du clickMultiplier × prestigeMultiplier", () => {
|
it("augmente les ressources du clickMultiplier × prestigeMultiplier", () => {
|
||||||
const state = { ...DEFAULT_STATE, clickMultiplier: 3, prestigeMultiplier: 2 };
|
const state = { ...DEFAULT_STATE, clickMultiplier: 3, prestigeMultiplier: 2 };
|
||||||
const result = applyClick(state);
|
const result = applyClick(state);
|
||||||
expect(result.resources).toBe(6);
|
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", () => {
|
describe("buyGenerator", () => {
|
||||||
it("retourne null si fonds insuffisants", () => {
|
it("retourne null si fonds insuffisants", () => {
|
||||||
const result = buyGenerator(DEFAULT_STATE, "manic");
|
const result = buyGenerator(DEFAULT_STATE, "nid");
|
||||||
expect(result).toBeNull(); // 0 ressources, coût = 15
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("achète correctement et déduit le coût", () => {
|
it("achète correctement et déduit le coût", () => {
|
||||||
const state = { ...DEFAULT_STATE, resources: 100 };
|
const state = { ...DEFAULT_STATE, resources: 100 };
|
||||||
const result = buyGenerator(state, "manic");
|
const result = buyGenerator(state, "nid");
|
||||||
expect(result).not.toBeNull();
|
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);
|
expect(result!.resources).toBe(100 - DEFAULT_GENERATORS[0].baseCost);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Prestige legacy ---
|
||||||
|
|
||||||
describe("prestige", () => {
|
describe("prestige", () => {
|
||||||
it("canPrestige retourne false si < 1 000 000 ressources", () => {
|
it("canPrestige retourne false si < 1 000 000 ressources", () => {
|
||||||
expect(canPrestige({ ...DEFAULT_STATE, resources: 999_999 })).toBe(false);
|
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);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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/boutiquecard.scss";
|
||||||
import "../scss/components/buttons.scss";
|
import "../scss/components/buttons.scss";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { useGameStore } from "../store/useGameStore";
|
||||||
|
|
||||||
export default function BoutiqueCard({
|
export default function BoutiqueCard({
|
||||||
name,
|
name,
|
||||||
@@ -9,89 +11,13 @@ export default function BoutiqueCard({
|
|||||||
incrementValue,
|
incrementValue,
|
||||||
description,
|
description,
|
||||||
image,
|
image,
|
||||||
link,
|
|
||||||
type,
|
type,
|
||||||
buyed,
|
|
||||||
}) {
|
}) {
|
||||||
BoutiqueCard.propTypes = {
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
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 {
|
// Legacy shop — disabled for now, generators are in GeneratorShop
|
||||||
wildCoin,
|
const canAfford = resources >= price;
|
||||||
incrementClick,
|
|
||||||
setWildCoin,
|
|
||||||
setIncrementClick,
|
|
||||||
incrementPerSecond,
|
|
||||||
setIncrementPerSecond,
|
|
||||||
setCoffee,
|
|
||||||
setSantaDrunk,
|
|
||||||
setManic,
|
|
||||||
setSnowman,
|
|
||||||
setBonnet,
|
|
||||||
setSugar,
|
|
||||||
setCookie,
|
|
||||||
setCouronne,
|
|
||||||
setEpice,
|
|
||||||
setBiere,
|
|
||||||
} = useWildCoin();
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className="shopcardcontainer">
|
<div className="shopcardcontainer">
|
||||||
<div className="shopcontainer">
|
<div className="shopcontainer">
|
||||||
@@ -105,7 +31,6 @@ export default function BoutiqueCard({
|
|||||||
<p className="itemname">{name}</p>
|
<p className="itemname">{name}</p>
|
||||||
<div className="price">
|
<div className="price">
|
||||||
<p className="itemprice">{price}</p>
|
<p className="itemprice">{price}</p>
|
||||||
|
|
||||||
<div className="priceicon" />
|
<div className="priceicon" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,12 +44,22 @@ export default function BoutiqueCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => acheterAmelioration(type, price, name)}
|
disabled={!canAfford}
|
||||||
className="primary-button"
|
className="primary-button"
|
||||||
|
style={{ opacity: canAfford ? 1 : 0.5 }}
|
||||||
>
|
>
|
||||||
Acheter
|
Bientôt
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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,
|
||||||
|
};
|
||||||
|
|||||||
99
Frontend/src/components/EvolutionTree.tsx
Normal file
99
Frontend/src/components/EvolutionTree.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
Frontend/src/components/GameSync.tsx
Normal file
22
Frontend/src/components/GameSync.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// GameSync.tsx — Bridge useSaveSync ↔ Zustand store
|
||||||
|
// Monter une seule fois dans App. Silencieux en mode invité (pas de token).
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useGameStore } from "../store/useGameStore";
|
||||||
|
import { useSaveSync } from "../hooks/useSaveSync";
|
||||||
|
|
||||||
|
export function GameSync() {
|
||||||
|
const state = useGameStore((s) => s.state);
|
||||||
|
const loadFromServer = useGameStore((s) => s.loadFromServer);
|
||||||
|
const playSeconds = useGameStore((s) => s.playSeconds);
|
||||||
|
|
||||||
|
const getGameState = useCallback(() => state, [state]);
|
||||||
|
|
||||||
|
useSaveSync({
|
||||||
|
getGameState,
|
||||||
|
onLoad: loadFromServer,
|
||||||
|
playTimeSeconds: playSeconds,
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
16
Frontend/src/components/GameTick.tsx
Normal file
16
Frontend/src/components/GameTick.tsx
Normal 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;
|
||||||
|
}
|
||||||
51
Frontend/src/components/GeneratorShop.tsx
Normal file
51
Frontend/src/components/GeneratorShop.tsx
Normal 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 · 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,154 +1,40 @@
|
|||||||
import "../../scss/components/Hud.scss";
|
// Hud.jsx — Stats HUD (Zustand)
|
||||||
import { useWildCoin } from "../WildCoin/WildCoinContext";
|
import { useGameStore } from "../../store/useGameStore";
|
||||||
import Timer from "../timer/Timer";
|
import { formatNumber } from "../../utils/formatNumber";
|
||||||
import propTypes from "prop-types";
|
|
||||||
|
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 }) {
|
function Hud({ isVisible }) {
|
||||||
Hud.propTypes = {
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
isVisible: propTypes.bool.isRequired,
|
const clickMultiplier = useGameStore((s) => s.state.clickMultiplier);
|
||||||
};
|
const productionPerSecond = useGameStore((s) => s.productionPerSecond);
|
||||||
|
const playSeconds = useGameStore((s) => s.playSeconds);
|
||||||
|
|
||||||
const {
|
if (isVisible) return null;
|
||||||
manic,
|
|
||||||
snowman,
|
|
||||||
bonnet,
|
|
||||||
sugar,
|
|
||||||
cookie,
|
|
||||||
couronne,
|
|
||||||
epice,
|
|
||||||
biere,
|
|
||||||
coffee,
|
|
||||||
} = useWildCoin();
|
|
||||||
|
|
||||||
const { incrementClick, incrementPerSecond } = useWildCoin();
|
|
||||||
const hiddenDiv = isVisible ? "none" : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hudContainer">
|
<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 style={{ display: hiddenDiv }} className="hudStats">
|
<div className="flex gap-6 text-sm">
|
||||||
<div className="time section">
|
<div className="flex flex-col items-center">
|
||||||
<p>Temps de jeu</p>
|
<span className="text-gray-400 text-xs">Temps</span>
|
||||||
<p><Timer /></p>
|
<span>{formatTime(playSeconds)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="auto section">
|
<div className="flex flex-col items-center">
|
||||||
<p>Auto CPS</p>
|
<span className="text-gray-400 text-xs">Têtards/s</span>
|
||||||
<p>{incrementPerSecond}</p>
|
<span>{formatNumber(productionPerSecond)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="player section">
|
<div className="flex flex-col items-center">
|
||||||
<p>Player Click</p>
|
<span className="text-gray-400 text-xs">Ponte</span>
|
||||||
<p>{incrementClick}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<div className="text-lg font-bold text-emerald-400">
|
||||||
{manic[0] === true ? (
|
{formatNumber(resources)}
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,50 +1,37 @@
|
|||||||
// MilestoneBar.tsx — Progression vers le prochain prestige
|
// 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;
|
const PRESTIGE_THRESHOLD = 1_000_000;
|
||||||
|
|
||||||
interface MilestoneBarProps {
|
export function MilestoneBar() {
|
||||||
resources: number;
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
}
|
|
||||||
|
|
||||||
export function MilestoneBar({ resources }: MilestoneBarProps) {
|
|
||||||
const progress = Math.min(resources / PRESTIGE_THRESHOLD, 1);
|
const progress = Math.min(resources / PRESTIGE_THRESHOLD, 1);
|
||||||
const progressPercent = (progress * 100).toFixed(1);
|
const progressPercent = (progress * 100).toFixed(1);
|
||||||
const remaining = Math.max(PRESTIGE_THRESHOLD - resources, 0);
|
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 (
|
return (
|
||||||
<div className="milestone-bar" aria-label="Progression vers le prestige">
|
<div className="flex flex-col gap-1 max-w-md w-full">
|
||||||
<div className="milestone-label">
|
<div className="text-xs text-gray-300 flex justify-between">
|
||||||
Prochain prestige : {formatNumber(resources)} / {formatNumber(PRESTIGE_THRESHOLD)}
|
<span>Prochaine Génération</span>
|
||||||
|
<span>
|
||||||
|
{formatNumber(resources)} / {formatNumber(PRESTIGE_THRESHOLD)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="milestone-track"
|
className="h-full bg-gradient-to-r from-purple-600 to-purple-400 transition-all duration-500 rounded-full"
|
||||||
role="progressbar"
|
|
||||||
aria-valuenow={Math.floor(progress * 100)}
|
|
||||||
aria-valuemin={0}
|
|
||||||
aria-valuemax={100}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="milestone-fill"
|
|
||||||
style={{ width: `${progressPercent}%` }}
|
style={{ width: `${progressPercent}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{remaining > 0 && (
|
<div className="text-xs text-gray-400 text-right">
|
||||||
<div className="milestone-remaining">
|
{remaining > 0
|
||||||
{formatNumber(remaining)} ressources restantes
|
? `${formatNumber(remaining)} têtards restants`
|
||||||
|
: "Nouvelle Génération disponible !"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{remaining === 0 && (
|
|
||||||
<div className="milestone-ready">Prestige disponible !</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,50 @@
|
|||||||
// PrestigePanel.tsx — Boucle de prestige long terme
|
// PrestigePanel.tsx — Nouvelle Génération (prestige)
|
||||||
// Visible uniquement quand canPrestige = true (ressources ≥ 1 000 000)
|
// 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 {
|
export function PrestigePanel() {
|
||||||
prestigeCount: number;
|
const { prestigeCount, prestigeMultiplier, ancestralDna, lifetimeTadpoles } =
|
||||||
prestigeMultiplier: number;
|
useGameStore((s) => s.state);
|
||||||
canPrestige: boolean;
|
const canPrestige = useGameStore((s) => s.canPrestige);
|
||||||
onPrestige: () => void;
|
const prestige = useGameStore((s) => s.prestige);
|
||||||
}
|
|
||||||
|
const dnaPreview = computePrestigeDna(lifetimeTadpoles);
|
||||||
|
|
||||||
export function PrestigePanel({
|
|
||||||
prestigeCount,
|
|
||||||
prestigeMultiplier,
|
|
||||||
canPrestige,
|
|
||||||
onPrestige,
|
|
||||||
}: PrestigePanelProps) {
|
|
||||||
const handlePrestige = () => {
|
const handlePrestige = () => {
|
||||||
const confirmed = window.confirm(
|
const confirmed = window.confirm(
|
||||||
`Prestige — Reset total : ressources et générateurs à zéro.\n` +
|
`Nouvelle Génération\n\n` +
|
||||||
`Récompense : +0.1× multiplicateur permanent.\n\n` +
|
`Reset : têtards et générateurs à zéro.\n` +
|
||||||
`Multiplicateur actuel : ×${prestigeMultiplier.toFixed(1)}\n` +
|
`Récompense : +${dnaPreview} ADN Ancestral\n` +
|
||||||
`Multiplicateur après : ×${(prestigeMultiplier + 0.1).toFixed(1)}\n\n` +
|
` +0.1x multiplicateur permanent\n\n` +
|
||||||
`Confirmer le prestige ?`
|
`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) {
|
if (confirmed) prestige();
|
||||||
onPrestige();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="prestige-panel">
|
<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="prestige-stats">
|
<div className="flex flex-wrap gap-4 text-sm text-purple-200">
|
||||||
<span className="prestige-count">Prestiges : {prestigeCount}</span>
|
<span>Générations : {prestigeCount}</span>
|
||||||
<span className="prestige-multiplier">
|
<span>Mult : x{prestigeMultiplier.toFixed(1)}</span>
|
||||||
Multiplicateur : ×{prestigeMultiplier.toFixed(1)}
|
<span>ADN : {ancestralDna}</span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canPrestige && (
|
{canPrestige && (
|
||||||
<div className="prestige-action">
|
<div className="flex flex-col gap-2 mt-2">
|
||||||
<div className="prestige-reward">
|
<p className="text-sm text-purple-100">
|
||||||
Récompense disponible : <strong>+0.1× multiplicateur permanent</strong>
|
Nouvelle Génération : <strong>+{dnaPreview} ADN</strong> + <strong>+0.1x mult</strong>
|
||||||
</div>
|
</p>
|
||||||
<button
|
<button
|
||||||
className="prestige-button"
|
|
||||||
onClick={handlePrestige}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -6,7 +6,8 @@ import "../scss/root.scss";
|
|||||||
|
|
||||||
import PrimaryButton from "./buttons/PrimaryButton";
|
import PrimaryButton from "./buttons/PrimaryButton";
|
||||||
import Burger from "./burger";
|
import Burger from "./burger";
|
||||||
import { useWildCoin } from "./WildCoin/WildCoinContext";
|
import { useGameStore } from "../store/useGameStore";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
import HUDON from "../../public/NavBar/HUDON.svg";
|
import HUDON from "../../public/NavBar/HUDON.svg";
|
||||||
import HUDOFF from "../../public/NavBar/HUDOFF.svg";
|
import HUDOFF from "../../public/NavBar/HUDOFF.svg";
|
||||||
import SnowOn from "../../public/NavBar/SnowOn.svg";
|
import SnowOn from "../../public/NavBar/SnowOn.svg";
|
||||||
@@ -17,24 +18,20 @@ export default function Navbar({
|
|||||||
navData,
|
navData,
|
||||||
isVisible,
|
isVisible,
|
||||||
setIsVisible,
|
setIsVisible,
|
||||||
toggleSnow,
|
toggleRain,
|
||||||
setToggleSnow,
|
setToggleRain,
|
||||||
}) {
|
}) {
|
||||||
Navbar.propTypes = {
|
Navbar.propTypes = {
|
||||||
isVisible: PropTypes.bool,
|
isVisible: PropTypes.bool,
|
||||||
setIsVisible: PropTypes.function,
|
setIsVisible: PropTypes.function,
|
||||||
setToggleSnow: PropTypes.function,
|
setToggleRain: PropTypes.function,
|
||||||
toggleSnow: PropTypes.bool,
|
toggleRain: PropTypes.bool,
|
||||||
}.isRequired;
|
}.isRequired;
|
||||||
|
|
||||||
const { wildCoin } = useWildCoin();
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
|
const { user, logout } = useAuth();
|
||||||
const [imageSrc, setImageSrc] = useState(HUDON);
|
const [imageSrc, setImageSrc] = useState(HUDON);
|
||||||
const [snowImageSrc, setSnowImageSrc] = useState(SnowOff);
|
const [snowImageSrc, setSnowImageSrc] = useState(SnowOff);
|
||||||
const [timerVisible, setTimerVisible] = useState(false);
|
|
||||||
const handleClickWildCoin = () => {
|
|
||||||
setTimerVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleHud = () => {
|
const toggleHud = () => {
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
@@ -44,12 +41,12 @@ export default function Navbar({
|
|||||||
setImageSrc(HUDON);
|
setImageSrc(HUDON);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function toggleSnowBtn() {
|
function toggleRainBtn() {
|
||||||
if (toggleSnow === false) {
|
if (toggleRain === false) {
|
||||||
setToggleSnow(true);
|
setToggleRain(true);
|
||||||
setSnowImageSrc(SnowOn);
|
setSnowImageSrc(SnowOn);
|
||||||
} else {
|
} else {
|
||||||
setToggleSnow(false);
|
setToggleRain(false);
|
||||||
setSnowImageSrc(SnowOff);
|
setSnowImageSrc(SnowOff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,11 +56,11 @@ export default function Navbar({
|
|||||||
className="logo"
|
className="logo"
|
||||||
to="/"
|
to="/"
|
||||||
aria-label="Retourner à la page d'accueil"
|
aria-label="Retourner à la page d'accueil"
|
||||||
title="Logo XmassClick"
|
title="Logo Clickerz"
|
||||||
/>
|
/>
|
||||||
<div className="navbar">
|
<div className="navbar">
|
||||||
<div className="wildCoin">
|
<div className="resource-counter">
|
||||||
{new Intl.NumberFormat().format(wildCoin)}
|
{new Intl.NumberFormat().format(Math.floor(resources))}
|
||||||
</div>
|
</div>
|
||||||
<ul className="nav-list">
|
<ul className="nav-list">
|
||||||
{navData.map((navIndex) => {
|
{navData.map((navIndex) => {
|
||||||
@@ -104,6 +101,18 @@ export default function Navbar({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
{user ? (
|
||||||
|
<div className="auth-nav">
|
||||||
|
<span className="auth-nickname">{user.nickname}</span>
|
||||||
|
<button className="auth-btn" onClick={logout} type="button">
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link className="mainLink" to="/login">
|
||||||
|
Connexion
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<img
|
<img
|
||||||
onClick={() => toggleHud()}
|
onClick={() => toggleHud()}
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
@@ -111,7 +120,7 @@ export default function Navbar({
|
|||||||
alt="boutton on"
|
alt="boutton on"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
onClick={() => toggleSnowBtn()}
|
onClick={() => toggleRainBtn()}
|
||||||
src={snowImageSrc}
|
src={snowImageSrc}
|
||||||
style={{ height: "28px" }}
|
style={{ height: "28px" }}
|
||||||
alt="boutton on"
|
alt="boutton on"
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -6,8 +6,8 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import axios from "axios";
|
const decodeJwtPayload = (token) =>
|
||||||
import { jwtDecode } from "jwt-decode";
|
JSON.parse(atob(token.split(".")[1]));
|
||||||
|
|
||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
@@ -21,13 +21,19 @@ import React, {
|
|||||||
|
|
||||||
if (jwtToken) {
|
if (jwtToken) {
|
||||||
try {
|
try {
|
||||||
const decodedPayload = jwtDecode(jwtToken);
|
const decodedPayload = decodeJwtPayload(jwtToken);
|
||||||
const res = await axios.get(
|
const res = await fetch(
|
||||||
`${import.meta.env.VITE_BACKEND_URL}/api/users/${decodedPayload.user}`
|
`${import.meta.env.VITE_BACKEND_URL}/api/users/${decodedPayload.user}`,
|
||||||
|
{
|
||||||
|
headers: { "x-auth-token": jwtToken },
|
||||||
|
}
|
||||||
);
|
);
|
||||||
setUser(res.data);
|
if (!res.ok) throw new Error("Failed to fetch user");
|
||||||
|
const data = await res.json();
|
||||||
|
setUser(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching user data:", error);
|
console.error("Error fetching user data:", error);
|
||||||
|
localStorage.removeItem("token");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -39,11 +45,26 @@ import React, {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loginWithOAuth = async (token) => {
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.VITE_BACKEND_URL}/api/auth/callback?code=${encodeURIComponent(token)}`
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data.message || "OAuth login failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("token", data.token);
|
||||||
|
setUser(data.user);
|
||||||
|
return data.user;
|
||||||
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
setUser(null);
|
setUser(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const editUser = async (updatedFields) => {
|
const editUser = async (updatedFields) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -52,7 +73,7 @@ import React, {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
"x-auth-token": localStorage.getItem("token"),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(updatedFields),
|
body: JSON.stringify(updatedFields),
|
||||||
}
|
}
|
||||||
@@ -114,6 +135,7 @@ import React, {
|
|||||||
user,
|
user,
|
||||||
loading,
|
loading,
|
||||||
logout,
|
logout,
|
||||||
|
loginWithOAuth,
|
||||||
editUser,
|
editUser,
|
||||||
sendPasswordResetEmail,
|
sendPasswordResetEmail,
|
||||||
setUser: (newUser) => {
|
setUser: (newUser) => {
|
||||||
@@ -140,3 +162,5 @@ import React, {
|
|||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { AuthProvider, useAuth };
|
||||||
|
|||||||
@@ -9,15 +9,94 @@ export interface Generator {
|
|||||||
owned: number;
|
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 {
|
export interface GameState {
|
||||||
resources: number;
|
resources: number;
|
||||||
clickMultiplier: number;
|
clickMultiplier: number;
|
||||||
generators: Generator[];
|
generators: Generator[];
|
||||||
lastTick: number; // timestamp ms — lazy calc reference
|
lastTick: number; // timestamp ms — lazy calc reference
|
||||||
prestigeCount: number;
|
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
|
// Coût d'achat du N-ième générateur : baseCost × 1.15^owned
|
||||||
export function generatorCost(gen: Generator): number {
|
export function generatorCost(gen: Generator): number {
|
||||||
return Math.floor(gen.baseCost * Math.pow(1.15, gen.owned));
|
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,
|
(sum, gen) => sum + gen.baseProduction * gen.owned,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
return base * state.prestigeMultiplier;
|
const treeMultiplier = getProductionMultiplierFromTree(state.evolutionTree);
|
||||||
|
return base * state.prestigeMultiplier * treeMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy calculation : ressources accumulées depuis lastTick
|
// Lazy calculation : ressources accumulées depuis lastTick
|
||||||
@@ -44,15 +124,19 @@ export function applyIdleGains(state: GameState, now: number): GameState {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
resources: state.resources + gains,
|
resources: state.resources + gains,
|
||||||
|
lifetimeTadpoles: state.lifetimeTadpoles + gains,
|
||||||
lastTick: now,
|
lastTick: now,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clic manuel
|
// Clic manuel
|
||||||
export function applyClick(state: GameState): GameState {
|
export function applyClick(state: GameState): GameState {
|
||||||
|
const treeClickMult = getClickMultiplierFromTree(state.evolutionTree);
|
||||||
|
const gain = state.clickMultiplier * state.prestigeMultiplier * treeClickMult;
|
||||||
return {
|
return {
|
||||||
...state,
|
...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 {
|
export function canPrestige(state: GameState): boolean {
|
||||||
return state.resources >= 1_000_000;
|
return state.resources >= 1_000_000;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyPrestige(state: GameState): GameState {
|
export function applyPrestige(state: GameState): GameState {
|
||||||
const newPrestigeCount = state.prestigeCount + 1;
|
const newPrestigeCount = state.prestigeCount + 1;
|
||||||
|
const dnaGained = computePrestigeDna(state.lifetimeTadpoles);
|
||||||
|
const startBonus = getStartBonusFromTree(state.evolutionTree);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
resources: 0,
|
resources: startBonus,
|
||||||
generators: state.generators.map((g) => ({ ...g, owned: 0 })),
|
generators: state.generators.map((g) => ({ ...g, owned: 0 })),
|
||||||
prestigeCount: newPrestigeCount,
|
prestigeCount: newPrestigeCount,
|
||||||
prestigeMultiplier: 1 + newPrestigeCount * 0.1,
|
prestigeMultiplier: 1 + newPrestigeCount * 0.1,
|
||||||
|
ancestralDna: state.ancestralDna + dnaGained,
|
||||||
|
lifetimeTadpoles: 0,
|
||||||
lastTick: Date.now(),
|
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[] = [
|
export const DEFAULT_GENERATORS: Generator[] = [
|
||||||
{ id: "manic", name: "Manic", baseCost: 10, baseProduction: 0.1, owned: 0 },
|
{ id: "nid", name: "Nid", baseCost: 10, baseProduction: 0.1, owned: 0 },
|
||||||
{ id: "coffee", name: "Tasse à café", baseCost: 100, baseProduction: 0.5, owned: 0 },
|
{ id: "mare", name: "Mare", baseCost: 100, baseProduction: 0.5, owned: 0 },
|
||||||
{ id: "sugar", name: "Sucre", baseCost: 1_000, baseProduction: 3, owned: 0 },
|
{ id: "marecage", name: "Marécage", baseCost: 1_000, baseProduction: 3, owned: 0 },
|
||||||
{ id: "factory", name: "Usine", baseCost: 10_000, baseProduction: 20, owned: 0 },
|
{ id: "etang", name: "Étang Ancien", baseCost: 10_000, baseProduction: 20, owned: 0 },
|
||||||
{ id: "portal", name: "Portail", baseCost: 100_000, baseProduction: 150, owned: 0 },
|
{ id: "lac", name: "Lac Mystique", baseCost: 100_000, baseProduction: 150, owned: 0 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_STATE: GameState = {
|
export const DEFAULT_STATE: GameState = {
|
||||||
@@ -108,4 +198,7 @@ export const DEFAULT_STATE: GameState = {
|
|||||||
lastTick: Date.now(),
|
lastTick: Date.now(),
|
||||||
prestigeCount: 0,
|
prestigeCount: 0,
|
||||||
prestigeMultiplier: 1,
|
prestigeMultiplier: 1,
|
||||||
|
ancestralDna: 0,
|
||||||
|
evolutionTree: DEFAULT_EVOLUTION_TREE,
|
||||||
|
lifetimeTadpoles: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 18,
|
"id": 18,
|
||||||
"name": "un pull de noël",
|
"name": "Peau de grenouille rare",
|
||||||
"founded": false,
|
"founded": false,
|
||||||
"image": "https://i.goopics.net/uwjwn1.jpg"
|
"image": "https://i.goopics.net/uwjwn1.jpg"
|
||||||
},
|
},
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 30,
|
"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,
|
"founded": false,
|
||||||
"image": "https://i.goopics.net/dakyj9.png"
|
"image": "https://i.goopics.net/dakyj9.png"
|
||||||
},
|
},
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 36,
|
"id": 36,
|
||||||
"name": "Chapeau de Noël clignotant",
|
"name": "Couronne de nénuphars lumineuse",
|
||||||
"founded": false,
|
"founded": false,
|
||||||
"image": "https://i.goopics.net/d4su7e.png"
|
"image": "https://i.goopics.net/d4su7e.png"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
"id": "1",
|
"id": "1",
|
||||||
"linkname": "Jeu",
|
"linkname": "Jeu",
|
||||||
"linkurl": "/",
|
"linkurl": "/jeu",
|
||||||
"btn": false
|
"btn": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,89 +1,89 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "Manic",
|
"name": "Griffes de Grenouille",
|
||||||
"price": 15,
|
"price": 15,
|
||||||
"incrementValue": 1,
|
"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": "/",
|
"link": "/",
|
||||||
"image": "./svg/Hand.svg",
|
"image": "./svg/Hand.svg",
|
||||||
"buyed": false,
|
"buyed": false,
|
||||||
"type": "actif"
|
"type": "actif"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Tasse à café",
|
"name": "Algues Nutritives",
|
||||||
"price": 15,
|
"price": 15,
|
||||||
"incrementValue": 1,
|
"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": "/",
|
"link": "/",
|
||||||
"image": "./svg/Tasse.svg",
|
"image": "./svg/Tasse.svg",
|
||||||
"buyed": false,
|
"buyed": false,
|
||||||
"type": "passif"
|
"type": "passif"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Mr Bonhomme",
|
"name": "Crapaud Gardien",
|
||||||
"price": 150,
|
"price": 150,
|
||||||
"incrementValue": 10,
|
"incrementValue": 10,
|
||||||
"description": "Un assistant idéal pour le click",
|
"description": "Un ancien du marais qui veille sur les pontes. +10 par clic.",
|
||||||
"link": "/",
|
"link": "/",
|
||||||
"image": "./svg/Bonhome.svg",
|
"image": "./svg/Bonhome.svg",
|
||||||
"buyed": false,
|
"buyed": false,
|
||||||
"type": "actif"
|
"type": "actif"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Bonnet",
|
"name": "Nénuphar Géant",
|
||||||
"price": 150,
|
"price": 150,
|
||||||
"incrementValue": 10,
|
"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": "/",
|
"link": "/",
|
||||||
"image": "./svg/Bonnet.svg",
|
"image": "./svg/Bonnet.svg",
|
||||||
"buyed": false,
|
"buyed": false,
|
||||||
"type": "passif"
|
"type": "passif"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Cookie",
|
"name": "Oeuf Doré",
|
||||||
"price": 1500,
|
"price": 1500,
|
||||||
"incrementValue": 100,
|
"incrementValue": 100,
|
||||||
"description": "Fait avec amour",
|
"description": "Un oeuf rare qui éclot en masse. +100 par clic.",
|
||||||
"link": "/",
|
"link": "/",
|
||||||
"image": "./svg/Cookie.svg",
|
"image": "./svg/Cookie.svg",
|
||||||
"buyed": false,
|
"buyed": false,
|
||||||
"type": "actif"
|
"type": "actif"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Canne en sucre",
|
"name": "Mousse Lumineuse",
|
||||||
"price": 1500,
|
"price": 1500,
|
||||||
"incrementValue": 100,
|
"incrementValue": 100,
|
||||||
"description": "Le sucre c'est connu, ca reboost",
|
"description": "La mousse phosphorescente accélère la croissance. +100 têtards/s.",
|
||||||
"link": "/",
|
"link": "/",
|
||||||
"image": "./svg/Canne.svg",
|
"image": "./svg/Canne.svg",
|
||||||
"buyed": false,
|
"buyed": false,
|
||||||
"type": "passif"
|
"type": "passif"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Couronne d'hiver",
|
"name": "Couronne de Roseaux",
|
||||||
"price": 15000,
|
"price": 15000,
|
||||||
"incrementValue": 1000,
|
"incrementValue": 1000,
|
||||||
"description": "Un bisous ou rien du tout !",
|
"description": "Le symbole du Gardien suprême du Marais. +1000 par clic.",
|
||||||
"link": "/",
|
"link": "/",
|
||||||
"image": "./svg/Courone.svg",
|
"image": "./svg/Courone.svg",
|
||||||
"buyed": false,
|
"buyed": false,
|
||||||
"type": "actif"
|
"type": "actif"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Mr pain d'épice",
|
"name": "Esprit du Marais",
|
||||||
"price": 15000,
|
"price": 15000,
|
||||||
"incrementValue": 1000,
|
"incrementValue": 1000,
|
||||||
"description": "Le meilleur c'est la tête",
|
"description": "L'esprit ancestral bénit les eaux. +1000 têtards/s.",
|
||||||
"link": "/",
|
"link": "/",
|
||||||
"image": "./svg/PainDep.svg",
|
"image": "./svg/PainDep.svg",
|
||||||
"buyed": false,
|
"buyed": false,
|
||||||
"type": "passif"
|
"type": "passif"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Bière",
|
"name": "Nectar de Lotus",
|
||||||
"price": 8000,
|
"price": 8000,
|
||||||
"incrementValue": 1000,
|
"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": "/",
|
"link": "/",
|
||||||
"image": "./svg/Beer.svg",
|
"image": "./svg/Beer.svg",
|
||||||
"buyed": false,
|
"buyed": false,
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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 };
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import Landing from "./pages/Landing";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import ErrorPage from "./pages/404";
|
import ErrorPage from "./pages/404";
|
||||||
import { WildCoinProvider } from "./components/WildCoin/WildCoinContext";
|
import Login from "./pages/Login";
|
||||||
import Ameliorations from "./components/WildCoin/Amelioration";
|
import AuthCallback from "./pages/AuthCallback";
|
||||||
|
import { AuthProvider } from "./context/AuthContext";
|
||||||
import Boutique from "./pages/Boutique";
|
import Boutique from "./pages/Boutique";
|
||||||
import Achievements from "./pages/Achievements";
|
import Achievements from "./pages/Achievements";
|
||||||
import Legal from "./pages/Legal";
|
import Legal from "./pages/Legal";
|
||||||
@@ -17,16 +19,12 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
element: <Landing />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/jeu",
|
||||||
element: <Home />,
|
element: <Home />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "*",
|
|
||||||
element: <ErrorPage />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/ameliorations",
|
|
||||||
element: <Ameliorations />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/boutique",
|
path: "/boutique",
|
||||||
element: <Boutique />,
|
element: <Boutique />,
|
||||||
@@ -43,6 +41,18 @@ const router = createBrowserRouter([
|
|||||||
path: "/cookies",
|
path: "/cookies",
|
||||||
element: <Cookie />,
|
element: <Cookie />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
element: <Login />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/callback",
|
||||||
|
element: <AuthCallback />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "*",
|
||||||
|
element: <ErrorPage />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -50,7 +60,7 @@ const router = createBrowserRouter([
|
|||||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<WildCoinProvider>
|
<AuthProvider>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</WildCoinProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import AchievementsCard from "../components/AchievementsCard";
|
import AchievementsCard from "../components/AchievementsCard";
|
||||||
import "../scss/achievements.scss";
|
import "../scss/achievements.scss";
|
||||||
import { useWildCoin } from "../components/WildCoin/WildCoinContext";
|
import { useGameStore } from "../store/useGameStore";
|
||||||
import achievements from "../data/Achievements.json";
|
import achievements from "../data/Achievements.json";
|
||||||
|
|
||||||
function Achievements() {
|
function Achievements() {
|
||||||
const { wildCoin } = useWildCoin();
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
let score = 1;
|
let score = 1;
|
||||||
if (wildCoin >= 25) {
|
if (resources >= 25) {
|
||||||
score = Math.floor((wildCoin - 25) / 400) + 1;
|
score = Math.floor((resources - 25) / 400) + 1;
|
||||||
} else {
|
} else {
|
||||||
score = 0;
|
score = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
46
Frontend/src/pages/AuthCallback.jsx
Normal file
46
Frontend/src/pages/AuthCallback.jsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useSearchParams, Link } from "react-router-dom";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import "../scss/pages.scss";
|
||||||
|
|
||||||
|
export default function AuthCallback() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { loginWithOAuth } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
setError("Token manquant dans l'URL.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loginWithOAuth(token)
|
||||||
|
.then(() => navigate("/", { replace: true }))
|
||||||
|
.catch((err) => setError(err.message || "Erreur de connexion."));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="containererror">
|
||||||
|
<h1>Erreur de connexion</h1>
|
||||||
|
<p className="message">{error}</p>
|
||||||
|
<Link className="btn-return" to="/login">
|
||||||
|
Retour au login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="containererror">
|
||||||
|
<p className="message">Connexion en cours...</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import BoutiqueCard from "../components/BoutiqueCard";
|
import BoutiqueCard from "../components/BoutiqueCard";
|
||||||
// import { useWildCoin } from "..components/WildCoin/WildCoinContext";
|
|
||||||
import "../scss/shop.scss";
|
import "../scss/shop.scss";
|
||||||
import shop from "../data/shop";
|
import shop from "../data/shop";
|
||||||
|
|
||||||
|
|||||||
@@ -3,32 +3,32 @@ function Cookie() {
|
|||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="item">
|
<div className="item">
|
||||||
<h2>Qu’est-ce qu’un cookie ?</h2>
|
<h2>Qu'est-ce qu'un cookie ?</h2>
|
||||||
<p>
|
<p>
|
||||||
Un cookie est un petit fichier texte sauvegardé sur votre ordinateur
|
Un cookie est un petit fichier texte sauvegardé sur votre ordinateur
|
||||||
lorsque vous visitez un site web. Ce fichier texte enregistre des
|
lorsque vous visitez un site web. Ce fichier texte enregistre des
|
||||||
informations qui peuvent être lues par un site web lorsque vous le
|
informations qui peuvent être lues par un site web lorsque vous le
|
||||||
visitez de nouveau plus tard. Certains de ces cookies sont nécessaires
|
visitez de nouveau plus tard. Certains de ces cookies sont nécessaires
|
||||||
pour accéder à certaines fonctionnalités d’un site. D’autres cookies
|
pour accéder à certaines fonctionnalités d'un site. D'autres cookies
|
||||||
sont d’utilité pratique pour le visiteur : ils sauvegardent de manière
|
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
|
sécurisée votre nom d'utilisateur ou vos préférences linguistiques par
|
||||||
exemple. Les cookies signifient tout simplement qu’à chaque fois que
|
exemple. Les cookies signifient tout simplement qu'à chaque fois que
|
||||||
vous visitez un site web, vous n’avez pas besoin de saisir à nouveau
|
vous visitez un site web, vous n'avez pas besoin de saisir à nouveau
|
||||||
les mêmes informations.
|
les mêmes informations.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="item">
|
<div className="item">
|
||||||
<h2>Pourquoi Xmass Clicker utilise des cookies ?</h2>
|
<h2>Pourquoi Clickerz utilise des cookies ?</h2>
|
||||||
<p>
|
<p>
|
||||||
Nous utilisons des cookies pour vous fournir une expérience
|
Nous utilisons des cookies pour vous fournir une expérience
|
||||||
utilisateur optimale et adaptée à vos préférences personnelles. En
|
utilisateur optimale et adaptée à vos préférences personnelles.
|
||||||
utilisant les cookies, Les cookies sont également utilisés pour
|
Les cookies sont également utilisés pour optimiser la performance
|
||||||
optimiser la performance du site. Xmass Clicker a pris toutes les
|
du site. Clickerz a pris toutes les mesures organisationnelles et
|
||||||
mesures organisationnelles et techniques pour protéger vos données
|
techniques pour protéger vos données personnelles ainsi que d'une
|
||||||
personnelles ainsi que d’une éventuelle perte d’informations ou de
|
éventuelle perte d'informations ou de toute forme de traitement
|
||||||
toute forme de traitement illicite. Pour davantage d’informations,
|
illicite. Pour davantage d'informations, consultez notre Politique
|
||||||
consultez notre Politique de confidentialité.
|
de confidentialité.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ function Cookie() {
|
|||||||
<p>
|
<p>
|
||||||
Vous pouvez paramétrer votre navigateur Internet pour désactiver les
|
Vous pouvez paramétrer votre navigateur Internet pour désactiver les
|
||||||
cookies. Notez toutefois que si vous désactivez les cookies, votre nom
|
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.
|
sur aucun site web.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,9 +49,9 @@ function Cookie() {
|
|||||||
2. Appuyez sur la touche « Alt » <br />
|
2. Appuyez sur la touche « Alt » <br />
|
||||||
3. Dans le menu en haut de la page cliquez sur « Outils » puis «
|
3. Dans le menu en haut de la page cliquez sur « Outils » puis «
|
||||||
Options » <br />
|
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 »,
|
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 />
|
» <br />
|
||||||
6. Un peu plus bas, décochez « Accepter les cookies » <br />
|
6. Un peu plus bas, décochez « Accepter les cookies » <br />
|
||||||
7. Sauvegardez vos préférences en cliquant sur « OK »
|
7. Sauvegardez vos préférences en cliquant sur « OK »
|
||||||
@@ -63,7 +63,7 @@ function Cookie() {
|
|||||||
<p>
|
<p>
|
||||||
1. Ouvrez Internet Explorer <br />
|
1. Ouvrez Internet Explorer <br />
|
||||||
2. Dans le menu « Outils », sélectionnez « Options Internet » <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 />
|
4. Cliquez sur « Avancé » et décochez « Accepter » <br />
|
||||||
5. Sauvegardez vos préférences en cliquant sur « OK »
|
5. Sauvegardez vos préférences en cliquant sur « OK »
|
||||||
</p>
|
</p>
|
||||||
@@ -75,7 +75,7 @@ function Cookie() {
|
|||||||
1. Ouvrez Safari <br />
|
1. Ouvrez Safari <br />
|
||||||
2. Dans la barre de menu en haut, cliquez sur « Safari », puis «
|
2. Dans la barre de menu en haut, cliquez sur « Safari », puis «
|
||||||
Préférences » <br />
|
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 />
|
4. À côté de « Accepter les cookies », cochez « Jamais » <br />
|
||||||
5. Si vous souhaitez voir les cookies qui sont déjà sauvegardés sur
|
5. Si vous souhaitez voir les cookies qui sont déjà sauvegardés sur
|
||||||
votre ordinateur, cliquez sur « Afficher les cookies »
|
votre ordinateur, cliquez sur « Afficher les cookies »
|
||||||
@@ -86,9 +86,9 @@ function Cookie() {
|
|||||||
<h2>Google Chrome :</h2>
|
<h2>Google Chrome :</h2>
|
||||||
<p>
|
<p>
|
||||||
1. Ouvrez Google Chrome <br />
|
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 />
|
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 «
|
5. Dans le menu déroulant « Paramètres des cookies », sélectionnez «
|
||||||
Bloquer tous les cookies »
|
Bloquer tous les cookies »
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,231 +1,131 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import PropTypes from "prop-types";
|
import { useEffect, useCallback } from "react";
|
||||||
import "../scss/home.scss";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
import { useWildCoin } from "../components/WildCoin/WildCoinContext";
|
import { useGameStore } from "../store/useGameStore";
|
||||||
|
import { formatNumber } from "../utils/formatNumber";
|
||||||
|
import { GeneratorShop } from "../components/GeneratorShop";
|
||||||
|
import { PrestigePanel } from "../components/PrestigePanel";
|
||||||
|
import { EvolutionTree } from "../components/EvolutionTree";
|
||||||
|
import { MilestoneBar } from "../components/MilestoneBar";
|
||||||
|
import "../scss/home.scss";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [toggleSnow, setToggleSnow] = useOutletContext();
|
const [toggleRain] = useOutletContext();
|
||||||
Home.propTypes = {
|
const click = useGameStore((s) => s.click);
|
||||||
setToggleSnow: PropTypes.function,
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
toggleSnow: PropTypes.bool,
|
const clickMultiplier = useGameStore((s) => s.state.clickMultiplier);
|
||||||
}.isRequired;
|
|
||||||
|
|
||||||
const { biere, setBiere, santaDrunk, setSantaDrunk } = useWildCoin();
|
const createParticle = useCallback((clientX, clientY) => {
|
||||||
|
const particle = document.createElement("span");
|
||||||
|
particle.className = "click-particle";
|
||||||
|
particle.textContent = `+${formatNumber(clickMultiplier)}`;
|
||||||
|
particle.style.left = `${clientX}px`;
|
||||||
|
particle.style.top = `${clientY}px`;
|
||||||
|
document.body.appendChild(particle);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (particle.parentNode) particle.parentNode.removeChild(particle);
|
||||||
|
}, 800);
|
||||||
|
}, [clickMultiplier]);
|
||||||
|
|
||||||
var snow = {
|
const handleIncrement = useCallback((e) => {
|
||||||
wind: 0,
|
click();
|
||||||
maxXrange: 40,
|
createParticle(e.clientX, e.clientY);
|
||||||
minXrange: 20,
|
}, [click, createParticle]);
|
||||||
maxSpeed: 1,
|
|
||||||
minSpeed: 3,
|
|
||||||
color: "#fff",
|
|
||||||
char: "*",
|
|
||||||
maxSize: 32,
|
|
||||||
minSize: 10,
|
|
||||||
|
|
||||||
flakes: [],
|
// Rain effect (ambiance)
|
||||||
WIDTH: -10,
|
useEffect(() => {
|
||||||
HEIGHT: 0,
|
const rain = {
|
||||||
|
wind: 0, maxXrange: 40, minXrange: 20, maxSpeed: 1, minSpeed: 3,
|
||||||
init: function (nb) {
|
color: "#8ecae6", char: "~", maxSize: 32, minSize: 10,
|
||||||
var o = this,
|
flakes: [], WIDTH: -10, HEIGHT: 0, running: false,
|
||||||
frag = document.createDocumentFragment();
|
init(nb) {
|
||||||
o.getSize();
|
const frag = document.createDocumentFragment();
|
||||||
|
this.getSize();
|
||||||
for (var i = 0; i < nb; i++) {
|
this.running = true;
|
||||||
var flake = {
|
for (let i = 0; i < nb; i++) {
|
||||||
x: o.random(o.WIDTH),
|
const flake = {
|
||||||
y: -o.maxSize,
|
x: this.random(this.WIDTH), y: -this.maxSize,
|
||||||
xrange: o.minXrange + o.random(o.maxXrange - o.minXrange),
|
xrange: this.minXrange + this.random(this.maxXrange - this.minXrange),
|
||||||
yspeed: o.minSpeed + o.random(o.maxSpeed - o.minSpeed, 100),
|
yspeed: this.minSpeed + this.random(this.maxSpeed - this.minSpeed, 100),
|
||||||
life: 0,
|
life: 0, size: this.minSize + this.random(this.maxSize - this.minSize),
|
||||||
size: o.minSize + o.random(o.maxSize - o.minSize),
|
|
||||||
html: document.createElement("span"),
|
html: document.createElement("span"),
|
||||||
};
|
};
|
||||||
|
Object.assign(flake.html.style, {
|
||||||
flake.html.style.position = "absolute";
|
position: "absolute", top: `${flake.y}px`, left: `${flake.x}px`,
|
||||||
flake.html.style.top = flake.y + "px";
|
fontSize: `${flake.size}px`, color: this.color, userSelect: "none", overflow: "hidden",
|
||||||
flake.html.style.left = flake.x + "px";
|
});
|
||||||
flake.html.style.fontSize = flake.size + "px";
|
flake.html.appendChild(document.createTextNode(this.char));
|
||||||
flake.html.style.color = o.color;
|
|
||||||
flake.html.appendChild(document.createTextNode(o.char));
|
|
||||||
frag.appendChild(flake.html);
|
frag.appendChild(flake.html);
|
||||||
flake.html.style.userSelect = "none";
|
this.flakes.push(flake);
|
||||||
flake.html.style.overflow = "hidden";
|
|
||||||
o.flakes.push(flake);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.appendChild(frag);
|
document.body.appendChild(frag);
|
||||||
o.animate();
|
this.animate();
|
||||||
|
window.onresize = () => this.getSize();
|
||||||
window.onresize = function () {
|
|
||||||
o.getSize();
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
animate() {
|
||||||
animate: function () {
|
if (!this.running) return;
|
||||||
var o = this;
|
for (const flake of this.flakes) {
|
||||||
for (var i = 0, c = o.flakes.length; i < c; i++) {
|
const top = flake.y + flake.yspeed;
|
||||||
var flake = o.flakes[i],
|
const left = flake.x + Math.sin(flake.life) * flake.xrange + this.wind;
|
||||||
top = flake.y + flake.yspeed,
|
if (top < this.HEIGHT - flake.size - 10 && left < this.WIDTH - flake.size && left > 0) {
|
||||||
left = flake.x + Math.sin(flake.life) * flake.xrange + o.wind;
|
flake.html.style.top = `${top}px`;
|
||||||
if (
|
flake.html.style.left = `${left}px`;
|
||||||
top < o.HEIGHT - flake.size - 10 &&
|
|
||||||
left < o.WIDTH - flake.size &&
|
|
||||||
left > 0
|
|
||||||
) {
|
|
||||||
flake.html.style.top = top + "px";
|
|
||||||
flake.html.style.left = left + "px";
|
|
||||||
flake.y = top;
|
flake.y = top;
|
||||||
flake.x += o.wind;
|
flake.x += this.wind;
|
||||||
flake.life += 0.01;
|
flake.life += 0.01;
|
||||||
} else {
|
} else {
|
||||||
flake.html.style.top = -o.maxSize + "px";
|
flake.html.style.top = `${-this.maxSize}px`;
|
||||||
flake.x = o.random(o.WIDTH);
|
flake.x = this.random(this.WIDTH);
|
||||||
flake.y = -o.maxSize;
|
flake.y = -this.maxSize;
|
||||||
flake.html.style.left = flake.x + "px";
|
flake.html.style.left = `${flake.x}px`;
|
||||||
flake.life = 0;
|
flake.life = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTimeout(function () {
|
setTimeout(() => this.animate(), 20);
|
||||||
o.animate();
|
|
||||||
}, 20);
|
|
||||||
},
|
},
|
||||||
|
stop() {
|
||||||
stop: function () {
|
this.running = false;
|
||||||
for (var i = 0, c = this.flakes.length; i < c; i++) {
|
for (const flake of this.flakes) {
|
||||||
document.body.removeChild(this.flakes[i].html);
|
if (flake.html.parentNode) flake.html.parentNode.removeChild(flake.html);
|
||||||
}
|
}
|
||||||
this.flakes = [];
|
this.flakes = [];
|
||||||
},
|
},
|
||||||
|
random(range, num = 1) {
|
||||||
random: function (range, num) {
|
|
||||||
num = num ? num : 1;
|
|
||||||
return Math.floor(Math.random() * (range + 1) * num) / num;
|
return Math.floor(Math.random() * (range + 1) * num) / num;
|
||||||
},
|
},
|
||||||
|
getSize() {
|
||||||
getSize: function () {
|
|
||||||
this.WIDTH = document.body.clientWidth || window.innerWidth;
|
this.WIDTH = document.body.clientWidth || window.innerWidth;
|
||||||
this.HEIGHT = document.body.clientHeight || window.innerHeight;
|
this.HEIGHT = document.body.clientHeight || window.innerHeight;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { incrementClick, incrementWildCoin } = useWildCoin();
|
if (toggleRain) rain.init(10);
|
||||||
|
return () => rain.stop();
|
||||||
const createParticle = (x, y) => {
|
}, [toggleRain]);
|
||||||
const cookieClicks = document.querySelector(".pieces");
|
|
||||||
|
|
||||||
const particle = document.createElement("a");
|
|
||||||
particle.style.backgroundImage = "url('/png/w-coin.png')";
|
|
||||||
particle.setAttribute("class", "pieces-particle");
|
|
||||||
particle.style.left = x + "%";
|
|
||||||
particle.style.bottom = y + "px";
|
|
||||||
|
|
||||||
cookieClicks.appendChild(particle);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
cookieClicks.removeChild(particle);
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleIncrement = () => {
|
|
||||||
incrementWildCoin(incrementClick);
|
|
||||||
createParticle(50, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (toggleSnow) {
|
|
||||||
snow.init(10);
|
|
||||||
} else {
|
|
||||||
snow.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
snow.stop();
|
|
||||||
};
|
|
||||||
}, [toggleSnow]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const main = document.querySelector(".bghomecover");
|
|
||||||
const santa = document.querySelector(".santaclaus");
|
|
||||||
if (main !== undefined) {
|
|
||||||
if (biere[1] >= 1) {
|
|
||||||
santa.style.background = `url("/svg/SantaClause-drink.svg")`;
|
|
||||||
if (santaDrunk === true) {
|
|
||||||
main.style.filter = `blur(${biere[1]}px)`;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
console.count("setTimeOut");
|
|
||||||
main.style.filter = `blur(0px)`;
|
|
||||||
setSantaDrunk(false);
|
|
||||||
}, biere[1] * 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [biere, setBiere]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="bghomecover">
|
<main className="game-cover">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<meta
|
<meta name="description" content="Clickerz — Clicker idle dans le Tetard Universe." />
|
||||||
name="description"
|
<title>Clickerz — Tetard Universe</title>
|
||||||
content="Xmass Click votre nouveau Clicker préféré !"
|
|
||||||
/>
|
|
||||||
<meta name="robots" content="index, follow" />
|
|
||||||
<meta
|
|
||||||
name="googlebot"
|
|
||||||
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
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" />
|
|
||||||
<meta property="og:locale" content="fr_FR" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:title" content="mywebsite | title" />
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
<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:description"
|
|
||||||
content="Xmass Click votre nouveau Clicker préféré !"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="twitter:image"
|
|
||||||
content="https://xmass.click/webp/share-cover.webp"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<title>Xmass Click</title>
|
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="santaposition">
|
|
||||||
<div className="pieces" />
|
{/* Clicker area — centre */}
|
||||||
<div className="santaclaus" onClick={handleIncrement} />
|
<div className="click-zone" onClick={handleIncrement}>
|
||||||
|
<div className="tadpole-sprite" />
|
||||||
|
<div className="text-center text-3xl md:text-4xl font-bold text-white drop-shadow-lg font-[var(--font)] select-none pointer-events-none">
|
||||||
|
{formatNumber(resources)}
|
||||||
</div>
|
</div>
|
||||||
<div className="boostList"></div>
|
</div>
|
||||||
|
|
||||||
|
{/* Game panels — sidebar (right desktop, bottom mobile) */}
|
||||||
|
<aside className="game-sidebar">
|
||||||
|
<MilestoneBar />
|
||||||
|
<GeneratorShop />
|
||||||
|
<PrestigePanel />
|
||||||
|
<EvolutionTree />
|
||||||
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
37
Frontend/src/pages/Landing.jsx
Normal file
37
Frontend/src/pages/Landing.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function Landing() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Clickerz — Tetard Universe</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Clickerz — Clicker idle dans le Tetard Universe. Fais éclore des têtards, évolue et domine le marais !"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
<main className="min-h-[92vh] mt-20 flex flex-col items-center justify-center gap-8 bg-[var(--bg-color)]">
|
||||||
|
<div className="flex flex-col items-center gap-4 text-center px-4">
|
||||||
|
<h1 className="text-4xl md:text-6xl font-bold text-gray-800 font-[var(--font)]">
|
||||||
|
Clickerz
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg md:text-xl text-gray-600 font-[var(--font)] max-w-md">
|
||||||
|
Fais éclore des têtards, construis ton empire et domine le marais.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/jeu"
|
||||||
|
className="px-8 py-4 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-lg font-semibold font-[var(--font)] transition-all hover:scale-105 shadow-lg shadow-emerald-600/30"
|
||||||
|
>
|
||||||
|
Entrer dans le Marais
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-500 font-[var(--font)]">
|
||||||
|
Pas de compte requis — joue en mode invité
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,20 +4,18 @@ function Legal() {
|
|||||||
<div className="mentionslegales">
|
<div className="mentionslegales">
|
||||||
<h2>Éditeur :</h2>
|
<h2>Éditeur :</h2>
|
||||||
<p>
|
<p>
|
||||||
Xmass'Click est un projet réalisé dans le cadre d'un hackathon sur 2
|
Clickerz est un projet indépendant faisant partie du Tetard Universe.
|
||||||
jours.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Coordonnées :</h2>
|
<h2>Coordonnées :</h2>
|
||||||
<p>
|
<p>
|
||||||
Téléphone : 04 22 52 10 10 <br />
|
E-mail : contact@tetardtek.com <br />
|
||||||
E-mail : pere-noel@laposte.net <br />
|
Site : https://tetardtek.com <br />
|
||||||
Adresse : 250 avenue des Nuages, 1000 Pôle Nord <br />
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Responsabilité :</h2>
|
<h2>Responsabilité :</h2>
|
||||||
<p>
|
<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
|
Les informations fournies sont à titre informatif et peuvent être
|
||||||
sujettes à des erreurs.
|
sujettes à des erreurs.
|
||||||
</p>
|
</p>
|
||||||
@@ -25,26 +23,26 @@ function Legal() {
|
|||||||
<h2>Propriété Intellectuelle :</h2>
|
<h2>Propriété Intellectuelle :</h2>
|
||||||
<p>
|
<p>
|
||||||
Tout le contenu du site (textes, images, etc.) reste la propriété de
|
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.
|
préalable.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Protection des Données Personnelles :</h2>
|
<h2>Protection des Données Personnelles :</h2>
|
||||||
<p>
|
<p>
|
||||||
Xmass'Click ne collecte pas de données personnelles. Aucune information
|
Clickerz utilise un système d'authentification via SuperOAuth.
|
||||||
personnelle n'est stockée lors de l'utilisation du site.
|
Les données de jeu sont sauvegardées côté serveur.
|
||||||
|
Aucune donnée personnelle n'est partagée avec des tiers.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Conditions Générales d'Utilisation :</h2>
|
<h2>Conditions Générales d'Utilisation :</h2>
|
||||||
<p>
|
<p>
|
||||||
Aucune condition générale d'utilisation n'est applicable. L'utilisation
|
L'utilisation du site Clickerz se fait à titre gratuit et sans engagement.
|
||||||
du site Xmass'Click se fait à titre gratuit et sans engagement.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Loi Applicable :</h2>
|
<h2>Loi Applicable :</h2>
|
||||||
<p>
|
<p>
|
||||||
Le présent site est régi par la loi du Pôle Nord. En cas de litige, les
|
Le présent site est régi par la loi française. En cas de litige, les
|
||||||
tribunaux du Père Noël seront compétents.
|
tribunaux compétents seront ceux du ressort du siège social de l'éditeur.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
32
Frontend/src/pages/Login.jsx
Normal file
32
Frontend/src/pages/Login.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import "../scss/pages.scss";
|
||||||
|
|
||||||
|
const SUPEROAUTH_URL = import.meta.env.VITE_SUPEROAUTH_URL;
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) navigate("/", { replace: true });
|
||||||
|
}, [user, navigate]);
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
const callbackUrl = `${window.location.origin}/callback`;
|
||||||
|
window.location.href = `${SUPEROAUTH_URL}/api/v1/oauth/discord?redirectUrl=${encodeURIComponent(callbackUrl)}&tenantId=clickerz`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="containererror">
|
||||||
|
<h1>Connexion</h1>
|
||||||
|
<p className="message">Connecte-toi pour sauvegarder ta progression.</p>
|
||||||
|
<button className="btn-return" onClick={handleLogin} type="button">
|
||||||
|
Se connecter avec SuperOAuth
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wildCoin {
|
.resource-counter {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
@@ -282,3 +282,33 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-grey);
|
color: var(--color-grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
font-family: var(--font);
|
||||||
|
|
||||||
|
.auth-nickname {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn {
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
border: 1px solid var(--color-grey);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: none;
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-grey);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-grey);
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,58 +1,105 @@
|
|||||||
.bghomecover {
|
// home.scss — Game view styles
|
||||||
|
|
||||||
|
.game-cover {
|
||||||
background-image: url("/webp/bg-cover.webp");
|
background-image: url("/webp/bg-cover.webp");
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: bottom;
|
background-position: bottom;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
filter: blur(0px);
|
min-height: 92vh;
|
||||||
transition: filter 1s ease-in-out;
|
position: relative;
|
||||||
}
|
|
||||||
.santaposition {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: end;
|
}
|
||||||
|
|
||||||
.santaclaus {
|
// --- Clicker zone ---
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 5vh;
|
|
||||||
|
|
||||||
min-width: 320px;
|
.click-zone {
|
||||||
width: 320px;
|
display: flex;
|
||||||
min-height: 320px;
|
flex-direction: column;
|
||||||
height: 320px;
|
align-items: center;
|
||||||
z-index: 1;
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
background: url("/svg/SantaClause-bag.svg");
|
padding-bottom: 2vh;
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
&:active {
|
// Desktop: center
|
||||||
transform: rotate(2deg);
|
@media (min-width: 768px) {
|
||||||
}
|
padding-right: 22rem; // offset for sidebar
|
||||||
}
|
|
||||||
|
|
||||||
.pieces-particle {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
animation: pieces-up 1.5s linear forwards;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@keyframes pieces-up {
|
|
||||||
|
.tadpole-sprite {
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
background: url("/svg/tadpole.svg") no-repeat center / contain;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: 320px;
|
||||||
|
height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-zone:active & {
|
||||||
|
transform: scale(0.95) rotate(2deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Click feedback particle ---
|
||||||
|
|
||||||
|
.click-particle {
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #34d399;
|
||||||
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 100;
|
||||||
|
animation: float-up 0.8s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-up {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: rotate3d(0, 1, 0, 180deg);
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
bottom: 100%;
|
transform: translateY(-60px) scale(1.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Game sidebar ---
|
||||||
|
|
||||||
|
.game-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 5.5rem;
|
||||||
|
bottom: 0.75rem;
|
||||||
|
width: 20rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 10;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
|
||||||
|
// Mobile: bottom drawer
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 45vh;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-top-left-radius: 1rem;
|
||||||
|
border-top-right-radius: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
146
Frontend/src/store/useGameStore.ts
Normal file
146
Frontend/src/store/useGameStore.ts
Normal 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,
|
||||||
|
}));
|
||||||
9
Frontend/src/utils/formatNumber.ts
Normal file
9
Frontend/src/utils/formatNumber.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// formatNumber.ts — Affichage formaté des grands nombres
|
||||||
|
|
||||||
|
export function formatNumber(n: number): string {
|
||||||
|
if (n >= 1e12) return `${(n / 1e12).toFixed(2)}T`;
|
||||||
|
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
||||||
|
if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
|
||||||
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
||||||
|
return Math.floor(n).toString();
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [tailwindcss(), react()],
|
||||||
test: {
|
test: {
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
},
|
},
|
||||||
|
|||||||
44
deploy/clickerz.tetardtek.com.conf
Normal file
44
deploy/clickerz.tetardtek.com.conf
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Apache vhost — clickerz.tetardtek.com
|
||||||
|
# Frontend: static build served from /var/www/clickerz
|
||||||
|
# Backend API: reverse proxy to pm2 on port 3310
|
||||||
|
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName clickerz.tetardtek.com
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName clickerz.tetardtek.com
|
||||||
|
|
||||||
|
# SSL (certbot)
|
||||||
|
SSLEngine on
|
||||||
|
SSLCertificateFile /etc/letsencrypt/live/clickerz.tetardtek.com/fullchain.pem
|
||||||
|
SSLCertificateKeyFile /etc/letsencrypt/live/clickerz.tetardtek.com/privkey.pem
|
||||||
|
|
||||||
|
# Frontend — SPA static files
|
||||||
|
DocumentRoot /var/www/clickerz
|
||||||
|
<Directory /var/www/clickerz>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
# SPA fallback — all non-file routes → index.html
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteBase /
|
||||||
|
RewriteRule ^index\.html$ - [L]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule . /index.html [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Backend API — reverse proxy
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass /api http://127.0.0.1:3310/api
|
||||||
|
ProxyPassReverse /api http://127.0.0.1:3310/api
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
Header always set X-Frame-Options "DENY"
|
||||||
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
</VirtualHost>
|
||||||
39
deploy/deploy.sh
Executable file
39
deploy/deploy.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# deploy.sh — Build & deploy clickerz to VPS
|
||||||
|
# Usage: ssh vps 'cd /opt/clickerz && bash deploy/deploy.sh'
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "=== Clickerz deploy ==="
|
||||||
|
|
||||||
|
# 1. Pull latest
|
||||||
|
git pull --ff-only
|
||||||
|
|
||||||
|
# 2. Build frontend
|
||||||
|
echo "--- Building frontend..."
|
||||||
|
cd Frontend
|
||||||
|
npm ci --production=false
|
||||||
|
npm run build
|
||||||
|
echo "--- Copying dist to /var/www/clickerz..."
|
||||||
|
sudo rm -rf /var/www/clickerz
|
||||||
|
sudo cp -r dist /var/www/clickerz
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 3. Backend deps
|
||||||
|
echo "--- Installing backend deps..."
|
||||||
|
cd Backend
|
||||||
|
npm ci --production
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 4. Run migrations
|
||||||
|
echo "--- Running DB migrations..."
|
||||||
|
cd Backend
|
||||||
|
npm run db:migrate
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 5. Restart pm2
|
||||||
|
echo "--- Restarting pm2..."
|
||||||
|
pm2 startOrRestart ecosystem.config.cjs --env production
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
echo "=== Deploy complete ==="
|
||||||
192
docs/GDD.md
192
docs/GDD.md
@@ -1,76 +1,188 @@
|
|||||||
# Clickerz — GDD Minimal
|
# Clickerz — GDD (Game Design Document)
|
||||||
|
|
||||||
> Sprint 1 — Step 1 output
|
> Tetard Universe — Clicker/idle hybride
|
||||||
> Date : 2026-03-17
|
> Dernière mise à jour : 2026-03-20
|
||||||
|
> Repo : git.tetardtek.com/Tetardtek/clickerz
|
||||||
|
> URL : https://clickerz.tetardtek.com/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Identité
|
||||||
|
|
||||||
|
**Clickerz** — Clicker/idle hybride dans le Tetard Universe.
|
||||||
|
Un marais mystérieux où des têtards naissent, évoluent, mutent et bâtissent un écosystème.
|
||||||
|
Chaque prestige = une nouvelle "génération" de têtards, plus forte, plus étrange.
|
||||||
|
|
||||||
|
**Archétype :** hybride — clicker au départ (onboarding immédiat), idle dominant en mid-game (rétention), couche stratégique en late-game (prestige + arbre permanent).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Univers — Tetard Universe
|
||||||
|
|
||||||
|
Le joueur est le **Gardien du Marais**. Les têtards sont la ressource vivante — ils naissent, travaillent, évoluent. Le marais grandit, se diversifie, attire de nouvelles espèces.
|
||||||
|
|
||||||
|
Cross-promo naturelle avec TetaRdPG (même univers, assets partageables à terme).
|
||||||
|
|
||||||
|
### Vocabulaire in-game
|
||||||
|
|
||||||
|
| Concept mécanique | Nom in-game | Icône |
|
||||||
|
|-------------------|-------------|-------|
|
||||||
|
| Ressource principale | **Têtards** | 🥚 |
|
||||||
|
| Click | **Ponte** | tap/click |
|
||||||
|
| Générateurs tier 1 | **Nid** | 🪹 |
|
||||||
|
| Générateurs tier 2 | **Mare** | 🌊 |
|
||||||
|
| Générateurs tier 3 | **Marécage** | 🏞️ |
|
||||||
|
| Générateurs tier 4 | **Étang Ancien** | 🏛️ |
|
||||||
|
| Générateurs tier 5 | **Lac Mystique** | ✨ |
|
||||||
|
| Prestige currency | **ADN Ancestral** | 🧬 |
|
||||||
|
| Prestige reset | **Nouvelle Génération** | 🔄🐸 |
|
||||||
|
| Arbre permanent | **Arbre d'Évolution** | 🌳 |
|
||||||
|
| Milestones | **Mutations** | 🧪 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Stack technique
|
## Stack technique
|
||||||
|
|
||||||
**React + TypeScript + Vite** — vanilla, sans moteur de jeu
|
| Couche | Techno | Justification |
|
||||||
|
|--------|--------|---------------|
|
||||||
Justification : prototype existant déjà en React/Vite (Xmass Clicker), shop.json et Achievements.json validés, backend Node.js en place. Phaser/PixiJS = overhead injustifié pour un clicker — la logique est dans les chiffres, pas dans le rendu.
|
| Frontend | React 18 + TypeScript + Vite | Existant validé, lazy calc pattern |
|
||||||
|
| State | Zustand | Game loop adapté, léger |
|
||||||
|
| Style | Tailwind CSS (migration SCSS → Tailwind) | Productivité, cohérence |
|
||||||
|
| Backend | Express + MySQL | Existant avec SuperOAuth câblé |
|
||||||
|
| Auth | SuperOAuth (clickerz.tetardtek.com) | SSO Tetardtek ecosystem |
|
||||||
|
| Tests | Vitest | 13 tests existants sur economy.ts |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mécaniques core
|
## Mécaniques core
|
||||||
|
|
||||||
**Ressource principale** : Cookies (ou ressource thématique à nommer)
|
### Ressource principale : Têtards
|
||||||
|
|
||||||
**Sources de production** :
|
**Sources de production :**
|
||||||
- Clic manuel : +1 ressource/clic (multiplicateur upgradable)
|
- Ponte (clic manuel) : +1 × clickMultiplier × prestigeMultiplier
|
||||||
- Générateurs idle : 5 tiers, coût `base × 1.15^n`, production `/s` cumulative
|
- Générateurs idle : 5 tiers, coût `base × 1.15^owned`, production/s cumulative
|
||||||
|
|
||||||
**Tiers upgrades** (structure shop.json existante — x10 de coût par tier validé) :
|
### Générateurs — progression tier
|
||||||
| Tier | Coût base | Production/s |
|
|
||||||
|------|-----------|-------------|
|
| Tier | Nom | Coût base | Production/s | Ratio scaling |
|
||||||
| 1 | 10 | 0.1 |
|
|------|-----|-----------|-------------|---------------|
|
||||||
| 2 | 100 | 0.5 |
|
| 1 | Nid | 10 | 0.1 | 1.15 |
|
||||||
| 3 | 1 000 | 3 |
|
| 2 | Mare | 100 | 0.5 | 1.15 |
|
||||||
| 4 | 10 000 | 20 |
|
| 3 | Marécage | 1 000 | 3 | 1.15 |
|
||||||
| 5 | 100 000 | 150 |
|
| 4 | Étang Ancien | 10 000 | 20 | 1.15 |
|
||||||
|
| 5 | Lac Mystique | 100 000 | 150 | 1.15 |
|
||||||
|
|
||||||
|
**Formule coût :** `coût = base_cost × 1.15^owned`
|
||||||
|
**Formule production :** `output = base_prod × owned × multipliers`
|
||||||
|
|
||||||
|
> Ratio 1.15 = standard éprouvé. Assez steep pour forcer la diversification, assez doux pour que chaque achat ait un impact.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Boucle de progression
|
## Boucle de progression — 3 couches
|
||||||
|
|
||||||
**Prestige / Reset**
|
```
|
||||||
- Seuil déclencheur : 1 000 000 ressources (ajustable à l'équilibrage)
|
┌─────────────────────────────────────────────────┐
|
||||||
- Récompense : +0.1× multiplicateur permanent par reset (stackable)
|
│ BOUCLE 1 — La Run (secondes → minutes) │
|
||||||
- Reset : ressources + générateurs à 0, upgrades prestige conservés
|
│ │
|
||||||
- Courbe : reset 1 = ×1.1, reset 10 = ×2.0, reset 50 = ×6.0
|
│ Ponte (click) → +têtards │
|
||||||
|
│ Acheter Nid → +têtards/sec (idle) │
|
||||||
|
│ Upgrades Nid → multiplicateur │
|
||||||
|
│ Débloquer Mare → nouveau tier, plus cher, │
|
||||||
|
│ plus rentable │
|
||||||
|
│ ...jusqu'à atteindre un plateau │
|
||||||
|
└──────────────────────┬──────────────────────────┘
|
||||||
|
│ plateau atteint
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ BOUCLE 2 — Nouvelle Génération (heures → jours) │
|
||||||
|
│ │
|
||||||
|
│ Prestige → reset générateurs + têtards │
|
||||||
|
│ Gain : ADN Ancestral (formule sur total têtards) │
|
||||||
|
│ ADN → Arbre d'Évolution (permanent) │
|
||||||
|
│ → +% production globale │
|
||||||
|
│ → débloquer nouvelles mutations │
|
||||||
|
│ → débloquer nouveaux types de générateurs │
|
||||||
|
│ Nouvelle run = plus rapide, va plus loin │
|
||||||
|
└──────────────────────┬──────────────────────────┘
|
||||||
|
│ arbre mature
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ BOUCLE 3 — Méta (semaines) [Sprint 3+] │
|
||||||
|
│ │
|
||||||
|
│ Espèces rares, événements saisonniers, │
|
||||||
|
│ leaderboard, objectifs communautaires │
|
||||||
|
│ (hors scope Sprint 1) │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
**Milestones visibles** : barre de progression vers prestige, compteur resets, multiplicateur actuel affiché
|
---
|
||||||
|
|
||||||
|
## Prestige — Nouvelle Génération
|
||||||
|
|
||||||
|
**Seuil :** 1 000 000 têtards produits (total lifetime de la run)
|
||||||
|
**Formule ADN :** `adn = floor(150 × sqrt(lifetime_tadpoles / 1e9))`
|
||||||
|
|
||||||
|
**Ce qui reset :** têtards, générateurs, upgrades de run
|
||||||
|
**Ce qui persiste :** ADN Ancestral, Arbre d'Évolution, achievements, stats
|
||||||
|
|
||||||
|
**Prestige actuel (hérité) :** `+0.1× multiplicateur permanent par reset`
|
||||||
|
**Cible Sprint 1 :** migrer vers ADN Ancestral + Arbre d'Évolution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arbre d'Évolution (permanent — jamais reset)
|
||||||
|
|
||||||
|
Sprint 1 — linéaire (5 nœuds). Sprint 2+ → branches.
|
||||||
|
|
||||||
|
| Nœud | Coût ADN | Effet |
|
||||||
|
|------|----------|-------|
|
||||||
|
| Ponte Améliorée | 1 | +100% click power |
|
||||||
|
| Instinct Grégaire | 3 | +50% production tous générateurs |
|
||||||
|
| Mémoire Génétique | 10 | Commence chaque run avec 100 têtards |
|
||||||
|
| Mutation Alpha | 25 | Débloquer tier 5 dès le début de la run |
|
||||||
|
| Symbiose | 50 | +1% production par achievement débloqué |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sauvegarde & Anti-triche
|
||||||
|
|
||||||
|
- Save côté serveur uniquement (backend Express + MySQL)
|
||||||
|
- Pas de localStorage pour les données de jeu (cache UI uniquement)
|
||||||
|
- Auto-save toutes les 30 secondes via API
|
||||||
|
- Validation snapshot : `elapsed_time × max_possible_production × 1.1 >= claimed_resources`
|
||||||
|
- Client considéré hostile — toute donnée validée côté serveur
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Monétisation
|
## Monétisation
|
||||||
|
|
||||||
**Cosmétiques only** — pas de pay-to-win
|
**Cosmétiques only** — pas de pay-to-win
|
||||||
|
- Thèmes visuels (couleurs, icônes marais)
|
||||||
- Thèmes visuels (couleurs, icônes)
|
- Titres / badges de génération
|
||||||
- Titres / badges de prestige
|
- Effets de ponte (particules)
|
||||||
- Effets de clic (particules)
|
|
||||||
- Raison : 0 compliance fiscale, 0 déséquilibre économie, communauté saine
|
- Raison : 0 compliance fiscale, 0 déséquilibre économie, communauté saine
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sauvegarde
|
|
||||||
|
|
||||||
- localStorage (sprint 1 — immédiat, zéro infra)
|
|
||||||
- Sync API backend (backend déjà en place — câblage sprint 1 si temps le permet)
|
|
||||||
- Auto-save toutes les 30 secondes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Hors scope Sprint 1
|
## Hors scope Sprint 1
|
||||||
|
|
||||||
- Leaderboard (exclu — infra ranking = sprint 2)
|
- Boucle 3 (méta, events, leaderboard)
|
||||||
|
- Branches arbre d'évolution (linéaire suffit)
|
||||||
|
- Cosmétiques / skins
|
||||||
|
- Monétisation effective
|
||||||
|
- Sound / musique
|
||||||
|
- Mobile responsive (desktop first)
|
||||||
|
- Offline gains calculés côté serveur (Sprint 2)
|
||||||
|
- Migration Express → Fastify (si besoin Sprint 3+)
|
||||||
- Intégration Twitch
|
- Intégration Twitch
|
||||||
- Multijoueur
|
- Multijoueur
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prochaines étapes → Step 2
|
## Changelog
|
||||||
|
|
||||||
Fondations techniques : init projet React/TS/Vite, boucle core clic → +ressource, 1er générateur idle, 1er upgrade.
|
| Date | Changement |
|
||||||
|
|------|------------|
|
||||||
|
| 2026-03-17 | GDD initial — sprint1-step1, stack React+TS+Vite, mécaniques core |
|
||||||
|
| 2026-03-20 | Refonte game-designer — Tetard Universe, Arbre d'Évolution, anti-triche backend, SuperOAuth, stack confirmée Express |
|
||||||
|
|||||||
167
docs/SPRINT1.md
Normal file
167
docs/SPRINT1.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# SPRINT1.md — Fondations Tetard Universe
|
||||||
|
|
||||||
|
> Brief technique — Sprint 1
|
||||||
|
> Date : 2026-03-20
|
||||||
|
> Réf GDD : docs/GDD.md
|
||||||
|
> Agents : game-designer (design) → build (implémentation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Transformer le prototype Xmass Clicker en Clickerz Tetard Universe avec :
|
||||||
|
- Rethème complet (noms, visuels, textes)
|
||||||
|
- Arbre d'Évolution (permanent, jamais reset)
|
||||||
|
- Save serveur (anti-triche, fin du localStorage)
|
||||||
|
- SuperOAuth login
|
||||||
|
- Core loop jouable de bout en bout : Ponte → Générateurs → Prestige → Arbre → Nouvelle run
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Existant à conserver
|
||||||
|
|
||||||
|
| Composant | Fichier | État |
|
||||||
|
|-----------|---------|------|
|
||||||
|
| Core economy | `Frontend/src/core/economy.ts` | ✅ Solide — lazy calc, 5 generators, prestige |
|
||||||
|
| Tests economy | `Frontend/src/__tests__/economy.test.ts` | ✅ 13 tests |
|
||||||
|
| Backend auth | `Backend/src/controllers/authControllers.js` | ✅ JWT + SuperOAuth ID |
|
||||||
|
| DB schema | `Backend/database/schema.sql` | ✅ users + super_oauth_id |
|
||||||
|
| Prestige UI | Commit 9f0ccda | ✅ PrestigePanel + MilestoneBar |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1 — Rethème Tetard Universe
|
||||||
|
|
||||||
|
**Scope :** Renommage complet, zéro changement mécanique.
|
||||||
|
|
||||||
|
- [ ] `economy.ts` : renommer generators (Manic → Nid, Tasse à café → Mare, Sucre → Marécage, Usine → Étang Ancien, Portail → Lac Mystique)
|
||||||
|
- [ ] `shop.json` : réécrire avec noms/descriptions Tetard Universe (ou supprimer si redondant avec economy.ts)
|
||||||
|
- [ ] `Achievements.json` : adapter les textes au thème marais
|
||||||
|
- [ ] UI : remplacer "cookies" / "Xmass" par "têtards" / "Clickerz" partout
|
||||||
|
- [ ] Pages : Cookie.jsx (page légale) → adapter mentions "Xmass Clicker" → "Clickerz"
|
||||||
|
- [ ] Titre / meta : "Clickerz — Tetard Universe"
|
||||||
|
|
||||||
|
**Critère done :** aucune référence à "cookie", "Xmass", "Noël" dans le code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2 — Arbre d'Évolution
|
||||||
|
|
||||||
|
**Scope :** Nouveau système permanent, intégré au prestige.
|
||||||
|
|
||||||
|
- [ ] Nouveau type `EvolutionNode` dans economy.ts :
|
||||||
|
```ts
|
||||||
|
interface EvolutionNode {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
cost: number; // en ADN Ancestral
|
||||||
|
effect: EffectType; // multiplicateur, flat bonus, unlock
|
||||||
|
value: number;
|
||||||
|
unlocked: boolean;
|
||||||
|
requires?: string; // id du nœud prérequis (linéaire Sprint 1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] Étendre `GameState` : ajouter `ancestralDna: number`, `evolutionTree: EvolutionNode[]`
|
||||||
|
- [ ] Modifier `applyPrestige()` : calculer ADN gagné via `floor(150 × sqrt(lifetime / 1e9))`, ajouter à `ancestralDna`
|
||||||
|
- [ ] 5 nœuds linéaires (GDD : Ponte Améliorée → Instinct Grégaire → Mémoire Génétique → Mutation Alpha → Symbiose)
|
||||||
|
- [ ] `buyEvolutionNode(state, nodeId)` : acheter si ADN suffisant + prérequis débloqué
|
||||||
|
- [ ] Appliquer les effets dans `totalProductionPerSecond()` et `applyClick()`
|
||||||
|
- [ ] UI : `EvolutionTree.tsx` — panel visible après premier prestige
|
||||||
|
- [ ] Tests : couvrir achat nœud, prérequis, effets sur production
|
||||||
|
|
||||||
|
**Critère done :** un joueur peut prestige, gagner de l'ADN, acheter des nœuds, et constater l'effet sur sa prochaine run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3 — Save serveur + anti-triche
|
||||||
|
|
||||||
|
**Scope :** Migrer la persistence de localStorage vers API backend.
|
||||||
|
|
||||||
|
- [ ] Backend : nouvelle table `game_saves`
|
||||||
|
```sql
|
||||||
|
CREATE TABLE game_saves (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL UNIQUE,
|
||||||
|
game_state JSON NOT NULL,
|
||||||
|
last_save TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
lifetime_tadpoles BIGINT DEFAULT 0,
|
||||||
|
prestige_count INT DEFAULT 0,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
- [ ] Backend routes : `POST /api/save` (write), `GET /api/save` (load) — JWT required
|
||||||
|
- [ ] Validation snapshot sur `POST /api/save` :
|
||||||
|
- Vérifier `elapsed_time × max_possible_production × 1.1 >= claimed_resources`
|
||||||
|
- Si invalide → rejeter le save, logger l'anomalie
|
||||||
|
- [ ] Frontend : `useSaveSync` hook — auto-save toutes les 30s via API
|
||||||
|
- [ ] Frontend : load state depuis API au login (pas de localStorage game data)
|
||||||
|
- [ ] Supprimer la persistence localStorage pour les données de jeu
|
||||||
|
|
||||||
|
**Critère done :** fermer l'onglet, rouvrir, login → retrouver exactement son état. F12 → modifier localStorage → aucun impact sur la save serveur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4 — SuperOAuth login
|
||||||
|
|
||||||
|
**Scope :** Câbler le login via SuperOAuth existant.
|
||||||
|
|
||||||
|
- [ ] Vérifier le flow SuperOAuth backend (verifyOAuth.js existe)
|
||||||
|
- [ ] Frontend : page Login avec bouton "Se connecter avec SuperOAuth"
|
||||||
|
- [ ] Redirect vers SuperOAuth → callback → JWT → session
|
||||||
|
- [ ] Protéger les routes /api/save derrière JWT
|
||||||
|
- [ ] Fallback : mode "invité" sans save serveur (localStorage temporaire) — optionnel
|
||||||
|
|
||||||
|
**Critère done :** login SuperOAuth → jeu avec save serveur. Pas de register custom (SuperOAuth gère).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5 — Migration SCSS → Tailwind + Zustand
|
||||||
|
|
||||||
|
**Scope :** Moderniser le frontend pour la suite.
|
||||||
|
|
||||||
|
- [ ] Installer Tailwind CSS + config Vite
|
||||||
|
- [ ] Migrer les composants clés (game view, shop, prestige panel) vers Tailwind
|
||||||
|
- [ ] Installer Zustand, créer `useGameStore` — remplacer le state lifting actuel
|
||||||
|
- [ ] Intégrer le game loop tick dans Zustand (requestAnimationFrame ou setInterval 1s)
|
||||||
|
- [ ] Supprimer les fichiers SCSS migrés
|
||||||
|
|
||||||
|
**Critère done :** game loop tourne via Zustand, UI en Tailwind, SCSS supprimé sur les composants migrés.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 6 — Polish & deploy
|
||||||
|
|
||||||
|
**Scope :** Rendre jouable et déployer.
|
||||||
|
|
||||||
|
- [ ] UI : écran d'accueil Clickerz (logo, "Entrer dans le Marais")
|
||||||
|
- [ ] UI : feedback visuel Ponte (animation click)
|
||||||
|
- [ ] UI : affichage formaté des grands nombres (1M, 1B, 1T...)
|
||||||
|
- [ ] UI : responsive basique (jouable desktop, pas cassé mobile)
|
||||||
|
- [ ] Deploy : clickerz.tetardtek.com (Apache vhost + pm2 backend)
|
||||||
|
- [ ] Tests e2e : flow complet login → jeu → prestige → arbre → save → reload
|
||||||
|
|
||||||
|
**Critère done :** un joueur peut aller sur clickerz.tetardtek.com, login SuperOAuth, jouer, prestige, acheter dans l'arbre, fermer, revenir, retrouver sa progression.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Résumé séquentiel
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 1 (rethème) → Step 2 (arbre évolution) → Step 3 (save serveur)
|
||||||
|
→ Step 4 (SuperOAuth) → Step 5 (Tailwind+Zustand) → Step 6 (polish+deploy)
|
||||||
|
```
|
||||||
|
|
||||||
|
Steps 1-2 = game design. Steps 3-4 = infra/auth. Step 5 = DX. Step 6 = ship.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risques identifiés
|
||||||
|
|
||||||
|
| Risque | Mitigation |
|
||||||
|
|--------|------------|
|
||||||
|
| Équilibrage ADN trop généreux/avare | Playtest après step 2, ajuster la formule |
|
||||||
|
| SuperOAuth SDK pas à jour | verifyOAuth.js existe déjà — vérifier compat |
|
||||||
|
| Migration SCSS → Tailwind longue | Migrer uniquement les composants touchés, pas tout |
|
||||||
|
| Validation anti-triche trop stricte (faux positifs) | Marge ×1.1, logs avant rejet, soft-block d'abord |
|
||||||
17
ecosystem.config.cjs
Normal file
17
ecosystem.config.cjs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// ecosystem.config.cjs — PM2 config for clickerz backend
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "clickerz-api",
|
||||||
|
cwd: "./Backend",
|
||||||
|
script: "index.js",
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
PORT: 3310,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user