diff --git a/src/components/Rendering/canvasRenderer.tsx b/src/components/Rendering/canvasRenderer.tsx index a50156b..0a09ad0 100644 --- a/src/components/Rendering/canvasRenderer.tsx +++ b/src/components/Rendering/canvasRenderer.tsx @@ -164,6 +164,7 @@ export function SimpleRenderCanvas({ layers, invert = false }: SimpleRenderProps } + // Advanced card rendering with canvas export function RenderImagesWithCanvas({layers, invert = false, spacing = false}: RenderCanvasProps) { const canvasRef = useRef(null); diff --git a/src/components/SeedInputAutoComplete.tsx b/src/components/SeedInputAutoComplete.tsx index 37517f5..879a620 100644 --- a/src/components/SeedInputAutoComplete.tsx +++ b/src/components/SeedInputAutoComplete.tsx @@ -1,16 +1,51 @@ -import {Autocomplete, Button, Group, NativeSelect, Paper} from "@mantine/core"; +import React, {useState, useRef} from "react"; +import { Autocomplete, Button, Group, NativeSelect, Paper } from "@mantine/core"; +import { useDebouncedCallback } from "@mantine/hooks"; import {popularSeeds, SeedsWithLegendary} from "../modules/const.ts"; import {useCardStore} from "../modules/state/store.ts"; +import {sanitizeSeed} from "../modules/utils.ts"; +const seedAutoCompleteData = [ + { + group: 'Popular Seeds', + items: popularSeeds + }, { + group: 'Generated Seeds With Legendary Jokers', + items: SeedsWithLegendary + } +]; + +const allSuggestions = [...popularSeeds, ...SeedsWithLegendary]; + +interface SeedInputProps { + seed: string; + setSeed: (seed: string) => void; + w?: number | string; + showDeckSelect?: boolean; + label?: string; + placeholder?: string; +} + +function SeedInputAutoComplete({ seed, setSeed, w, showDeckSelect, label = 'Seed', placeholder = 'Enter Seed' }: SeedInputProps) { + const [localSeed, setLocalSeed] = useState(seed); + const isDirty = useRef(false); + + // Sync from store when not actively editing + if (!isDirty.current && localSeed !== seed) { + setLocalSeed(seed); + } + + const debouncedSetSeed = useDebouncedCallback((value: string) => { + setLocalSeed(sanitizeSeed(value)); + if (value) setSeed(value); + isDirty.current = false; + }, 160); -export function QuickAnalyze() { - const seed = useCardStore(state => state.immolateState.seed); - const setSeed = useCardStore(state => state.setSeed); const deck = useCardStore(state => state.immolateState.deck); const setDeck = useCardStore(state => state.setDeck); - const setStart = useCardStore(state => state.setStart); + const sectionWidth = 130; - const select = ( + const deckSelect = showDeckSelect ? ( Plasma Deck + ) : undefined; + + return ( + { + isDirty.current = true; + setLocalSeed(value); + if (allSuggestions.includes(value)) { + setSeed(value); + isDirty.current = false; + } else { + debouncedSetSeed(value); + } + }} + rightSection={deckSelect} + rightSectionWidth={showDeckSelect ? sectionWidth : undefined} + /> ); +} + +export function QuickAnalyze() { + const seed = useCardStore(state => state.immolateState.seed); + const setSeed = useCardStore(state => state.setSeed); + const setStart = useCardStore(state => state.setStart); + return ( - setSeed(e)} - rightSection={select} - rightSectionWidth={sectionWidth} /> - + ); - } -export default function SeedInputAutoComplete({seed, setSeed}: { seed: string, setSeed: (seed: string) => void }) { - return ( - setSeed(e)} - data={[ - { - group: 'Popular Seeds', - items: popularSeeds - }, { - group: 'Generated Seeds With Legendary Jokers', - items: SeedsWithLegendary - - } - ]} - /> - ); -} \ No newline at end of file +export default SeedInputAutoComplete; \ No newline at end of file diff --git a/src/components/blueprint/layout/footer.tsx b/src/components/blueprint/layout/footer.tsx index ff222a7..53aefea 100644 --- a/src/components/blueprint/layout/footer.tsx +++ b/src/components/blueprint/layout/footer.tsx @@ -17,7 +17,7 @@ import {GaEvent} from "../../../modules/useGA.ts"; export default function Footer() { - const {data: supporters, isPending} = useQuery>({ + const {data: supporters, isPending: isPending} = useQuery>({ queryKey: ['supporters'], queryFn: async () => { const response = await fetch('https://ttyyetpmvt.a.pinggy.link/supporters', { diff --git a/src/components/blueprint/layout/navbar.tsx b/src/components/blueprint/layout/navbar.tsx index 918dcd0..3b07f03 100644 --- a/src/components/blueprint/layout/navbar.tsx +++ b/src/components/blueprint/layout/navbar.tsx @@ -17,7 +17,7 @@ import { useMantineColorScheme, useMantineTheme } from "@mantine/core"; -import React from "react"; +import React, {useState, useEffect} from "react"; import { IconFileText, IconJoker, @@ -30,9 +30,10 @@ import { import { useCardStore } from "../../../modules/state/store.ts"; import UnlocksModal from "../../unlocksModal.tsx"; import FeaturesModal from "../../FeaturesModal.tsx"; -import { RerollCalculatorModal } from "../../RerollCalculatorModal.tsx"; +import {RerollCalculatorModal} from "../../RerollCalculatorModal.tsx"; +import {GaEvent} from "../../../modules/useGA.ts"; +import { useDebouncedCallback } from "@mantine/hooks"; import { DrawSimulatorModal } from "../../DrawSimulatorModal.tsx"; -import { GaEvent } from "../../../modules/useGA.ts"; import SeedInputAutoComplete from "../../SeedInputAutoComplete.tsx"; import { useBlueprintTheme } from "../../../modules/state/themeProvider.tsx"; import type { KnownThemes } from "../../../modules/state/themeProvider.tsx"; @@ -72,13 +73,16 @@ export default function NavBar() { const reset = useCardStore(state => state.reset); const hasSettingsChanged = useCardStore((state) => state.applicationState.hasSettingsChanged); + const [localAntes, setLocalAntes] = useState(antes); + useEffect(() => { setLocalAntes(antes); }, [antes]); + const debouncedSetAntes = useDebouncedCallback((val: number) => { + if (val !== antes) setAntes(val); + }, 200); + const handleAnalyzeClick = () => { setStart(true); } - - - return ( @@ -160,16 +164,11 @@ export default function NavBar() { id="setting-max-ante" label={'Max Ante'} defaultValue={8} - value={antes} - onChange={(val) => { - // Guard against null/NaN from the NumberInput - if (val === null || Number.isNaN(Number(val))) { - // keep previous value (do not set) or fallback to 1 to be defensive - // Here we fallback to 1 to avoid passing invalid values into the engine - setAntes(1); - } else { - setAntes(Math.floor(Number(val))); - } + value={localAntes} + onChange={(val: number | string) => { + const num = typeof val === 'string' ? parseInt(val) || 8 : val; + setLocalAntes(num); + debouncedSetAntes(num); }} /> (null); - const [visibleAntes, setVisibleAntes] = useState>([1]); // Start with first ante visible - const [loadingNextAnte, setLoadingNextAnte] = useState(2); // Track which ante is loading - const selectedAnte = useCardStore(state => state.applicationState.selectedAnte); - const setSelectedAnte = useCardStore(state => state.setSelectedAnte); - const debouncedSetSelectedAnte = useDebouncedCallback(setSelectedAnte, 500) + const [visibleAntes, setVisibleAntes] = useState>([1]); + const [loadingNextAnte, setLoadingNextAnte] = useState(2); const lockedCards = useCardStore(state => state.lockState.lockedCards); const clearLockedCards = useCardStore(state => state.clearLockedCards); const hasLockedCards = Object.keys(lockedCards).length > 0; @@ -373,12 +370,6 @@ function Simple() { useEffect(() => { if (entry?.isIntersecting) { - // When this ante is visible, make the next one available - const currentAnte = anteNumber; - if (currentAnte !== selectedAnte) { - debouncedSetSelectedAnte(currentAnte); - } - // Set the current ante as selected const nextAnte = anteNumber + 1; if (nextAnte <= anteEntries.length && !visibleAntes.includes(nextAnte)) { diff --git a/src/components/blueprint/standardView/index.tsx b/src/components/blueprint/standardView/index.tsx index b72e783..adf3cb8 100644 --- a/src/components/blueprint/standardView/index.tsx +++ b/src/components/blueprint/standardView/index.tsx @@ -556,7 +556,7 @@ export function Blueprint() { const outputOpened = useCardStore(state => state.applicationState.asideOpen); const download = useDownloadSeedResults() useEffect(() => { - if (typeof window !== 'undefined') { + if(typeof window !== 'undefined' && !!download) { window.saveSeedDebug = download } }, [download]); diff --git a/src/modules/ImmolateWrapper/CardEngines/Cards.ts b/src/modules/ImmolateWrapper/CardEngines/Cards.ts index 1e05a78..6c682b0 100644 --- a/src/modules/ImmolateWrapper/CardEngines/Cards.ts +++ b/src/modules/ImmolateWrapper/CardEngines/Cards.ts @@ -295,9 +295,11 @@ export class Pack { } export class SeedResultsContainer { + isLoading: boolean; antes: { [key: number]: Ante }; constructor() { this.antes = {} + this.isLoading = true; } } export interface Blind { diff --git a/src/modules/ImmolateWrapper/index.ts b/src/modules/ImmolateWrapper/index.ts index 34a57aa..ecd452c 100644 --- a/src/modules/ImmolateWrapper/index.ts +++ b/src/modules/ImmolateWrapper/index.ts @@ -25,11 +25,14 @@ import { StandardCard_Final, Tarot_Final } from "./CardEngines/Cards.ts"; + import type { Voucher } from "../balatrots/enum/Voucher.ts"; import { Edition, EditionItem } from "../balatrots/enum/Edition.ts"; import { Seal, SealItem } from "../balatrots/enum/Seal.ts"; import type { DeckCard } from "../deckUtils.ts"; +import {sanitizeSeed} from "../utils.ts"; + export type SpoilableItems = "The Soul" | "Judgement" | "Wraith"; export interface MiscCardSource { name: SpoilableItems | string; @@ -489,7 +492,7 @@ export const getMiscCardSources: (maxCards: number) => Array = ( export function analyzeSeed(settings: AnalyzeSettings, analyzeOptions: AnalyzeOptions) { - const seed = settings.seed.toUpperCase().replace(/0/g, 'O').trim(); + const seed = sanitizeSeed(settings.seed); if (!seed) return; // Sanitize antes coming from settings (could be null/NaN/0 if UI or URL provided empty value) @@ -498,6 +501,7 @@ export function analyzeSeed(settings: AnalyzeSettings, analyzeOptions: AnalyzeOp const maxAntes = Math.max(1, safeAntes); const output = new SeedResultsContainer(); + // isLoading starts true in constructor, analysis populates antes, provider sets false after const deck = new Deck(deckMap[settings.deck]) const stake = new Stake(settings.stake as StakeType) const version = Number(settings.gameVersion) diff --git a/src/modules/state/analysisResultProvider.tsx b/src/modules/state/analysisResultProvider.tsx index c741950..6815a50 100644 --- a/src/modules/state/analysisResultProvider.tsx +++ b/src/modules/state/analysisResultProvider.tsx @@ -9,7 +9,6 @@ export const SeedResultContext = createContext - {children} + {children} ) - - -} +} \ No newline at end of file diff --git a/src/modules/state/downloadProvider.tsx b/src/modules/state/downloadProvider.tsx index d918a44..ea921a0 100644 --- a/src/modules/state/downloadProvider.tsx +++ b/src/modules/state/downloadProvider.tsx @@ -1,7 +1,8 @@ import React, { createContext, useCallback, useContext } from "react"; -import { useCardStore } from "./store.ts"; -import { useSeedOptionsContainer } from "./optionsProvider.tsx"; -import { useSeedResultsContainer } from "./analysisResultProvider.tsx"; +import {useCardStore} from "./store.ts"; +import {useSeedOptionsContainer} from "./optionsProvider.tsx"; +import {SeedResultContext} from "./analysisResultProvider.tsx"; + export type DownloadSeedResultFunction = () => void; export const DownloadSeedResultContext = createContext(undefined); @@ -18,7 +19,7 @@ export function DownloadSeedResultProvider({ children }: { children: React.React const analyzeState = useCardStore(state => state.immolateState); const options = useSeedOptionsContainer() - const SeedResults = useSeedResultsContainer() + const SeedResults = useContext(SeedResultContext); const downloadImmolateResults = useCallback(() => { const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent( diff --git a/src/modules/state/store.ts b/src/modules/state/store.ts index b0ae4e6..5ac5155 100644 --- a/src/modules/state/store.ts +++ b/src/modules/state/store.ts @@ -2,6 +2,7 @@ import { create } from "zustand/index"; import { createJSONStorage, devtools, persist } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; import { LOCATIONS, LOCATION_TYPES, options } from "../const.ts"; +import { sanitizeSeed } from "../utils.ts"; import { convertToDeckCard, generateStartingDeck, convertGameCardToDeckCard } from "../deckUtils.ts"; import type { DeckCard } from "../deckUtils.ts"; import { Game } from "../balatrots/Game.ts"; @@ -201,7 +202,7 @@ const blueprintStorage: StateStorage = { getItem: (): string => { const immolateState = getImmolateStateFromUrl(); - + const hasSeed = !!immolateState.seed; const results = { state: { immolateState: { @@ -210,7 +211,8 @@ const blueprintStorage: StateStorage = { }, applicationState: { ...initialState.applicationState, - start: !!immolateState.seed + start: hasSeed, + settingsOpen: !hasSeed, }, shoppingState: { ...initialState.shoppingState, @@ -243,8 +245,10 @@ function getImmolateStateFromUrl() { const params = new URLSearchParams(window.location.search); const antesParam = params.get('antes'); const parsedAntes = antesParam !== null && antesParam !== '' && !Number.isNaN(Number(antesParam)) ? Number(antesParam) : undefined; + const seedParam = params.get('seed'); + const parsedSeed = seedParam ? sanitizeSeed(seedParam) : initialState.immolateState.seed; return { - seed: params.get('seed') || initialState.immolateState.seed, + seed: parsedSeed, deck: params.get('deck') || initialState.immolateState.deck, cardsPerAnte: parseInt(params.get('cardsPerAnte') || initialState.immolateState.cardsPerAnte.toString()), antes: parsedAntes !== undefined ? parsedAntes : initialState.immolateState.antes, @@ -267,7 +271,8 @@ export const useCardStore = create()( prev.applicationState.viewMode = viewMode; }, undefined, 'Global/SetViewMode'), setSeed: (seed) => set((prev) => { - prev.immolateState.seed = seed.toUpperCase(); + const sanitized = sanitizeSeed(seed); + prev.immolateState.seed = sanitized; prev.shoppingState = initialState.shoppingState prev.searchState = initialState.searchState; prev.applicationState.hasSettingsChanged = true; diff --git a/src/modules/utils.ts b/src/modules/utils.ts index a3e9665..d998bf2 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -1,3 +1,14 @@ +/** + * Sanitize a Balatro seed: uppercase, replace 0→O, strip invalid chars, max 8 chars. + */ +export function sanitizeSeed(seed: string): string { + return seed + .toUpperCase() + .replace(/0/g, 'O') + .replace(/[^A-Z1-9]/g, '') + .slice(0, 8); +} + export function getStandardCardPosition(rank: string, suit: string) { const rankMap:{ [key:string] : number } = { '2': 0, '3': 1, '4': 2, '5': 3, '6': 4, '7': 5, '8': 6, '9': 7, '10': 8, 'Jack': 9, 'Queen': 10, 'King': 11, 'Ace': 12