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).
This commit is contained in:
97
Frontend/src/core/cosmetics.ts
Normal file
97
Frontend/src/core/cosmetics.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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] };
|
||||
}
|
||||
Reference in New Issue
Block a user