feat: migrate frontend React 18 → Svelte 5 + SvelteKit
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 22s
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)
This commit is contained in:
193
Frontend/src/lib/components/EvolutionTree.svelte
Normal file
193
Frontend/src/lib/components/EvolutionTree.svelte
Normal file
@@ -0,0 +1,193 @@
|
||||
<script lang="ts">
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import {
|
||||
canBuyEvolutionNode,
|
||||
getSpentDna,
|
||||
getTreeResetCost,
|
||||
canResetTree,
|
||||
getRepeatableCost,
|
||||
canUpgradeConvergence,
|
||||
type EvolutionNode,
|
||||
type Branch,
|
||||
} from '$lib/core/economy';
|
||||
import { formatNumber } from '$lib/utils/formatNumber';
|
||||
|
||||
const EFFECT_LABELS: Record<string, (v: number, n?: EvolutionNode) => string> = {
|
||||
click_multiplier: (v) => `x${v} ponte`,
|
||||
production_multiplier: (v) => `x${v} production`,
|
||||
start_bonus: (v) => `+${v} tetards au depart`,
|
||||
unlock_generator: () => `Lac Mystique des le debut`,
|
||||
double_click_chance: (v) => `${(v * 100).toFixed(0)}% chance double ponte`,
|
||||
auto_click: (v, n) => n?.repeatable ? `+${(v * 100).toFixed(0)}%/achat` : `${v} auto-ponte/s`,
|
||||
auto_click_scaling: (v) => `${v} auto-ponte/s (scale)`,
|
||||
crit_click_chance: (v) => `${(v * 100).toFixed(0)}% chance crit x10`,
|
||||
generator_boost: (v) => `x${v} Nid`,
|
||||
generator_synergy: (v, n) => n?.repeatable ? `+${(v * 100).toFixed(0)}%/achat` : `+${(v * 100).toFixed(0)}% par type`,
|
||||
cost_reduction: (v) => `-${(v * 100).toFixed(0)}% cout generateurs`,
|
||||
prestige_dna_bonus: (v) => `+${(v * 100).toFixed(0)}% ADN prestige`,
|
||||
offline_boost: (v, n) => n?.repeatable ? `+${(v * 100).toFixed(0)}%/achat` : `+${(v * 100).toFixed(0)}% gains offline`,
|
||||
offline_cap_boost: (v) => `Offline cap → ${(v * 100).toFixed(0)}%, duree 8h`,
|
||||
prestige_threshold_reduction: (v) => `Prestige a ${((1 - v) * 100).toFixed(0)}% du seuil`,
|
||||
all_effects_boost: (v) => `+${(v * 100).toFixed(0)}% tous effets`,
|
||||
post_capstone_discount: (v) => `-${(v * 100).toFixed(0)}% cout post-capstones`,
|
||||
};
|
||||
|
||||
const BRANCH_CONFIG: Record<string, { label: string; color: string; accent: string }> = {
|
||||
ponte: { label: 'Ponte', color: 'border-emerald-500/30', accent: 'gp-accent-green' },
|
||||
marais: { label: 'Marais', color: 'border-blue-500/30', accent: 'text-blue-400' },
|
||||
adaptation: { label: 'Adaptation', color: 'border-amber-500/30', accent: 'gp-accent-amber' },
|
||||
cross: { label: 'Convergence', color: 'border-purple-500/30', accent: 'gp-accent-purple' },
|
||||
};
|
||||
|
||||
const BRANCHES: Branch[] = ['ponte', 'marais', 'adaptation'];
|
||||
|
||||
let activeBranch = $state<Branch>('ponte');
|
||||
|
||||
let branchConfig = $derived(BRANCH_CONFIG[activeBranch]);
|
||||
let branchNodes = $derived(gameStore.state.evolutionTree.filter((n) => n.branch === activeBranch));
|
||||
let spentDna = $derived(getSpentDna(gameStore.state.evolutionTree));
|
||||
let hasUnlocked = $derived(spentDna > 0);
|
||||
let resetCost = $derived(getTreeResetCost(gameStore.state));
|
||||
let canReset = $derived(canResetTree(gameStore.state));
|
||||
let conv = $derived(gameStore.state.evolutionTree.find((n) => n.id === 'convergence'));
|
||||
let canBuyConv = $derived(canBuyEvolutionNode(gameStore.state, 'convergence'));
|
||||
let canUpgradeConv = $derived(canUpgradeConvergence(gameStore.state));
|
||||
|
||||
function handleReset() {
|
||||
if (!canReset) return;
|
||||
const costLabel = resetCost > 0 ? ` (coute ${resetCost} ADN)` : ' (gratuit)';
|
||||
const confirmed = window.confirm(
|
||||
`Reinitialiser l'Arbre d'Evolution ?\n\nTu recuperes ${spentDna} ADN Ancestral.${costLabel}\nTous les noeuds seront verrouilles.\n\nConfirmer ?`
|
||||
);
|
||||
if (confirmed) gameStore.resetTree();
|
||||
}
|
||||
|
||||
function getNodeRowClass(node: EvolutionNode, isExcluded: boolean, canBuy: boolean): string {
|
||||
if (node.unlocked) return node.capstone ? 'gp-row gp-row--unlocked border-amber-400/40!' : 'gp-row gp-row--unlocked';
|
||||
if (isExcluded) return 'gp-row gp-row--locked opacity-30!';
|
||||
if (canBuy) return node.capstone ? 'gp-row gp-row--evolution border-amber-400/30!' : 'gp-row gp-row--evolution';
|
||||
return 'gp-row gp-row--locked';
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if gameStore.state.prestigeCount >= 1}
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center px-1">
|
||||
<span class="gp-title">Evolution</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="gp-value gp-accent-amber">{formatNumber(gameStore.state.ancestralDna)} ADN</span>
|
||||
{#if hasUnlocked}
|
||||
<button
|
||||
onclick={handleReset}
|
||||
disabled={!canReset}
|
||||
class="gp-btn text-[0.55rem]! {canReset ? 'gp-btn--disabled hover:bg-red-500/20! hover:text-red-400!' : 'gp-btn--disabled'}"
|
||||
title="Recuperer {spentDna} ADN{resetCost > 0 ? ` (coute ${resetCost})` : ' (gratuit)'}"
|
||||
>
|
||||
Reset{resetCost > 0 ? ` (${resetCost})` : ''}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Branch tabs -->
|
||||
<div class="flex gap-1">
|
||||
{#each BRANCHES as branch}
|
||||
{@const config = BRANCH_CONFIG[branch]}
|
||||
{@const isActive = activeBranch === branch}
|
||||
<button
|
||||
onclick={() => activeBranch = branch}
|
||||
class="gp-btn flex-1 py-1.5! text-[0.7rem]! font-bold! uppercase! tracking-wider! {isActive ? `gp-btn--buy ${config.accent}` : 'gp-btn--disabled'}"
|
||||
>
|
||||
{config.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Active branch -->
|
||||
<div class="gp flex-1 min-w-0 border-t-2 {branchConfig.color}">
|
||||
<span class="gp-title text-center {branchConfig.accent}">{branchConfig.label}</span>
|
||||
{#each branchNodes as node}
|
||||
{@const isExcluded = node.exclusive_with ? (gameStore.state.evolutionTree.find((n) => n.id === node.exclusive_with)?.unlocked ?? false) : false}
|
||||
{@const canBuy = canBuyEvolutionNode(gameStore.state, node.id)}
|
||||
{@const cost = node.repeatable && node.unlocked ? getRepeatableCost(node) : node.cost}
|
||||
<div class={getNodeRowClass(node, isExcluded, canBuy)}>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex items-center gap-1">
|
||||
{#if node.capstone}<span class="text-amber-400 text-[0.6rem]">★</span>{/if}
|
||||
<span class="gp-value text-[0.7rem]!">{node.name}</span>
|
||||
{#if node.repeatable && node.unlocked}
|
||||
<span class="gp-label text-[0.55rem]!">x{node.purchased ?? 0}</span>
|
||||
{/if}
|
||||
{#if node.exclusive_with && !node.unlocked && !isExcluded}
|
||||
<span class="gp-label text-[0.55rem]!">OU</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="gp-label">{EFFECT_LABELS[node.effect]?.(node.value, node) ?? node.effect}</span>
|
||||
</div>
|
||||
{#if node.unlocked && !node.repeatable}
|
||||
<span class="gp-label gp-accent-green">OK</span>
|
||||
{:else if isExcluded}
|
||||
<span class="gp-label text-[0.55rem]!">verrouille</span>
|
||||
{:else}
|
||||
<button
|
||||
disabled={!canBuy}
|
||||
onclick={() => gameStore.buyNode(node.id)}
|
||||
class="gp-btn {canBuy ? 'gp-btn--buy' : 'gp-btn--disabled'}"
|
||||
>
|
||||
{formatNumber(cost)}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Convergence -->
|
||||
{#if conv}
|
||||
<div class="gp border-t-2 border-purple-500/30">
|
||||
<span class="gp-title text-center gp-accent-purple">
|
||||
Convergence {conv.unlocked ? ((conv.tier ?? 1) >= 2 ? 'Omega' : 'Alpha') : ''}
|
||||
</span>
|
||||
{#if conv.unlocked}
|
||||
{@const tier = conv.tier ?? 1}
|
||||
{@const maxTier = conv.maxTier ?? 2}
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="gp-row gp-row--unlocked border-purple-400/30!">
|
||||
<div class="flex flex-col">
|
||||
<span class="gp-value text-[0.7rem]!">{tier >= 2 ? 'Omega' : 'Alpha'} (tier {tier}/{maxTier})</span>
|
||||
<span class="gp-label">
|
||||
{tier >= 2 ? '+10% tous effets + -20% cout post-capstones' : "+10% a tous les effets de l'arbre"}
|
||||
</span>
|
||||
</div>
|
||||
<span class="gp-label gp-accent-green">OK</span>
|
||||
</div>
|
||||
{#if tier < maxTier}
|
||||
<button
|
||||
disabled={!canUpgradeConv}
|
||||
onclick={() => gameStore.upgradeConvergence()}
|
||||
class="gp-btn {canUpgradeConv ? 'gp-btn--buy' : 'gp-btn--disabled'} w-full"
|
||||
>
|
||||
{canUpgradeConv ? `Evoluer → Omega (${conv.tierUpgradeCost} ADN)` : `Requis : 2 capstones (${conv.tierUpgradeCost} ADN)`}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="gp-row gp-row--locked">
|
||||
<div class="flex flex-col">
|
||||
<span class="gp-value text-[0.7rem]!">Convergence Alpha</span>
|
||||
<span class="gp-label">+10% a tous les effets de l'arbre</span>
|
||||
<span class="gp-label text-[0.55rem]!">Requis : 1 capstone + tier 3 d'une 2e branche</span>
|
||||
</div>
|
||||
<button
|
||||
disabled={!canBuyConv}
|
||||
onclick={() => gameStore.buyNode('convergence')}
|
||||
class="gp-btn {canBuyConv ? 'gp-btn--buy' : 'gp-btn--disabled'}"
|
||||
>
|
||||
{conv.cost}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user