fix: refactor store to direct $state exports + Object.assign mutation
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
Svelte 5 can't export reassigned $state — use const $state + Object.assign. All components now import state/actions directly (no gameStore wrapper). Deep reactivity works: evolutionTree nodes, generators, cosmetics all tracked.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state, getProductionPerSecond, getCurrentClickGain } from '$lib/stores/game.svelte';
|
||||
import { formatNumber } from '$lib/utils/formatNumber';
|
||||
</script>
|
||||
|
||||
@@ -7,23 +7,23 @@
|
||||
<div class="grid grid-cols-5 gap-0.5 px-1">
|
||||
<div class="gp-stat" title="Production automatique par seconde">
|
||||
<span class="gp-label">Prod/s</span>
|
||||
<span class="gp-value gp-accent-green text-[0.8rem]!">{formatNumber(gameStore.productionPerSecond)}</span>
|
||||
<span class="gp-value gp-accent-green text-[0.8rem]!">{formatNumber(getProductionPerSecond())}</span>
|
||||
</div>
|
||||
<div class="gp-stat" title="Tetards gagnes par clic">
|
||||
<span class="gp-label">/clic</span>
|
||||
<span class="gp-value text-[0.8rem]!">{formatNumber(gameStore.getClickGain())}</span>
|
||||
<span class="gp-value text-[0.8rem]!">{formatNumber(getCurrentClickGain())}</span>
|
||||
</div>
|
||||
<div class="gp-stat" title="Multiplicateur global (prestige)">
|
||||
<span class="gp-label">Mult</span>
|
||||
<span class="gp-value text-[0.8rem]!">x{gameStore.state.prestigeMultiplier.toFixed(1)}</span>
|
||||
<span class="gp-value text-[0.8rem]!">x{state.prestigeMultiplier.toFixed(1)}</span>
|
||||
</div>
|
||||
<div class="gp-stat" title="ADN Ancestral">
|
||||
<span class="gp-label">ADN</span>
|
||||
<span class="gp-value gp-accent-purple text-[0.8rem]!">{gameStore.state.ancestralDna}</span>
|
||||
<span class="gp-value gp-accent-purple text-[0.8rem]!">{state.ancestralDna}</span>
|
||||
</div>
|
||||
<div class="gp-stat" title="Nombre de prestiges">
|
||||
<span class="gp-label">Gen.</span>
|
||||
<span class="gp-value text-[0.8rem]!">{gameStore.state.prestigeCount}</span>
|
||||
<span class="gp-value text-[0.8rem]!">{state.prestigeCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state, equipCosmetic, unequipCosmetic } from '$lib/stores/game.svelte';
|
||||
import { COSMETICS, type CosmeticSlot } from '$lib/core/cosmetics';
|
||||
import CollapsiblePanel from './CollapsiblePanel.svelte';
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
};
|
||||
const SLOT_ORDER: CosmeticSlot[] = ['hat', 'eyes', 'body', 'tail', 'accessory'];
|
||||
|
||||
let inventory = $derived(gameStore.state.cosmeticInventory);
|
||||
let equipped = $derived(gameStore.state.cosmeticEquipped);
|
||||
let inventory = $derived(state.cosmeticInventory);
|
||||
let equipped = $derived(state.cosmeticEquipped);
|
||||
let ownedCosmetics = $derived(COSMETICS.filter((c) => inventory.includes(c.id)));
|
||||
</script>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<span class="gp-label">{cos.description}</span>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => isEquipped ? gameStore.unequipCosmetic(slot) : gameStore.equipCosmetic(cos.id)}
|
||||
onclick={() => isEquipped ? unequipCosmetic(slot) : equipCosmetic(cos.id)}
|
||||
class="gp-btn {isEquipped ? 'gp-btn--disabled' : 'gp-btn--buy'}"
|
||||
>
|
||||
{isEquipped ? 'Retirer' : 'Equiper'}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state, buyNode, doResetTree, doUpgradeConvergence } from '$lib/stores/game.svelte';
|
||||
import {
|
||||
canBuyEvolutionNode,
|
||||
getSpentDna,
|
||||
@@ -44,14 +44,14 @@
|
||||
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 branchNodes = $derived(state.evolutionTree.filter((n) => n.branch === activeBranch));
|
||||
let spentDna = $derived(getSpentDna(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));
|
||||
let resetCost = $derived(getTreeResetCost(state));
|
||||
let canReset = $derived(canResetTree(state));
|
||||
let conv = $derived(state.evolutionTree.find((n) => n.id === 'convergence'));
|
||||
let canBuyConv = $derived(canBuyEvolutionNode(state, 'convergence'));
|
||||
let canUpgradeConv = $derived(canUpgradeConvergence(state));
|
||||
|
||||
function handleReset() {
|
||||
if (!canReset) return;
|
||||
@@ -59,7 +59,7 @@
|
||||
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();
|
||||
if (confirmed) doResetTree();
|
||||
}
|
||||
|
||||
function getNodeRowClass(node: EvolutionNode, isExcluded: boolean, canBuy: boolean): string {
|
||||
@@ -70,13 +70,13 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if gameStore.state.prestigeCount >= 1}
|
||||
{#if 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>
|
||||
<span class="gp-value gp-accent-amber">{formatNumber(state.ancestralDna)} ADN</span>
|
||||
{#if hasUnlocked}
|
||||
<button
|
||||
onclick={handleReset}
|
||||
@@ -108,8 +108,8 @@
|
||||
<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 isExcluded = node.exclusive_with ? (state.evolutionTree.find((n) => n.id === node.exclusive_with)?.unlocked ?? false) : false}
|
||||
{@const canBuy = canBuyEvolutionNode(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">
|
||||
@@ -132,7 +132,7 @@
|
||||
{:else}
|
||||
<button
|
||||
disabled={!canBuy}
|
||||
onclick={() => gameStore.buyNode(node.id)}
|
||||
onclick={() => buyNode(node.id)}
|
||||
class="gp-btn {canBuy ? 'gp-btn--buy' : 'gp-btn--disabled'}"
|
||||
>
|
||||
{formatNumber(cost)}
|
||||
@@ -164,7 +164,7 @@
|
||||
{#if tier < maxTier}
|
||||
<button
|
||||
disabled={!canUpgradeConv}
|
||||
onclick={() => gameStore.upgradeConvergence()}
|
||||
onclick={() => doUpgradeConvergence()}
|
||||
class="gp-btn {canUpgradeConv ? 'gp-btn--buy' : 'gp-btn--disabled'} w-full"
|
||||
>
|
||||
{canUpgradeConv ? `Evoluer → Omega (${conv.tierUpgradeCost} ADN)` : `Requis : 2 capstones (${conv.tierUpgradeCost} ADN)`}
|
||||
@@ -180,7 +180,7 @@
|
||||
</div>
|
||||
<button
|
||||
disabled={!canBuyConv}
|
||||
onclick={() => gameStore.buyNode('convergence')}
|
||||
onclick={() => buyNode('convergence')}
|
||||
class="gp-btn {canBuyConv ? 'gp-btn--buy' : 'gp-btn--disabled'}"
|
||||
>
|
||||
{conv.cost}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { refs, initGuest } from '$lib/stores/game.svelte';
|
||||
import {
|
||||
loadFromServer,
|
||||
startAutoSave,
|
||||
@@ -16,13 +16,13 @@
|
||||
// Load save or init guest
|
||||
if (authStore.user) {
|
||||
const loaded = await loadFromServer();
|
||||
if (!loaded && !gameStore.ready) {
|
||||
gameStore.initGuest();
|
||||
if (!loaded && !refs.ready) {
|
||||
initGuest();
|
||||
}
|
||||
startAutoSave();
|
||||
setupVisibilitySync();
|
||||
} else {
|
||||
gameStore.initGuest();
|
||||
initGuest();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { tick } from '$lib/stores/game.svelte';
|
||||
|
||||
let interval: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
onMount(() => {
|
||||
interval = setInterval(() => gameStore.tick(), 1000);
|
||||
interval = setInterval(() => tick(), 1000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { scale } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state, buy, getProductionPerSecond, getGeneratorCostWithTree } from '$lib/stores/game.svelte';
|
||||
import { formatNumber } from '$lib/utils/formatNumber';
|
||||
import CollapsiblePanel from './CollapsiblePanel.svelte';
|
||||
</script>
|
||||
|
||||
<CollapsiblePanel
|
||||
title="Generateurs"
|
||||
badge="{formatNumber(gameStore.productionPerSecond)}/s"
|
||||
badge="{formatNumber(getProductionPerSecond())}/s"
|
||||
accentClass=""
|
||||
>
|
||||
{#each gameStore.state.generators as gen, i}
|
||||
{@const cost = gameStore.generatorCostWithTree(gen)}
|
||||
{@const canAfford = gameStore.state.resources >= cost}
|
||||
{#each state.generators as gen, i}
|
||||
{@const cost = getGeneratorCostWithTree(gen)}
|
||||
{@const canAfford = state.resources >= cost}
|
||||
{@const currentProd = gen.baseProduction * gen.owned}
|
||||
<div
|
||||
class="gp-row {canAfford ? 'gp-row--active' : 'gp-row--locked'}"
|
||||
@@ -39,7 +39,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => gameStore.buy(gen.id)}
|
||||
onclick={() => buy(gen.id)}
|
||||
disabled={!canAfford}
|
||||
class="gp-btn {canAfford ? 'gp-btn--buy' : 'gp-btn--disabled'}"
|
||||
>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state } from '$lib/stores/game.svelte';
|
||||
import { formatNumber } from '$lib/utils/formatNumber';
|
||||
import { getPrestigeThreshold } from '$lib/core/economy';
|
||||
|
||||
let threshold = $derived(getPrestigeThreshold(gameStore.state));
|
||||
let progress = $derived(Math.min(gameStore.state.resources / threshold, 1));
|
||||
let threshold = $derived(getPrestigeThreshold(state));
|
||||
let progress = $derived(Math.min(state.resources / threshold, 1));
|
||||
let progressPercent = $derived((progress * 100).toFixed(1));
|
||||
let remaining = $derived(Math.max(threshold - gameStore.state.resources, 0));
|
||||
let remaining = $derived(Math.max(threshold - state.resources, 0));
|
||||
</script>
|
||||
|
||||
<div class="gp gap-1">
|
||||
<div class="flex justify-between">
|
||||
<span class="gp-label">Prochaine Generation</span>
|
||||
<span class="gp-label">{formatNumber(gameStore.state.resources)} / {formatNumber(threshold)}</span>
|
||||
<span class="gp-label">{formatNumber(state.resources)} / {formatNumber(threshold)}</span>
|
||||
</div>
|
||||
<div class="gp-progress">
|
||||
<div class="gp-progress-fill bg-gradient-to-r from-violet-600 to-violet-400" style="width: {progressPercent}%"></div>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { fly, scale } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state, doClaimMilestone } from '$lib/stores/game.svelte';
|
||||
import { getClaimableMilestones, getNextMilestone } from '$lib/core/economy';
|
||||
import { PRESTIGE_MILESTONES } from '$lib/data/prestigeMilestones';
|
||||
import CollapsiblePanel from './CollapsiblePanel.svelte';
|
||||
|
||||
let claimable = $derived(getClaimableMilestones(gameStore.state));
|
||||
let nextMilestone = $derived(getNextMilestone(gameStore.state));
|
||||
let claimed = $derived(gameStore.state.claimedMilestones ?? []);
|
||||
let claimable = $derived(getClaimableMilestones(state));
|
||||
let nextMilestone = $derived(getNextMilestone(state));
|
||||
let claimed = $derived(state.claimedMilestones ?? []);
|
||||
let totalClaimed = $derived(claimed.length);
|
||||
</script>
|
||||
|
||||
{#if gameStore.state.prestigeCount >= 1}
|
||||
{#if state.prestigeCount >= 1}
|
||||
<CollapsiblePanel
|
||||
title="Milestones"
|
||||
badge="{totalClaimed}/{PRESTIGE_MILESTONES.length}"
|
||||
@@ -29,7 +29,7 @@
|
||||
<span class="gp-value text-[0.7rem]!">{m.name}</span>
|
||||
<span class="gp-label">{m.reward.label}</span>
|
||||
</div>
|
||||
<button onclick={() => gameStore.claimMilestone(m.id)} class="gp-btn gp-btn--buy">
|
||||
<button onclick={() => doClaimMilestone(m.id)} class="gp-btn gp-btn--buy">
|
||||
Claim
|
||||
</button>
|
||||
</div>
|
||||
@@ -38,11 +38,11 @@
|
||||
{/if}
|
||||
|
||||
{#if nextMilestone}
|
||||
{@const progressPct = Math.min((gameStore.state.prestigeCount / nextMilestone.threshold) * 100, 100).toFixed(1)}
|
||||
{@const progressPct = Math.min((state.prestigeCount / nextMilestone.threshold) * 100, 100).toFixed(1)}
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex justify-between">
|
||||
<span class="gp-label">Prochain : {nextMilestone.name}</span>
|
||||
<span class="gp-label">{gameStore.state.prestigeCount}/{nextMilestone.threshold}</span>
|
||||
<span class="gp-label">{state.prestigeCount}/{nextMilestone.threshold}</span>
|
||||
</div>
|
||||
<div class="gp-progress">
|
||||
<div class="gp-progress-fill bg-gradient-to-r from-purple-600 to-purple-400" style="width: {progressPct}%"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { fade, fly, scale } from 'svelte/transition';
|
||||
import { backOut, quintOut } from 'svelte/easing';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { refs, dismissOfflineReport } from '$lib/stores/game.svelte';
|
||||
import { formatNumber } from '$lib/utils/formatNumber';
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
@@ -12,16 +12,16 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={(e) => { if (e.key === 'Escape' && gameStore.offlineReport) gameStore.dismissOfflineReport(); }} />
|
||||
<svelte:window onkeydown={(e) => { if (e.key === 'Escape' && refs.offlineReport) dismissOfflineReport(); }} />
|
||||
|
||||
{#if gameStore.offlineReport}
|
||||
{#if refs.offlineReport}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background: rgba(0,0,0,0.7); backdrop-filter: blur(6px);"
|
||||
transition:fade={{ duration: 250 }}
|
||||
onclick={() => gameStore.dismissOfflineReport()}
|
||||
onclick={() => dismissOfflineReport()}
|
||||
>
|
||||
<div
|
||||
class="gp max-w-sm w-full mx-4 text-center"
|
||||
@@ -32,7 +32,7 @@
|
||||
<div in:fly={{ y: -15, delay: 100, duration: 350, easing: quintOut }}>
|
||||
<h2 class="gp-title text-lg!">Retour au Marais</h2>
|
||||
<p class="gp-label mt-2">
|
||||
Absent pendant <span class="gp-accent-green">{formatDuration(gameStore.offlineReport.duration)}</span>
|
||||
Absent pendant <span class="gp-accent-green">{formatDuration(refs.offlineReport.duration)}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -41,17 +41,17 @@
|
||||
class="gp-value text-3xl! mt-4 mb-2 gp-accent-green"
|
||||
style="text-shadow: 0 0 15px rgba(52,211,153,0.3);"
|
||||
>
|
||||
+{formatNumber(gameStore.offlineReport.gains)} tetards
|
||||
+{formatNumber(refs.offlineReport.gains)} tetards
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="gp-label" in:fade={{ delay: 300, duration: 300 }}>
|
||||
Efficacite : {Math.round(gameStore.offlineReport.efficiency * 100)}%
|
||||
Efficacite : {Math.round(refs.offlineReport.efficiency * 100)}%
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="gp-btn gp-btn--buy mt-4 w-full py-2.5! text-[0.8rem]!"
|
||||
onclick={() => gameStore.dismissOfflineReport()}
|
||||
onclick={() => dismissOfflineReport()}
|
||||
in:fly={{ y: 15, delay: 400, duration: 300, easing: quintOut }}
|
||||
>
|
||||
Continuer
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { scale } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state, getCanPrestige, openPrestige } from '$lib/stores/game.svelte';
|
||||
import { computePrestigeDna, getPrestigeDnaBonus, getPrestigeThreshold } from '$lib/core/economy';
|
||||
import { formatNumber } from '$lib/utils/formatNumber';
|
||||
import CollapsiblePanel from './CollapsiblePanel.svelte';
|
||||
|
||||
let baseDna = $derived(computePrestigeDna(gameStore.state.lifetimeTadpoles, gameStore.state.prestigeCount));
|
||||
let dnaBonus = $derived(getPrestigeDnaBonus(gameStore.state.evolutionTree));
|
||||
let baseDna = $derived(computePrestigeDna(state.lifetimeTadpoles, state.prestigeCount));
|
||||
let dnaBonus = $derived(getPrestigeDnaBonus(state.evolutionTree));
|
||||
let dnaPreview = $derived(Math.floor(baseDna * (1 + dnaBonus)));
|
||||
let threshold = $derived(getPrestigeThreshold(gameStore.state));
|
||||
let progress = $derived(Math.min(gameStore.state.lifetimeTadpoles / threshold * 100, 100));
|
||||
let threshold = $derived(getPrestigeThreshold(state));
|
||||
let progress = $derived(Math.min(state.lifetimeTadpoles / threshold * 100, 100));
|
||||
</script>
|
||||
|
||||
<CollapsiblePanel title="Prestige" accentClass="gp-accent-purple">
|
||||
{#if gameStore.canPrestige}
|
||||
{#if getCanPrestige()}
|
||||
<div class="flex flex-col gap-2" in:scale={{ duration: 300, start: 0.9, easing: quintOut }}>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="gp-value gp-accent-purple">+{dnaPreview} ADN</span>
|
||||
<span class="gp-label">+0.1x mult</span>
|
||||
</div>
|
||||
<button onclick={() => gameStore.openPrestige()} class="gp-btn gp-btn--prestige w-full py-2.5!">
|
||||
<button onclick={() => openPrestige()} class="gp-btn gp-btn--prestige w-full py-2.5!">
|
||||
Nouvelle Generation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { fly, scale, fade } from 'svelte/transition';
|
||||
import { quintOut, backOut } from 'svelte/easing';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state, refs, prestige, closePrestige } from '$lib/stores/game.svelte';
|
||||
import { computePrestigeDna, getPrestigeDnaBonus, getPrestigeThreshold } from '$lib/core/economy';
|
||||
import { formatNumber } from '$lib/utils/formatNumber';
|
||||
|
||||
@@ -15,24 +15,24 @@
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
let baseDna = $derived(computePrestigeDna(gameStore.state.lifetimeTadpoles, gameStore.state.prestigeCount));
|
||||
let dnaBonus = $derived(getPrestigeDnaBonus(gameStore.state.evolutionTree));
|
||||
let baseDna = $derived(computePrestigeDna(state.lifetimeTadpoles, state.prestigeCount));
|
||||
let dnaBonus = $derived(getPrestigeDnaBonus(state.evolutionTree));
|
||||
let dnaPreview = $derived(Math.floor(baseDna * (1 + dnaBonus)));
|
||||
let threshold = $derived(getPrestigeThreshold(gameStore.state));
|
||||
let canPrestige = $derived(gameStore.state.lifetimeTadpoles >= threshold);
|
||||
let runDuration = $derived(Date.now() - gameStore.state.runStats.startedAt);
|
||||
let bestRun = $derived(gameStore.state.runStats.bestRun);
|
||||
let threshold = $derived(getPrestigeThreshold(state));
|
||||
let canPrestige = $derived(state.lifetimeTadpoles >= threshold);
|
||||
let runDuration = $derived(Date.now() - state.runStats.startedAt);
|
||||
let bestRun = $derived(state.runStats.bestRun);
|
||||
let isBestAdn = $derived(!bestRun || dnaPreview > bestRun.adn);
|
||||
let isBestTadpoles = $derived(!bestRun || gameStore.state.lifetimeTadpoles > bestRun.tadpoles);
|
||||
let isBestTadpoles = $derived(!bestRun || state.lifetimeTadpoles > bestRun.tadpoles);
|
||||
|
||||
function handlePrestige() {
|
||||
if (canPrestige) gameStore.prestige();
|
||||
if (canPrestige) prestige();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={(e) => { if (e.key === 'Escape' && gameStore.showPrestigeScreen) gameStore.closePrestige(); }} />
|
||||
<svelte:window onkeydown={(e) => { if (e.key === 'Escape' && refs.showPrestigeScreen) closePrestige(); }} />
|
||||
|
||||
{#if gameStore.showPrestigeScreen}
|
||||
{#if refs.showPrestigeScreen}
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
@@ -48,7 +48,7 @@
|
||||
<!-- Header with generation number -->
|
||||
<div class="text-center" in:fly={{ y: -20, delay: 100, duration: 400, easing: quintOut }}>
|
||||
<span class="gp-title text-lg!">Nouvelle Generation</span>
|
||||
<p class="gp-label mt-1">Generation #{gameStore.state.prestigeCount + 1}</p>
|
||||
<p class="gp-label mt-1">Generation #{state.prestigeCount + 1}</p>
|
||||
</div>
|
||||
|
||||
<div class="gp-sep"></div>
|
||||
@@ -68,7 +68,7 @@
|
||||
{#if dnaBonus > 0}
|
||||
<span class="gp-label">(base {formatNumber(baseDna)} + {Math.round(dnaBonus * 100)}% arbre)</span>
|
||||
{/if}
|
||||
<span class="gp-label mt-1">Total apres : {formatNumber(gameStore.state.ancestralDna + dnaPreview)} ADN</span>
|
||||
<span class="gp-label mt-1">Total apres : {formatNumber(state.ancestralDna + dnaPreview)} ADN</span>
|
||||
</div>
|
||||
|
||||
<div class="gp-sep"></div>
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="flex justify-between">
|
||||
<span class="gp-label">Tetards produits</span>
|
||||
<span class="gp-value {isBestTadpoles ? 'gp-accent-green' : ''}">
|
||||
{formatNumber(gameStore.state.lifetimeTadpoles)}
|
||||
{formatNumber(state.lifetimeTadpoles)}
|
||||
{#if isBestTadpoles && bestRun} ★{/if}
|
||||
</span>
|
||||
</div>
|
||||
@@ -141,7 +141,7 @@
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2 mt-1" in:fly={{ y: 20, delay: 450, duration: 300, easing: quintOut }}>
|
||||
<button
|
||||
onclick={() => gameStore.closePrestige()}
|
||||
onclick={() => closePrestige()}
|
||||
class="gp-btn flex-1 py-2.5! text-[0.8rem]!"
|
||||
style="background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.6);"
|
||||
>
|
||||
@@ -153,7 +153,7 @@
|
||||
</button>
|
||||
{:else}
|
||||
<button class="gp-btn gp-btn--disabled flex-1 py-2.5!" disabled>
|
||||
{formatNumber(threshold - gameStore.state.lifetimeTadpoles)} manquants
|
||||
{formatNumber(threshold - state.lifetimeTadpoles)} manquants
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state } from '$lib/stores/game.svelte';
|
||||
import { COSMETICS, type CosmeticSlot } from '$lib/core/cosmetics';
|
||||
|
||||
const SLOT_ORDER: CosmeticSlot[] = ['body', 'tail', 'eyes', 'hat', 'accessory'];
|
||||
@@ -7,7 +7,7 @@
|
||||
let overlays = $derived(
|
||||
SLOT_ORDER
|
||||
.map((slot) => {
|
||||
const cosId = gameStore.state.cosmeticEquipped[slot];
|
||||
const cosId = state.cosmeticEquipped[slot];
|
||||
if (!cosId) return null;
|
||||
return COSMETICS.find((c) => c.id === cosId) ?? null;
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// save-sync.ts — Auto-save game state to backend every 30s
|
||||
// Server = authority. NEVER save before server state is loaded (ready guard).
|
||||
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state, refs, loadFromServer as storeLoadFromServer, initGuest } from '$lib/stores/game.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { migrateSave } from '$lib/core/migrateSave';
|
||||
import type { GameState } from '$lib/core/economy';
|
||||
@@ -27,12 +27,12 @@ let loaded = false;
|
||||
let saveInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
export async function saveToServer() {
|
||||
if (!authStore.user || !gameStore.ready) return;
|
||||
if (!authStore.user || !refs.ready) return;
|
||||
const result = await apiRequest('/save', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
gameState: gameStore.state,
|
||||
playTimeSeconds: gameStore.playSeconds,
|
||||
gameState: state,
|
||||
playTimeSeconds: refs.playSeconds,
|
||||
}),
|
||||
});
|
||||
if (result?.lastSave) {
|
||||
@@ -51,7 +51,7 @@ export async function loadFromServer(): Promise<boolean> {
|
||||
const data = await apiRequest('/save');
|
||||
if (data?.gameState) {
|
||||
const migrated = migrateSave(data.gameState);
|
||||
gameStore.loadFromServer(migrated);
|
||||
storeLoadFromServer(migrated);
|
||||
lastSave = data.lastSave;
|
||||
console.info('[SaveSync] Loaded save from server (v%d)', migrated.saveVersion);
|
||||
return true;
|
||||
@@ -67,7 +67,7 @@ export async function loadFromServer(): Promise<boolean> {
|
||||
export function startAutoSave() {
|
||||
stopAutoSave();
|
||||
saveInterval = setInterval(() => {
|
||||
if (authStore.user && gameStore.ready) saveToServer();
|
||||
if (authStore.user && refs.ready) saveToServer();
|
||||
}, SAVE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export function setupVisibilitySync() {
|
||||
if (data?.gameState && data.lastSave) {
|
||||
if (!lastSave || new Date(data.lastSave) > new Date(lastSave)) {
|
||||
const migrated = migrateSave(data.gameState);
|
||||
gameStore.loadFromServer(migrated);
|
||||
storeLoadFromServer(migrated);
|
||||
lastSave = data.lastSave;
|
||||
console.info('[SaveSync] Reloaded from server on focus');
|
||||
}
|
||||
@@ -97,14 +97,14 @@ export function setupVisibilitySync() {
|
||||
});
|
||||
|
||||
window.addEventListener('blur', () => {
|
||||
if (authStore.user && gameStore.ready) saveToServer();
|
||||
if (authStore.user && refs.ready) saveToServer();
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (!authStore.user || !gameStore.ready) return;
|
||||
if (!authStore.user || !refs.ready) return;
|
||||
const payload = JSON.stringify({
|
||||
gameState: gameStore.state,
|
||||
playTimeSeconds: gameStore.playSeconds,
|
||||
gameState: state,
|
||||
playTimeSeconds: refs.playSeconds,
|
||||
});
|
||||
fetch(`${BACKEND_URL}/api/save`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// game.svelte.ts — Game store (Svelte 5 runes)
|
||||
// Server = authority. localStorage = fallback guest only.
|
||||
// Architecture: $state for all reactive values, direct access (no getter indirection).
|
||||
|
||||
import {
|
||||
type GameState,
|
||||
@@ -33,8 +34,6 @@ import {
|
||||
const SAVE_KEY = 'clickerz_state';
|
||||
const OFFLINE_THRESHOLD = 60_000;
|
||||
|
||||
// --- Offline report ---
|
||||
|
||||
export interface OfflineReport {
|
||||
wasOffline: boolean;
|
||||
duration: number;
|
||||
@@ -42,31 +41,22 @@ export interface OfflineReport {
|
||||
efficiency: number;
|
||||
}
|
||||
|
||||
// --- Reactive state (Svelte 5 runes) ---
|
||||
// $state.raw for GameState — replaced entirely on every mutation, never deep-patched.
|
||||
// This ensures Svelte detects every change by reference equality.
|
||||
// --- All reactive state ---
|
||||
// Svelte 5 modules cannot export $state that is reassigned.
|
||||
// Use a reactive container object + property mutation instead.
|
||||
|
||||
let _stateVersion = $state(0);
|
||||
let _state: GameState = { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||
let playSeconds = $state(0);
|
||||
let ready = $state(false);
|
||||
let offlineReport = $state<OfflineReport | null>(null);
|
||||
let showPrestigeScreen = $state(false);
|
||||
let lastClickGain = $state(0);
|
||||
let lastClickDouble = $state(false);
|
||||
let lastClickCrit = $state(false);
|
||||
export const refs = $state({
|
||||
ready: false,
|
||||
playSeconds: 0,
|
||||
offlineReport: null as OfflineReport | null,
|
||||
showPrestigeScreen: false,
|
||||
lastClickGain: 0,
|
||||
lastClickDouble: false,
|
||||
lastClickCrit: false,
|
||||
});
|
||||
|
||||
// Bump version to trigger reactivity on state reads
|
||||
function setState(newState: GameState) {
|
||||
_state = newState;
|
||||
_stateVersion++;
|
||||
}
|
||||
|
||||
function getState(): GameState {
|
||||
// Read _stateVersion to create a reactive dependency
|
||||
void _stateVersion;
|
||||
return _state;
|
||||
}
|
||||
// Main game state: exported as $state object, mutated via Object.assign (never reassigned).
|
||||
export const state = $state<GameState>({ ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() });
|
||||
|
||||
// --- Local storage ---
|
||||
|
||||
@@ -99,29 +89,25 @@ function hydrateWithOffline(saved: GameState, now: number): { state: GameState;
|
||||
const fullGains = pps * (elapsed / 1000);
|
||||
const avgEfficiency = fullGains > 0 ? gains / fullGains : 0;
|
||||
|
||||
const hydrated: GameState = {
|
||||
...saved,
|
||||
resources: saved.resources + gains,
|
||||
lifetimeTadpoles: saved.lifetimeTadpoles + gains,
|
||||
lastTick: now,
|
||||
lastOnline: now,
|
||||
};
|
||||
|
||||
return {
|
||||
state: hydrated,
|
||||
state: {
|
||||
...saved,
|
||||
resources: saved.resources + gains,
|
||||
lifetimeTadpoles: saved.lifetimeTadpoles + gains,
|
||||
lastTick: now,
|
||||
lastOnline: now,
|
||||
},
|
||||
report: { wasOffline: true, duration: elapsed, gains, efficiency: avgEfficiency },
|
||||
};
|
||||
}
|
||||
|
||||
// --- Actions ---
|
||||
// --- Actions (exported directly, no wrapper object) ---
|
||||
|
||||
function tick() {
|
||||
if (!ready) return;
|
||||
export function tick() {
|
||||
if (!refs.ready) return;
|
||||
const now = Date.now();
|
||||
const updated = applyIdleGains(_state, now);
|
||||
updated.lastOnline = now;
|
||||
const updated = { ...applyIdleGains(state, now), lastOnline: now };
|
||||
|
||||
// Auto-click from evolution tree
|
||||
const autoClicks = getAutoClicksPerSecond(updated.evolutionTree);
|
||||
if (autoClicks > 0) {
|
||||
const autoGain = getClickGain(updated) * autoClicks;
|
||||
@@ -129,8 +115,7 @@ function tick() {
|
||||
updated.lifetimeTadpoles += autoGain;
|
||||
}
|
||||
|
||||
// Check cosmetic unlocks every 5s
|
||||
if (playSeconds % 5 === 0) {
|
||||
if (refs.playSeconds % 5 === 0) {
|
||||
const cosState = { inventory: updated.cosmeticInventory, equipped: updated.cosmeticEquipped };
|
||||
const newUnlocks = computeNewUnlocks(updated, cosState);
|
||||
if (newUnlocks.length > 0) {
|
||||
@@ -141,167 +126,144 @@ function tick() {
|
||||
}
|
||||
|
||||
saveLocal(updated);
|
||||
setState(updated);
|
||||
playSeconds += 1;
|
||||
Object.assign(state, updated);
|
||||
refs.playSeconds += 1;
|
||||
}
|
||||
|
||||
function click() {
|
||||
if (!ready) return;
|
||||
const result = applyClick(applyIdleGains(_state, Date.now()));
|
||||
export function click() {
|
||||
if (!refs.ready) return;
|
||||
const result = applyClick(applyIdleGains(state, Date.now()));
|
||||
saveLocal(result.state);
|
||||
setState(result.state);
|
||||
lastClickGain = result.gain;
|
||||
lastClickDouble = result.isDouble;
|
||||
lastClickCrit = result.isCrit;
|
||||
Object.assign(state, result.state);
|
||||
refs.lastClickGain = result.gain;
|
||||
refs.lastClickDouble = result.isDouble;
|
||||
refs.lastClickCrit = result.isCrit;
|
||||
}
|
||||
|
||||
function buy(genId: string) {
|
||||
if (!ready) return;
|
||||
const withIdle = applyIdleGains(_state, Date.now());
|
||||
const updated = buyGenerator(withIdle, genId);
|
||||
export function buy(genId: string) {
|
||||
if (!refs.ready) return;
|
||||
const updated = buyGenerator(applyIdleGains(state, Date.now()), genId);
|
||||
if (!updated) return;
|
||||
saveLocal(updated);
|
||||
setState(updated);
|
||||
Object.assign(state, updated);
|
||||
}
|
||||
|
||||
function buyNode(nodeId: string) {
|
||||
if (!ready) return;
|
||||
const updated = buyEvolutionNode(_state, nodeId);
|
||||
export function buyNode(nodeId: string) {
|
||||
if (!refs.ready) return;
|
||||
const updated = buyEvolutionNode(state, nodeId);
|
||||
if (!updated) return;
|
||||
const node = updated.evolutionTree.find((n) => n.id === nodeId);
|
||||
saveLocal(updated);
|
||||
if (node?.capstone) {
|
||||
toast(`Capstone debloque : ${node.name} !`, 'reward', 5000);
|
||||
}
|
||||
setState(updated);
|
||||
if (node?.capstone) toast(`Capstone debloque : ${node.name} !`, 'reward', 5000);
|
||||
Object.assign(state, updated);
|
||||
}
|
||||
|
||||
function prestige() {
|
||||
if (!ready) return;
|
||||
if (!canPrestigeCheck(_state)) return;
|
||||
const updated = applyPrestige(_state);
|
||||
export function prestige() {
|
||||
if (!refs.ready) return;
|
||||
if (!canPrestigeCheck(state)) return;
|
||||
const updated = applyPrestige(state);
|
||||
saveLocal(updated);
|
||||
toast(`Generation #${updated.prestigeCount} — Nouvelle vie !`, 'success', 4000);
|
||||
setState(updated);
|
||||
showPrestigeScreen = false;
|
||||
Object.assign(state, updated);
|
||||
refs.showPrestigeScreen = false;
|
||||
}
|
||||
|
||||
function equipCosmetic(cosmeticId: string) {
|
||||
if (!ready) return;
|
||||
const cosState = { inventory: _state.cosmeticInventory, equipped: _state.cosmeticEquipped };
|
||||
export function equipCosmetic(cosmeticId: string) {
|
||||
if (!refs.ready) return;
|
||||
const cosState = { inventory: state.cosmeticInventory, equipped: state.cosmeticEquipped };
|
||||
const updated = equipCosmeticFn(cosState, cosmeticId);
|
||||
const newState = { ..._state, cosmeticEquipped: updated.equipped };
|
||||
saveLocal(newState);
|
||||
setState(newState);
|
||||
state.cosmeticEquipped = updated.equipped;
|
||||
saveLocal(state);
|
||||
}
|
||||
|
||||
function unequipCosmetic(slot: CosmeticSlot) {
|
||||
if (!ready) return;
|
||||
const cosState = { inventory: _state.cosmeticInventory, equipped: _state.cosmeticEquipped };
|
||||
export function unequipCosmetic(slot: CosmeticSlot) {
|
||||
if (!refs.ready) return;
|
||||
const cosState = { inventory: state.cosmeticInventory, equipped: state.cosmeticEquipped };
|
||||
const updated = unequipSlotFn(cosState, slot);
|
||||
const newState = { ..._state, cosmeticEquipped: updated.equipped };
|
||||
saveLocal(newState);
|
||||
setState(newState);
|
||||
state.cosmeticEquipped = updated.equipped;
|
||||
saveLocal(state);
|
||||
}
|
||||
|
||||
function doResetTree() {
|
||||
if (!ready) return;
|
||||
if (!canResetTree(_state)) return;
|
||||
const updated = resetEvolutionTree(_state);
|
||||
export function doResetTree() {
|
||||
if (!refs.ready) return;
|
||||
if (!canResetTree(state)) return;
|
||||
const updated = resetEvolutionTree(state);
|
||||
saveLocal(updated);
|
||||
setState(updated);
|
||||
Object.assign(state, updated);
|
||||
}
|
||||
|
||||
function doUpgradeConvergence() {
|
||||
if (!ready) return;
|
||||
const updated = upgradeConvergence(_state);
|
||||
export function doUpgradeConvergence() {
|
||||
if (!refs.ready) return;
|
||||
const updated = upgradeConvergence(state);
|
||||
if (!updated) return;
|
||||
saveLocal(updated);
|
||||
setState(updated);
|
||||
Object.assign(state, updated);
|
||||
}
|
||||
|
||||
function doClaimMilestone(milestoneId: string) {
|
||||
if (!ready) return;
|
||||
const updated = claimMilestoneFn(_state, milestoneId);
|
||||
export function doClaimMilestone(milestoneId: string) {
|
||||
if (!refs.ready) return;
|
||||
const updated = claimMilestoneFn(state, milestoneId);
|
||||
if (!updated) return;
|
||||
saveLocal(updated);
|
||||
toast('Milestone debloque !', 'reward', 4000);
|
||||
setState(updated);
|
||||
Object.assign(state, updated);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
export function reset() {
|
||||
const fresh = { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||
saveLocal(fresh);
|
||||
setState(fresh);
|
||||
playSeconds = 0;
|
||||
ready = true;
|
||||
offlineReport = null;
|
||||
Object.assign(state, fresh);
|
||||
refs.playSeconds = 0;
|
||||
refs.ready = true;
|
||||
refs.offlineReport = null;
|
||||
}
|
||||
|
||||
function loadFromServer(serverState: GameState) {
|
||||
export function loadFromServer(serverState: GameState) {
|
||||
const migrated = migrateSave(serverState as unknown as Record<string, unknown>);
|
||||
const result = hydrateWithOffline(migrated, Date.now());
|
||||
saveLocal(result.state);
|
||||
setState(result.state);
|
||||
ready = true;
|
||||
offlineReport = result.report;
|
||||
Object.assign(state, result.state);
|
||||
refs.ready = true;
|
||||
refs.offlineReport = result.report;
|
||||
}
|
||||
|
||||
function initGuest() {
|
||||
export function initGuest() {
|
||||
const local = loadLocalState();
|
||||
const result = hydrateWithOffline(local, Date.now());
|
||||
saveLocal(result.state);
|
||||
setState(result.state);
|
||||
ready = true;
|
||||
offlineReport = result.report;
|
||||
Object.assign(state, result.state);
|
||||
refs.ready = true;
|
||||
refs.offlineReport = result.report;
|
||||
}
|
||||
|
||||
function dismissOfflineReport() {
|
||||
offlineReport = null;
|
||||
export function dismissOfflineReport() {
|
||||
refs.offlineReport = null;
|
||||
}
|
||||
|
||||
function openPrestige() {
|
||||
showPrestigeScreen = true;
|
||||
export function openPrestige() {
|
||||
refs.showPrestigeScreen = true;
|
||||
}
|
||||
|
||||
function closePrestige() {
|
||||
showPrestigeScreen = false;
|
||||
export function closePrestige() {
|
||||
refs.showPrestigeScreen = false;
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
// All state reads go through getState() which depends on _stateVersion.
|
||||
// This guarantees that any component reading gameStore.state re-renders
|
||||
// whenever setState() is called — even for deep properties like evolutionTree[i].branch.
|
||||
// --- Computed helpers (call from templates, they read `state` reactively) ---
|
||||
|
||||
export const gameStore = {
|
||||
get state() { return getState(); },
|
||||
get playSeconds() { return playSeconds; },
|
||||
get ready() { return ready; },
|
||||
get offlineReport() { return offlineReport; },
|
||||
get showPrestigeScreen() { return showPrestigeScreen; },
|
||||
get lastClickGain() { return lastClickGain; },
|
||||
get lastClickDouble() { return lastClickDouble; },
|
||||
get lastClickCrit() { return lastClickCrit; },
|
||||
get canPrestige() { return canPrestigeCheck(getState()); },
|
||||
get productionPerSecond() { return totalProductionPerSecond(getState()); },
|
||||
export function getProductionPerSecond() {
|
||||
return totalProductionPerSecond(state);
|
||||
}
|
||||
|
||||
tick,
|
||||
click,
|
||||
buy,
|
||||
buyNode,
|
||||
prestige,
|
||||
equipCosmetic,
|
||||
unequipCosmetic,
|
||||
resetTree: doResetTree,
|
||||
upgradeConvergence: doUpgradeConvergence,
|
||||
claimMilestone: doClaimMilestone,
|
||||
reset,
|
||||
loadFromServer,
|
||||
initGuest,
|
||||
dismissOfflineReport,
|
||||
openPrestige,
|
||||
closePrestige,
|
||||
generatorCost: genCost,
|
||||
generatorCostWithTree: (gen: Parameters<typeof genCost>[0]) => genCost(gen, getState().evolutionTree),
|
||||
getClickGain: () => getClickGain(getState()),
|
||||
};
|
||||
export function getCanPrestige() {
|
||||
return canPrestigeCheck(state);
|
||||
}
|
||||
|
||||
export function getCurrentClickGain() {
|
||||
return getClickGain(state);
|
||||
}
|
||||
|
||||
export function getGeneratorCostWithTree(gen: Parameters<typeof genCost>[0]) {
|
||||
return genCost(gen, state.evolutionTree);
|
||||
}
|
||||
|
||||
export { genCost as generatorCost };
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { fly, scale, fade } from 'svelte/transition';
|
||||
import { quintOut, backOut } from 'svelte/easing';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state } from '$lib/stores/game.svelte';
|
||||
import { ACHIEVEMENTS } from '$lib/data/achievements';
|
||||
|
||||
let filter = $state<'all' | 'unlocked' | 'locked'>('all');
|
||||
|
||||
let unlocked = $derived(ACHIEVEMENTS.filter((a) => a.check(gameStore.state)));
|
||||
let locked = $derived(ACHIEVEMENTS.filter((a) => !a.check(gameStore.state)));
|
||||
let unlocked = $derived(ACHIEVEMENTS.filter((a) => a.check(state)));
|
||||
let locked = $derived(ACHIEVEMENTS.filter((a) => !a.check(state)));
|
||||
|
||||
let displayed = $derived(
|
||||
filter === 'unlocked' ? unlocked
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { fly, scale, fade } from 'svelte/transition';
|
||||
import { quintOut, elasticOut } from 'svelte/easing';
|
||||
import { gameStore } from '$lib/stores/game.svelte';
|
||||
import { state, refs, click } 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';
|
||||
@@ -16,7 +16,7 @@
|
||||
import SidebarTabs from '$lib/components/SidebarTabs.svelte';
|
||||
import { ACHIEVEMENTS } from '$lib/data/achievements';
|
||||
|
||||
let achieveCount = $derived(ACHIEVEMENTS.filter((a) => a.check(gameStore.state)).length);
|
||||
let achieveCount = $derived(ACHIEVEMENTS.filter((a) => a.check(state)).length);
|
||||
|
||||
const sidebarTabs = [
|
||||
{ id: 'production', label: 'Production', icon: '🏭' },
|
||||
@@ -29,8 +29,8 @@
|
||||
let tadpoleSprite: ReturnType<typeof TadpoleSprite>;
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
gameStore.click();
|
||||
clickParticles?.spawn(e.clientX, e.clientY, gameStore.lastClickGain, gameStore.lastClickDouble, gameStore.lastClickCrit);
|
||||
click();
|
||||
clickParticles?.spawn(e.clientX, e.clientY, refs.lastClickGain, refs.lastClickDouble, refs.lastClickCrit);
|
||||
tadpoleSprite?.bounce();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<meta name="description" content="Clickerz — Clicker idle dans le Tetard Universe." />
|
||||
</svelte:head>
|
||||
|
||||
{#if !gameStore.ready}
|
||||
{#if !refs.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>
|
||||
@@ -66,7 +66,7 @@
|
||||
class="click-zone-counter"
|
||||
in:fly={{ y: 20, duration: 400, easing: quintOut }}
|
||||
>
|
||||
{formatNumber(gameStore.state.resources)}
|
||||
{formatNumber(state.resources)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user