feat(sprint1-step3): PrestigePanel + MilestoneBar + équilibrage générateurs fixes
This commit is contained in:
@@ -11,6 +11,50 @@ import {
|
|||||||
DEFAULT_GENERATORS,
|
DEFAULT_GENERATORS,
|
||||||
} from "../core/economy";
|
} from "../core/economy";
|
||||||
|
|
||||||
|
// PrestigePanel visibility guard — canPrestige drives render condition
|
||||||
|
// Ces tests valident l'invariant : le panneau prestige ne doit jamais être
|
||||||
|
// visible (canPrestige = false) si les ressources sont inférieures au seuil.
|
||||||
|
describe("PrestigePanel visibility (canPrestige guard)", () => {
|
||||||
|
it("canPrestige = false pour resources = 0 → panneau non visible", () => {
|
||||||
|
expect(canPrestige({ ...DEFAULT_STATE, resources: 0 })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("canPrestige = false pour resources = 999 999 → panneau non visible", () => {
|
||||||
|
expect(canPrestige({ ...DEFAULT_STATE, resources: 999_999 })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("canPrestige = true pour resources = 1 000 000 → panneau visible", () => {
|
||||||
|
expect(canPrestige({ ...DEFAULT_STATE, resources: 1_000_000 })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyPrestige — post-prestige state", () => {
|
||||||
|
const prestigeState = {
|
||||||
|
...DEFAULT_STATE,
|
||||||
|
resources: 1_500_000,
|
||||||
|
generators: DEFAULT_STATE.generators.map((g) => ({ ...g, owned: 3 })),
|
||||||
|
prestigeCount: 0,
|
||||||
|
prestigeMultiplier: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("ressources = 0 après prestige", () => {
|
||||||
|
expect(applyPrestige(prestigeState).resources).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("multiplicateur = 1.1 après premier prestige", () => {
|
||||||
|
expect(applyPrestige(prestigeState).prestigeMultiplier).toBeCloseTo(1.1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tous les générateurs owned = 0 après prestige", () => {
|
||||||
|
const result = applyPrestige(prestigeState);
|
||||||
|
expect(result.generators.every((g) => g.owned === 0)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prestigeCount incrémenté à 1 après premier prestige", () => {
|
||||||
|
expect(applyPrestige(prestigeState).prestigeCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("generatorCost", () => {
|
describe("generatorCost", () => {
|
||||||
it("retourne baseCost quand owned = 0", () => {
|
it("retourne baseCost quand owned = 0", () => {
|
||||||
const gen = { ...DEFAULT_GENERATORS[0], owned: 0 };
|
const gen = { ...DEFAULT_GENERATORS[0], owned: 0 };
|
||||||
|
|||||||
50
Frontend/src/components/MilestoneBar.tsx
Normal file
50
Frontend/src/components/MilestoneBar.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// MilestoneBar.tsx — Progression vers le prochain prestige
|
||||||
|
// Barre visuelle ressources / 1 000 000 + indicateur restant
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const PRESTIGE_THRESHOLD = 1_000_000;
|
||||||
|
|
||||||
|
interface MilestoneBarProps {
|
||||||
|
resources: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MilestoneBar({ resources }: MilestoneBarProps) {
|
||||||
|
const progress = Math.min(resources / PRESTIGE_THRESHOLD, 1);
|
||||||
|
const progressPercent = (progress * 100).toFixed(1);
|
||||||
|
const remaining = Math.max(PRESTIGE_THRESHOLD - resources, 0);
|
||||||
|
|
||||||
|
const formatNumber = (n: number): string => {
|
||||||
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
||||||
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
||||||
|
return Math.floor(n).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="milestone-bar" aria-label="Progression vers le prestige">
|
||||||
|
<div className="milestone-label">
|
||||||
|
Prochain prestige : {formatNumber(resources)} / {formatNumber(PRESTIGE_THRESHOLD)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="milestone-track"
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={Math.floor(progress * 100)}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="milestone-fill"
|
||||||
|
style={{ width: `${progressPercent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{remaining > 0 && (
|
||||||
|
<div className="milestone-remaining">
|
||||||
|
{formatNumber(remaining)} ressources restantes
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{remaining === 0 && (
|
||||||
|
<div className="milestone-ready">Prestige disponible !</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
Frontend/src/components/PrestigePanel.tsx
Normal file
57
Frontend/src/components/PrestigePanel.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// PrestigePanel.tsx — Boucle de prestige long terme
|
||||||
|
// Visible uniquement quand canPrestige = true (ressources ≥ 1 000 000)
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface PrestigePanelProps {
|
||||||
|
prestigeCount: number;
|
||||||
|
prestigeMultiplier: number;
|
||||||
|
canPrestige: boolean;
|
||||||
|
onPrestige: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PrestigePanel({
|
||||||
|
prestigeCount,
|
||||||
|
prestigeMultiplier,
|
||||||
|
canPrestige,
|
||||||
|
onPrestige,
|
||||||
|
}: PrestigePanelProps) {
|
||||||
|
const handlePrestige = () => {
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
`Prestige — Reset total : ressources et générateurs à zéro.\n` +
|
||||||
|
`Récompense : +0.1× multiplicateur permanent.\n\n` +
|
||||||
|
`Multiplicateur actuel : ×${prestigeMultiplier.toFixed(1)}\n` +
|
||||||
|
`Multiplicateur après : ×${(prestigeMultiplier + 0.1).toFixed(1)}\n\n` +
|
||||||
|
`Confirmer le prestige ?`
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
onPrestige();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="prestige-panel">
|
||||||
|
<div className="prestige-stats">
|
||||||
|
<span className="prestige-count">Prestiges : {prestigeCount}</span>
|
||||||
|
<span className="prestige-multiplier">
|
||||||
|
Multiplicateur : ×{prestigeMultiplier.toFixed(1)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{canPrestige && (
|
||||||
|
<div className="prestige-action">
|
||||||
|
<div className="prestige-reward">
|
||||||
|
Récompense disponible : <strong>+0.1× multiplicateur permanent</strong>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="prestige-button"
|
||||||
|
onClick={handlePrestige}
|
||||||
|
aria-label="Déclencher le prestige"
|
||||||
|
>
|
||||||
|
Prestige
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user