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).
98 lines
4.9 KiB
TypeScript
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] };
|
|
}
|