1093 lines
52 KiB
PHP
1093 lines
52 KiB
PHP
<?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>
|