fix: refactor store to singleton class pattern (s.subscribe fix)
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 21s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 21s
Exported $state proxies were confused with Svelte stores by SvelteKit
runtime, causing "s.subscribe is not a function" on /jeu.
Fix: encapsulate all $state fields in a Game class, export singleton.
Components import { game } and access game.state, game.click(), etc.
Class fields are proper $state — no raw proxy exported.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { state, getProductionPerSecond, getCurrentClickGain } from '$lib/stores/game.svelte';
|
||||
import { game } 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(getProductionPerSecond())}</span>
|
||||
<span class="gp-value gp-accent-green text-[0.8rem]!">{formatNumber(game.productionPerSecond)}</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(getCurrentClickGain())}</span>
|
||||
<span class="gp-value text-[0.8rem]!">{formatNumber(game.clickGain)}</span>
|
||||
</div>
|
||||
<div class="gp-stat" title="Multiplicateur global (prestige)">
|
||||
<span class="gp-label">Mult</span>
|
||||
<span class="gp-value text-[0.8rem]!">x{state.prestigeMultiplier.toFixed(1)}</span>
|
||||
<span class="gp-value text-[0.8rem]!">x{game.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]!">{state.ancestralDna}</span>
|
||||
<span class="gp-value gp-accent-purple text-[0.8rem]!">{game.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]!">{state.prestigeCount}</span>
|
||||
<span class="gp-value text-[0.8rem]!">{game.state.prestigeCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { state, equipCosmetic, unequipCosmetic } from '$lib/stores/game.svelte';
|
||||
import { game } 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(state.cosmeticInventory);
|
||||
let equipped = $derived(state.cosmeticEquipped);
|
||||
let inventory = $derived(game.state.cosmeticInventory);
|
||||
let equipped = $derived(game.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 ? unequipCosmetic(slot) : equipCosmetic(cos.id)}
|
||||
onclick={() => isEquipped ? game.unequipCosmetic(slot) : game.equipCosmetic(cos.id)}
|
||||
class="gp-btn {isEquipped ? 'gp-btn--disabled' : 'gp-btn--buy'}"
|
||||
>
|
||||
{isEquipped ? 'Retirer' : 'Equiper'}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { state, buyNode, doResetTree, doUpgradeConvergence } from '$lib/stores/game.svelte';
|
||||
import { game } 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(state.evolutionTree.filter((n) => n.branch === activeBranch));
|
||||
let spentDna = $derived(getSpentDna(state.evolutionTree));
|
||||
let branchNodes = $derived(game.state.evolutionTree.filter((n) => n.branch === activeBranch));
|
||||
let spentDna = $derived(getSpentDna(game.state.evolutionTree));
|
||||
let hasUnlocked = $derived(spentDna > 0);
|
||||
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));
|
||||
let resetCost = $derived(getTreeResetCost(game.state));
|
||||
let canReset = $derived(canResetTree(game.state));
|
||||
let conv = $derived(game.state.evolutionTree.find((n) => n.id === 'convergence'));
|
||||
let canBuyConv = $derived(canBuyEvolutionNode(game.state, 'convergence'));
|
||||
let canUpgradeConv = $derived(canUpgradeConvergence(game.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) doResetTree();
|
||||
if (confirmed) game.resetTree();
|
||||
}
|
||||
|
||||
function getNodeRowClass(node: EvolutionNode, isExcluded: boolean, canBuy: boolean): string {
|
||||
@@ -70,13 +70,13 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if state.prestigeCount >= 1}
|
||||
{#if game.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(state.ancestralDna)} ADN</span>
|
||||
<span class="gp-value gp-accent-amber">{formatNumber(game.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 ? (state.evolutionTree.find((n) => n.id === node.exclusive_with)?.unlocked ?? false) : false}
|
||||
{@const canBuy = canBuyEvolutionNode(state, node.id)}
|
||||
{@const isExcluded = node.exclusive_with ? (game.state.evolutionTree.find((n) => n.id === node.exclusive_with)?.unlocked ?? false) : false}
|
||||
{@const canBuy = canBuyEvolutionNode(game.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={() => buyNode(node.id)}
|
||||
onclick={() => game.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={() => doUpgradeConvergence()}
|
||||
onclick={() => game.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)`}
|
||||
@@ -180,7 +180,7 @@
|
||||
</div>
|
||||
<button
|
||||
disabled={!canBuyConv}
|
||||
onclick={() => buyNode('convergence')}
|
||||
onclick={() => game.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 { refs, initGuest } from '$lib/stores/game.svelte';
|
||||
import { game } 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 && !refs.ready) {
|
||||
initGuest();
|
||||
if (!loaded && !game.ready) {
|
||||
game.initGuest();
|
||||
}
|
||||
startAutoSave();
|
||||
setupVisibilitySync();
|
||||
} else {
|
||||
initGuest();
|
||||
game.initGuest();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { tick } from '$lib/stores/game.svelte';
|
||||
import { game } from '$lib/stores/game.svelte';
|
||||
|
||||
let interval: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
onMount(() => {
|
||||
interval = setInterval(() => tick(), 1000);
|
||||
interval = setInterval(() => game.tick(), 1000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { scale } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { state, buy, getProductionPerSecond, getGeneratorCostWithTree } from '$lib/stores/game.svelte';
|
||||
import { game } from '$lib/stores/game.svelte';
|
||||
import { formatNumber } from '$lib/utils/formatNumber';
|
||||
import CollapsiblePanel from './CollapsiblePanel.svelte';
|
||||
</script>
|
||||
|
||||
<CollapsiblePanel
|
||||
title="Generateurs"
|
||||
badge="{formatNumber(getProductionPerSecond())}/s"
|
||||
badge="{formatNumber(game.productionPerSecond)}/s"
|
||||
accentClass=""
|
||||
>
|
||||
{#each state.generators as gen, i}
|
||||
{@const cost = getGeneratorCostWithTree(gen)}
|
||||
{@const canAfford = state.resources >= cost}
|
||||
{#each game.state.generators as gen, i}
|
||||
{@const cost = game.generatorCostWithTree(gen)}
|
||||
{@const canAfford = game.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={() => buy(gen.id)}
|
||||
onclick={() => game.buy(gen.id)}
|
||||
disabled={!canAfford}
|
||||
class="gp-btn {canAfford ? 'gp-btn--buy' : 'gp-btn--disabled'}"
|
||||
>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { state } from '$lib/stores/game.svelte';
|
||||
import { game } from '$lib/stores/game.svelte';
|
||||
import { formatNumber } from '$lib/utils/formatNumber';
|
||||
import { getPrestigeThreshold } from '$lib/core/economy';
|
||||
|
||||
let threshold = $derived(getPrestigeThreshold(state));
|
||||
let progress = $derived(Math.min(state.resources / threshold, 1));
|
||||
let threshold = $derived(getPrestigeThreshold(game.state));
|
||||
let progress = $derived(Math.min(game.state.resources / threshold, 1));
|
||||
let progressPercent = $derived((progress * 100).toFixed(1));
|
||||
let remaining = $derived(Math.max(threshold - state.resources, 0));
|
||||
let remaining = $derived(Math.max(threshold - game.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(state.resources)} / {formatNumber(threshold)}</span>
|
||||
<span class="gp-label">{formatNumber(game.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 { state, doClaimMilestone } from '$lib/stores/game.svelte';
|
||||
import { game } 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(state));
|
||||
let nextMilestone = $derived(getNextMilestone(state));
|
||||
let claimed = $derived(state.claimedMilestones ?? []);
|
||||
let claimable = $derived(getClaimableMilestones(game.state));
|
||||
let nextMilestone = $derived(getNextMilestone(game.state));
|
||||
let claimed = $derived(game.state.claimedMilestones ?? []);
|
||||
let totalClaimed = $derived(claimed.length);
|
||||
</script>
|
||||
|
||||
{#if state.prestigeCount >= 1}
|
||||
{#if game.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={() => doClaimMilestone(m.id)} class="gp-btn gp-btn--buy">
|
||||
<button onclick={() => game.claimMilestone(m.id)} class="gp-btn gp-btn--buy">
|
||||
Claim
|
||||
</button>
|
||||
</div>
|
||||
@@ -38,11 +38,11 @@
|
||||
{/if}
|
||||
|
||||
{#if nextMilestone}
|
||||
{@const progressPct = Math.min((state.prestigeCount / nextMilestone.threshold) * 100, 100).toFixed(1)}
|
||||
{@const progressPct = Math.min((game.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">{state.prestigeCount}/{nextMilestone.threshold}</span>
|
||||
<span class="gp-label">{game.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 { refs, dismissOfflineReport } from '$lib/stores/game.svelte';
|
||||
import { game } 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' && refs.offlineReport) dismissOfflineReport(); }} />
|
||||
<svelte:window onkeydown={(e) => { if (e.key === 'Escape' && game.offlineReport) game.dismissOfflineReport(); }} />
|
||||
|
||||
{#if refs.offlineReport}
|
||||
{#if game.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={() => dismissOfflineReport()}
|
||||
onclick={() => game.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(refs.offlineReport.duration)}</span>
|
||||
Absent pendant <span class="gp-accent-green">{formatDuration(game.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(refs.offlineReport.gains)} tetards
|
||||
+{formatNumber(game.offlineReport.gains)} tetards
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="gp-label" in:fade={{ delay: 300, duration: 300 }}>
|
||||
Efficacite : {Math.round(refs.offlineReport.efficiency * 100)}%
|
||||
Efficacite : {Math.round(game.offlineReport.efficiency * 100)}%
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="gp-btn gp-btn--buy mt-4 w-full py-2.5! text-[0.8rem]!"
|
||||
onclick={() => dismissOfflineReport()}
|
||||
onclick={() => game.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 { state, getCanPrestige, openPrestige } from '$lib/stores/game.svelte';
|
||||
import { game } 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(state.lifetimeTadpoles, state.prestigeCount));
|
||||
let dnaBonus = $derived(getPrestigeDnaBonus(state.evolutionTree));
|
||||
let baseDna = $derived(computePrestigeDna(game.state.lifetimeTadpoles, game.state.prestigeCount));
|
||||
let dnaBonus = $derived(getPrestigeDnaBonus(game.state.evolutionTree));
|
||||
let dnaPreview = $derived(Math.floor(baseDna * (1 + dnaBonus)));
|
||||
let threshold = $derived(getPrestigeThreshold(state));
|
||||
let progress = $derived(Math.min(state.lifetimeTadpoles / threshold * 100, 100));
|
||||
let threshold = $derived(getPrestigeThreshold(game.state));
|
||||
let progress = $derived(Math.min(game.state.lifetimeTadpoles / threshold * 100, 100));
|
||||
</script>
|
||||
|
||||
<CollapsiblePanel title="Prestige" accentClass="gp-accent-purple">
|
||||
{#if getCanPrestige()}
|
||||
{#if game.canPrestige}
|
||||
<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={() => openPrestige()} class="gp-btn gp-btn--prestige w-full py-2.5!">
|
||||
<button onclick={() => game.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 { state, refs, prestige, closePrestige } from '$lib/stores/game.svelte';
|
||||
import { game } 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(state.lifetimeTadpoles, state.prestigeCount));
|
||||
let dnaBonus = $derived(getPrestigeDnaBonus(state.evolutionTree));
|
||||
let baseDna = $derived(computePrestigeDna(game.game.state.lifetimeTadpoles, game.game.state.prestigeCount));
|
||||
let dnaBonus = $derived(getPrestigeDnaBonus(game.state.evolutionTree));
|
||||
let dnaPreview = $derived(Math.floor(baseDna * (1 + dnaBonus)));
|
||||
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 threshold = $derived(getPrestigeThreshold(game.state));
|
||||
let canPrestige = $derived(game.game.state.lifetimeTadpoles >= threshold);
|
||||
let runDuration = $derived(Date.now() - game.state.runStats.startedAt);
|
||||
let bestRun = $derived(game.state.runStats.bestRun);
|
||||
let isBestAdn = $derived(!bestRun || dnaPreview > bestRun.adn);
|
||||
let isBestTadpoles = $derived(!bestRun || state.lifetimeTadpoles > bestRun.tadpoles);
|
||||
let isBestTadpoles = $derived(!bestRun || game.game.state.lifetimeTadpoles > bestRun.tadpoles);
|
||||
|
||||
function handlePrestige() {
|
||||
if (canPrestige) prestige();
|
||||
if (canPrestige) game.prestige();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={(e) => { if (e.key === 'Escape' && refs.showPrestigeScreen) closePrestige(); }} />
|
||||
<svelte:window onkeydown={(e) => { if (e.key === 'Escape' && game.showPrestigeScreen) game.game.closePrestige(); }} />
|
||||
|
||||
{#if refs.showPrestigeScreen}
|
||||
{#if game.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 #{state.prestigeCount + 1}</p>
|
||||
<p class="gp-label mt-1">Generation #{game.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(state.ancestralDna + dnaPreview)} ADN</span>
|
||||
<span class="gp-label mt-1">Total apres : {formatNumber(game.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(state.lifetimeTadpoles)}
|
||||
{formatNumber(game.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={() => closePrestige()}
|
||||
onclick={() => game.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 - state.lifetimeTadpoles)} manquants
|
||||
{formatNumber(threshold - game.state.lifetimeTadpoles)} manquants
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { state } from '$lib/stores/game.svelte';
|
||||
import { game } 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 = state.cosmeticEquipped[slot];
|
||||
const cosId = game.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 { state, refs, loadFromServer as storeLoadFromServer, initGuest } from '$lib/stores/game.svelte';
|
||||
import { game } 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 || !refs.ready) return;
|
||||
if (!authStore.user || !game.ready) return;
|
||||
const result = await apiRequest('/save', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
gameState: state,
|
||||
playTimeSeconds: refs.playSeconds,
|
||||
gameState: game.state,
|
||||
playTimeSeconds: game.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);
|
||||
storeLoadFromServer(migrated);
|
||||
game.loadFromServer(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 && refs.ready) saveToServer();
|
||||
if (authStore.user && game.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);
|
||||
storeLoadFromServer(migrated);
|
||||
game.loadFromServer(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 && refs.ready) saveToServer();
|
||||
if (authStore.user && game.ready) saveToServer();
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (!authStore.user || !refs.ready) return;
|
||||
if (!authStore.user || !game.ready) return;
|
||||
const payload = JSON.stringify({
|
||||
gameState: state,
|
||||
playTimeSeconds: refs.playSeconds,
|
||||
gameState: game.state,
|
||||
playTimeSeconds: game.playSeconds,
|
||||
});
|
||||
fetch(`${BACKEND_URL}/api/save`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// game.svelte.ts — Game store (Svelte 5 runes)
|
||||
// game.svelte.ts — Game store (Svelte 5 class pattern)
|
||||
// Server = authority. localStorage = fallback guest only.
|
||||
// Architecture: $state for all reactive values, direct access (no getter indirection).
|
||||
// Pattern: singleton class with $state fields — the officially recommended
|
||||
// Svelte 5 approach for shared reactive state across components.
|
||||
|
||||
import {
|
||||
type GameState,
|
||||
@@ -41,72 +42,61 @@ export interface OfflineReport {
|
||||
efficiency: number;
|
||||
}
|
||||
|
||||
// --- All reactive state ---
|
||||
// Svelte 5 modules cannot export $state that is reassigned.
|
||||
// Use a reactive container object + property mutation instead.
|
||||
class Game {
|
||||
// --- Reactive fields ---
|
||||
state = $state<GameState>({ ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() });
|
||||
playSeconds = $state(0);
|
||||
ready = $state(false);
|
||||
offlineReport = $state<OfflineReport | null>(null);
|
||||
showPrestigeScreen = $state(false);
|
||||
lastClickGain = $state(0);
|
||||
lastClickDouble = $state(false);
|
||||
lastClickCrit = $state(false);
|
||||
|
||||
export const refs = $state({
|
||||
ready: false,
|
||||
playSeconds: 0,
|
||||
offlineReport: null as OfflineReport | null,
|
||||
showPrestigeScreen: false,
|
||||
lastClickGain: 0,
|
||||
lastClickDouble: false,
|
||||
lastClickCrit: false,
|
||||
});
|
||||
// --- Derived (computed live from state) ---
|
||||
get canPrestige() { return canPrestigeCheck(this.state); }
|
||||
get productionPerSecond() { return totalProductionPerSecond(this.state); }
|
||||
get clickGain() { return getClickGain(this.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 ---
|
||||
|
||||
function loadLocalState(): GameState {
|
||||
// --- Private helpers ---
|
||||
private loadLocalState(): GameState {
|
||||
try {
|
||||
const raw = localStorage.getItem(SAVE_KEY);
|
||||
if (!raw) return { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||
const parsed = JSON.parse(raw);
|
||||
const saved = migrateSave(parsed);
|
||||
return applyIdleGains(saved, Date.now());
|
||||
return applyIdleGains(migrateSave(JSON.parse(raw)), Date.now());
|
||||
} catch {
|
||||
return { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||
}
|
||||
}
|
||||
|
||||
function saveLocal(s: GameState): void {
|
||||
localStorage.setItem(SAVE_KEY, JSON.stringify(s));
|
||||
}
|
||||
|
||||
function hydrateWithOffline(saved: GameState, now: number): { state: GameState; report: OfflineReport | null } {
|
||||
const elapsed = now - saved.lastTick;
|
||||
|
||||
if (elapsed <= OFFLINE_THRESHOLD) {
|
||||
const hydrated = applyIdleGains(saved, now);
|
||||
return { state: { ...hydrated, lastOnline: now }, report: null };
|
||||
}
|
||||
|
||||
private saveLocal(s: GameState) {
|
||||
localStorage.setItem(SAVE_KEY, JSON.stringify(s));
|
||||
}
|
||||
|
||||
private hydrateWithOffline(saved: GameState, now: number) {
|
||||
const elapsed = now - saved.lastTick;
|
||||
if (elapsed <= OFFLINE_THRESHOLD) {
|
||||
return { state: { ...applyIdleGains(saved, now), lastOnline: now }, report: null };
|
||||
}
|
||||
const gains = computeOfflineGains(saved, now);
|
||||
const pps = totalProductionPerSecond(saved);
|
||||
const fullGains = pps * (elapsed / 1000);
|
||||
const avgEfficiency = fullGains > 0 ? gains / fullGains : 0;
|
||||
|
||||
return {
|
||||
state: {
|
||||
...saved,
|
||||
resources: saved.resources + gains,
|
||||
lifetimeTadpoles: saved.lifetimeTadpoles + gains,
|
||||
lastTick: now,
|
||||
lastOnline: now,
|
||||
},
|
||||
report: { wasOffline: true, duration: elapsed, gains, efficiency: avgEfficiency },
|
||||
state: { ...saved, resources: saved.resources + gains, lifetimeTadpoles: saved.lifetimeTadpoles + gains, lastTick: now, lastOnline: now },
|
||||
report: { wasOffline: true, duration: elapsed, gains, efficiency: fullGains > 0 ? gains / fullGains : 0 } as OfflineReport,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// --- Actions (exported directly, no wrapper object) ---
|
||||
private applyState(updated: GameState) {
|
||||
this.saveLocal(updated);
|
||||
Object.assign(this.state, updated);
|
||||
}
|
||||
|
||||
export function tick() {
|
||||
if (!refs.ready) return;
|
||||
// --- Actions ---
|
||||
tick() {
|
||||
if (!this.ready) return;
|
||||
const now = Date.now();
|
||||
const updated = { ...applyIdleGains(state, now), lastOnline: now };
|
||||
const updated = { ...applyIdleGains(this.state, now), lastOnline: now };
|
||||
|
||||
const autoClicks = getAutoClicksPerSecond(updated.evolutionTree);
|
||||
if (autoClicks > 0) {
|
||||
@@ -115,155 +105,117 @@ export function tick() {
|
||||
updated.lifetimeTadpoles += autoGain;
|
||||
}
|
||||
|
||||
if (refs.playSeconds % 5 === 0) {
|
||||
if (this.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;
|
||||
updated.cosmeticInventory = addToInventory(cosState, newUnlocks).inventory;
|
||||
newUnlocks.forEach(() => toast('Nouveau cosmetique debloque !', 'reward'));
|
||||
}
|
||||
}
|
||||
|
||||
saveLocal(updated);
|
||||
Object.assign(state, updated);
|
||||
refs.playSeconds += 1;
|
||||
}
|
||||
this.applyState(updated);
|
||||
this.playSeconds += 1;
|
||||
}
|
||||
|
||||
export function click() {
|
||||
if (!refs.ready) return;
|
||||
const result = applyClick(applyIdleGains(state, Date.now()));
|
||||
saveLocal(result.state);
|
||||
Object.assign(state, result.state);
|
||||
refs.lastClickGain = result.gain;
|
||||
refs.lastClickDouble = result.isDouble;
|
||||
refs.lastClickCrit = result.isCrit;
|
||||
}
|
||||
click() {
|
||||
if (!this.ready) return;
|
||||
const result = applyClick(applyIdleGains(this.state, Date.now()));
|
||||
this.applyState(result.state);
|
||||
this.lastClickGain = result.gain;
|
||||
this.lastClickDouble = result.isDouble;
|
||||
this.lastClickCrit = result.isCrit;
|
||||
}
|
||||
|
||||
export function buy(genId: string) {
|
||||
if (!refs.ready) return;
|
||||
const updated = buyGenerator(applyIdleGains(state, Date.now()), genId);
|
||||
if (!updated) return;
|
||||
saveLocal(updated);
|
||||
Object.assign(state, updated);
|
||||
}
|
||||
buy(genId: string) {
|
||||
if (!this.ready) return;
|
||||
const updated = buyGenerator(applyIdleGains(this.state, Date.now()), genId);
|
||||
if (updated) this.applyState(updated);
|
||||
}
|
||||
|
||||
export function buyNode(nodeId: string) {
|
||||
if (!refs.ready) return;
|
||||
const updated = buyEvolutionNode(state, nodeId);
|
||||
buyNode(nodeId: string) {
|
||||
if (!this.ready) return;
|
||||
const updated = buyEvolutionNode(this.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);
|
||||
Object.assign(state, updated);
|
||||
}
|
||||
this.applyState(updated);
|
||||
}
|
||||
|
||||
export function prestige() {
|
||||
if (!refs.ready) return;
|
||||
if (!canPrestigeCheck(state)) return;
|
||||
const updated = applyPrestige(state);
|
||||
saveLocal(updated);
|
||||
prestige() {
|
||||
if (!this.ready || !canPrestigeCheck(this.state)) return;
|
||||
const updated = applyPrestige(this.state);
|
||||
toast(`Generation #${updated.prestigeCount} — Nouvelle vie !`, 'success', 4000);
|
||||
Object.assign(state, updated);
|
||||
refs.showPrestigeScreen = false;
|
||||
}
|
||||
this.applyState(updated);
|
||||
this.showPrestigeScreen = false;
|
||||
}
|
||||
|
||||
export function equipCosmetic(cosmeticId: string) {
|
||||
if (!refs.ready) return;
|
||||
const cosState = { inventory: state.cosmeticInventory, equipped: state.cosmeticEquipped };
|
||||
const updated = equipCosmeticFn(cosState, cosmeticId);
|
||||
state.cosmeticEquipped = updated.equipped;
|
||||
saveLocal(state);
|
||||
}
|
||||
equipCosmetic(cosmeticId: string) {
|
||||
if (!this.ready) return;
|
||||
const cosState = { inventory: this.state.cosmeticInventory, equipped: this.state.cosmeticEquipped };
|
||||
this.state.cosmeticEquipped = equipCosmeticFn(cosState, cosmeticId).equipped;
|
||||
this.saveLocal(this.state);
|
||||
}
|
||||
|
||||
export function unequipCosmetic(slot: CosmeticSlot) {
|
||||
if (!refs.ready) return;
|
||||
const cosState = { inventory: state.cosmeticInventory, equipped: state.cosmeticEquipped };
|
||||
const updated = unequipSlotFn(cosState, slot);
|
||||
state.cosmeticEquipped = updated.equipped;
|
||||
saveLocal(state);
|
||||
}
|
||||
unequipCosmetic(slot: CosmeticSlot) {
|
||||
if (!this.ready) return;
|
||||
const cosState = { inventory: this.state.cosmeticInventory, equipped: this.state.cosmeticEquipped };
|
||||
this.state.cosmeticEquipped = unequipSlotFn(cosState, slot).equipped;
|
||||
this.saveLocal(this.state);
|
||||
}
|
||||
|
||||
export function doResetTree() {
|
||||
if (!refs.ready) return;
|
||||
if (!canResetTree(state)) return;
|
||||
const updated = resetEvolutionTree(state);
|
||||
saveLocal(updated);
|
||||
Object.assign(state, updated);
|
||||
}
|
||||
resetTree() {
|
||||
if (!this.ready || !canResetTree(this.state)) return;
|
||||
this.applyState(resetEvolutionTree(this.state));
|
||||
}
|
||||
|
||||
export function doUpgradeConvergence() {
|
||||
if (!refs.ready) return;
|
||||
const updated = upgradeConvergence(state);
|
||||
upgradeConvergence() {
|
||||
if (!this.ready) return;
|
||||
const updated = upgradeConvergence(this.state);
|
||||
if (updated) this.applyState(updated);
|
||||
}
|
||||
|
||||
claimMilestone(milestoneId: string) {
|
||||
if (!this.ready) return;
|
||||
const updated = claimMilestoneFn(this.state, milestoneId);
|
||||
if (!updated) return;
|
||||
saveLocal(updated);
|
||||
Object.assign(state, updated);
|
||||
}
|
||||
|
||||
export function doClaimMilestone(milestoneId: string) {
|
||||
if (!refs.ready) return;
|
||||
const updated = claimMilestoneFn(state, milestoneId);
|
||||
if (!updated) return;
|
||||
saveLocal(updated);
|
||||
toast('Milestone debloque !', 'reward', 4000);
|
||||
Object.assign(state, updated);
|
||||
}
|
||||
this.applyState(updated);
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
reset() {
|
||||
const fresh = { ...DEFAULT_STATE, lastTick: Date.now(), lastOnline: Date.now() };
|
||||
saveLocal(fresh);
|
||||
Object.assign(state, fresh);
|
||||
refs.playSeconds = 0;
|
||||
refs.ready = true;
|
||||
refs.offlineReport = null;
|
||||
}
|
||||
this.applyState(fresh);
|
||||
this.playSeconds = 0;
|
||||
this.ready = true;
|
||||
this.offlineReport = null;
|
||||
}
|
||||
|
||||
export function loadFromServer(serverState: GameState) {
|
||||
loadFromServer(serverState: GameState) {
|
||||
const migrated = migrateSave(serverState as unknown as Record<string, unknown>);
|
||||
const result = hydrateWithOffline(migrated, Date.now());
|
||||
saveLocal(result.state);
|
||||
Object.assign(state, result.state);
|
||||
refs.ready = true;
|
||||
refs.offlineReport = result.report;
|
||||
const result = this.hydrateWithOffline(migrated, Date.now());
|
||||
this.applyState(result.state);
|
||||
this.ready = true;
|
||||
this.offlineReport = result.report;
|
||||
}
|
||||
|
||||
initGuest() {
|
||||
const local = this.loadLocalState();
|
||||
const result = this.hydrateWithOffline(local, Date.now());
|
||||
this.applyState(result.state);
|
||||
this.ready = true;
|
||||
this.offlineReport = result.report;
|
||||
}
|
||||
|
||||
dismissOfflineReport() { this.offlineReport = null; }
|
||||
openPrestige() { this.showPrestigeScreen = true; }
|
||||
closePrestige() { this.showPrestigeScreen = false; }
|
||||
|
||||
generatorCost = genCost;
|
||||
generatorCostWithTree(gen: Parameters<typeof genCost>[0]) {
|
||||
return genCost(gen, this.state.evolutionTree);
|
||||
}
|
||||
}
|
||||
|
||||
export function initGuest() {
|
||||
const local = loadLocalState();
|
||||
const result = hydrateWithOffline(local, Date.now());
|
||||
saveLocal(result.state);
|
||||
Object.assign(state, result.state);
|
||||
refs.ready = true;
|
||||
refs.offlineReport = result.report;
|
||||
}
|
||||
|
||||
export function dismissOfflineReport() {
|
||||
refs.offlineReport = null;
|
||||
}
|
||||
|
||||
export function openPrestige() {
|
||||
refs.showPrestigeScreen = true;
|
||||
}
|
||||
|
||||
export function closePrestige() {
|
||||
refs.showPrestigeScreen = false;
|
||||
}
|
||||
|
||||
// --- Computed helpers (call from templates, they read `state` reactively) ---
|
||||
|
||||
export function getProductionPerSecond() {
|
||||
return totalProductionPerSecond(state);
|
||||
}
|
||||
|
||||
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 };
|
||||
// Singleton — import { game } from '$lib/stores/game.svelte';
|
||||
export const game = new Game();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { fly, scale, fade } from 'svelte/transition';
|
||||
import { quintOut, backOut } from 'svelte/easing';
|
||||
import { state } from '$lib/stores/game.svelte';
|
||||
import { game } 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(state)));
|
||||
let locked = $derived(ACHIEVEMENTS.filter((a) => !a.check(state)));
|
||||
let unlocked = $derived(ACHIEVEMENTS.filter((a) => a.check(game.state)));
|
||||
let locked = $derived(ACHIEVEMENTS.filter((a) => !a.check(game.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 { state, refs, click } from '$lib/stores/game.svelte';
|
||||
import { game } 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(state)).length);
|
||||
let achieveCount = $derived(ACHIEVEMENTS.filter((a) => a.check(game.state)).length);
|
||||
|
||||
const sidebarTabs = [
|
||||
{ id: 'production', label: 'Production', icon: '🏭' },
|
||||
@@ -29,8 +29,8 @@
|
||||
let tadpoleSprite: ReturnType<typeof TadpoleSprite>;
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
click();
|
||||
clickParticles?.spawn(e.clientX, e.clientY, refs.lastClickGain, refs.lastClickDouble, refs.lastClickCrit);
|
||||
game.click();
|
||||
clickParticles?.spawn(e.clientX, e.clientY, game.lastClickGain, game.lastClickDouble, game.lastClickCrit);
|
||||
tadpoleSprite?.bounce();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<meta name="description" content="Clickerz — Clicker idle dans le Tetard Universe." />
|
||||
</svelte:head>
|
||||
|
||||
{#if !refs.ready}
|
||||
{#if !game.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(state.resources)}
|
||||
{formatNumber(game.state.resources)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user