From 457dd2eda237fbce11069b49b64bc2df4590d795 Mon Sep 17 00:00:00 2001 From: robojumper Date: Wed, 1 Jan 2025 12:55:44 +0100 Subject: [PATCH 01/10] Support randomized crystal counts --- src/data/prettyItemNames.json | 9 ---- .../CrystalAmountsChooser.module.css | 12 +++++ .../CrystalAmountsChooser.module.css.d.ts | 4 ++ src/locationTracker/CrystalAmountsChooser.tsx | 40 +++++++++++++++ src/locationTracker/Locations.tsx | 11 +++++ src/locationTracker/mapTracker/MapMarker.tsx | 15 +++++- src/logic/Logic.ts | 10 +++- src/logic/Mappers.ts | 19 +++++++ .../ThingsThatWouldBeNiceToHaveInTheDump.ts | 7 +++ src/logic/TrackerModifications.ts | 20 ++++++-- src/options/Options.tsx | 1 + src/permalink/SettingsTypes.ts | 3 ++ src/tracker/Selectors.ts | 49 ++++++++++++++++++- src/tracker/Slice.ts | 12 +++++ 14 files changed, 197 insertions(+), 15 deletions(-) create mode 100644 src/locationTracker/CrystalAmountsChooser.module.css create mode 100644 src/locationTracker/CrystalAmountsChooser.module.css.d.ts create mode 100644 src/locationTracker/CrystalAmountsChooser.tsx diff --git a/src/data/prettyItemNames.json b/src/data/prettyItemNames.json index a94a1cad..2c12e9d4 100644 --- a/src/data/prettyItemNames.json +++ b/src/data/prettyItemNames.json @@ -30,15 +30,6 @@ "1": "Slingshot", "2": "Scattershot" }, - "Gratitude Crystal": { - "5": "5 Gratitude Crystals", - "10": "10 Gratitude Crystals", - "30": "30 Gratitude Crystals", - "40": "40 Gratitude Crystals", - "50": "50 Gratitude Crystals", - "70": "70 Gratitude Crystals", - "80": "80 Gratitude Crystals" - }, "Progressive Wallet": { "1": "Medium Wallet", "2": "Big Wallet", diff --git a/src/locationTracker/CrystalAmountsChooser.module.css b/src/locationTracker/CrystalAmountsChooser.module.css new file mode 100644 index 00000000..41b9f904 --- /dev/null +++ b/src/locationTracker/CrystalAmountsChooser.module.css @@ -0,0 +1,12 @@ +.chooser { + padding: 4px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); + + align-items: center; + + > span { + grid-column: 1 / span 2; + margin-right: 20px; + } +} diff --git a/src/locationTracker/CrystalAmountsChooser.module.css.d.ts b/src/locationTracker/CrystalAmountsChooser.module.css.d.ts new file mode 100644 index 00000000..f93a33ea --- /dev/null +++ b/src/locationTracker/CrystalAmountsChooser.module.css.d.ts @@ -0,0 +1,4 @@ +declare const classNames: { + readonly chooser: 'chooser'; +}; +export = classNames; diff --git a/src/locationTracker/CrystalAmountsChooser.tsx b/src/locationTracker/CrystalAmountsChooser.tsx new file mode 100644 index 00000000..7f0b5909 --- /dev/null +++ b/src/locationTracker/CrystalAmountsChooser.tsx @@ -0,0 +1,40 @@ +import { range } from 'es-toolkit'; +import { useDispatch, useSelector } from 'react-redux'; +import { numBatreauxRewardLevels } from '../logic/ThingsThatWouldBeNiceToHaveInTheDump'; +import { requiredCrystalCountsSelector } from '../tracker/Selectors'; +import { setRequiredCrystalCounts } from '../tracker/Slice'; +import styles from './CrystalAmountsChooser.module.css'; + +export function CrystalAmountsChooser() { + const dispatch = useDispatch(); + const counts = useSelector(requiredCrystalCountsSelector); + const updateCount = (val: string, idx: number) => { + let num; + if (val === '') { + num = 0; + } else { + num = parseInt(val, 10); + if (isNaN(num) || num < 0 || num > 80) { + return; + } + } + const newCounts = [...counts]; + newCounts[idx] = num; + dispatch(setRequiredCrystalCounts(newCounts)); + }; + return ( +
+ Enter Crystal Counts: + {range(numBatreauxRewardLevels).map((level) => ( +
+ updateCount(e.target.value, level)} + /> +
+ ))} +
+ ); +} diff --git a/src/locationTracker/Locations.tsx b/src/locationTracker/Locations.tsx index 5305d59f..1747f831 100644 --- a/src/locationTracker/Locations.tsx +++ b/src/locationTracker/Locations.tsx @@ -1,5 +1,8 @@ import React from 'react'; +import { useSelector } from 'react-redux'; import type { HintRegion } from '../logic/Locations'; +import { settingSelector } from '../tracker/Selectors'; +import { CrystalAmountsChooser } from './CrystalAmountsChooser'; import LocationGroup from './LocationGroup'; export function Locations({ @@ -11,8 +14,16 @@ export function Locations({ hintRegion: HintRegion; onChooseEntrance: (exitId: string) => void; }) { + const randomCrystals = + useSelector(settingSelector('batreaux-counts')) === 'Random'; return ( <> + {randomCrystals && hintRegion.name === "Batreaux's House" && ( + <> + +
+ + )} {Boolean(data.checks.numAccessible) && data.checks.numAccessible} + {showEnterBatCounts && '?'} ); } diff --git a/src/logic/Logic.ts b/src/logic/Logic.ts index 8a631df5..1a256eb2 100644 --- a/src/logic/Logic.ts +++ b/src/logic/Logic.ts @@ -21,6 +21,7 @@ import { cubeCheckToCubeCollected, cubeCollectedToCubeCheck, dungeonCompletionItems, + needEnterBatreauxCountsItem, } from './TrackerModifications'; import { type RawArea, @@ -51,6 +52,7 @@ export interface Logic { checksByHintRegion: Record; exitsByHintRegion: Record; dungeonCompletionRequirements: { [dungeon: string]: string }; + needsDynamicBatreauxCrystalCounts: boolean; } export interface LogicalCheck { @@ -326,6 +328,7 @@ export function parseLogic(raw: RawLogic): Logic { ...newItems, ...Object.keys(cubeCollectedToCubeCheck), ...Object.values(dungeonCompletionItems), + needEnterBatreauxCountsItem, ]; // Pessimistically, all items are opaque @@ -960,6 +963,10 @@ export function parseLogic(raw: RawLogic): Logic { exitsByHintRegion, dungeonCompletionRequirements: raw.dungeon_completion_requirements, areaGraph, + // Check if this is a dump that requires additional crystal logic + needsDynamicBatreauxCrystalCounts: Boolean( + itemLookup['\\31 Gratitude Crystals'], + ), }; } @@ -988,7 +995,8 @@ function mapAreaToBitLogic( // Hack: We keep these virtual locations opaque... if ( !locName.endsWith('Gratitude Crystals') && - !locName.includes("\\Gondo's Upgrades\\Upgrade to") + !locName.includes("\\Gondo's Upgrades\\Upgrade to") && + !locName.includes('Can Receive Batreaux Level') ) { opaqueItems.clearBit(b.bit(locName)); } diff --git a/src/logic/Mappers.ts b/src/logic/Mappers.ts index eec93e1f..921b8c5d 100644 --- a/src/logic/Mappers.ts +++ b/src/logic/Mappers.ts @@ -12,11 +12,13 @@ import { gotRaisingReq, hordeDoorReq, impaSongCheck, + numBatreauxRewardLevels, runtimeOptions, swordsToAdd, } from './ThingsThatWouldBeNiceToHaveInTheDump'; import { dungeonCompletionItems, + needEnterBatreauxCountsItem, sothItemReplacement, sothItems, triforceItemReplacement, @@ -30,6 +32,7 @@ export function mapSettings( settings: TypedOptions, exits: ExitMapping[], requiredDungeons: string[], + requiredCrystalCounts: number[], ) { const requirements: Requirements = {}; const b = new LogicBuilder(logic.allItems, logic.itemLookup, requirements); @@ -70,6 +73,22 @@ export function mapSettings( } } + if (logic.needsDynamicBatreauxCrystalCounts) { + for (let i = 0; i < numBatreauxRewardLevels; i++) { + const reqName = `\\Can Receive Batreaux Level ${i + 1} Rewards`; + if (requiredCrystalCounts[i] !== 0) { + b.set( + reqName, + b.singleBit( + `\\${requiredCrystalCounts[i]} Gratitude Crystals`, + ), + ); + } else { + b.set(reqName, b.singleBit(needEnterBatreauxCountsItem)); + } + } + } + const raiseGotExpr = settings['got-start'] === 'Raised' ? b.true() diff --git a/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts b/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts index 41159390..d695a80e 100644 --- a/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts +++ b/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts @@ -107,3 +107,10 @@ export const doesHintDistroUseGossipStone: Record< export const gotOpeningReq = 'GoT Opening Requirement'; export const gotRaisingReq = 'GoT Raising Requirement'; export const hordeDoorReq = 'Horde Door Requirement'; + +export const defaultBatreauxRequiredCrystals = [5, 10, 30, 40, 50, 70, 80]; +export const numBatreauxRewardLevels = defaultBatreauxRequiredCrystals.length; + +export const halfBatreauxRequiredCrystals = defaultBatreauxRequiredCrystals.map( + (amt) => Math.floor(amt / 2), +); diff --git a/src/logic/TrackerModifications.ts b/src/logic/TrackerModifications.ts index b9b48209..67f6b51c 100644 --- a/src/logic/TrackerModifications.ts +++ b/src/logic/TrackerModifications.ts @@ -6,7 +6,10 @@ import { BitVector } from './bitlogic/BitVector'; import { type InventoryItem, isItem, itemMaxes, itemName } from './Inventory'; import type { DungeonName } from './Locations'; import type { Logic } from './Logic'; -import { swordsToAdd } from './ThingsThatWouldBeNiceToHaveInTheDump'; +import { + defaultBatreauxRequiredCrystals, + swordsToAdd, +} from './ThingsThatWouldBeNiceToHaveInTheDump'; const collectedCubeSuffix = '_TR_Cube_Collected'; @@ -62,6 +65,11 @@ export const dungeonCompletionItems: Record = { 'Sky Keep': '\\Tracker\\Sky Keep Completed', } satisfies Record; +// A fake item that's required by dynamic Batreaux rewards until we know +// how many crystals are required. +export const needEnterBatreauxCountsItem = + '\\Tracker\\Enter required crystal count'; + export function getInitialItems( settings: TypedOptions, ): TrackerState['inventory'] { @@ -176,8 +184,14 @@ export function getTooltipOpaqueBits( } // No point in revealing that the math behind 80 crystals is 13*5+15 - for (const amt of [5, 10, 30, 40, 50, 70, 80]) { - set(`\\${amt} Gratitude Crystals`); + if (logic.needsDynamicBatreauxCrystalCounts) { + for (let amt = 1; amt <= 80; amt++) { + set(`\\${amt} Gratitude Crystals`); + } + } else { + for (const amt of defaultBatreauxRequiredCrystals) { + set(`\\${amt} Gratitude Crystals`); + } } if (settings['gondo-upgrades'] === false) { diff --git a/src/options/Options.tsx b/src/options/Options.tsx index 4f3b09a1..c906dd13 100644 --- a/src/options/Options.tsx +++ b/src/options/Options.tsx @@ -98,6 +98,7 @@ const optionCategorization_ = { Miscellaneous: [ 'logic-mode', 'bit-patches', + 'batreaux-counts', 'damage-multiplier', 'enabled-tricks-bitless', 'enabled-tricks-glitched', diff --git a/src/permalink/SettingsTypes.ts b/src/permalink/SettingsTypes.ts index d88329dd..b39073e0 100644 --- a/src/permalink/SettingsTypes.ts +++ b/src/permalink/SettingsTypes.ts @@ -85,6 +85,9 @@ export interface AllTypedOptions | 'Normal' | 'Beatable Only' | 'Beatable Then Banned'; + + // Random Batreaux Crystal Counts + 'batreaux-counts': 'Vanilla' | 'Half' | 'Random'; } export type TypedOptions = Pick; diff --git a/src/tracker/Selectors.ts b/src/tracker/Selectors.ts index ca8194a2..c6990ac1 100644 --- a/src/tracker/Selectors.ts +++ b/src/tracker/Selectors.ts @@ -40,7 +40,12 @@ import { computeSemiLogic, getVisibleTricksEnabledRequirements, } from '../logic/SemiLogic'; -import { doesHintDistroUseGossipStone } from '../logic/ThingsThatWouldBeNiceToHaveInTheDump'; +import { + defaultBatreauxRequiredCrystals, + doesHintDistroUseGossipStone, + halfBatreauxRequiredCrystals, + numBatreauxRewardLevels, +} from '../logic/ThingsThatWouldBeNiceToHaveInTheDump'; import { cubeCheckToGoddessChestCheck, dungeonCompletionItems, @@ -188,6 +193,47 @@ export const totalGratitudeCrystalsSelector = createSelector( }, ); +export const stillNeedToEnterCrystalCountsSelector = createSelector( + [ + settingSelector('batreaux-counts'), + (state: RootState) => state.tracker.requiredBatreauxCrystals, + ], + (setting, counts) => { + if (setting !== 'Random') { + return false; + } + return ( + counts.length < numBatreauxRewardLevels || + counts.some( + (count, idx) => + idx < numBatreauxRewardLevels && count < 1 && count > 80, + ) + ); + }, +); + +export const requiredCrystalCountsSelector = createSelector( + [ + settingSelector('batreaux-counts'), + (state: RootState) => state.tracker.requiredBatreauxCrystals, + ], + (setting, counts) => { + if (setting === 'Vanilla') { + return defaultBatreauxRequiredCrystals; + } else if (setting === 'Half') { + return halfBatreauxRequiredCrystals; + } else { + return defaultBatreauxRequiredCrystals.map((_value, idx) => + counts[idx] !== undefined && + counts[idx] >= 1 && + counts[idx] <= 80 + ? counts[idx] + : 0, + ); + } + }, +); + const allowedStartingEntrancesSelector = createSelector( [logicSelector, settingSelector('random-start-entrance')], getAllowedStartingEntrances, @@ -273,6 +319,7 @@ export const settingsRequirementsSelector = createSelector( settingsSelector, exitsSelector, requiredDungeonsSelector, + requiredCrystalCountsSelector, ], mapSettings, ); diff --git a/src/tracker/Slice.ts b/src/tracker/Slice.ts index ae223703..cf0c9141 100644 --- a/src/tracker/Slice.ts +++ b/src/tracker/Slice.ts @@ -50,6 +50,12 @@ export interface TrackerState { * The last tracked location, for auto item-at-location tracking. */ lastCheckedLocation: string | undefined; + /** + * If the settings have randomized batreaux counts, + * this is what the user entered after discovering + * the required counts. + */ + requiredBatreauxCrystals: number[]; } const initialState: TrackerState = { @@ -63,6 +69,7 @@ const initialState: TrackerState = { settings: {}, userHintsText: '', lastCheckedLocation: undefined, + requiredBatreauxCrystals: [], }; export function preloadedTrackerState(): TrackerState { @@ -213,6 +220,10 @@ const trackerSlice = createSlice({ cancelItemAssignment: (state) => { state.lastCheckedLocation = undefined; }, + setRequiredCrystalCounts: (state, action: PayloadAction) => { + state.requiredBatreauxCrystals = action.payload; + state.hasBeenModified = true; + }, acceptSettings: ( state, action: PayloadAction<{ settings: AllTypedOptions }>, @@ -250,6 +261,7 @@ export const { reset, setHint, setHintsText, + setRequiredCrystalCounts, loadTracker, } = trackerSlice.actions; From 0189565bca2f386cb254218e9f4ae4b5e53eeaf9 Mon Sep 17 00:00:00 2001 From: robojumper Date: Wed, 1 Jan 2025 13:02:55 +0100 Subject: [PATCH 02/10] Layout fix --- src/locationTracker/CrystalAmountsChooser.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locationTracker/CrystalAmountsChooser.module.css b/src/locationTracker/CrystalAmountsChooser.module.css index 41b9f904..ab3d6fdc 100644 --- a/src/locationTracker/CrystalAmountsChooser.module.css +++ b/src/locationTracker/CrystalAmountsChooser.module.css @@ -1,7 +1,7 @@ .chooser { padding: 4px; display: grid; - grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(70px, 1fr)); align-items: center; From 0c6b27f1f087768cba73d58413db801b77901522 Mon Sep 17 00:00:00 2001 From: robojumper Date: Wed, 1 Jan 2025 17:21:18 +0100 Subject: [PATCH 03/10] Small styling improvements --- src/locationTracker/CrystalAmountsChooser.module.css | 3 ++- src/locationTracker/CrystalAmountsChooser.tsx | 2 +- src/permalink/SettingsTypes.ts | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/locationTracker/CrystalAmountsChooser.module.css b/src/locationTracker/CrystalAmountsChooser.module.css index ab3d6fdc..78cd6dc3 100644 --- a/src/locationTracker/CrystalAmountsChooser.module.css +++ b/src/locationTracker/CrystalAmountsChooser.module.css @@ -1,7 +1,8 @@ .chooser { padding: 4px; display: grid; - grid-template-columns: repeat(auto-fit, minmax(70px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(45px, 1fr)); + gap: 4px; align-items: center; diff --git a/src/locationTracker/CrystalAmountsChooser.tsx b/src/locationTracker/CrystalAmountsChooser.tsx index 7f0b5909..2572e7b5 100644 --- a/src/locationTracker/CrystalAmountsChooser.tsx +++ b/src/locationTracker/CrystalAmountsChooser.tsx @@ -29,7 +29,7 @@ export function CrystalAmountsChooser() {
updateCount(e.target.value, level)} /> diff --git a/src/permalink/SettingsTypes.ts b/src/permalink/SettingsTypes.ts index b39073e0..3ccf99bb 100644 --- a/src/permalink/SettingsTypes.ts +++ b/src/permalink/SettingsTypes.ts @@ -87,7 +87,8 @@ export interface AllTypedOptions | 'Beatable Then Banned'; // Random Batreaux Crystal Counts - 'batreaux-counts': 'Vanilla' | 'Half' | 'Random'; + // https://github.com/ssrando/ssrando/pull/587 + 'batreaux-counts': 'Vanilla' | 'Half' | 'Random' | undefined; } export type TypedOptions = Pick; From b69c2c078dbb0a9d4221bcf2498010642aac9598 Mon Sep 17 00:00:00 2001 From: robojumper Date: Wed, 1 Jan 2025 17:31:49 +0100 Subject: [PATCH 04/10] Fixes --- .../CrystalAmountsChooser.module.css | 8 +++---- .../CrystalAmountsChooser.module.css.d.ts | 1 + src/locationTracker/CrystalAmountsChooser.tsx | 24 ++++++++++--------- src/locationTracker/mapTracker/MapMarker.tsx | 16 ++++++------- src/tracker/Selectors.ts | 2 +- 5 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/locationTracker/CrystalAmountsChooser.module.css b/src/locationTracker/CrystalAmountsChooser.module.css index 78cd6dc3..1094d8c6 100644 --- a/src/locationTracker/CrystalAmountsChooser.module.css +++ b/src/locationTracker/CrystalAmountsChooser.module.css @@ -1,13 +1,11 @@ .chooser { padding: 4px; +} + +.inputs { display: grid; grid-template-columns: repeat(auto-fit, minmax(45px, 1fr)); gap: 4px; align-items: center; - - > span { - grid-column: 1 / span 2; - margin-right: 20px; - } } diff --git a/src/locationTracker/CrystalAmountsChooser.module.css.d.ts b/src/locationTracker/CrystalAmountsChooser.module.css.d.ts index f93a33ea..7552f83b 100644 --- a/src/locationTracker/CrystalAmountsChooser.module.css.d.ts +++ b/src/locationTracker/CrystalAmountsChooser.module.css.d.ts @@ -1,4 +1,5 @@ declare const classNames: { readonly chooser: 'chooser'; + readonly inputs: 'inputs'; }; export = classNames; diff --git a/src/locationTracker/CrystalAmountsChooser.tsx b/src/locationTracker/CrystalAmountsChooser.tsx index 2572e7b5..cd26fae5 100644 --- a/src/locationTracker/CrystalAmountsChooser.tsx +++ b/src/locationTracker/CrystalAmountsChooser.tsx @@ -24,17 +24,19 @@ export function CrystalAmountsChooser() { }; return (
- Enter Crystal Counts: - {range(numBatreauxRewardLevels).map((level) => ( -
- updateCount(e.target.value, level)} - /> -
- ))} +
Enter Crystal Counts:
+
+ {range(numBatreauxRewardLevels).map((level) => ( +
+ updateCount(e.target.value, level)} + /> +
+ ))} +
); } diff --git a/src/locationTracker/mapTracker/MapMarker.tsx b/src/locationTracker/mapTracker/MapMarker.tsx index 2b43ee16..3771e696 100644 --- a/src/locationTracker/mapTracker/MapMarker.tsx +++ b/src/locationTracker/mapTracker/MapMarker.tsx @@ -62,6 +62,13 @@ function MapMarker({ if (dragPreviewHint && isOver) { hints = [...hints, dragPreviewHint]; } + const needsEnterBatCounts = useSelector( + stillNeedToEnterCrystalCountsSelector, + ); + const showEnterBatCounts = + needsEnterBatCounts && + title === "Batreaux's House" && + data.checks.numAccessible === 0; const tooltip = (
@@ -71,6 +78,7 @@ function MapMarker({ {hints.map((hint, idx) => ( ))} + {showEnterBatCounts && 'Click to enter required Gratitude Crystals'}
); @@ -83,14 +91,6 @@ function MapMarker({ } }; - const needsEnterBatCounts = useSelector( - stillNeedToEnterCrystalCountsSelector, - ); - const showEnterBatCounts = - needsEnterBatCounts && - title === "Batreaux's House" && - data.checks.numAccessible === 0; - return ( - idx < numBatreauxRewardLevels && count < 1 && count > 80, + idx < numBatreauxRewardLevels && (count < 1 || count > 80), ) ); }, From dfd07c4ff6379d8985345a86507f635245435111 Mon Sep 17 00:00:00 2001 From: robojumper Date: Wed, 1 Jan 2025 20:31:15 +0100 Subject: [PATCH 05/10] Make sure imports continue to work --- src/ImportExport.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImportExport.tsx b/src/ImportExport.tsx index 682a6284..fcc05f61 100644 --- a/src/ImportExport.tsx +++ b/src/ImportExport.tsx @@ -8,7 +8,7 @@ const version = 'SSRANDO-TRACKER-NG-V2'; export interface ExportState { version: string; - state: TrackerState; + state: Partial; logicBranch: RemoteReference | undefined; } From 911465814cafb6248f664415dd50e32a95209d6f Mon Sep 17 00:00:00 2001 From: robojumper Date: Thu, 2 Jan 2025 11:27:11 +0100 Subject: [PATCH 06/10] Comment --- src/locationTracker/CrystalAmountsChooser.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/locationTracker/CrystalAmountsChooser.tsx b/src/locationTracker/CrystalAmountsChooser.tsx index cd26fae5..0f69c4af 100644 --- a/src/locationTracker/CrystalAmountsChooser.tsx +++ b/src/locationTracker/CrystalAmountsChooser.tsx @@ -6,6 +6,10 @@ import { setRequiredCrystalCounts } from '../tracker/Slice'; import styles from './CrystalAmountsChooser.module.css'; export function CrystalAmountsChooser() { + // TODO: This setup is pretty bad because every number typed + // commits the result directly to Redux and that causes the + // tooltips worker to restart etc. Doesn't seem to be a huge + // problem yet but maybe there's a more efficient design. const dispatch = useDispatch(); const counts = useSelector(requiredCrystalCountsSelector); const updateCount = (val: string, idx: number) => { From 6d871a44cdce83e70b49ed986d634bfee2361fe6 Mon Sep 17 00:00:00 2001 From: robojumper Date: Sat, 11 Jan 2025 00:28:54 +0100 Subject: [PATCH 07/10] Rebase fixes --- src/locationTracker/CrystalAmountsChooser.module.css | 8 ++++++++ src/locationTracker/CrystalAmountsChooser.tsx | 2 +- src/logic/TrackerModifications.ts | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/locationTracker/CrystalAmountsChooser.module.css b/src/locationTracker/CrystalAmountsChooser.module.css index 1094d8c6..58962922 100644 --- a/src/locationTracker/CrystalAmountsChooser.module.css +++ b/src/locationTracker/CrystalAmountsChooser.module.css @@ -8,4 +8,12 @@ gap: 4px; align-items: center; + + > div { + display: flex; + > input { + flex: 1; + min-width: 0; + } + } } diff --git a/src/locationTracker/CrystalAmountsChooser.tsx b/src/locationTracker/CrystalAmountsChooser.tsx index 0f69c4af..060f6204 100644 --- a/src/locationTracker/CrystalAmountsChooser.tsx +++ b/src/locationTracker/CrystalAmountsChooser.tsx @@ -33,7 +33,7 @@ export function CrystalAmountsChooser() { {range(numBatreauxRewardLevels).map((level) => (
updateCount(e.target.value, level)} diff --git a/src/logic/TrackerModifications.ts b/src/logic/TrackerModifications.ts index 67f6b51c..c788bdf2 100644 --- a/src/logic/TrackerModifications.ts +++ b/src/logic/TrackerModifications.ts @@ -193,6 +193,7 @@ export function getTooltipOpaqueBits( set(`\\${amt} Gratitude Crystals`); } } + set(needEnterBatreauxCountsItem); if (settings['gondo-upgrades'] === false) { set( From 4427af06344b4204c4e103e5ca3676b4b6ea2ff3 Mon Sep 17 00:00:00 2001 From: robojumper Date: Sat, 11 Jan 2025 00:32:53 +0100 Subject: [PATCH 08/10] Singular case --- src/tooltips/TooltipExpression.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tooltips/TooltipExpression.ts b/src/tooltips/TooltipExpression.ts index 8d85368d..2e82e890 100644 --- a/src/tooltips/TooltipExpression.ts +++ b/src/tooltips/TooltipExpression.ts @@ -143,6 +143,11 @@ function getReadableItemName(logic: Logic, item: string) { return prettyItemNames[item][1]; } + if (item === '\\1 Gratitude Crystals') { + // macro name + return '1 Gratitude Crystal'; + } + const match = item.match(itemCountPat); if (match) { const [, baseName, count] = match; From 9be609dca81575c6e48578a46dd755aa8863c89b Mon Sep 17 00:00:00 2001 From: robojumper Date: Sat, 11 Jan 2025 20:22:47 +0100 Subject: [PATCH 09/10] Optimize chooser --- src/locationTracker/CrystalAmountsChooser.tsx | 50 +++++++++++-------- src/tracker/Slice.ts | 7 ++- src/utils/React.ts | 44 +++++++++++++++- 3 files changed, 78 insertions(+), 23 deletions(-) diff --git a/src/locationTracker/CrystalAmountsChooser.tsx b/src/locationTracker/CrystalAmountsChooser.tsx index 060f6204..b9a091d4 100644 --- a/src/locationTracker/CrystalAmountsChooser.tsx +++ b/src/locationTracker/CrystalAmountsChooser.tsx @@ -1,31 +1,40 @@ import { range } from 'es-toolkit'; -import { useDispatch, useSelector } from 'react-redux'; +import { useState } from 'react'; +import { useSelector } from 'react-redux'; import { numBatreauxRewardLevels } from '../logic/ThingsThatWouldBeNiceToHaveInTheDump'; import { requiredCrystalCountsSelector } from '../tracker/Selectors'; import { setRequiredCrystalCounts } from '../tracker/Slice'; +import { useDeferredReduxUpdate } from '../utils/React'; import styles from './CrystalAmountsChooser.module.css'; export function CrystalAmountsChooser() { - // TODO: This setup is pretty bad because every number typed - // commits the result directly to Redux and that causes the - // tooltips worker to restart etc. Doesn't seem to be a huge - // problem yet but maybe there's a more efficient design. - const dispatch = useDispatch(); - const counts = useSelector(requiredCrystalCountsSelector); + const reduxCounts = useSelector(requiredCrystalCountsSelector); + const [crystalCounts, setCrystalCounts, flushCrystalCounts] = + useDeferredReduxUpdate( + reduxCounts.map((i) => i.toString(10)), + (counts) => { + const newCounts = [...reduxCounts]; + for (let i = 0; i < numBatreauxRewardLevels; i++) { + const num = parseInt(counts[i], 10); + newCounts[i] = num; + } + return setRequiredCrystalCounts(newCounts); + }, + ); + + // Pull new state from Redux if it changes for some other reason + const [prevCounts, setPrevCounts] = useState(reduxCounts); + if (reduxCounts !== prevCounts) { + setCrystalCounts(reduxCounts.map((i) => i.toString(10))); + setPrevCounts(reduxCounts); + } + const updateCount = (val: string, idx: number) => { - let num; - if (val === '') { - num = 0; - } else { - num = parseInt(val, 10); - if (isNaN(num) || num < 0 || num > 80) { - return; - } - } - const newCounts = [...counts]; - newCounts[idx] = num; - dispatch(setRequiredCrystalCounts(newCounts)); + const newCounts = [...crystalCounts]; + newCounts[idx] = val; + setCrystalCounts(newCounts); }; + return (
Enter Crystal Counts:
@@ -35,8 +44,9 @@ export function CrystalAmountsChooser() { updateCount(e.target.value, level)} + onBlur={flushCrystalCounts} />
))} diff --git a/src/tracker/Slice.ts b/src/tracker/Slice.ts index cf0c9141..feece48d 100644 --- a/src/tracker/Slice.ts +++ b/src/tracker/Slice.ts @@ -6,6 +6,7 @@ import { type InventoryItem, isItem, itemMaxes } from '../logic/Inventory'; import type { RegularDungeon } from '../logic/Locations'; import { getInitialItems } from '../logic/TrackerModifications'; import type { AllTypedOptions } from '../permalink/SettingsTypes'; +import { isEqual } from 'es-toolkit'; export interface TrackerState { /** @@ -221,8 +222,10 @@ const trackerSlice = createSlice({ state.lastCheckedLocation = undefined; }, setRequiredCrystalCounts: (state, action: PayloadAction) => { - state.requiredBatreauxCrystals = action.payload; - state.hasBeenModified = true; + if (!isEqual(state.requiredBatreauxCrystals, action.payload)) { + state.requiredBatreauxCrystals = action.payload; + state.hasBeenModified = true; + } }, acceptSettings: ( state, diff --git a/src/utils/React.ts b/src/utils/React.ts index 63fb17f6..4a303960 100644 --- a/src/utils/React.ts +++ b/src/utils/React.ts @@ -1,5 +1,13 @@ import useResizeObserver from '@react-hook/resize-observer'; -import React, { cloneElement, useLayoutEffect, useState } from 'react'; +import React, { + cloneElement, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'react'; +import { useDispatch } from 'react-redux'; +import type { AppAction } from '../store/Store'; /** places a divider between each element of arr */ export function addDividers( @@ -42,3 +50,37 @@ export function useElementSize(ref: React.RefObject) { return { measuredWidth, measuredHeight }; } + +/** + * Pull an initial value from Redux, store it in local state, + * and only commit when unmounting (or explicitly calling `storeCurrentState`). + * + * https://dev.to/ietxaniz/deferred-redux-update-pattern-1d33 + */ +export const useDeferredReduxUpdate = ( + initialState: T, + updateAction: (arg: T) => AppAction, +) => { + // All of this is a crutch, especially `isInitialized`, + // which hacks around React Strict Mode behavior. + const [state, setState] = useState(initialState); + const [isInitialized, setIsInitialized] = useState(false); + const dispatch = useDispatch(); + + const cleanupRef = useRef(() => {}); + + cleanupRef.current = () => { + if (isInitialized) { + dispatch(updateAction(state)); + } + }; + + useEffect(() => { + setIsInitialized(true); + return () => cleanupRef.current(); + }, []); + + const storeCurrentState = cleanupRef.current; + + return [state, setState, storeCurrentState] as const; +}; From afb8f1185e74f00fc94822f0e0725cfe72d16236 Mon Sep 17 00:00:00 2001 From: robojumper Date: Sat, 11 Jan 2025 21:16:59 +0100 Subject: [PATCH 10/10] Format --- src/locationTracker/CrystalAmountsChooser.tsx | 2 +- src/tracker/Slice.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locationTracker/CrystalAmountsChooser.tsx b/src/locationTracker/CrystalAmountsChooser.tsx index b9a091d4..f51a5895 100644 --- a/src/locationTracker/CrystalAmountsChooser.tsx +++ b/src/locationTracker/CrystalAmountsChooser.tsx @@ -21,7 +21,7 @@ export function CrystalAmountsChooser() { return setRequiredCrystalCounts(newCounts); }, ); - + // Pull new state from Redux if it changes for some other reason const [prevCounts, setPrevCounts] = useState(reduxCounts); if (reduxCounts !== prevCounts) { diff --git a/src/tracker/Slice.ts b/src/tracker/Slice.ts index feece48d..58fc858e 100644 --- a/src/tracker/Slice.ts +++ b/src/tracker/Slice.ts @@ -1,4 +1,5 @@ import { type PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { isEqual } from 'es-toolkit'; import { getStoredTrackerState } from '../LocalStorage'; import { migrateTrackerState } from '../TrackerStateMigrations'; import type { Hint } from '../hints/Hints'; @@ -6,7 +7,6 @@ import { type InventoryItem, isItem, itemMaxes } from '../logic/Inventory'; import type { RegularDungeon } from '../logic/Locations'; import { getInitialItems } from '../logic/TrackerModifications'; import type { AllTypedOptions } from '../permalink/SettingsTypes'; -import { isEqual } from 'es-toolkit'; export interface TrackerState { /**