Files
Portfolio-Esteban/pages/citron.php
T
2026-07-03 10:56:54 +02:00

1093 lines
52 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// Presse-Citron : easter egg déclenché par le code Konami (voir elements/footer.php).
// Mini-jeu incrémental façon "Cookie Clicker", auto-suffisant (pas de header/footer du
// portfolio : pensé pour être chargé dans l'iframe modale ouverte par le code Konami).
// Comptes séparés des comptes admin (table lemon_players, voir migration Juillet 2026 (7)).
require_once(__DIR__ . '/../functions/security.php');
require_once('../functions/databaseConnection.php');
// Si une session de jeu existe déjà, on récupère l'état pour éviter un aller-retour
// supplémentaire en JS au chargement de la page
$joueurConnecte = null;
if (!empty($_SESSION['lemon_player_id'])) {
try {
$stmt = $dbh->prepare('
SELECT id, username, score, lifetime_score, prestige_level, prestige_bonus, game_state
FROM lemon_players WHERE id = :id
');
$stmt->bindValue(':id', (int) $_SESSION['lemon_player_id'], PDO::PARAM_INT);
$stmt->execute();
$joueurConnecte = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$joueurConnecte) {
// Le compte a été supprimé entre-temps : on nettoie la session
unset($_SESSION['lemon_player_id'], $_SESSION['lemon_username']);
}
} catch (PDOException $e) {
$joueurConnecte = null;
}
}
$jetonCsrf = csrfToken();
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Presse-Citron</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
<link rel="stylesheet" href="../styles/citron.css">
</head>
<body>
<div class="citron-shell">
<header class="citron-header">
<div class="citron-title"><span class="lemon-mark" aria-hidden="true"><span class="lemon-mark-leaf"></span></span> Presse-Citron</div>
<div class="citron-player-info" id="citron-player-bar" style="display:none;">
<button type="button" class="citron-avatar-wrap" id="citron-avatar-wrap" title="Changer de photo de profil">
<span class="citron-avatar-fallback" id="citron-avatar-fallback"></span>
<img class="citron-avatar-img" id="citron-avatar-img" alt="" style="display:none;">
<span class="citron-avatar-edit"><i class="fa-solid fa-camera"></i></span>
</button>
<input type="file" id="citron-avatar-input" accept="image/jpeg,image/png,image/webp" hidden>
<span><strong id="citron-player-name"></strong></span>
<button class="citron-btn" id="citron-avatar-remove-btn" title="Supprimer la photo de profil"><i class="fa-solid fa-trash"></i></button>
<button class="citron-btn" id="citron-logout-btn"><i class="fa-solid fa-right-from-bracket"></i> Déconnexion</button>
</div>
</header>
<!-- Écran de connexion / inscription -->
<section class="citron-auth" id="citron-auth-screen">
<div class="citron-auth-card">
<span class="lemon-mark lemon-mark-big" aria-hidden="true"><span class="lemon-mark-leaf"></span></span>
<h1>Presse-Citron</h1>
<p class="subtitle">Un compte à part, rien à voir avec l'admin du portfolio. Presse des citrons à l'infini.</p>
<div class="citron-auth-tabs">
<button type="button" class="active" data-tab="login"><i class="fa-solid fa-right-to-bracket"></i> Connexion</button>
<button type="button" data-tab="register"><i class="fa-solid fa-user-plus"></i> Créer un compte</button>
</div>
<div class="citron-auth-error" id="citron-auth-error"></div>
<form id="citron-login-form">
<div class="citron-field">
<label for="citron-login-user"><i class="fa-solid fa-user"></i> Nom de pressier</label>
<input type="text" id="citron-login-user" name="username" autocomplete="username" required maxlength="50">
</div>
<div class="citron-field">
<label for="citron-login-pass"><i class="fa-solid fa-lock"></i> Mot de passe</label>
<input type="password" id="citron-login-pass" name="password" autocomplete="current-password" required>
</div>
<button type="submit" class="citron-btn primary citron-auth-submit"><i class="fa-solid fa-hand-pointer"></i> Presser des citrons</button>
</form>
<form id="citron-register-form" style="display:none;">
<div class="citron-field">
<label for="citron-register-user"><i class="fa-solid fa-user"></i> Nom de pressier</label>
<input type="text" id="citron-register-user" name="username" autocomplete="username" required maxlength="50">
</div>
<div class="citron-field">
<label for="citron-register-pass"><i class="fa-solid fa-lock"></i> Mot de passe</label>
<input type="password" id="citron-register-pass" name="password" autocomplete="new-password" required minlength="4">
</div>
<button type="submit" class="citron-btn primary citron-auth-submit"><i class="fa-solid fa-user-plus"></i> Créer mon compte</button>
</form>
</div>
</section>
<!-- Écran de jeu -->
<section class="citron-game" id="citron-game-screen">
<div class="citron-score-bar">
<div class="citron-score" id="citron-score">0</div>
<div class="citron-score-label"><i class="fa-solid fa-droplet"></i> gouttes de citron</div>
<div class="citron-cps" id="citron-cps">0 goutte/s</div>
</div>
<div class="citron-main">
<div class="citron-click-zone" id="citron-click-zone">
<button type="button" class="citron-press" id="citron-press" aria-label="Presser le citron">
<span class="lemon-visual">
<span class="lemon-shine"></span>
<span class="lemon-leaf"></span>
</span>
</button>
<p class="citron-press-hint">Clique sur le citron pour le presser</p>
<div class="citron-prestige" id="citron-prestige-wrap" style="display:none;">
<button type="button" class="citron-btn citron-prestige-btn" id="citron-prestige-btn"><i class="fa-solid fa-star"></i> Nouvelle récolte</button>
<p class="citron-prestige-note" id="citron-prestige-note"></p>
</div>
</div>
<div class="citron-panel">
<div class="citron-panel-tabs">
<button type="button" class="active" data-panel="buildings"><i class="fa-solid fa-building"></i> Bâtiments</button>
<button type="button" data-panel="upgrades"><i class="fa-solid fa-arrow-trend-up"></i> Améliorations</button>
<button type="button" data-panel="leaderboard"><i class="fa-solid fa-trophy"></i> Classement</button>
</div>
<div class="citron-panel-body" id="citron-buildings-body"></div>
<div class="citron-panel-body" id="citron-upgrades-body" style="display:none;"></div>
<div class="citron-panel-body" id="citron-leaderboard-body" style="display:none;"></div>
</div>
</div>
</section>
</div>
<div class="citron-toast" id="citron-toast"></div>
<script>
window.LEMON_CSRF = <?php echo json_encode($jetonCsrf); ?>;
window.LEMON_INITIAL = <?php echo $joueurConnecte ? json_encode($joueurConnecte) : 'null'; ?>;
</script>
<script>
(function () {
'use strict';
// ---- Contenu du jeu : bâtiments (générateurs passifs) ----
// ---- Bâtiments : les 20 bâtiments de Cookie Clicker, version citronnée (mêmes coûts
// et CpS de base que le jeu d'origine) ----
var BUILDINGS = [
{ id: 'presse_citron', name: 'Presse-citron', icon: 'fa-hand-pointer', baseCost: 15, baseCps: 0.1, desc: "À la force du poignet." },
{ id: 'grand_mere', name: 'Grand-mère au citron', icon: 'fa-mug-hot', baseCost: 100, baseCps: 1, desc: "Elle presse plus vite que son ombre." },
{ id: 'ferme', name: 'Ferme de citronniers', icon: 'fa-wheat-awn', baseCost: 1100, baseCps: 8, desc: "Des rangées entières d'arbres." },
{ id: 'mine', name: 'Mine de zeste', icon: 'fa-mountain', baseCost: 12000, baseCps: 47, desc: "On y extrait le zeste à la pioche." },
{ id: 'usine', name: 'Usine au citron', icon: 'fa-industry', baseCost: 130000, baseCps: 260, desc: "Production industrielle de jus." },
{ id: 'banque', name: 'Banque de pépins', icon: 'fa-building-columns', baseCost: 1400000, baseCps: 1400, desc: "Les pépins rapportent des intérêts." },
{ id: 'temple', name: 'Temple du citron', icon: 'fa-place-of-worship', baseCost: 20000000, baseCps: 7800, desc: "On y vénère l'agrume suprême." },
{ id: 'tour_sorcier', name: 'Tour de sorcier citronnée', icon: 'fa-hat-wizard', baseCost: 330000000, baseCps: 44000, desc: "De la magie pour presser plus fort." },
{ id: 'expedition', name: 'Expédition de citrons', icon: 'fa-rocket', baseCost: 5100000000, baseCps: 260000, desc: "On importe des citrons de l'espace." },
{ id: 'labo_alchimie', name: "Laboratoire d'alchimie au citron", icon: 'fa-flask', baseCost: 75000000000, baseCps: 1600000, desc: "De la science pour presser plus fort." },
{ id: 'portail_agrumes', name: "Portail d'agrumes", icon: 'fa-circle-notch', baseCost: 1000000000000, baseCps: 10000000, desc: "Un citron infini, dans l'infini." },
{ id: 'temps', name: 'Machine à voyager dans le temps au citron', icon: 'fa-clock-rotate-left', baseCost: 14000000000000, baseCps: 65000000, desc: "On va chercher des citrons dans le passé." },
{ id: 'antimatiere', name: 'Condenseur d\'antimatière citronnée', icon: 'fa-atom', baseCost: 170000000000000, baseCps: 430000000, desc: "Un citron et son anti-citron." },
{ id: 'prisme', name: 'Prisme citronné', icon: 'fa-gem', baseCost: 2.1e15, baseCps: 2900000000, desc: "Décompose la lumière en jus de citron." },
{ id: 'fabrique_chance', name: 'Fabrique de chance citronnée', icon: 'fa-dice', baseCost: 26e15, baseCps: 21000000000, desc: "Fabrique du hasard, citronné." },
{ id: 'moteur_fractal', name: 'Moteur fractal citronné', icon: 'fa-infinity', baseCost: 330e15, baseCps: 170000000000, desc: "Des citrons dans des citrons dans des citrons." },
{ id: 'console_js', name: 'Console JavaScript citronnée', icon: 'fa-code', baseCost: 4.1e18, baseCps: 1100000000000, desc: "console.log('citron')." },
{ id: 'idleverse', name: 'Idleverse de limonade', icon: 'fa-meteor', baseCost: 51e18, baseCps: 55000000000000, desc: "Un univers entier de citronnade." },
{ id: 'boulanger_cortex', name: 'Boulanger du cortex citronné', icon: 'fa-brain', baseCost: 710e18, baseCps: 560000000000000, desc: "Presse des citrons avec sa pensée." },
{ id: 'toi_empereur', name: 'Toi, empereur du citron', icon: 'fa-crown', baseCost: 9.1e21, baseCps: 4400000000000000, desc: "Le citron, c'est toi." }
];
// ---- Alias : correspondance entre les anciens identifiants de bâtiments et les
// nouveaux, pour que les parties déjà sauvegardées (score, bâtiments possédés,
// améliorations achetées) se reportent automatiquement sans aucune perte de donnée
// quand ce contenu a été ajouté (voir migrateSavedState plus bas) ----
var BUILDING_ALIASES = {
presse_main: 'presse_citron',
goutteur: 'grand_mere',
ouvrier: 'ferme',
citronnier: 'mine',
verger: 'usine',
presse_hydraulique: 'banque',
usine_limonade: 'temple',
camion_citron: 'tour_sorcier',
cartel_citron: 'expedition',
labo_acidite: 'labo_alchimie',
portail: 'portail_agrumes'
};
// ---- Améliorations de clic (débloquées par le score cumulé, permanentes) ----
var CLICK_UPGRADES = [
{ id: 'click_1', name: 'Poignet renforcé', threshold: 100, mult: 2 },
{ id: 'click_2', name: 'Gants de pressage', threshold: 1000, mult: 2 },
{ id: 'click_3', name: 'Presse portative', threshold: 10000, mult: 2 },
{ id: 'click_4', name: 'Bras robotique', threshold: 100000, mult: 2 },
{ id: 'click_5', name: 'Exosquelette à citron', threshold: 1000000, mult: 2 },
{ id: 'click_6', name: 'Nanotechnologie agrume', threshold: 10000000, mult: 2 },
{ id: 'click_7', name: 'Champ de force pressurisant', threshold: 100000000, mult: 2 },
{ id: 'click_8', name: 'Singularité citronnée', threshold: 1000000000, mult: 2 },
{ id: 'click_9', name: "Volonté pure d'agrume", threshold: 10000000000, mult: 2 },
{ id: 'click_10', name: 'Transcendance du citron', threshold: 100000000000, mult: 2 }
];
// ---- Améliorations de bâtiments : les vraies améliorations citronnées (nom + coût
// d'origine) pour les 11 premiers bâtiments, complétées par des améliorations générées
// automatiquement pour les 9 bâtiments les plus tardifs => beaucoup de contenu ----
// Paliers de déblocage (nombre de bâtiments possédés) : correspond à peu près à la
// progression du jeu d'origine (1er/2e/3e paliers rapides, puis de plus en plus espacés)
var UNLOCK_THRESHOLDS = [1, 1, 1, 5, 25, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500];
function makeRealUpgrades(buildingId, entries) {
return entries.map(function (entry, i) {
return {
id: buildingId + '_up_' + i,
buildingId: buildingId,
name: entry[0],
cost: entry[1],
threshold: UNLOCK_THRESHOLDS[i] || (500 + (i - UNLOCK_THRESHOLDS.length + 1) * 50),
mult: 2
};
});
}
var UPGRADE_DATA = {
presse_citron: [
['Doigt de citron renforcé', 100],
['Crème citronnée anti-tunnel carpien', 500],
['Double-main citronnée', 10000],
['Mille doigts de citron', 100000],
['Million de doigts citronnés', 10e6],
['Milliard de doigts citronnés', 100e6],
['Trillion de doigts citronnés', 1e9],
['Quadrillion de doigts citronnés', 10e9],
['Quintillion de doigts citronnés', 10e12],
['Sextillion de doigts citronnés', 10e15],
['Septillion de doigts citronnés', 10e18],
['Octillion de doigts citronnés', 10e21],
['Nonillion de doigts citronnés', 10e24],
['Décillion de doigts citronnés', 10e27],
['Undécillion de doigts citronnés', 10e30]
],
grand_mere: [
['Message citronné de mamie', 1000],
['Rouleaux à pâtisserie citron-acier', 5000],
['Dentier citronné lubrifié', 50000],
['Jus de prune au citron', 5e6],
['Lunettes citronnées à double foyer', 500e6],
['Agents de vieillissement au citron', 50e9],
['Trotteurs citronnés extrêmes', 50e12],
['La délivrance du citron', 50e15],
['Démence inversée citronnée', 50e18],
['Colorations anti-âge au citron', 50e21],
['Bonnes manières citronnées', 500e24],
['Dégénérescence citronnée générationnelle', 5e30],
['Visites citronnées', 50e33],
['Placards de cuisine au citron', 500e36],
['Cannes citronnées à embouts mous', 5e42]
],
ferme: [
['Houes citronnées', 11000],
['Engrais citronné', 55000],
['Arbres à cookies citronnés', 550000],
['Cookies génétiquement citronnés', 55e6],
['Épouvantails en pain d\'épice citronnés', 5.5e9],
['Arroseurs pulsars citronnés', 550e9],
['Champignons fondants citronnés', 550e12],
['Triffides de blé citronnées', 550e15],
['Pesticides humains au citron', 550e18],
['Étoiles de grange citronnées', 550e21],
['Lindworms citronnés', 5.5e27],
['Réserve mondiale de semences citronnée', 55e30],
['Véganisme inversé citronné', 550e33],
['Paillis de cookies citronné', 5.5e39],
['Tracteurs autonomes au citron', 55e42]
],
mine: [
['Gaz sucré citronné', 120000],
['Mégaforeuse citronnée', 600000],
['Ultraforeuse citronnée', 6e6],
['Ultimaforeuse citronnée', 600e6],
['Minage citronné par bombe H', 60e9],
['Forge du noyau citronnée', 6e12],
['Écarteurs de planètes citronnés', 6e15],
["Puits d'huile de canola citronnés", 6e18],
['Peuple-taupe citronné', 6e21],
['Canaris de mine citronnés', 6e24],
['Re-creuser au citron', 60e27],
['Exploitation aérienne citronnée', 600e30],
['Alliages au caramel citronnés', 6e36],
['Minéralogie délicieuse citronnée', 60e39],
['Supports de galerie citronnés', 600e42]
],
usine: [
['Convoyeurs citronnés plus solides', 1.3e6],
['Travail des enfants au citron', 6.5e6],
['Atelier citronné de misère', 65e6],
['Réacteurs citronnés au radium', 6.5e9],
['Recombobulateurs citronnés', 650e9],
['Procédé de cuisson profonde au citron', 65e12],
["Main-d'œuvre cyborg citronnée", 65e15],
['Journées de 78 heures au citron', 65e18],
['Apprentissage automatique citronné', 65e21],
['Système de points brownie citronné', 65e24],
['Stagiaires volontaires citronnés', 650e27],
['Reframing comportemental citronné', 6.5e33],
["Moteur de l'infini citronné", 65e36],
['Chaînes d\'assemblage citronnées n-dimensionnelles', 650e39],
['Automatisation universelle au citron', 6.5e45]
],
banque: [
['Guichetiers citronnés plus grands', 14e6],
['Cartes de crédit citronnées anti-ciseaux', 70e6],
["Coffres citronnés résistants à l'acide", 700e6],
['Pièces de chocolat citronnées', 70e9],
["Taux d'intérêt citronnés exponentiels", 7e12],
['Zénitude financière au citron', 700e12],
['Voie citronnée du portefeuille', 700e15],
['Raison des choses citronnée', 700e18],
['Argent comestible citronné', 700e21],
['Supercycle général citronné', 700e24],
["Règles d'acquisition citronnées", 7e30],
['Boucle altruiste citronnée', 70e33],
['Retours fiscaux décroissants citronnés', 700e36],
['Points de cookies citronnés', 7e42],
['Le petit craqué citronné', 70e45]
],
temple: [
['Idoles dorées au citron', 200e6],
['Sacrifices citronnés', 1e9],
['Bénédiction délicieuse au citron', 10e9],
['Festival solaire citronné', 1e12],
['Panthéon agrandi au citron', 100e12],
['Grand Boulanger du ciel citronné', 10e15],
['Mythe de la création citronné', 10e18],
['Théocratie citronnée', 10e21],
['Prières râpées citronnées', 10e24],
['Lecture de psaumes citronnée', 10e27],
['Guerre des dieux citronnée', 100e30],
['Une idée nouvelle citronnée', 1e36],
['Apparitions citronnées', 10e39],
['Négathéisme citronné', 100e42],
['Pièges du temple citronnés', 1e48]
],
tour_sorcier: [
['Chapeaux citronnés plus pointus', 3.3e9],
['Barbes citronnées plus barbes', 16.5e9],
['Grimoires anciens citronnés', 165e9],
['Malédictions citronnées de cuisine', 16.5e12],
['École de sorcellerie citronnée', 1.65e15],
['Formules sombres citronnées', 165e15],
['Cookiemancie citronnée', 165e18],
['Tour de lapin citronnée', 165e21],
['Baguettes citronnées sur mesure deluxe', 165e24],
['Sorts immobiles citronnés', 165e27],
['Électricité citronnée', 1.65e33],
['Abeilles orthographes citronnées', 16.5e36],
['Sous-sols de sorciers citronnés', 165e39],
['Réalisme magique citronné', 1.65e45],
['Polymorphisme citronné', 16.5e48]
],
expedition: [
['Nébuleuses citronnées à la vanille', 51e9],
['Trous de ver citronnés', 255e9],
['Voyageur fréquent citronné', 2.55e12],
['Moteur de distorsion citronné', 255e12],
['Monolithes chocolatés citronnés', 25.5e15],
['Vaisseau générationnel citronné', 2.55e18],
['Sphère de Dyson citronnée', 2.55e21],
['La frontière finale citronnée', 2.55e24],
['Pilotage automatique citronné', 2.55e27],
["Restaurants citronnés au bout de l'univers", 2.55e30],
['Alphabet universel citronné', 25.5e33],
['Univers toroïdal citronné', 255e36],
['Directive première citronnée', 2.55e42],
['Rayonnement de fond cosmique citronné', 25.5e45],
['À domicile en 30 minutes citronné ou remboursé', 255e48]
],
labo_alchimie: [
['Antimoine citronné', 750e9],
['Essence de pâte citronnée', 3.75e12],
['Vrai chocolat citronné', 37.5e12],
['Ambroisie citronnée', 3.75e15],
['Aqua crustulae citronnée', 375e15],
['Creuset originel citronné', 37.5e18],
['Théorie de la fluidité atomique citronnée', 37.5e21],
['Goo beige citronné', 37.5e24],
["L'avènement de la chimie citronné", 37.5e27],
['À bien y réfléchir citronné', 37.5e30],
['Bien public citronné', 375e33],
['Réconciliation hermétique citronnée', 3.75e39],
['Cyclage chromatique citronné', 37.5e42],
['Verrerie arcanisée citronnée', 375e45],
['La dose fait le poison citronnée', 3.75e51]
],
portail_agrumes: [
['Tablette antique citronnée', 10e12],
["Ouvriers de l'avoine citronnés", 50e12],
["Liaison d'âme citronnée", 500e12],
['Danse citronnée de la santé mentale', 50e15],
['Transplantation citronnée de brane', 5e18],
['Portails géants citronnés', 500e18],
['Plan de secours citronné pour la fin des temps', 500e21],
['Chants dérangeants citronnés', 500e24],
['Le monde réel citronné', 500e27],
['Broyeur de déchets dimensionnel citronné', 500e30],
['Microportails intégrés citronnés', 5e36],
["Son avènement citronné", 50e39]
]
};
var BUILDING_UPGRADES = [];
Object.keys(UPGRADE_DATA).forEach(function (buildingId) {
BUILDING_UPGRADES = BUILDING_UPGRADES.concat(makeRealUpgrades(buildingId, UPGRADE_DATA[buildingId]));
});
// Les 9 bâtiments les plus tardifs n'ont pas d'améliorations "réelles" fournies :
// on garde le système généré automatiquement (6 paliers, x2 à chaque fois)
var PROCEDURAL_TIERS = [10, 25, 50, 100, 200, 400];
var PROCEDURAL_LABELS = ['I', 'II', 'III', 'IV', 'V', 'VI'];
BUILDINGS.forEach(function (b) {
if (UPGRADE_DATA[b.id]) return; // déjà couvert par les vraies améliorations ci-dessus
PROCEDURAL_TIERS.forEach(function (threshold, i) {
BUILDING_UPGRADES.push({
id: b.id + '_up_' + i,
buildingId: b.id,
name: b.name + ' — Amélioration ' + PROCEDURAL_LABELS[i],
threshold: threshold,
cost: Math.round(b.baseCost * threshold * 10),
mult: 2
});
});
});
var ALL_UPGRADES_BY_ID = {};
BUILDING_UPGRADES.concat(CLICK_UPGRADES).forEach(function (u) { ALL_UPGRADES_BY_ID[u.id] = u; });
// ---- Migration des sauvegardes existantes vers les nouveaux identifiants (alias) :
// aucune perte de donnée, les bâtiments/améliorations déjà possédés sont reportés tels
// quels sur leur équivalent citronné ----
function migrateSavedState(parsed) {
var buildings = {};
BUILDINGS.forEach(function (b) { buildings[b.id] = 0; });
if (parsed.buildings) {
Object.keys(parsed.buildings).forEach(function (oldId) {
var newId = BUILDING_ALIASES[oldId] || oldId;
if (buildings.hasOwnProperty(newId)) {
buildings[newId] += Number(parsed.buildings[oldId]) || 0;
}
});
}
var upgrades = [];
if (Array.isArray(parsed.upgrades)) {
parsed.upgrades.forEach(function (oldUpgradeId) {
var m = oldUpgradeId.match(/^(.+)_up_(\d+)$/);
if (m) {
var newBuildingId = BUILDING_ALIASES[m[1]] || m[1];
var newUpgradeId = newBuildingId + '_up_' + m[2];
if (ALL_UPGRADES_BY_ID[newUpgradeId]) upgrades.push(newUpgradeId);
} else if (ALL_UPGRADES_BY_ID[oldUpgradeId]) {
// Améliorations de clic : identifiant inchangé
upgrades.push(oldUpgradeId);
}
});
}
return { buildings: buildings, upgrades: upgrades, clickPower: Number(parsed.clickPower) || 1 };
}
var SUFFIXES = ['', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'Dc', 'UDc', 'DDc', 'TDc', 'QaDc', 'QiDc', 'SxDc'];
function formatNumber(n) {
if (!isFinite(n)) return '∞';
var neg = n < 0;
n = Math.abs(n);
var out;
if (n < 1000) {
out = (n < 10 && n !== Math.floor(n)) ? n.toFixed(1) : Math.floor(n).toString();
} else {
var tier = Math.min(Math.floor(Math.log10(n) / 3), SUFFIXES.length - 1);
var scaled = n / Math.pow(1000, tier);
out = (scaled < 10 ? scaled.toFixed(2) : scaled < 100 ? scaled.toFixed(1) : Math.floor(scaled)) + SUFFIXES[tier];
}
return (neg ? '-' : '') + out;
}
function escapeHtml(s) {
var div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
// ---- État du jeu ----
var state = {
score: 0,
lifetimeScore: 0,
prestigeLevel: 0,
prestigeBonus: 0,
clickPower: 1,
buildings: {},
upgrades: []
};
BUILDINGS.forEach(function (b) { state.buildings[b.id] = 0; });
var loggedIn = false;
var myPlayerId = null;
function avatarUrl(playerId, bust) {
return '../scripts/lemonAvatar.php?player_id=' + encodeURIComponent(playerId) + (bust ? '&t=' + Date.now() : '');
}
// ---- Références DOM ----
var authScreen = document.getElementById('citron-auth-screen');
var gameScreen = document.getElementById('citron-game-screen');
var playerBar = document.getElementById('citron-player-bar');
var playerNameEl = document.getElementById('citron-player-name');
var authError = document.getElementById('citron-auth-error');
var scoreEl = document.getElementById('citron-score');
var cpsEl = document.getElementById('citron-cps');
var buildingsBody = document.getElementById('citron-buildings-body');
var upgradesBody = document.getElementById('citron-upgrades-body');
var leaderboardBody = document.getElementById('citron-leaderboard-body');
var pressBtn = document.getElementById('citron-press');
var clickZone = document.getElementById('citron-click-zone');
var prestigeWrap = document.getElementById('citron-prestige-wrap');
var prestigeNote = document.getElementById('citron-prestige-note');
var toastEl = document.getElementById('citron-toast');
// ---- Calculs du jeu ----
function costOf(building) {
var owned = state.buildings[building.id] || 0;
return Math.ceil(building.baseCost * Math.pow(1.15, owned));
}
function buildingMultiplier(buildingId) {
var mult = 1;
BUILDING_UPGRADES.forEach(function (u) {
if (u.buildingId === buildingId && state.upgrades.indexOf(u.id) !== -1) mult *= u.mult;
});
return mult;
}
function cpsOf(building) {
var owned = state.buildings[building.id] || 0;
return building.baseCps * owned * buildingMultiplier(building.id);
}
function totalCps() {
var total = 0;
BUILDINGS.forEach(function (b) { total += cpsOf(b); });
return total * (1 + state.prestigeBonus);
}
function clickMultiplier() {
var mult = 1;
CLICK_UPGRADES.forEach(function (u) {
if (state.upgrades.indexOf(u.id) !== -1) mult *= u.mult;
});
return mult;
}
function clickGain() {
return state.clickPower * clickMultiplier() * (1 + state.prestigeBonus);
}
function prestigeGain() {
return Math.floor(Math.cbrt(state.lifetimeScore / 1000000));
}
// ---- Teinte du fond qui dérive avec la progression : "changement de couleur" ----
function updateHue() {
var totalOwned = 0;
BUILDINGS.forEach(function (b) { totalOwned += state.buildings[b.id] || 0; });
// Reste toujours dans une teinte crédible pour un agrume (jaune profond -> citron ->
// vert lime) : contrairement à la version précédente, ne boucle jamais jusqu'au
// bleu/violet même après beaucoup de bâtiments ou de prestige
var hue = 40 + Math.min(55, totalOwned * 0.3 + state.prestigeLevel * 4);
document.documentElement.style.setProperty('--lemon-hue', hue.toFixed(0));
}
// ---- Rendu ----
function renderNumbers() {
scoreEl.textContent = formatNumber(state.score);
cpsEl.textContent = formatNumber(totalCps()) + ' goutte/s';
updateHue();
// Active/désactive les boutons déjà affichés selon le score actuel (pas de reconstruction du DOM)
var buttons = buildingsBody.querySelectorAll('button[data-cost]').length
? buildingsBody.querySelectorAll('button[data-cost]')
: [];
buttons.forEach(function (btn) {
btn.disabled = state.score < Number(btn.getAttribute('data-cost'));
});
upgradesBody.querySelectorAll('button[data-cost]').forEach(function (btn) {
btn.disabled = state.score < Number(btn.getAttribute('data-cost'));
});
if (prestigeGain() > 0) {
prestigeWrap.style.display = '';
prestigeNote.textContent = 'Remet ton stock de citrons et tes bâtiments à zéro, contre +' + (prestigeGain() * 2) + '% de production permanente (bonus actuel : +' + Math.round(state.prestigeBonus * 100) + '%).';
} else {
prestigeWrap.style.display = 'none';
}
}
function renderBuildings() {
buildingsBody.innerHTML = '';
BUILDINGS.forEach(function (b) {
var cost = costOf(b);
var owned = state.buildings[b.id] || 0;
var btn = document.createElement('button');
btn.className = 'citron-item';
btn.type = 'button';
btn.setAttribute('data-cost', cost);
btn.disabled = state.score < cost;
btn.innerHTML =
'<span class="icon"><i class="fa-solid ' + b.icon + '"></i></span>' +
'<span class="infos"><span class="name">' + escapeHtml(b.name) + '</span>' +
'<span class="desc">' + escapeHtml(b.desc) + ' · ' + formatNumber(cpsOf(b)) + ' goutte/s</span></span>' +
'<span class="cost"><i class="fa-solid fa-droplet"></i> ' + formatNumber(cost) + '</span>' +
'<span class="owned">' + owned + '</span>';
btn.addEventListener('click', function () { buyBuilding(b); });
buildingsBody.appendChild(btn);
});
}
function requirementLabel(upgrade) {
if (upgrade.buildingId) {
return 'Débloqué à ' + upgrade.threshold + ' possédé(s)';
}
return 'Débloqué à ' + formatNumber(upgrade.threshold) + ' gouttes cumulées';
}
function requirementMet(upgrade) {
if (upgrade.buildingId) {
return (state.buildings[upgrade.buildingId] || 0) >= upgrade.threshold;
}
return state.lifetimeScore >= upgrade.threshold;
}
function renderUpgrades() {
upgradesBody.innerHTML = '';
var visibles = BUILDING_UPGRADES.concat(CLICK_UPGRADES).filter(function (u) {
return state.upgrades.indexOf(u.id) === -1 && requirementMet(u);
});
if (!visibles.length) {
upgradesBody.innerHTML = '<p class="citron-empty"><i class="fa-solid fa-flask"></i> Aucune amélioration disponible pour le moment. Continue à presser !</p>';
return;
}
visibles.forEach(function (u) {
var cost = u.cost || (u.threshold * 3);
var btn = document.createElement('button');
btn.className = 'citron-item';
btn.type = 'button';
btn.setAttribute('data-cost', cost);
btn.disabled = state.score < cost;
btn.innerHTML =
'<span class="icon"><i class="fa-solid ' + (u.buildingId ? 'fa-arrow-trend-up' : 'fa-hand-fist') + '"></i></span>' +
'<span class="infos"><span class="name">' + escapeHtml(u.name) + '</span>' +
'<span class="desc">×' + u.mult + ' production</span></span>' +
'<span class="cost"><i class="fa-solid fa-droplet"></i> ' + formatNumber(cost) + '</span>';
btn.addEventListener('click', function () { buyUpgrade(u, cost); });
upgradesBody.appendChild(btn);
});
}
function render() {
renderBuildings();
renderUpgrades();
renderNumbers();
}
function showToast(icon, message) {
toastEl.innerHTML = '<i class="fa-solid ' + icon + '"></i> ' + escapeHtml(message);
toastEl.classList.add('visible');
clearTimeout(showToast._t);
showToast._t = setTimeout(function () { toastEl.classList.remove('visible'); }, 2600);
}
// ---- Actions ----
function buyBuilding(building) {
var cost = costOf(building);
if (state.score < cost) return;
state.score -= cost;
state.buildings[building.id] = (state.buildings[building.id] || 0) + 1;
render();
scheduleSave();
}
function buyUpgrade(upgrade, cost) {
if (state.upgrades.indexOf(upgrade.id) !== -1 || state.score < cost) return;
state.score -= cost;
state.upgrades.push(upgrade.id);
render();
scheduleSave();
showToast('fa-arrow-trend-up', 'Amélioration : ' + upgrade.name);
}
function spawnFloatingText(x, y, text) {
var el = document.createElement('div');
el.className = 'citron-float';
el.style.left = x + 'px';
el.style.top = y + 'px';
el.textContent = text;
clickZone.appendChild(el);
setTimeout(function () { el.remove(); }, 900);
}
function spawnDrops(x, y) {
for (var i = 0; i < 6; i++) {
var el = document.createElement('i');
el.className = 'citron-drop fa-solid fa-droplet';
el.style.left = x + 'px';
el.style.top = y + 'px';
el.style.setProperty('--dx', (Math.random() * 140 - 70) + 'px');
el.style.setProperty('--dy', (-(60 + Math.random() * 80)) + 'px');
clickZone.appendChild(el);
(function (node) { setTimeout(function () { node.remove(); }, 700); })(el);
}
}
function handleClick(evt) {
var gain = clickGain();
state.score += gain;
state.lifetimeScore += gain;
var rect = clickZone.getBoundingClientRect();
var x = (evt.clientX || rect.left + rect.width / 2) - rect.left;
var y = (evt.clientY || rect.top + rect.height / 2) - rect.top;
spawnFloatingText(x, y, '+' + formatNumber(gain));
spawnDrops(x, y);
pressBtn.classList.add('squish');
setTimeout(function () { pressBtn.classList.remove('squish'); }, 120);
renderNumbers();
// Les nouvelles améliorations débloquées par le score cumulé doivent réapparaître
if (CLICK_UPGRADES.some(function (u) { return requirementMet(u) && state.upgrades.indexOf(u.id) === -1; })) {
renderUpgrades();
}
scheduleSave();
}
pressBtn.addEventListener('click', handleClick);
function doPrestige() {
var gain = prestigeGain();
if (gain <= 0) return;
var conf = confirm(
'Tout presser à nouveau depuis le début pour +' + (gain * 2) + '% de production permanente ?\n' +
'Ton total cumulé et ton classement ne changent pas.'
);
if (!conf) return;
state.prestigeLevel += 1;
state.prestigeBonus += gain * 0.02;
state.score = 0;
BUILDINGS.forEach(function (b) { state.buildings[b.id] = 0; });
// Les améliorations de bâtiments retombent (leurs seuils de possession repartent à
// zéro) ; les améliorations de clic, liées au score CUMULÉ qui ne baisse jamais, restent acquises
state.upgrades = state.upgrades.filter(function (id) {
return CLICK_UPGRADES.some(function (u) { return u.id === id; });
});
render();
save();
showToast('fa-star', 'Nouvelle récolte ! Bonus permanent : +' + Math.round(state.prestigeBonus * 100) + '%');
}
document.getElementById('citron-prestige-btn').addEventListener('click', doPrestige);
// ---- Sauvegarde ----
var saveTimer = null;
function scheduleSave() {
if (saveTimer) return;
saveTimer = setTimeout(function () { saveTimer = null; save(); }, 3000);
}
function save() {
if (!loggedIn) return;
var body = new URLSearchParams();
body.set('csrf_token', window.LEMON_CSRF);
body.set('score', String(state.score));
body.set('lifetime_score', String(state.lifetimeScore));
body.set('prestige_level', String(state.prestigeLevel));
body.set('prestige_bonus', String(state.prestigeBonus));
body.set('game_state', JSON.stringify({
buildings: state.buildings,
upgrades: state.upgrades,
clickPower: state.clickPower
}));
fetch('../scripts/lemonSave.php', { method: 'POST', body: body, credentials: 'same-origin' });
}
setInterval(save, 15000);
window.addEventListener('beforeunload', save);
window.addEventListener('pagehide', save);
// ---- Boucle de production passive ----
setInterval(function () {
var gain = totalCps() / 10;
if (gain > 0) {
state.score += gain;
state.lifetimeScore += gain;
renderNumbers();
}
}, 100);
// ---- Authentification ----
function refreshAvatar() {
var img = document.getElementById('citron-avatar-img');
var fallback = document.getElementById('citron-avatar-fallback');
if (!myPlayerId) return;
fallback.textContent = (playerNameEl.textContent || '?').charAt(0).toUpperCase();
img.onerror = function () { img.style.display = 'none'; };
img.onload = function () { img.style.display = ''; };
img.src = avatarUrl(myPlayerId, true);
}
function showGameScreen(username) {
loggedIn = true;
authScreen.style.display = 'none';
gameScreen.classList.add('visible');
playerBar.style.display = '';
playerNameEl.textContent = username;
refreshAvatar();
render();
loadLeaderboard();
}
function applyServerState(data) {
myPlayerId = Number(data.id) || null;
state.score = Number(data.score) || 0;
state.lifetimeScore = Number(data.lifetime_score) || 0;
state.prestigeLevel = Number(data.prestige_level) || 0;
state.prestigeBonus = Number(data.prestige_bonus) || 0;
if (data.game_state) {
try {
var parsed = JSON.parse(data.game_state);
// Passe par les alias : une sauvegarde faite avant l'ajout des nouveaux
// bâtiments/améliorations est reportée à l'identique sur leurs équivalents
// citronnés (aucune perte de progression)
var migrated = migrateSavedState(parsed);
state.buildings = migrated.buildings;
state.upgrades = migrated.upgrades;
state.clickPower = migrated.clickPower;
} catch (e) {
// Sauvegarde corrompue : on repart sur un état neuf plutôt que de planter
}
}
}
function showAuthError(message) {
authError.textContent = message;
authError.classList.add('visible');
}
function hideAuthError() {
authError.classList.remove('visible');
}
document.querySelectorAll('.citron-auth-tabs button').forEach(function (tabBtn) {
tabBtn.addEventListener('click', function () {
document.querySelectorAll('.citron-auth-tabs button').forEach(function (b) { b.classList.remove('active'); });
tabBtn.classList.add('active');
hideAuthError();
var isLogin = tabBtn.getAttribute('data-tab') === 'login';
document.getElementById('citron-login-form').style.display = isLogin ? '' : 'none';
document.getElementById('citron-register-form').style.display = isLogin ? 'none' : '';
});
});
document.getElementById('citron-login-form').addEventListener('submit', function (e) {
e.preventDefault();
hideAuthError();
var body = new URLSearchParams();
body.set('csrf_token', window.LEMON_CSRF);
body.set('username', document.getElementById('citron-login-user').value);
body.set('password', document.getElementById('citron-login-pass').value);
fetch('../scripts/lemonLogin.php', { method: 'POST', body: body, credentials: 'same-origin' })
.then(function (r) { return r.json().then(function (data) { return { status: r.status, data: data }; }); })
.then(function (res) {
if (res.status !== 200) { showAuthError(res.data.error || 'Erreur de connexion.'); return; }
applyServerState(res.data);
showGameScreen(res.data.username);
})
.catch(function () { showAuthError('Impossible de contacter le serveur.'); });
});
document.getElementById('citron-register-form').addEventListener('submit', function (e) {
e.preventDefault();
hideAuthError();
var body = new URLSearchParams();
body.set('csrf_token', window.LEMON_CSRF);
body.set('username', document.getElementById('citron-register-user').value);
body.set('password', document.getElementById('citron-register-pass').value);
fetch('../scripts/lemonRegister.php', { method: 'POST', body: body, credentials: 'same-origin' })
.then(function (r) { return r.json().then(function (data) { return { status: r.status, data: data }; }); })
.then(function (res) {
if (res.status !== 200) { showAuthError(res.data.error || 'Erreur lors de la création du compte.'); return; }
applyServerState(res.data);
showGameScreen(res.data.username);
})
.catch(function () { showAuthError('Impossible de contacter le serveur.'); });
});
document.getElementById('citron-logout-btn').addEventListener('click', function () {
save();
fetch('../scripts/lemonLogout.php', { method: 'POST', credentials: 'same-origin' }).then(function () {
location.reload();
});
});
// ---- Photo de profil ----
// Recadre l'image en carré (recadrage centré) et la compresse en JPEG avant l'envoi :
// ça évite d'avoir besoin de GD côté serveur et garde des avatars légers pour tout le
// monde (le classement peut afficher jusqu'à 15 photos d'un coup)
function resizeImageFile(file) {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function (e) {
var img = new Image();
img.onload = function () {
var taille = 256;
var canvas = document.createElement('canvas');
canvas.width = taille;
canvas.height = taille;
var ctx = canvas.getContext('2d');
var cote = Math.min(img.width, img.height);
var sx = (img.width - cote) / 2;
var sy = (img.height - cote) / 2;
ctx.drawImage(img, sx, sy, cote, cote, 0, 0, taille, taille);
canvas.toBlob(function (blob) {
if (!blob) { reject(new Error('Impossible de traiter cette image.')); return; }
resolve(blob);
}, 'image/jpeg', 0.85);
};
img.onerror = function () { reject(new Error("Le fichier n'est pas une image valide.")); };
img.src = e.target.result;
};
reader.onerror = function () { reject(new Error('Impossible de lire le fichier.')); };
reader.readAsDataURL(file);
});
}
document.getElementById('citron-avatar-wrap').addEventListener('click', function () {
document.getElementById('citron-avatar-input').click();
});
document.getElementById('citron-avatar-input').addEventListener('change', function (e) {
var file = e.target.files[0];
e.target.value = ''; // permet de resélectionner le même fichier plus tard si besoin
if (!file) return;
resizeImageFile(file)
.then(function (blob) {
var body = new FormData();
body.set('csrf_token', window.LEMON_CSRF);
body.append('avatar', blob, 'avatar.jpg');
return fetch('../scripts/lemonAvatarUpload.php', { method: 'POST', body: body, credentials: 'same-origin' });
})
.then(function (r) { return r.json().then(function (data) { return { status: r.status, data: data }; }); })
.then(function (res) {
if (res.status !== 200) {
showToast('fa-triangle-exclamation', res.data.error || "Erreur lors de l'envoi de la photo.");
return;
}
refreshAvatar();
loadLeaderboard();
showToast('fa-camera', 'Photo de profil mise à jour !');
})
.catch(function (err) {
showToast('fa-triangle-exclamation', err.message || "Erreur lors de l'envoi de la photo.");
});
});
document.getElementById('citron-avatar-remove-btn').addEventListener('click', function () {
var body = new URLSearchParams();
body.set('csrf_token', window.LEMON_CSRF);
fetch('../scripts/lemonAvatarRemove.php', { method: 'POST', body: body, credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data.ok) return;
document.getElementById('citron-avatar-img').style.display = 'none';
loadLeaderboard();
showToast('fa-trash', 'Photo de profil supprimée.');
});
});
// ---- Onglets du panneau latéral ----
document.querySelectorAll('.citron-panel-tabs button').forEach(function (tabBtn) {
tabBtn.addEventListener('click', function () {
document.querySelectorAll('.citron-panel-tabs button').forEach(function (b) { b.classList.remove('active'); });
tabBtn.classList.add('active');
var panel = tabBtn.getAttribute('data-panel');
buildingsBody.style.display = panel === 'buildings' ? '' : 'none';
upgradesBody.style.display = panel === 'upgrades' ? '' : 'none';
leaderboardBody.style.display = panel === 'leaderboard' ? '' : 'none';
if (panel === 'leaderboard') loadLeaderboard();
});
});
function relativeTime(dateStr) {
if (!dateStr) return 'jamais joué';
var then = new Date(dateStr.replace(' ', 'T'));
if (isNaN(then.getTime())) return 'jamais joué';
var diffSec = Math.max(0, (Date.now() - then.getTime()) / 1000);
if (diffSec < 60) return "à l'instant";
var diffMin = diffSec / 60;
if (diffMin < 60) return 'il y a ' + Math.floor(diffMin) + ' min';
var diffH = diffMin / 60;
if (diffH < 24) return 'il y a ' + Math.floor(diffH) + ' h';
var diffJ = diffH / 24;
if (diffJ < 30) return 'il y a ' + Math.floor(diffJ) + ' j';
return 'il y a ' + Math.floor(diffJ / 30) + ' mois';
}
function loadLeaderboard() {
leaderboardBody.innerHTML = '<p class="citron-empty"><i class="fa-solid fa-spinner fa-spin"></i> Chargement du classement...</p>';
fetch('../scripts/lemonLeaderboard.php', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (rows) {
if (!rows.length) {
leaderboardBody.innerHTML = '<p class="citron-empty"><i class="fa-solid fa-trophy"></i> Personne n\'a encore pressé de citron ici. Sois le premier !</p>';
return;
}
var currentUsername = playerNameEl.textContent;
var medals = ['lb-gold', 'lb-silver', 'lb-bronze'];
leaderboardBody.innerHTML = rows.map(function (r, i) {
var isMe = r.username === currentUsername;
var rankHtml = i < 3
? '<i class="fa-solid fa-trophy ' + medals[i] + '"></i>'
: '<span class="lb-rank-number">#' + (i + 1) + '</span>';
var initiale = escapeHtml(r.username.charAt(0).toUpperCase());
var prestigeBadge = r.prestige_level > 0
? '<span class="lb-prestige"><i class="fa-solid fa-star"></i> ' + r.prestige_level + '</span>'
: '';
return '<div class="citron-leaderboard-row' + (isMe ? ' lb-me' : '') + '">' +
'<span class="lb-rank">' + rankHtml + '</span>' +
'<span class="lb-avatar-wrap">' +
'<span class="lb-avatar">' + initiale + '</span>' +
'<img class="lb-avatar-img" alt="" src="' + avatarUrl(r.id, true) + '" onerror="this.style.display=\'none\'">' +
'</span>' +
'<div class="lb-main">' +
'<div class="lb-name">' + escapeHtml(r.username) + prestigeBadge + '</div>' +
'<div class="lb-sub"><i class="fa-solid fa-building"></i> ' + r.total_buildings + ' bâtiments · <i class="fa-regular fa-clock"></i> ' + relativeTime(r.last_played) + '</div>' +
'</div>' +
'<div class="lb-score">' +
'<div class="lb-score-value">' + formatNumber(r.lifetime_score) + '</div>' +
'<div class="lb-score-label">gouttes au total</div>' +
'</div>' +
'</div>';
}).join('');
})
.catch(function () {
leaderboardBody.innerHTML = '<p class="citron-empty"><i class="fa-solid fa-triangle-exclamation"></i> Classement indisponible pour le moment.</p>';
});
}
// ---- Fermeture depuis l'intérieur du jeu (Échap géré aussi côté page parente) ----
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && window.parent && window.parent !== window) {
window.parent.postMessage('citron-close', '*');
}
});
// ---- Démarrage : reprend la session si déjà connecté (voir PHP en haut de page) ----
if (window.LEMON_INITIAL) {
applyServerState(window.LEMON_INITIAL);
showGameScreen(window.LEMON_INITIAL.username);
}
})();
</script>
</body>
</html>