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