All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 22s
Core logic portable (economy, balance, cosmetics, migrateSave) — zero rewrite. 136 tests green, identiques. Backend inchangé. - Svelte 5 runes stores (game, auth, toast) remplacent Zustand - SvelteKit adapter-static SPA (dist/ output, fallback index.html) - Tailwind v4 conservé, design system .gp-* porté - Transitions natives : slide, fly, scale, fade sur toute l'UI - Sidebar tabbée (Production/Evolution/Collection) + CollapsiblePanel - Mobile bottom sheet avec FAB toggle + backdrop blur - Click particles réactifs Svelte (plus de DOM impératif) - TadpoleSprite bounce + glow ring au clic - Guide refait en accordéon, Achievements avec filtres - a11y : focus-visible, Escape modals, aria-current, aria-labels - CI/CD adapté (tests + build + rsync) - Build 504K (vs ~1.2MB React)
152 lines
5.5 KiB
Svelte
152 lines
5.5 KiB
Svelte
<script lang="ts">
|
|
import { fly, scale, fade } from 'svelte/transition';
|
|
import { quintOut, elasticOut } from 'svelte/easing';
|
|
import { gameStore } from '$lib/stores/game.svelte';
|
|
import { formatNumber } from '$lib/utils/formatNumber';
|
|
import CockpitHeader from '$lib/components/CockpitHeader.svelte';
|
|
import GeneratorShop from '$lib/components/GeneratorShop.svelte';
|
|
import PrestigePanel from '$lib/components/PrestigePanel.svelte';
|
|
import EvolutionTree from '$lib/components/EvolutionTree.svelte';
|
|
import MilestoneBar from '$lib/components/MilestoneBar.svelte';
|
|
import MilestonesPanel from '$lib/components/MilestonesPanel.svelte';
|
|
import CosmeticsPanel from '$lib/components/CosmeticsPanel.svelte';
|
|
import TadpoleSprite from '$lib/components/TadpoleSprite.svelte';
|
|
import PrestigeScreen from '$lib/components/PrestigeScreen.svelte';
|
|
import ClickParticles from '$lib/components/ClickParticles.svelte';
|
|
import SidebarTabs from '$lib/components/SidebarTabs.svelte';
|
|
import { ACHIEVEMENTS } from '$lib/data/achievements';
|
|
|
|
let achieveCount = $derived(ACHIEVEMENTS.filter((a) => a.check(gameStore.state)).length);
|
|
|
|
const sidebarTabs = [
|
|
{ id: 'production', label: 'Production', icon: '🏭' },
|
|
{ id: 'evolution', label: 'Evolution', icon: '🧬' },
|
|
{ id: 'collection', label: 'Collection', icon: '✨' },
|
|
];
|
|
|
|
// Component refs for imperative calls
|
|
let clickParticles: ReturnType<typeof ClickParticles>;
|
|
let tadpoleSprite: ReturnType<typeof TadpoleSprite>;
|
|
|
|
function handleClick(e: MouseEvent) {
|
|
gameStore.click();
|
|
clickParticles?.spawn(e.clientX, e.clientY, gameStore.lastClickGain, gameStore.lastClickDouble, gameStore.lastClickCrit);
|
|
tadpoleSprite?.bounce();
|
|
}
|
|
|
|
// Mobile sidebar toggle
|
|
let sidebarOpen = $state(false);
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Clickerz — Tetard Universe</title>
|
|
<meta name="description" content="Clickerz — Clicker idle dans le Tetard Universe." />
|
|
</svelte:head>
|
|
|
|
{#if !gameStore.ready}
|
|
<div class="flex items-center justify-center min-h-[80vh]" in:fade>
|
|
<div class="flex flex-col items-center gap-4">
|
|
<div class="w-16 h-16 border-4 border-emerald-500/30 border-t-emerald-500 rounded-full animate-spin"></div>
|
|
<p class="text-slate-400" style="font-family: var(--font);">Chargement de ta progression...</p>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div class="zone" data-zone="swamp" in:fade={{ duration: 400 }}>
|
|
<PrestigeScreen />
|
|
<ClickParticles bind:this={clickParticles} />
|
|
|
|
<!-- Click zone -->
|
|
<div class="click-zone" onclick={handleClick}>
|
|
<div in:scale={{ duration: 500, start: 0.8, easing: elasticOut }}>
|
|
<TadpoleSprite bind:this={tadpoleSprite} />
|
|
</div>
|
|
<div
|
|
class="click-zone-counter"
|
|
in:fly={{ y: 20, duration: 400, easing: quintOut }}
|
|
>
|
|
{formatNumber(gameStore.state.resources)}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Desktop sidebar -->
|
|
<aside class="game-sidebar hidden md:flex" in:fly={{ x: 100, duration: 500, easing: quintOut }}>
|
|
<!-- Pinned: always visible -->
|
|
<CockpitHeader />
|
|
<MilestoneBar />
|
|
|
|
<!-- Tabbed content -->
|
|
<SidebarTabs tabs={sidebarTabs}>
|
|
{#snippet children(activeTab)}
|
|
{#if activeTab === 'production'}
|
|
<GeneratorShop />
|
|
<PrestigePanel />
|
|
{:else if activeTab === 'evolution'}
|
|
<EvolutionTree />
|
|
<MilestonesPanel />
|
|
{:else if activeTab === 'collection'}
|
|
<CosmeticsPanel />
|
|
<a href="/achievements" class="achieve-badge">
|
|
{achieveCount}/{ACHIEVEMENTS.length} succes
|
|
</a>
|
|
<a href="/guide" class="achieve-badge" style="border-color: rgba(139, 92, 246, 0.2); background: rgba(139, 92, 246, 0.08); color: #a78bfa;">
|
|
Guide du Gardien
|
|
</a>
|
|
{/if}
|
|
{/snippet}
|
|
</SidebarTabs>
|
|
</aside>
|
|
|
|
<!-- Mobile bottom bar: toggle button -->
|
|
<button
|
|
class="md:hidden fixed bottom-4 right-4 z-30 w-14 h-14 rounded-full flex items-center justify-center shadow-xl"
|
|
style="background: rgba(17,17,17,0.9); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.1);"
|
|
onclick={() => sidebarOpen = !sidebarOpen}
|
|
>
|
|
<span class="text-2xl">{sidebarOpen ? '✕' : '🎮'}</span>
|
|
</button>
|
|
|
|
<!-- Mobile bottom sheet -->
|
|
{#if sidebarOpen}
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="md:hidden fixed inset-0 z-20"
|
|
style="background: rgba(0,0,0,0.5);"
|
|
transition:fade={{ duration: 200 }}
|
|
onclick={() => sidebarOpen = false}
|
|
></div>
|
|
<div
|
|
class="md:hidden fixed bottom-0 left-0 right-0 z-25 flex flex-col gap-3 max-h-[75vh] overflow-y-auto rounded-t-2xl p-4 pb-20"
|
|
style="background: rgba(10,10,10,0.95); backdrop-filter: blur(12px); border-top: 1px solid rgba(255,255,255,0.08);"
|
|
transition:fly={{ y: 300, duration: 350, easing: quintOut }}
|
|
>
|
|
<!-- Drag handle -->
|
|
<div class="flex justify-center">
|
|
<div class="w-10 h-1 rounded-full" style="background: rgba(255,255,255,0.2);"></div>
|
|
</div>
|
|
|
|
<CockpitHeader />
|
|
<MilestoneBar />
|
|
|
|
<SidebarTabs tabs={sidebarTabs}>
|
|
{#snippet children(activeTab)}
|
|
{#if activeTab === 'production'}
|
|
<GeneratorShop />
|
|
<PrestigePanel />
|
|
{:else if activeTab === 'evolution'}
|
|
<EvolutionTree />
|
|
<MilestonesPanel />
|
|
{:else if activeTab === 'collection'}
|
|
<CosmeticsPanel />
|
|
<a href="/achievements" class="achieve-badge">
|
|
{achieveCount}/{ACHIEVEMENTS.length} succes
|
|
</a>
|
|
{/if}
|
|
{/snippet}
|
|
</SidebarTabs>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|