feat: Sprint 3 — Prestige Loop endless
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 35s
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 35s
- Migration saves: saveVersion pattern + migrateSave lazy (v1→v2) - Formule ADN rebalancée: log10 + clamp min 1 + cap bonus ×4 - Prestige Experience: modal fullscreen, preview ADN, stats run, best run - Arbre V2: 25 nœuds, 3 capstones, post-capstones repeatables (scaling par tranche) - Convergence évolutif Alpha→Omega (tier system) - Reset arbre: 1 gratuit/prestige, payant linéaire au-delà - Milestones prestige: 8 paliers (1→100), cosmétiques exclusifs, bonus gameplay - balance.ts: constantes centralisées pour playtest - 136 tests green, 0 regression
This commit is contained in:
@@ -1,28 +1,43 @@
|
||||
// EvolutionTree.tsx — Arbre d'Évolution 3 voies (permanent — jamais reset par prestige)
|
||||
// EvolutionTree.tsx — Arbre d'Évolution V2 (Sprint 3)
|
||||
// 3 branches + capstones + post-capstone repeatables + Convergence évolutif
|
||||
|
||||
import { useGameStore } from "../store/useGameStore";
|
||||
import { canBuyEvolutionNode, getSpentDna } from "../core/economy";
|
||||
import {
|
||||
canBuyEvolutionNode,
|
||||
getSpentDna,
|
||||
getTreeResetCost,
|
||||
canResetTree,
|
||||
getRepeatableCost,
|
||||
canUpgradeConvergence,
|
||||
} from "../core/economy";
|
||||
import type { EvolutionNode, Branch } from "../core/economy";
|
||||
import { formatNumber } from "../utils/formatNumber";
|
||||
|
||||
const EFFECT_LABELS: Record<string, (v: number) => string> = {
|
||||
const EFFECT_LABELS: Record<string, (v: number, n?: EvolutionNode) => string> = {
|
||||
click_multiplier: (v) => `x${v} ponte`,
|
||||
production_multiplier: (v) => `x${v} production`,
|
||||
start_bonus: (v) => `+${v} têtards au départ`,
|
||||
unlock_generator: () => `Lac Mystique dès le début`,
|
||||
start_bonus: (v) => `+${v} tetards au depart`,
|
||||
unlock_generator: () => `Lac Mystique des le debut`,
|
||||
double_click_chance: (v) => `${(v * 100).toFixed(0)}% chance double ponte`,
|
||||
auto_click: (v) => `${v} auto-ponte/s`,
|
||||
auto_click: (v, n) => n?.repeatable ? `+${(v * 100).toFixed(0)}%/achat` : `${v} auto-ponte/s`,
|
||||
auto_click_scaling: (v) => `${v} auto-ponte/s (scale)`,
|
||||
crit_click_chance: (v) => `${(v * 100).toFixed(0)}% chance crit x10`,
|
||||
generator_boost: (v) => `x${v} Nid`,
|
||||
cost_reduction: (v) => `-${(v * 100).toFixed(0)}% coût générateurs`,
|
||||
generator_synergy: (v, n) => n?.repeatable ? `+${(v * 100).toFixed(0)}%/achat` : `+${(v * 100).toFixed(0)}% par type`,
|
||||
cost_reduction: (v) => `-${(v * 100).toFixed(0)}% cout generateurs`,
|
||||
prestige_dna_bonus: (v) => `+${(v * 100).toFixed(0)}% ADN prestige`,
|
||||
offline_boost: (v) => `+${(v * 100).toFixed(0)}% gains offline`,
|
||||
prestige_threshold_reduction: (v) => `Prestige à ${((1 - v) * 100).toFixed(0)}% du seuil`,
|
||||
offline_boost: (v, n) => n?.repeatable ? `+${(v * 100).toFixed(0)}%/achat` : `+${(v * 100).toFixed(0)}% gains offline`,
|
||||
offline_cap_boost: (v) => `Offline cap → ${(v * 100).toFixed(0)}%, duree 8h`,
|
||||
prestige_threshold_reduction: (v) => `Prestige a ${((1 - v) * 100).toFixed(0)}% du seuil`,
|
||||
all_effects_boost: (v) => `+${(v * 100).toFixed(0)}% tous effets`,
|
||||
post_capstone_discount: (v) => `-${(v * 100).toFixed(0)}% cout post-capstones`,
|
||||
};
|
||||
|
||||
const BRANCH_CONFIG: Record<Branch, { label: string; color: string; accent: string }> = {
|
||||
ponte: { label: "Ponte", color: "border-emerald-500/30", accent: "gp-accent-green" },
|
||||
marais: { label: "Marais", color: "border-blue-500/30", accent: "text-blue-400" },
|
||||
adaptation: { label: "Adaptation", color: "border-amber-500/30", accent: "gp-accent-amber" },
|
||||
const BRANCH_CONFIG: Record<Branch | "cross", { label: string; color: string; accent: string }> = {
|
||||
ponte: { label: "Ponte", color: "border-emerald-500/30", accent: "gp-accent-green" },
|
||||
marais: { label: "Marais", color: "border-blue-500/30", accent: "text-blue-400" },
|
||||
adaptation: { label: "Adaptation", color: "border-amber-500/30", accent: "gp-accent-amber" },
|
||||
cross: { label: "Convergence", color: "border-purple-500/30", accent: "gp-accent-purple" },
|
||||
};
|
||||
|
||||
function NodeRow({
|
||||
@@ -36,36 +51,62 @@ function NodeRow({
|
||||
isExcluded: boolean;
|
||||
onBuy: () => void;
|
||||
}) {
|
||||
const isCapstone = node.capstone;
|
||||
const isRepeatable = node.repeatable;
|
||||
const purchased = node.purchased ?? 0;
|
||||
|
||||
const rowClass = node.unlocked
|
||||
? "gp-row gp-row--unlocked"
|
||||
? isCapstone
|
||||
? "gp-row gp-row--unlocked border-amber-400/40!"
|
||||
: "gp-row gp-row--unlocked"
|
||||
: isExcluded
|
||||
? "gp-row gp-row--locked opacity-30!"
|
||||
: canBuy
|
||||
? "gp-row gp-row--evolution"
|
||||
? isCapstone
|
||||
? "gp-row gp-row--evolution border-amber-400/30!"
|
||||
: "gp-row gp-row--evolution"
|
||||
: "gp-row gp-row--locked";
|
||||
|
||||
const cost = isRepeatable && node.unlocked
|
||||
? getRepeatableCost(node)
|
||||
: isRepeatable
|
||||
? node.cost
|
||||
: node.cost;
|
||||
|
||||
return (
|
||||
<div className={rowClass}>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="flex items-center gap-1">
|
||||
{isCapstone && <span className="text-amber-400 text-[0.6rem]">★</span>}
|
||||
<span className="gp-value text-[0.7rem]!">{node.name}</span>
|
||||
{isRepeatable && node.unlocked && (
|
||||
<span className="gp-label text-[0.55rem]!">x{purchased}</span>
|
||||
)}
|
||||
{node.exclusive_with && !node.unlocked && !isExcluded && (
|
||||
<span className="gp-label text-[0.55rem]!">OU</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="gp-label">{EFFECT_LABELS[node.effect]?.(node.value) ?? node.effect}</span>
|
||||
<span className="gp-label">{EFFECT_LABELS[node.effect]?.(node.value, node) ?? node.effect}</span>
|
||||
</div>
|
||||
{node.unlocked ? (
|
||||
{node.unlocked && !isRepeatable ? (
|
||||
<span className="gp-label gp-accent-green">OK</span>
|
||||
) : node.unlocked && isRepeatable ? (
|
||||
<button
|
||||
disabled={!canBuy}
|
||||
onClick={onBuy}
|
||||
className={`gp-btn ${canBuy ? "gp-btn--buy" : "gp-btn--disabled"}`}
|
||||
>
|
||||
{formatNumber(cost)}
|
||||
</button>
|
||||
) : isExcluded ? (
|
||||
<span className="gp-label text-[0.55rem]!">verrouillé</span>
|
||||
<span className="gp-label text-[0.55rem]!">verrouille</span>
|
||||
) : (
|
||||
<button
|
||||
disabled={!canBuy}
|
||||
onClick={onBuy}
|
||||
className={`gp-btn ${canBuy ? "gp-btn--buy" : "gp-btn--disabled"}`}
|
||||
>
|
||||
{node.cost}
|
||||
{formatNumber(cost)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -99,6 +140,74 @@ function BranchColumn({ branch }: { branch: Branch }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ConvergenceSection() {
|
||||
const state = useGameStore((s) => s.state);
|
||||
const buyNode = useGameStore((s) => s.buyNode);
|
||||
const upgradeConv = useGameStore((s) => s.upgradeConvergenceNode);
|
||||
const conv = state.evolutionTree.find((n) => n.id === "convergence");
|
||||
|
||||
if (!conv) return null;
|
||||
|
||||
const canBuy = canBuyEvolutionNode(state, "convergence");
|
||||
const canUpgrade = canUpgradeConvergence(state);
|
||||
const tier = conv.tier ?? 1;
|
||||
const maxTier = conv.maxTier ?? 2;
|
||||
const tierName = tier >= 2 ? "Omega" : "Alpha";
|
||||
|
||||
return (
|
||||
<div className="gp border-t-2 border-purple-500/30">
|
||||
<span className="gp-title text-center gp-accent-purple">
|
||||
Convergence {conv.unlocked ? tierName : ""}
|
||||
</span>
|
||||
{conv.unlocked ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="gp-row gp-row--unlocked border-purple-400/30!">
|
||||
<div className="flex flex-col">
|
||||
<span className="gp-value text-[0.7rem]!">
|
||||
{tier >= 2 ? "Omega" : "Alpha"} (tier {tier}/{maxTier})
|
||||
</span>
|
||||
<span className="gp-label">
|
||||
{tier >= 2
|
||||
? "+10% tous effets + -20% cout post-capstones"
|
||||
: "+10% a tous les effets de l'arbre"
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<span className="gp-label gp-accent-green">OK</span>
|
||||
</div>
|
||||
{tier < maxTier && (
|
||||
<button
|
||||
disabled={!canUpgrade}
|
||||
onClick={upgradeConv}
|
||||
className={`gp-btn ${canUpgrade ? "gp-btn--buy" : "gp-btn--disabled"} w-full`}
|
||||
>
|
||||
{canUpgrade
|
||||
? `Evoluer → Omega (${conv.tierUpgradeCost} ADN)`
|
||||
: `Requis : 2 capstones (${conv.tierUpgradeCost} ADN)`
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="gp-row gp-row--locked">
|
||||
<div className="flex flex-col">
|
||||
<span className="gp-value text-[0.7rem]!">Convergence Alpha</span>
|
||||
<span className="gp-label">+10% a tous les effets de l'arbre</span>
|
||||
<span className="gp-label text-[0.55rem]!">Requis : 1 capstone + tier 3 d'une 2e branche</span>
|
||||
</div>
|
||||
<button
|
||||
disabled={!canBuy}
|
||||
onClick={() => buyNode("convergence")}
|
||||
className={`gp-btn ${canBuy ? "gp-btn--buy" : "gp-btn--disabled"}`}
|
||||
>
|
||||
{conv.cost}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EvolutionTree() {
|
||||
const state = useGameStore((s) => s.state);
|
||||
const resetTree = useGameStore((s) => s.resetTree);
|
||||
@@ -108,13 +217,16 @@ export function EvolutionTree() {
|
||||
|
||||
const spentDna = getSpentDna(evolutionTree);
|
||||
const hasUnlocked = spentDna > 0;
|
||||
const resetCost = getTreeResetCost(state);
|
||||
const canReset = canResetTree(state);
|
||||
|
||||
const handleReset = () => {
|
||||
if (!hasUnlocked) return;
|
||||
if (!canReset) return;
|
||||
const costLabel = resetCost > 0 ? ` (coute ${resetCost} ADN)` : " (gratuit)";
|
||||
const confirmed = window.confirm(
|
||||
`Réinitialiser l'Arbre d'Évolution ?\n\n` +
|
||||
`Tu récupères ${spentDna} ADN Ancestral.\n` +
|
||||
`Tous les nœuds seront verrouillés.\n\n` +
|
||||
`Reinitialiser l'Arbre d'Evolution ?\n\n` +
|
||||
`Tu recuperes ${spentDna} ADN Ancestral.${costLabel}\n` +
|
||||
`Tous les noeuds seront verrouilles.\n\n` +
|
||||
`Confirmer ?`
|
||||
);
|
||||
if (confirmed) resetTree();
|
||||
@@ -123,16 +235,21 @@ export function EvolutionTree() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center px-1">
|
||||
<span className="gp-title">Évolution</span>
|
||||
<span className="gp-title">Evolution</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="gp-value gp-accent-amber">{ancestralDna} ADN</span>
|
||||
<span className="gp-value gp-accent-amber">{formatNumber(ancestralDna)} ADN</span>
|
||||
{hasUnlocked && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="gp-btn gp-btn--disabled text-[0.55rem]! hover:bg-red-500/20! hover:text-red-400!"
|
||||
title={`Récupérer ${spentDna} ADN`}
|
||||
disabled={!canReset}
|
||||
className={`gp-btn text-[0.55rem]! ${
|
||||
canReset
|
||||
? "gp-btn--disabled hover:bg-red-500/20! hover:text-red-400!"
|
||||
: "gp-btn--disabled"
|
||||
}`}
|
||||
title={`Recuperer ${spentDna} ADN${resetCost > 0 ? ` (coute ${resetCost})` : " (gratuit)"}`}
|
||||
>
|
||||
Reset
|
||||
Reset{resetCost > 0 ? ` (${resetCost})` : ""}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -142,6 +259,7 @@ export function EvolutionTree() {
|
||||
<BranchColumn branch="marais" />
|
||||
<BranchColumn branch="adaptation" />
|
||||
</div>
|
||||
<ConvergenceSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
89
Frontend/src/components/MilestonesPanel.tsx
Normal file
89
Frontend/src/components/MilestonesPanel.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
// MilestonesPanel.tsx — Paliers de prestige (Sprint 3)
|
||||
// Progress bar vers le prochain milestone, claim button, preview reward
|
||||
|
||||
import { useGameStore } from "../store/useGameStore";
|
||||
import { getClaimableMilestones, getNextMilestone } from "../core/economy";
|
||||
import { PRESTIGE_MILESTONES } from "../data/prestigeMilestones";
|
||||
|
||||
export function MilestonesPanel() {
|
||||
const state = useGameStore((s) => s.state);
|
||||
const claim = useGameStore((s) => s.claimMilestone);
|
||||
|
||||
if (state.prestigeCount < 1) return null;
|
||||
|
||||
const claimable = getClaimableMilestones(state);
|
||||
const nextMilestone = getNextMilestone(state);
|
||||
const totalClaimed = state.claimedMilestones.length;
|
||||
|
||||
return (
|
||||
<div className="gp">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="gp-title">Milestones</span>
|
||||
<span className="gp-label">{totalClaimed}/{PRESTIGE_MILESTONES.length}</span>
|
||||
</div>
|
||||
|
||||
{/* Claimable milestones */}
|
||||
{claimable.length > 0 && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{claimable.map((m) => (
|
||||
<div key={m.id} className="gp-row gp-row--evolution border-purple-400/30!">
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="gp-value text-[0.7rem]!">{m.name}</span>
|
||||
<span className="gp-label">{m.reward.label}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => claim(m.id)}
|
||||
className="gp-btn gp-btn--buy"
|
||||
>
|
||||
Claim
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress vers le prochain milestone */}
|
||||
{nextMilestone && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="gp-label">Prochain : {nextMilestone.name}</span>
|
||||
<span className="gp-label">
|
||||
{state.prestigeCount}/{nextMilestone.threshold}
|
||||
</span>
|
||||
</div>
|
||||
<div className="gp-progress">
|
||||
<div
|
||||
className="gp-progress-fill bg-gradient-to-r from-purple-600 to-purple-400"
|
||||
style={{
|
||||
width: `${Math.min((state.prestigeCount / nextMilestone.threshold) * 100, 100).toFixed(1)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="gp-label">{nextMilestone.reward.label}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tous les milestones réclamés */}
|
||||
{!nextMilestone && claimable.length === 0 && (
|
||||
<span className="gp-label text-center gp-accent-purple">
|
||||
Tous les milestones reclames !
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Liste compacte des milestones passés */}
|
||||
{totalClaimed > 0 && claimable.length === 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{PRESTIGE_MILESTONES.filter((m) => state.claimedMilestones.includes(m.id)).map((m) => (
|
||||
<span
|
||||
key={m.id}
|
||||
className="gp-label text-[0.55rem]! px-1.5 py-0.5 rounded bg-purple-500/10 border border-purple-500/20"
|
||||
title={`${m.name} — ${m.description}`}
|
||||
>
|
||||
{m.threshold}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,28 +5,15 @@ import { computePrestigeDna, getPrestigeDnaBonus, getPrestigeThreshold } from ".
|
||||
import { formatNumber } from "../utils/formatNumber";
|
||||
|
||||
export function PrestigePanel() {
|
||||
const { lifetimeTadpoles } = useGameStore((s) => s.state);
|
||||
const canPrestige = useGameStore((s) => s.canPrestige);
|
||||
const prestige = useGameStore((s) => s.prestige);
|
||||
|
||||
const state = useGameStore((s) => s.state);
|
||||
const baseDna = computePrestigeDna(lifetimeTadpoles);
|
||||
const canPrestige = useGameStore((s) => s.canPrestige);
|
||||
const openPrestigeScreen = useGameStore((s) => s.openPrestigeScreen);
|
||||
|
||||
const baseDna = computePrestigeDna(state.lifetimeTadpoles, state.prestigeCount);
|
||||
const dnaBonus = getPrestigeDnaBonus(state.evolutionTree);
|
||||
const dnaPreview = Math.floor(baseDna * (1 + dnaBonus));
|
||||
const threshold = getPrestigeThreshold(state);
|
||||
|
||||
const handlePrestige = () => {
|
||||
const confirmed = window.confirm(
|
||||
`Nouvelle Génération\n\n` +
|
||||
`Reset : têtards et générateurs à zéro.\n` +
|
||||
`Récompense : +${dnaPreview} ADN Ancestral\n` +
|
||||
` +0.1x multiplicateur permanent\n\n` +
|
||||
`L'Arbre d'Évolution persiste.\n\n` +
|
||||
`Confirmer ?`
|
||||
);
|
||||
if (confirmed) prestige();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="gp">
|
||||
<span className="gp-title" title="Recommence à zéro en échange d'un bonus permanent — tes têtards et générateurs sont réinitialisés mais tu gagnes de l'ADN et un multiplicateur">Prestige</span>
|
||||
@@ -35,12 +22,12 @@ export function PrestigePanel() {
|
||||
<span className="gp-value gp-accent-purple">
|
||||
+{dnaPreview} ADN · +0.1x mult
|
||||
</span>
|
||||
<button onClick={handlePrestige} className="gp-btn gp-btn--prestige">
|
||||
Nouvelle Génération
|
||||
<button onClick={openPrestigeScreen} className="gp-btn gp-btn--prestige">
|
||||
Nouvelle Generation
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="gp-label">Atteins {formatNumber(threshold)} têtards pour prestige</span>
|
||||
<span className="gp-label">Atteins {formatNumber(threshold)} tetards pour prestige</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
182
Frontend/src/components/PrestigeScreen.tsx
Normal file
182
Frontend/src/components/PrestigeScreen.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
// PrestigeScreen.tsx — Écran de prestige fullscreen (Sprint 3)
|
||||
// Preview ADN, stats de run, comparaison meilleure run, confirmation
|
||||
|
||||
import { useGameStore } from "../store/useGameStore";
|
||||
import {
|
||||
computePrestigeDna,
|
||||
getPrestigeDnaBonus,
|
||||
getPrestigeThreshold,
|
||||
} from "../core/economy";
|
||||
import { formatNumber } from "../utils/formatNumber";
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
export function PrestigeScreen() {
|
||||
const show = useGameStore((s) => s.showPrestigeScreen);
|
||||
const close = useGameStore((s) => s.closePrestigeScreen);
|
||||
const prestige = useGameStore((s) => s.prestige);
|
||||
const state = useGameStore((s) => s.state);
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
const baseDna = computePrestigeDna(state.lifetimeTadpoles, state.prestigeCount);
|
||||
const dnaBonus = getPrestigeDnaBonus(state.evolutionTree);
|
||||
const dnaPreview = Math.floor(baseDna * (1 + dnaBonus));
|
||||
const threshold = getPrestigeThreshold(state);
|
||||
const canPrestige = state.lifetimeTadpoles >= threshold;
|
||||
|
||||
// Run stats
|
||||
const now = Date.now();
|
||||
const runDuration = now - state.runStats.startedAt;
|
||||
const bestRun = state.runStats.bestRun;
|
||||
|
||||
// Comparison with best run
|
||||
const isBestAdn = !bestRun || dnaPreview > bestRun.adn;
|
||||
const isBestTadpoles = !bestRun || state.lifetimeTadpoles > bestRun.tadpoles;
|
||||
|
||||
const handlePrestige = () => {
|
||||
if (canPrestige) prestige();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/85 backdrop-blur-sm">
|
||||
<div className="gp max-w-md w-full mx-4">
|
||||
{/* Header */}
|
||||
<div className="text-center">
|
||||
<span className="gp-title text-lg!">Nouvelle Generation</span>
|
||||
<p className="gp-label mt-1">
|
||||
Generation #{state.prestigeCount + 1}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="gp-sep" />
|
||||
|
||||
{/* ADN Preview */}
|
||||
<div className="flex flex-col items-center gap-1 py-2">
|
||||
<span className="gp-label">ADN Ancestral</span>
|
||||
<span className="text-3xl font-extrabold" style={{ color: "#a78bfa", fontFamily: "var(--font)" }}>
|
||||
+{formatNumber(dnaPreview)}
|
||||
</span>
|
||||
{dnaBonus > 0 && (
|
||||
<span className="gp-label">
|
||||
(base {formatNumber(baseDna)} + {Math.round(dnaBonus * 100)}% arbre)
|
||||
</span>
|
||||
)}
|
||||
<span className="gp-label mt-1">
|
||||
Total apres : {formatNumber(state.ancestralDna + dnaPreview)} ADN
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="gp-sep" />
|
||||
|
||||
{/* Run Stats */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="gp-zone-label">Stats de la run</span>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span className="gp-label">Duree</span>
|
||||
<span className="gp-value">{formatDuration(runDuration)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span className="gp-label">Tetards produits</span>
|
||||
<span className={`gp-value ${isBestTadpoles ? "gp-accent-green" : ""}`}>
|
||||
{formatNumber(state.lifetimeTadpoles)}
|
||||
{isBestTadpoles && bestRun && " ★"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span className="gp-label">ADN cette run</span>
|
||||
<span className={`gp-value ${isBestAdn ? "gp-accent-green" : ""}`}>
|
||||
{formatNumber(dnaPreview)}
|
||||
{isBestAdn && bestRun && " ★"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{bestRun && (
|
||||
<div className="flex justify-between">
|
||||
<span className="gp-label">Vitesse vs meilleure</span>
|
||||
<span className={`gp-value ${
|
||||
runDuration < bestRun.duration ? "gp-accent-green" : "gp-accent-amber"
|
||||
}`}>
|
||||
{runDuration < bestRun.duration
|
||||
? `${Math.round((1 - runDuration / bestRun.duration) * 100)}% plus rapide`
|
||||
: runDuration > bestRun.duration
|
||||
? `${Math.round((runDuration / bestRun.duration - 1) * 100)}% plus lent`
|
||||
: "identique"
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{bestRun && (
|
||||
<>
|
||||
<div className="gp-sep" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="gp-zone-label">Meilleure run</span>
|
||||
<div className="flex justify-between">
|
||||
<span className="gp-label">Duree</span>
|
||||
<span className="gp-value">{formatDuration(bestRun.duration)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="gp-label">ADN</span>
|
||||
<span className="gp-value gp-accent-purple">{formatNumber(bestRun.adn)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="gp-sep" />
|
||||
|
||||
{/* Reset info */}
|
||||
<div className="text-center">
|
||||
<p className="gp-label">
|
||||
Tetards et generateurs remis a zero.
|
||||
</p>
|
||||
<p className="gp-label">
|
||||
Arbre d'Evolution et cosmetiques conserves.
|
||||
</p>
|
||||
<p className="gp-label mt-1">
|
||||
+1 reset d'arbre gratuit offert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 mt-1">
|
||||
<button
|
||||
onClick={close}
|
||||
className="gp-btn flex-1 py-2!"
|
||||
style={{ background: "rgba(255,255,255,0.08)", color: "rgba(255,255,255,0.7)" }}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
{canPrestige ? (
|
||||
<button
|
||||
onClick={handlePrestige}
|
||||
className="gp-btn gp-btn--prestige flex-1 py-2!"
|
||||
>
|
||||
Nouvelle Generation
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="gp-btn gp-btn--disabled flex-1 py-2!"
|
||||
disabled
|
||||
>
|
||||
{formatNumber(threshold - state.lifetimeTadpoles)} tetards manquants
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user