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:
@@ -18,6 +18,14 @@ import {
|
||||
computeOfflineGains,
|
||||
offlineEfficiency,
|
||||
} from "../core/economy";
|
||||
import {
|
||||
computeNewUnlocks,
|
||||
equipCosmetic as equipCosmeticFn,
|
||||
unequipSlot as unequipSlotFn,
|
||||
addToInventory,
|
||||
DEFAULT_COSMETIC_STATE,
|
||||
type CosmeticSlot,
|
||||
} from "../core/cosmetics";
|
||||
|
||||
const SAVE_KEY = "clickerz_state";
|
||||
const OFFLINE_THRESHOLD = 60_000; // 60s — same as economy.ts
|
||||
@@ -27,8 +35,10 @@ function loadLocalState(): GameState {
|
||||
const raw = localStorage.getItem(SAVE_KEY);
|
||||
if (!raw) return { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||
const saved = JSON.parse(raw) as GameState;
|
||||
// Backfill lastOnline for old saves
|
||||
// Backfill for old saves
|
||||
if (!saved.lastOnline) saved.lastOnline = saved.lastTick;
|
||||
if (!saved.cosmeticInventory) saved.cosmeticInventory = [];
|
||||
if (!saved.cosmeticEquipped) saved.cosmeticEquipped = {};
|
||||
return applyIdleGains(saved, Date.now());
|
||||
} catch {
|
||||
return { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||
@@ -67,6 +77,8 @@ interface GameStore {
|
||||
buyNode: (nodeId: string) => void;
|
||||
prestige: () => void;
|
||||
resetTree: () => void;
|
||||
equipCosmetic: (cosmeticId: string) => void;
|
||||
unequipCosmetic: (slot: CosmeticSlot) => void;
|
||||
reset: () => void;
|
||||
loadFromServer: (serverState: GameState) => void;
|
||||
initGuest: () => void;
|
||||
@@ -75,8 +87,10 @@ interface GameStore {
|
||||
}
|
||||
|
||||
function hydrateWithOffline(saved: GameState, now: number): { state: GameState; report: OfflineReport | null } {
|
||||
// Backfill lastOnline for old saves
|
||||
// Backfill for old saves
|
||||
if (!saved.lastOnline) saved.lastOnline = saved.lastTick;
|
||||
if (!saved.cosmeticInventory) saved.cosmeticInventory = [];
|
||||
if (!saved.cosmeticEquipped) saved.cosmeticEquipped = {};
|
||||
|
||||
const elapsed = now - saved.lastTick;
|
||||
|
||||
@@ -131,8 +145,18 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
const now = Date.now();
|
||||
set((s) => {
|
||||
const updated = applyIdleGains(s.state, now);
|
||||
// Mark as actively online
|
||||
updated.lastOnline = now;
|
||||
|
||||
// Check cosmetic unlocks every 5s
|
||||
if (s.playSeconds % 5 === 0) {
|
||||
const cosState = { inventory: updated.cosmeticInventory, equipped: updated.cosmeticEquipped };
|
||||
const newUnlocks = computeNewUnlocks(updated, cosState);
|
||||
if (newUnlocks.length > 0) {
|
||||
const newCos = addToInventory(cosState, newUnlocks);
|
||||
updated.cosmeticInventory = newCos.inventory;
|
||||
}
|
||||
}
|
||||
|
||||
saveLocal(updated);
|
||||
return {
|
||||
state: updated,
|
||||
@@ -197,6 +221,28 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
equipCosmetic: (cosmeticId: string) => {
|
||||
if (!get().ready) return;
|
||||
set((s) => {
|
||||
const cosState = { inventory: s.state.cosmeticInventory, equipped: s.state.cosmeticEquipped };
|
||||
const updated = equipCosmeticFn(cosState, cosmeticId);
|
||||
const newState = { ...s.state, cosmeticEquipped: updated.equipped };
|
||||
saveLocal(newState);
|
||||
return { state: newState };
|
||||
});
|
||||
},
|
||||
|
||||
unequipCosmetic: (slot: CosmeticSlot) => {
|
||||
if (!get().ready) return;
|
||||
set((s) => {
|
||||
const cosState = { inventory: s.state.cosmeticInventory, equipped: s.state.cosmeticEquipped };
|
||||
const updated = unequipSlotFn(cosState, slot);
|
||||
const newState = { ...s.state, cosmeticEquipped: updated.equipped };
|
||||
saveLocal(newState);
|
||||
return { state: newState };
|
||||
});
|
||||
},
|
||||
|
||||
resetTree: () => {
|
||||
if (!get().ready) return;
|
||||
set((s) => {
|
||||
|
||||
Reference in New Issue
Block a user