Files
ClickerZ/Frontend/src/core/cosmetics.ts
Tetardtek 2a242e97cc feat: cosmétiques V1 — 5 slots SVG, récompenses achievements + prestige
10 cosmétiques (2/slot), unlock auto sur achievements et prestige tiers.
TadpoleSprite composant SVG stack (base + overlays équipés).
CosmeticsPanel dans la sidebar — inventaire, équiper/retirer par slot.
GameState étendu (cosmeticInventory + cosmeticEquipped), backfill saves.
17 nouveaux tests cosmétiques (92 total, tous passent).
2026-03-28 12:09:26 +01:00

98 lines
4.9 KiB
TypeScript

// cosmetics.ts — Système cosmétique (récompenses achievements + prestige)
import type { GameState } from "./economy";
import { ACHIEVEMENTS } from "../data/achievements";
export type CosmeticSlot = "hat" | "eyes" | "body" | "tail" | "accessory";
export interface Cosmetic {
id: string;
name: string;
slot: CosmeticSlot;
svg: string; // chemin vers le SVG overlay (/svg/cosmetics/...)
source: "achievement" | "prestige";
sourceId: string; // achievement id ou "prestige_N"
description: string;
}
export interface CosmeticState {
inventory: string[]; // ids des cosmétiques débloqués
equipped: Partial<Record<CosmeticSlot, string>>; // slot → cosmetic id
}
export const DEFAULT_COSMETIC_STATE: CosmeticState = {
inventory: [],
equipped: {},
};
// --- Catalogue des cosmétiques ---
export const COSMETICS: Cosmetic[] = [
// Hat
{ id: "crown", name: "Couronne Ancestrale", slot: "hat", svg: "/svg/cosmetics/crown.svg", source: "prestige", sourceId: "prestige_10", description: "10 prestiges — la royauté du marais" },
{ id: "cap_swamp", name: "Casquette du Marais", slot: "hat", svg: "/svg/cosmetics/cap-swamp.svg", source: "achievement", sourceId: "industriel", description: "10 générateurs au total" },
// Eyes
{ id: "glasses_savant", name: "Lunettes du Savant", slot: "eyes", svg: "/svg/cosmetics/glasses-savant.svg", source: "prestige", sourceId: "prestige_5", description: "5 prestiges — la sagesse" },
{ id: "mask_frog", name: "Masque Grenouille", slot: "eyes", svg: "/svg/cosmetics/mask-frog.svg", source: "achievement", sourceId: "empire", description: "1M têtards — le regard de l'empire" },
// Body
{ id: "cape_algae", name: "Cape d'Algues", slot: "body", svg: "/svg/cosmetics/cape-algae.svg", source: "prestige", sourceId: "prestige_25", description: "25 prestiges — tissée par le marais" },
{ id: "armor_scales", name: "Armure d'Écailles", slot: "body", svg: "/svg/cosmetics/armor-scales.svg", source: "achievement", sourceId: "tycoon", description: "100 générateurs — blindage total" },
// Tail
{ id: "flame_tail", name: "Queue Enflammée", slot: "tail", svg: "/svg/cosmetics/flame-tail.svg", source: "prestige", sourceId: "prestige_50", description: "50 prestiges — la traîne de feu" },
{ id: "ribbon", name: "Ruban du Nouveau-Né", slot: "tail", svg: "/svg/cosmetics/ribbon.svg", source: "achievement", sourceId: "first_prestige", description: "Premier prestige — le début de tout" },
// Accessory
{ id: "aura_swamp", name: "Aura du Marais", slot: "accessory", svg: "/svg/aura-swamp.svg", source: "achievement", sourceId: "veteran", description: "5 prestiges — l'aura des anciens" },
{ id: "particles_gold", name: "Particules Dorées", slot: "accessory", svg: "/svg/cosmetics/particles-gold.svg", source: "prestige", sourceId: "prestige_3", description: "3 prestiges — poussière d'étoiles" },
];
// --- Fonctions cosmétiques ---
// Vérifie si un cosmétique devrait être débloqué
export function shouldUnlockCosmetic(cosmetic: Cosmetic, state: GameState): boolean {
if (cosmetic.source === "prestige") {
const tier = parseInt(cosmetic.sourceId.replace("prestige_", ""), 10);
return state.prestigeCount >= tier;
}
if (cosmetic.source === "achievement") {
const achievement = ACHIEVEMENTS.find((a) => a.id === cosmetic.sourceId);
return achievement ? achievement.check(state) : false;
}
return false;
}
// Calcule les cosmétiques nouvellement débloqués (pas encore dans l'inventaire)
export function computeNewUnlocks(state: GameState, cosmeticState: CosmeticState): string[] {
return COSMETICS
.filter((c) => !cosmeticState.inventory.includes(c.id) && shouldUnlockCosmetic(c, state))
.map((c) => c.id);
}
// Équiper un cosmétique (retourne le nouvel état)
export function equipCosmetic(cosmeticState: CosmeticState, cosmeticId: string): CosmeticState {
const cosmetic = COSMETICS.find((c) => c.id === cosmeticId);
if (!cosmetic) return cosmeticState;
if (!cosmeticState.inventory.includes(cosmeticId)) return cosmeticState;
return {
...cosmeticState,
equipped: { ...cosmeticState.equipped, [cosmetic.slot]: cosmeticId },
};
}
// Déséquiper un slot
export function unequipSlot(cosmeticState: CosmeticState, slot: CosmeticSlot): CosmeticState {
const { [slot]: _, ...rest } = cosmeticState.equipped;
return { ...cosmeticState, equipped: rest };
}
// Ajouter des cosmétiques à l'inventaire
export function addToInventory(cosmeticState: CosmeticState, ids: string[]): CosmeticState {
const newIds = ids.filter((id) => !cosmeticState.inventory.includes(id));
if (newIds.length === 0) return cosmeticState;
return { ...cosmeticState, inventory: [...cosmeticState.inventory, ...newIds] };
}