From 5b10caf44b3d0e922591265d8d9d7aea99ce3509 Mon Sep 17 00:00:00 2001 From: robojumper Date: Sat, 11 May 2024 20:21:41 +0200 Subject: [PATCH 1/4] Counters --- src/logic/Inventory.ts | 48 +++--- src/logic/LegacySupport.ts | 89 +++++++++++ src/logic/Logic.ts | 8 +- src/logic/SemiLogic.ts | 48 +++--- .../ThingsThatWouldBeNiceToHaveInTheDump.ts | 38 ----- src/logic/UpstreamTypes.ts | 46 ++++-- src/logic/booleanlogic/ExpressionParse.ts | 2 +- src/logic/selectors.ts | 2 +- src/tooltips/TooltipComputations.ts | 4 +- src/tooltips/TooltipHooks.tsx | 7 +- src/tracker/selectors.ts | 143 ++++++++++-------- 11 files changed, 277 insertions(+), 158 deletions(-) create mode 100644 src/logic/LegacySupport.ts diff --git a/src/logic/Inventory.ts b/src/logic/Inventory.ts index 86592b4f..f630497d 100644 --- a/src/logic/Inventory.ts +++ b/src/logic/Inventory.ts @@ -1,4 +1,4 @@ -import { OptionDefs, TypedOptions } from '../permalink/SettingsTypes'; +import { TypedOptions } from '../permalink/SettingsTypes'; import { BitVector } from './bitlogic/BitVector'; import { itemName, Logic } from './Logic'; import { @@ -91,7 +91,12 @@ export function isItem(id: string): id is InventoryItem { * Returns a BitVector containing all the expressions that should be visible in the tooltips * and not recursively expanded (items and various item-like requirements). */ -export function getTooltipOpaqueBits(logic: Logic, options: OptionDefs, settings: TypedOptions, expertMode: boolean, consideredTricks: Set) { +export function getTooltipOpaqueBits( + logic: Logic, + settings: TypedOptions, + expertMode: boolean, + consideredTricks: Set, +) { const items = new BitVector(); const set = (id: string) => { const bit = logic.itemBits[id]; @@ -102,28 +107,31 @@ export function getTooltipOpaqueBits(logic: Logic, options: OptionDefs, settings } }; - for (const option of options) { + for (const [req, condition] of Object.entries(logic.optionConditions)) { if ( - option.type === 'multichoice' && - (option.command === 'enabled-tricks-glitched' || - option.command === 'enabled-tricks-bitless') + condition.type === 'query' && + condition.op === 'in' && + (condition.option === 'enabled-tricks-glitched' || + condition.option === 'enabled-tricks-bitless') ) { - const vals = option.choices; - for (const opt of vals) { - const considered = - settings[option.command].includes(opt) || - (expertMode && - (!consideredTricks.size || consideredTricks.has(opt))); - if (considered) { - set(`${opt} Trick`); - } + const value = condition.value as string; + const considered = + settings[condition.option].includes(value) || + (expertMode && + (!consideredTricks.size || consideredTricks.has(value))); + if (considered) { + set(req); } } } // All actual inventory items are shown in the tooltips for (const [item, count] of Object.entries(itemMaxes)) { - if (count === undefined || item === 'Sailcloth' || item === 'Tumbleweed') { + if ( + count === undefined || + item === 'Sailcloth' || + item === 'Tumbleweed' + ) { continue; } if (item === sothItemReplacement) { @@ -157,8 +165,12 @@ export function getTooltipOpaqueBits(logic: Logic, options: OptionDefs, settings } if (settings['gondo-upgrades'] === false) { - set('\\Skyloft\\Central Skyloft\\Bazaar\\Gondo\'s Upgrades\\Upgrade to Quick Beetle'); - set('\\Skyloft\\Central Skyloft\\Bazaar\\Gondo\'s Upgrades\\Upgrade to Tough Beetle'); + set( + "\\Skyloft\\Central Skyloft\\Bazaar\\Gondo's Upgrades\\Upgrade to Quick Beetle", + ); + set( + "\\Skyloft\\Central Skyloft\\Bazaar\\Gondo's Upgrades\\Upgrade to Tough Beetle", + ); } return items; diff --git a/src/logic/LegacySupport.ts b/src/logic/LegacySupport.ts new file mode 100644 index 00000000..384d9fc2 --- /dev/null +++ b/src/logic/LegacySupport.ts @@ -0,0 +1,89 @@ +/** + * Sometimes the rando adds things to the dump that were + * previously hardcoded in the tracker. This file contains + * that data so that we can essentially migrate older but still + * supported releases to the new format and reduce the number + * of special cases we have to handle in core tracker code. + */ + +import { MultiChoiceOption, OptionDefs, TypedOptions } from '../permalink/SettingsTypes'; +import { dungeonNames } from './Locations'; +import { OptionQuery, RawLogic } from './UpstreamTypes'; + +const m = ( + option: K, + op: OptionQuery['op'], + value: Exclude, + negation = false, +): OptionQuery => { + return { + type: 'query', + op, + option, + value, + negation, + }; +}; + +const legacyHardcodedOptions: RawLogic['options'] = { + 'Open Thunderhead option': m('open-thunderhead', 'eq', 'Open'), + 'Open ET option': m('open-et', 'eq', true), + 'Open LMF option': m('open-lmf', 'eq', 'Open'), + 'LMF Nodes On option': m('open-lmf', 'eq', 'Main Node'), + 'Open Lake Floria option': m('open-lake-floria', 'eq', 'Open'), + 'Talk to Yerbal option': m('open-lake-floria', 'eq', 'Talk to Yerbal'), + 'Vanilla Lake Floria option': m('open-lake-floria', 'eq', 'Vanilla'), + 'Randomized Beedle option': m( + 'shopsanity', + 'eq', + 'Vanilla', + /* negation */ true, + ), + 'Gondo Upgrades On option': m('gondo-upgrades', 'eq', false), + 'No BiT crashes': m('bit-patches', 'eq', 'Fix BiT Crashes'), + 'Nonlethal Hot Cave': m('damage-multiplier', 'lt', 12), + 'Upgraded Skyward Strike option': m('upgraded-skyward-strike', 'eq', true), + 'FS Lava Flow option': m('fs-lava-flow', 'eq', true), +}; + +/** + * Get Query conditions (settings value or required dungeon tests, tricks) + */ +export function getOptionConditions(options: OptionDefs) { + const conditions = {...legacyHardcodedOptions}; + const enabledTricksOption = options.find((v) => v.command === 'enabled-tricks-bitless'); + const enabledTricksGlitchedOption = options.find((v) => v.command === 'enabled-tricks-glitched'); + + const handleTricks = (option: MultiChoiceOption) => { + for (const choice of option.choices) { + conditions[`${choice} Trick`] = { + type: 'query', + option: option.command, + op: 'in', + value: choice, + negation: false, + }; + } + }; + + // https://github.com/NindyBK/ssrnppbuild/pull/1 + if (options.some((o) => o.command === 'open-shortcuts')) { + conditions['Open Dungeon Shortcuts option'] = m('open-shortcuts', 'eq', 'All Dungeons'); + conditions['Open Unrequired Shortcuts option'] = m('open-shortcuts', 'eq', 'Unrequired Dungeons Only'); + conditions['Default Dungeon Behavior option'] = m('open-shortcuts', 'eq', 'None'); + + for (const dungeon of dungeonNames) { + conditions[`${dungeon} Required`] = { type: 'req_dungeon', dungeon, negation: false }; + conditions[`${dungeon} Unrequired`] = { type: 'req_dungeon', dungeon, negation: true }; + } + } + + if (enabledTricksOption?.type === 'multichoice') { + handleTricks(enabledTricksOption); + } + if (enabledTricksGlitchedOption?.type === 'multichoice') { + handleTricks(enabledTricksGlitchedOption); + } + + return conditions; +} \ No newline at end of file diff --git a/src/logic/Logic.ts b/src/logic/Logic.ts index 815eeed8..20811206 100644 --- a/src/logic/Logic.ts +++ b/src/logic/Logic.ts @@ -26,6 +26,8 @@ import { } from './booleanlogic/ExpressionParse'; import { dungeonNames } from './Locations'; import { LogicBuilder } from './LogicBuilder'; +import { OptionDefs } from '../permalink/SettingsTypes'; +import { getOptionConditions } from './LegacySupport'; export interface Logic { numRequirements: number; @@ -47,7 +49,8 @@ export interface Logic { hintRegions: string[]; checksByHintRegion: Record; exitsByHintRegion: Record; - dungeonCompletionRequirements: { [dungeon: string]: string } + dungeonCompletionRequirements: { [dungeon: string]: string }; + optionConditions: Exclude; } export interface LogicalCheck { @@ -311,7 +314,7 @@ function preprocessItems(raw: string[]): { const checkAreaPlaceholder = 'filled-in-later'; -export function parseLogic(raw: RawLogic): Logic { +export function parseLogic(raw: RawLogic, options: OptionDefs): Logic { const start = performance.now(); const { newItems, impliedBy, implies } = preprocessItems( @@ -905,6 +908,7 @@ export function parseLogic(raw: RawLogic): Logic { exitsByHintRegion, dungeonCompletionRequirements: raw.dungeon_completion_requirements, areaGraph, + optionConditions: raw.options ?? getOptionConditions(options), }; } diff --git a/src/logic/SemiLogic.ts b/src/logic/SemiLogic.ts index 7278e064..cdce6b67 100644 --- a/src/logic/SemiLogic.ts +++ b/src/logic/SemiLogic.ts @@ -1,11 +1,15 @@ -import { OptionDefs, TypedOptions } from '../permalink/SettingsTypes'; +import { TypedOptions } from '../permalink/SettingsTypes'; import { mapInventory, getAdditionalItems } from '../tracker/selectors'; import { InventoryItem, isItem, itemMaxes } from './Inventory'; import { PotentialLocations, getSemiLogicKeys } from './KeyLogic'; import { Logic } from './Logic'; import { LogicBuilder } from './LogicBuilder'; import { cubeCheckToCubeCollected } from './TrackerModifications'; -import { Requirements, computeLeastFixedPoint, mergeRequirements } from './bitlogic/BitLogic'; +import { + Requirements, + computeLeastFixedPoint, + mergeRequirements, +} from './bitlogic/BitLogic'; import { BitVector } from './bitlogic/BitVector'; export interface SemiLogicState { @@ -14,30 +18,37 @@ export interface SemiLogicState { assumedChecks: Set; } -/** Requirements that assume every trick is enabled. */ -export function getAllTricksEnabledRequirements( +/** + * Requirements that assume every considered trick is enabled. Enables + * all tricks if consideredTricks is empty. + */ +export function getVisibleTricksEnabledRequirements( logic: Logic, - options: OptionDefs, settings: TypedOptions, consideredTricks: Set, ): Requirements { const requirements: Requirements = {}; const b = new LogicBuilder(logic.allItems, logic.itemLookup, requirements); - for (const option of options) { + for (const [requirement, condition] of Object.entries( + logic.optionConditions, + )) { if ( - option.type === 'multichoice' && - (option.command === 'enabled-tricks-glitched' || - option.command === 'enabled-tricks-bitless') + condition.type === 'query' && + (condition.option === 'enabled-tricks-glitched' || + condition.option === 'enabled-tricks-bitless') ) { - const vals = option.choices; - for (const opt of vals) { - const considered = - settings[option.command].includes(opt) || + const settingsEnabledTricks = settings[condition.option]; + if ( + Array.isArray(settingsEnabledTricks) && + typeof condition.value === 'string' + ) { + const visible = + settingsEnabledTricks.includes(condition.value) || !consideredTricks.size || - consideredTricks.has(opt); - if (considered) { - b.set(`${opt} Trick`, b.true()); + consideredTricks.has(condition.value); + if (visible) { + b.set(requirement, b.true()); } } } @@ -78,7 +89,10 @@ export function computeSemiLogic( const inSemiLogicBits = semiLogicState.semiLogicBits.clone(); if (!expertMode) { - return { inSemiLogicBits, inTrickLogicBits: semiLogicState.semiLogicBits }; + return { + inSemiLogicBits, + inTrickLogicBits: semiLogicState.semiLogicBits, + }; } const settingsRequirementsWithTricks = { diff --git a/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts b/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts index 4dcc5869..eee4fac9 100644 --- a/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts +++ b/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts @@ -1,9 +1,3 @@ -import { - OptionType, - OptionValue, - TypedOptions, -} from '../permalink/SettingsTypes'; - /** Exits that are not randomized even if ER is on. */ export const nonRandomizedExits = [ '\\Faron\\Sealed Grounds\\Sealed Temple\\Gate of Time Exit', @@ -26,38 +20,6 @@ export const bannedExitsAndEntrances = [ /** The exit that leads from LMF to the temple of time. */ export const lmfSecondExit = '\\Lanayru Mining Facility\\Hall of Ancient Robots\\End\\Exit to Temple of Time'; -type OptionMapping = [ - string, - keyof TypedOptions, - OptionValue | ((val: OptionValue) => boolean), -]; -const m = ( - item: string, - settingsKey: K, - value: TypedOptions[K] | ((value: TypedOptions[K]) => boolean), -): OptionMapping => [item, settingsKey, value as OptionType]; - -export const runtimeOptions: OptionMapping[] = [ - m('Open Thunderhead option', 'open-thunderhead', 'Open'), - m('Open ET option', 'open-et', true), - m('Open LMF option', 'open-lmf', 'Open'), - m('LMF Nodes On option', 'open-lmf', 'Main Node'), - m('Open Lake Floria option', 'open-lake-floria', 'Open'), - m('Talk to Yerbal option', 'open-lake-floria', 'Talk to Yerbal'), - m('Vanilla Lake Floria option', 'open-lake-floria', 'Vanilla'), - m('Randomized Beedle option', 'shopsanity', (val) => val !== 'Vanilla'), - m('Gondo Upgrades On option', 'gondo-upgrades', false), - m('No BiT crashes', 'bit-patches', 'Fix BiT Crashes'), - m('Nonlethal Hot Cave', 'damage-multiplier', (val) => val < 12), - m('Upgraded Skyward Strike option', 'upgraded-skyward-strike', true), - m('FS Lava Flow option', 'fs-lava-flow', true), - - // https://github.com/NindyBK/ssrnppbuild/pull/1 - m('Open Dungeon Shortcuts option', 'open-shortcuts', 'All Dungeons'), - m('Open Unrequired Shortcuts option', 'open-shortcuts', 'Unrequired Dungeons Only'), - m('Default Dungeon Behavior option', 'open-shortcuts', 'None'), -]; - export const impaSongCheck = '\\Faron\\Sealed Grounds\\Sealed Temple\\Song from Impa'; export const completeTriforceReq = '\\Complete Triforce'; diff --git a/src/logic/UpstreamTypes.ts b/src/logic/UpstreamTypes.ts index b6a2c946..189cae10 100644 --- a/src/logic/UpstreamTypes.ts +++ b/src/logic/UpstreamTypes.ts @@ -1,3 +1,5 @@ +import { OptionValue, OptionsCommand } from '../permalink/SettingsTypes'; + export enum TimeOfDay { DayOnly = 1, NightOnly = 2, @@ -44,27 +46,47 @@ export interface RawCheck { } export interface ExitLink { - exit_from_outside: string | string[], - exit_from_inside: string, + exit_from_outside: string | string[]; + exit_from_inside: string; +} + +export interface OptionQuery { + type: 'query'; + option: OptionsCommand; + op: 'eq' | 'in' | 'lt' | 'gt'; + negation: boolean; + value: OptionValue; +} + +export interface DungeonQuery { + type: 'req_dungeon'; + dungeon: string; + negation: boolean; } +export type SettingsQuery = DungeonQuery | OptionQuery; + export interface RawLogic { items: string[]; checks: Record; /** LocationId -> Area - Location Name */ gossip_stones: Record; - exits: Record, - entrances: Record, + exits: Record; + entrances: Record; areas: RawArea; linked_entrances: { silent_realms: { - [realm: string]: ExitLink - }, + [realm: string]: ExitLink; + }; dungeons: { - [dungeon: string]: ExitLink - } - } + [dungeon: string]: ExitLink; + }; + }; dungeon_completion_requirements: { - [dungeon: string]: string, - } -} \ No newline at end of file + [dungeon: string]: string; + }; + well_known_requirements?: { + [key in 'open_got' | 'raise_got' | 'horde_door' | 'impa_song_check' | 'complete_triforce']: string; + }; + options?: Record; +} diff --git a/src/logic/booleanlogic/ExpressionParse.ts b/src/logic/booleanlogic/ExpressionParse.ts index 8480d2f9..f20885e0 100644 --- a/src/logic/booleanlogic/ExpressionParse.ts +++ b/src/logic/booleanlogic/ExpressionParse.ts @@ -56,7 +56,7 @@ export function booleanExprToLogicalExpr( } else { if (expr === 'True') { return [new BitVector()]; - } else if (expr === 'False') { + } else if (expr === 'False' || expr === 'Unknown') { return []; } else { const bit_idx = lookup(expr); diff --git a/src/logic/selectors.ts b/src/logic/selectors.ts index b2baf4b4..c54a67a8 100644 --- a/src/logic/selectors.ts +++ b/src/logic/selectors.ts @@ -10,7 +10,7 @@ export const optionsSelector = (state: RootState) => state.logic.loaded!.options export const isLogicLoadedSelector = (state: RootState) => Boolean(state.logic.loaded); /** Select parsed logic. Throws if logic hasn't loaded yet (guard with `isLogicLoadedSelector`). */ -export const logicSelector = createSelector([rawLogicSelector], parseLogic); +export const logicSelector = createSelector([rawLogicSelector, optionsSelector], parseLogic); export const areaGraphSelector = createSelector( [logicSelector], diff --git a/src/tooltips/TooltipComputations.ts b/src/tooltips/TooltipComputations.ts index cf2a0e83..9e84866a 100644 --- a/src/tooltips/TooltipComputations.ts +++ b/src/tooltips/TooltipComputations.ts @@ -4,7 +4,7 @@ import BooleanExpression from '../logic/booleanlogic/BooleanExpression'; import { BitLogic, } from '../logic/bitlogic/BitLogic'; -import { OptionDefs, TypedOptions } from '../permalink/SettingsTypes'; +import { TypedOptions } from '../permalink/SettingsTypes'; import { WorkerRequest, WorkerResponse } from './worker/Types'; import { deserializeBooleanExpression, serializeLogicalExpression } from './worker/Utils'; import _ from 'lodash'; @@ -25,7 +25,6 @@ export class TooltipComputer { constructor( logic: Logic, - options: OptionDefs, settings: TypedOptions, expertMode: boolean, trickLogicTricks: Set, @@ -36,7 +35,6 @@ export class TooltipComputer { this.isWorking = false; const opaqueBits = getTooltipOpaqueBits( logic, - options, settings, expertMode, trickLogicTricks, diff --git a/src/tooltips/TooltipHooks.tsx b/src/tooltips/TooltipHooks.tsx index 97bbe870..36ab262a 100644 --- a/src/tooltips/TooltipHooks.tsx +++ b/src/tooltips/TooltipHooks.tsx @@ -20,7 +20,7 @@ import { settingSelector, settingsSelector, } from '../tracker/selectors'; -import { logicSelector, optionsSelector } from '../logic/selectors'; +import { logicSelector } from '../logic/selectors'; import { RootTooltipExpression, booleanExprToTooltipExpr, @@ -38,7 +38,6 @@ export function MakeTooltipsAvailable({ children }: { children: ReactNode }) { const [analyzer, setAnalyzer] = useState(null); const logic = useSelector(logicSelector); - const options = useSelector(optionsSelector); const settings = useSelector(settingsSelector); const settingsRequirements = useSelector(settingsRequirementsSelector); const expertMode = useSelector(trickSemiLogicSelector); @@ -51,7 +50,7 @@ export function MakeTooltipsAvailable({ children }: { children: ReactNode }) { settingsRequirements, ); setAnalyzer( - new TooltipComputer(logic, options, settings, expertMode, consideredTricks, bitLogic), + new TooltipComputer(logic, settings, expertMode, consideredTricks, bitLogic), ); return () => { setAnalyzer((oldAnalyzer) => { @@ -59,7 +58,7 @@ export function MakeTooltipsAvailable({ children }: { children: ReactNode }) { return null; }); }; - }, [settingsRequirements, logic, options, expertMode, consideredTricks, settings]); + }, [settingsRequirements, logic, expertMode, consideredTricks, settings]); return ( diff --git a/src/tracker/selectors.ts b/src/tracker/selectors.ts index 14c8bcde..b2f0bdc1 100644 --- a/src/tracker/selectors.ts +++ b/src/tracker/selectors.ts @@ -4,7 +4,7 @@ import { logicSelector, optionsSelector, } from '../logic/selectors'; -import { OptionDefs, TypedOptions } from '../permalink/SettingsTypes'; +import { TypedOptions } from '../permalink/SettingsTypes'; import { RootState } from '../store/store'; import { currySelector } from '../utils/redux'; import { @@ -14,7 +14,6 @@ import { hordeDoorReq, impaSongCheck, knownNoGossipStoneHintDistros, - runtimeOptions, swordsToAdd, } from '../logic/ThingsThatWouldBeNiceToHaveInTheDump'; import { @@ -26,11 +25,7 @@ import { isDungeon, LogicalState, } from '../logic/Locations'; -import { - Logic, - LogicalCheck, - itemName, -} from '../logic/Logic'; +import { Logic, LogicalCheck, itemName } from '../logic/Logic'; import { cubeCheckToCubeCollected, cubeCheckToGoddessChestCheck, @@ -43,21 +38,39 @@ import { } from '../logic/TrackerModifications'; import _ from 'lodash'; import { LogicalExpression } from '../logic/bitlogic/LogicalExpression'; -import { TimeOfDay } from '../logic/UpstreamTypes'; -import { Requirements, computeLeastFixedPoint, mergeRequirements } from '../logic/bitlogic/BitLogic'; +import { SettingsQuery, TimeOfDay } from '../logic/UpstreamTypes'; +import { + Requirements, + computeLeastFixedPoint, + mergeRequirements, +} from '../logic/bitlogic/BitLogic'; import { validateSettings } from '../permalink/Settings'; import { LogicBuilder } from '../logic/LogicBuilder'; import { exploreAreaGraph } from '../logic/Pathfinding'; import { keyData } from '../logic/KeyLogic'; import { BitVector } from '../logic/bitlogic/BitVector'; import { InventoryItem, itemMaxes } from '../logic/Inventory'; -import { getAllowedStartingEntrances, getEntrancePools, getExitRules, getExits, getUsedEntrances } from '../logic/Entrances'; -import { computeSemiLogic, getAllTricksEnabledRequirements } from '../logic/SemiLogic'; -import { counterBasisSelector, trickSemiLogicSelector, trickSemiLogicTrickListSelector } from '../customization/selectors'; +import { + getAllowedStartingEntrances, + getEntrancePools, + getExitRules, + getExits, + getUsedEntrances, +} from '../logic/Entrances'; +import { + computeSemiLogic, + getVisibleTricksEnabledRequirements, +} from '../logic/SemiLogic'; +import { + counterBasisSelector, + trickSemiLogicSelector, + trickSemiLogicTrickListSelector, +} from '../customization/selectors'; const bitVectorMemoizeOptions = { memoizeOptions: { - resultEqualityCheck: (a: BitVector, b: BitVector) => (a instanceof BitVector && b instanceof BitVector && a.equals(b)), + resultEqualityCheck: (a: BitVector, b: BitVector) => + a instanceof BitVector && b instanceof BitVector && a.equals(b), }, }; @@ -71,7 +84,8 @@ export const areaHintSelector = currySelector( /** * All hinted items. */ -export const checkHintsSelector = (state: RootState) => state.tracker.checkHints; +export const checkHintsSelector = (state: RootState) => + state.tracker.checkHints; /** * Selects the hinted item for a given check @@ -208,7 +222,6 @@ export const entrancePoolsSelector = createSelector( const mappedExitsSelector = (state: RootState) => state.tracker.mappedExits; - /** Defines how exits should be resolved. */ export const exitRulesSelector = createSelector( [ @@ -260,7 +273,6 @@ export const requiredDungeonsSelector = createSelector( export const settingsRequirementsSelector = createSelector( [ logicSelector, - optionsSelector, settingsSelector, exitsSelector, requiredDungeonsSelector, @@ -268,9 +280,45 @@ export const settingsRequirementsSelector = createSelector( mapSettings, ); +function evalCondition( + condition: SettingsQuery, + settings: TypedOptions, + requiredDungeons: string[], +) { + + const evalInner = () => { + const ty = condition.type; + switch (ty) { + case 'query': { + const settingsValue = settings[condition.option as keyof TypedOptions]; + // eslint-disable-next-line sonarjs/no-nested-switch + switch (condition.op) { + case 'eq': + return settingsValue === condition.value; + case 'in': + return Array.isArray(settingsValue) && settingsValue.includes(condition.value as string); + case 'lt': + return (settingsValue as number) < (condition.value as number); + case 'gt': + return (settingsValue as number) > (condition.value as number); + default: + console.warn('unknown op', condition.op) + return false; + } + } + case 'req_dungeon': + return requiredDungeons.includes(condition.dungeon); + default: + console.warn('unknown options type', ty) + return false; + } + } + + return evalInner() !== condition.negation; +} + function mapSettings( logic: Logic, - options: OptionDefs, settings: TypedOptions, exits: ExitMapping[], requiredDungeons: string[], @@ -278,39 +326,11 @@ function mapSettings( const requirements: Requirements = {}; const b = new LogicBuilder(logic.allItems, logic.itemLookup, requirements); - for (const option of runtimeOptions) { - const [item, command, expect] = option; - const val = settings[command]; - const match = - val !== undefined && - (typeof expect === 'function' ? expect(val) : expect === val); - if (match) { - console.log('setting', item); - b.set(item, b.true()); - } - } - - // https://github.com/NindyBK/ssrnppbuild/pull/1 - if (logic.itemBits['Lanayru Mining Facility Unrequired'] !== undefined) { - for (const dungeon of dungeonNames) { - if (!requiredDungeons.includes(dungeon)) { - b.trySet(`${dungeon} Unrequired`, b.true()); - } else { - b.trySet(`${dungeon} Required`, b.true()); - } - } - } - - for (const option of options) { - if ( - option.type === 'multichoice' && - (option.command === 'enabled-tricks-glitched' || - option.command === 'enabled-tricks-bitless') - ) { - const vals = settings[option.command]; - for (const option of vals) { - b.set(`${option} Trick`, b.true()); - } + for (const [requirement, condition] of Object.entries( + logic.optionConditions, + )) { + if (evalCondition(condition, settings, requiredDungeons)) { + b.trySet(requirement, b.true()); } } @@ -397,7 +417,11 @@ export function mapInventory(logic: Logic, itemCounts: Record) { const b = new LogicBuilder(logic.allItems, logic.itemLookup, requirements); for (const [item, count] of Object.entries(itemCounts)) { - if (count === undefined || item === 'Sailcloth' || item === 'Tumbleweed') { + if ( + count === undefined || + item === 'Sailcloth' || + item === 'Tumbleweed' + ) { continue; } if (item === sothItemReplacement) { @@ -446,11 +470,7 @@ export const inLogicBitsSelector = createSelector( const optimisticInventoryItemRequirementsSelector = createSelector( [logicSelector], - (logic) => - mapInventory( - logic, - itemMaxes, - ), + (logic) => mapInventory(logic, itemMaxes), ); /** @@ -632,14 +652,13 @@ const dungeonKeyLogicSelector = createSelector( ); /** A selector for the requirements that assume every trick enabled in customization is enabled. */ -const allTricksRequirementsSelector = createSelector( +const visibleTricksRequirementsSelector = createSelector( [ logicSelector, - optionsSelector, settingsSelector, trickSemiLogicTrickListSelector, ], - getAllTricksEnabledRequirements, + getVisibleTricksEnabledRequirements, ); export const inTrickLogicBitsSelector = createSelector( @@ -649,7 +668,7 @@ export const inTrickLogicBitsSelector = createSelector( settingsRequirementsSelector, inventoryRequirementsSelector, checkRequirementsSelector, - allTricksRequirementsSelector, + visibleTricksRequirementsSelector, ], ( logic, @@ -683,7 +702,7 @@ const semiLogicBitsSelector = createSelector( settingsRequirementsSelector, checkHintsSelector, trickSemiLogicSelector, - allTricksRequirementsSelector, + visibleTricksRequirementsSelector, ], computeSemiLogic, ); @@ -855,7 +874,7 @@ export const totalCountersSelector = createSelector( export const usedEntrancesSelector = createSelector( [entrancePoolsSelector, exitsSelector], - getUsedEntrances + getUsedEntrances, ); export const inLogicPathfindingSelector = createSelector( From 507e9e8f4bba04b59463113f24c638231ad5670a Mon Sep 17 00:00:00 2001 From: robojumper Date: Sat, 18 May 2024 18:26:05 +0200 Subject: [PATCH 2/4] Counters support --- src/logic/Inventory.ts | 13 +++-- src/logic/LegacySupport.ts | 31 ++++++++++-- src/logic/Logic.ts | 48 ++++++++++++++++--- .../ThingsThatWouldBeNiceToHaveInTheDump.ts | 9 ---- src/logic/UpstreamTypes.ts | 28 ++++++++++- src/logic/booleanlogic/ExpressionParse.ts | 38 +++++++++++---- src/tooltips/TooltipExpression.ts | 5 ++ src/tracker/selectors.ts | 43 ++++++++++++----- 8 files changed, 169 insertions(+), 46 deletions(-) diff --git a/src/logic/Inventory.ts b/src/logic/Inventory.ts index f630497d..cd106dfb 100644 --- a/src/logic/Inventory.ts +++ b/src/logic/Inventory.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import { TypedOptions } from '../permalink/SettingsTypes'; import { BitVector } from './bitlogic/BitVector'; import { itemName, Logic } from './Logic'; @@ -159,9 +160,15 @@ export function getTooltipOpaqueBits( set(cubeItem); } - // 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 (_.isEmpty(logic.counterThresholds)) { + // 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`); + } + } else { + for (const counter of Object.keys(logic.counterThresholds)) { + set(counter); + } } if (settings['gondo-upgrades'] === false) { diff --git a/src/logic/LegacySupport.ts b/src/logic/LegacySupport.ts index 384d9fc2..8e0019f6 100644 --- a/src/logic/LegacySupport.ts +++ b/src/logic/LegacySupport.ts @@ -8,6 +8,7 @@ import { MultiChoiceOption, OptionDefs, TypedOptions } from '../permalink/SettingsTypes'; import { dungeonNames } from './Locations'; +import { Logic } from './Logic'; import { OptionQuery, RawLogic } from './UpstreamTypes'; const m = ( @@ -49,10 +50,14 @@ const legacyHardcodedOptions: RawLogic['options'] = { /** * Get Query conditions (settings value or required dungeon tests, tricks) */ -export function getOptionConditions(options: OptionDefs) { - const conditions = {...legacyHardcodedOptions}; - const enabledTricksOption = options.find((v) => v.command === 'enabled-tricks-bitless'); - const enabledTricksGlitchedOption = options.find((v) => v.command === 'enabled-tricks-glitched'); +export function getLegacyOptionConditions(options: OptionDefs) { + const conditions = { ...legacyHardcodedOptions }; + const enabledTricksOption = options.find( + (v) => v.command === 'enabled-tricks-bitless', + ); + const enabledTricksGlitchedOption = options.find( + (v) => v.command === 'enabled-tricks-glitched', + ); const handleTricks = (option: MultiChoiceOption) => { for (const choice of option.choices) { @@ -86,4 +91,20 @@ export function getOptionConditions(options: OptionDefs) { } return conditions; -} \ No newline at end of file +} + +const gotOpeningReq = 'GoT Opening Requirement'; +const gotRaisingReq = 'GoT Raising Requirement'; +const hordeDoorReq = 'Horde Door Requirement'; +const impaSongCheck = '\\Faron\\Sealed Grounds\\Sealed Temple\\Song from Impa'; +const completeTriforceReq = '\\Complete Triforce'; + +export function getLegacyWellKnownRequirements(): Logic['wellKnownRequirements'] { + return { + open_got: gotOpeningReq, + raise_got: gotRaisingReq, + horde_door: hordeDoorReq, + impa_song_check: impaSongCheck, + complete_triforce: completeTriforceReq, + }; +} diff --git a/src/logic/Logic.ts b/src/logic/Logic.ts index 20811206..dc79d6e0 100644 --- a/src/logic/Logic.ts +++ b/src/logic/Logic.ts @@ -7,6 +7,7 @@ import { TimeOfDay, RawEntrance, RawExit, + Counter, } from './UpstreamTypes'; import { cubeCheckToCubeCollected, @@ -22,12 +23,13 @@ import { } from './bitlogic/BitLogic'; import { booleanExprToLogicalExpr, + parseCounterThreshold, parseExpression, } from './booleanlogic/ExpressionParse'; import { dungeonNames } from './Locations'; import { LogicBuilder } from './LogicBuilder'; import { OptionDefs } from '../permalink/SettingsTypes'; -import { getOptionConditions } from './LegacySupport'; +import { getLegacyOptionConditions, getLegacyWellKnownRequirements } from './LegacySupport'; export interface Logic { numRequirements: number; @@ -51,6 +53,14 @@ export interface Logic { exitsByHintRegion: Record; dungeonCompletionRequirements: { [dungeon: string]: string }; optionConditions: Exclude; + wellKnownRequirements: Exclude; + counters: Record | undefined; + counterThresholds: Record; +} + +export interface CounterThreshold { + item: string; + count: number; } export interface LogicalCheck { @@ -326,6 +336,8 @@ export function parseLogic(raw: RawLogic, options: OptionDefs): Logic { ...Object.values(dungeonCompletionItems), ]; + const counterThresholds: Logic['counterThresholds'] = {}; + // Pessimistically, all items are opaque const opaqueItems = new BitVector(); for (let i = 0; i < rawItems.length; i++) { @@ -650,7 +662,19 @@ export function parseLogic(raw: RawLogic, options: OptionDefs): Logic { ? location : `${area.id}\\${location}`; - const expr = parseExpr(locationRequirementExpression); + let expr: LogicalExpression | undefined = undefined; + if (area.abstract) { + // only abstract areas can contain counters + const counter = parseCounterThreshold(locationRequirementExpression); + if (counter) { + expr = LogicalExpression.false(); + counterThresholds[locationId] = counter; + } + } + + if (!expr) { + expr = parseExpr(locationRequirementExpression); + } const check: LogicalCheck | undefined = checks[locationId]; const isPrimaryLocation = check && !location.startsWith('\\'); @@ -836,7 +860,7 @@ export function parseLogic(raw: RawLogic, options: OptionDefs): Logic { // Now map our area graph to BitLogic const newBuilder = new LogicBuilder(rawItems, itemLookup, staticRequirements); - mapAreaToBitLogic(newBuilder, areaGraph, opaqueItems); + mapAreaToBitLogic(newBuilder, areaGraph, opaqueItems, counterThresholds); // check for orphaned locations. This again should probably not be in here @@ -908,7 +932,13 @@ export function parseLogic(raw: RawLogic, options: OptionDefs): Logic { exitsByHintRegion, dungeonCompletionRequirements: raw.dungeon_completion_requirements, areaGraph, - optionConditions: raw.options ?? getOptionConditions(options), + optionConditions: raw.options ?? getLegacyOptionConditions(options), + wellKnownRequirements: { + ...getLegacyWellKnownRequirements(), + ...raw.well_known_requirements, + }, + counters: raw.counters, + counterThresholds, }; } @@ -916,10 +946,11 @@ function mapAreaToBitLogic( b: LogicBuilder, areaGraph: AreaGraph, opaqueItems: BitVector, + counters: Logic['counterThresholds'], area = areaGraph.rootArea, ) { for (const subArea of Object.values(area.subAreas)) { - mapAreaToBitLogic(b, areaGraph, opaqueItems, subArea); + mapAreaToBitLogic(b, areaGraph, opaqueItems, counters, subArea); } if (area.canSleep) { @@ -934,10 +965,13 @@ function mapAreaToBitLogic( case 'mapExit': { const locName = location.id; - // Hack: We keep these virtual locations opaque... + if ( + // Hack: We keep these virtual locations opaque... !locName.endsWith('Gratitude Crystals') && - !locName.includes("\\Gondo's Upgrades\\Upgrade to") + !locName.includes("\\Gondo's Upgrades\\Upgrade to") && + // Counters are populated at runtime + !counters[location.id] ) { opaqueItems.clearBit(b.bit(locName)); } diff --git a/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts b/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts index eee4fac9..93b56210 100644 --- a/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts +++ b/src/logic/ThingsThatWouldBeNiceToHaveInTheDump.ts @@ -20,10 +20,6 @@ export const bannedExitsAndEntrances = [ /** The exit that leads from LMF to the temple of time. */ export const lmfSecondExit = '\\Lanayru Mining Facility\\Hall of Ancient Robots\\End\\Exit to Temple of Time'; -export const impaSongCheck = - '\\Faron\\Sealed Grounds\\Sealed Temple\\Song from Impa'; -export const completeTriforceReq = '\\Complete Triforce'; - export const swordsToAdd = { Swordless: 0, 'Practice Sword': 1, @@ -42,8 +38,3 @@ export const knownNoGossipStoneHintDistros = [ 'Strong Dowsing All Dungeons', ]; -// These requirements are populated based on required dungeons - -export const gotOpeningReq = 'GoT Opening Requirement'; -export const gotRaisingReq = 'GoT Raising Requirement'; -export const hordeDoorReq = 'Horde Door Requirement'; diff --git a/src/logic/UpstreamTypes.ts b/src/logic/UpstreamTypes.ts index 189cae10..f9987cd9 100644 --- a/src/logic/UpstreamTypes.ts +++ b/src/logic/UpstreamTypes.ts @@ -1,4 +1,5 @@ import { OptionValue, OptionsCommand } from '../permalink/SettingsTypes'; +import { InventoryItem } from './Inventory'; export enum TimeOfDay { DayOnly = 1, @@ -66,6 +67,25 @@ export interface DungeonQuery { export type SettingsQuery = DungeonQuery | OptionQuery; +export type CounterExpression = + | { + type: 'mul'; + factor: number; + } + | { + type: 'lookup'; + dict: Record; + }; + +export interface CounterAddend { + item: InventoryItem; + expression: CounterExpression; +} + +export interface Counter { + targets: CounterAddend[]; +} + export interface RawLogic { items: string[]; checks: Record; @@ -86,7 +106,13 @@ export interface RawLogic { [dungeon: string]: string; }; well_known_requirements?: { - [key in 'open_got' | 'raise_got' | 'horde_door' | 'impa_song_check' | 'complete_triforce']: string; + [key in + | 'open_got' + | 'raise_got' + | 'horde_door' + | 'impa_song_check' + | 'complete_triforce']: string; }; options?: Record; + counters?: Record; } diff --git a/src/logic/booleanlogic/ExpressionParse.ts b/src/logic/booleanlogic/ExpressionParse.ts index f20885e0..768c21cc 100644 --- a/src/logic/booleanlogic/ExpressionParse.ts +++ b/src/logic/booleanlogic/ExpressionParse.ts @@ -1,19 +1,34 @@ -import _ from "lodash"; -import BooleanExpression, { Item } from "./BooleanExpression"; -import { BitVector } from "../bitlogic/BitVector"; -import { andToDnf } from "../bitlogic/LogicalExpression"; +import _ from 'lodash'; +import BooleanExpression, { Item } from './BooleanExpression'; +import { BitVector } from '../bitlogic/BitVector'; +import { andToDnf } from '../bitlogic/LogicalExpression'; +import { CounterThreshold } from '../Logic'; + +export function parseCounterThreshold( + expression: string, +): CounterThreshold | undefined { + const split = expression.split('>='); + if (split.length === 2) { + const count = parseInt(split[1].trim(), 10); + if (!isNaN(count)) { + return { item: split[0].trim(), count }; + } + } + + return undefined; +} export function parseExpression(expression: string) { return booleanExpressionForTokens(splitExpression(expression)); } function splitExpression(expression: string) { - return _.compact( - _.map(expression.split(/\s*([(&|)])\s*/g), _.trim), - ); + return _.compact(_.map(expression.split(/\s*([(&|)])\s*/g), _.trim)); } -function booleanExpressionForTokens(expressionTokens: string[]): BooleanExpression { +function booleanExpressionForTokens( + expressionTokens: string[], +): BooleanExpression { const itemsForExpression = []; let expressionTypeToken; while (!_.isEmpty(expressionTokens)) { @@ -21,7 +36,8 @@ function booleanExpressionForTokens(expressionTokens: string[]): BooleanExpressi if (currentToken === '&' || currentToken === '|') { expressionTypeToken = currentToken; } else if (currentToken === '(') { - const childExpression = booleanExpressionForTokens(expressionTokens); + const childExpression = + booleanExpressionForTokens(expressionTokens); itemsForExpression.push(childExpression); } else if (currentToken === ')') { break; @@ -46,7 +62,9 @@ export function booleanExprToLogicalExpr( booleanExprToLogicalExpr(item, lookup), ); case 'and': { - const mapped = expr.items.map((i) => booleanExprToLogicalExpr(i, lookup)); + const mapped = expr.items.map((i) => + booleanExprToLogicalExpr(i, lookup), + ); return andToDnf(mapped); } default: { diff --git a/src/tooltips/TooltipExpression.ts b/src/tooltips/TooltipExpression.ts index dfe5330f..b58b271f 100644 --- a/src/tooltips/TooltipExpression.ts +++ b/src/tooltips/TooltipExpression.ts @@ -158,6 +158,11 @@ function getReadableItemName(logic: Logic, item: string) { return prettyItemNames[item][1]; } + const counter = logic.counterThresholds?.[item]; + if (counter) { + return `${counter.item} ≥ ${counter.count}`; + } + const match = item.match(itemCountPat); if (match) { const [, baseName, count] = match; diff --git a/src/tracker/selectors.ts b/src/tracker/selectors.ts index b2f0bdc1..38e9278a 100644 --- a/src/tracker/selectors.ts +++ b/src/tracker/selectors.ts @@ -8,11 +8,6 @@ import { TypedOptions } from '../permalink/SettingsTypes'; import { RootState } from '../store/store'; import { currySelector } from '../utils/redux'; import { - completeTriforceReq, - gotOpeningReq, - gotRaisingReq, - hordeDoorReq, - impaSongCheck, knownNoGossipStoneHintDistros, swordsToAdd, } from '../logic/ThingsThatWouldBeNiceToHaveInTheDump'; @@ -312,7 +307,7 @@ function evalCondition( console.warn('unknown options type', ty) return false; } - } + }; return evalInner() !== condition.negation; } @@ -334,14 +329,16 @@ function mapSettings( } } + const runtimeReqs = logic.wellKnownRequirements; + const raiseGotExpr = settings['got-start'] === 'Raised' ? b.true() - : b.singleBit(impaSongCheck); + : b.singleBit(runtimeReqs.impa_song_check); const neededSwords = swordsToAdd[settings['got-sword-requirement']]; let openGotExpr = b.singleBit(`Progressive Sword x ${neededSwords}`); let hordeDoorExpr = settings['triforce-required'] - ? b.singleBit(completeTriforceReq) + ? b.singleBit(runtimeReqs.complete_triforce) : b.true(); const allRequiredDungeonsBits = requiredDungeons.reduce((acc, dungeon) => { @@ -358,9 +355,9 @@ function mapSettings( hordeDoorExpr = hordeDoorExpr.and(dungeonsExpr); } - b.set(gotOpeningReq, openGotExpr); - b.set(gotRaisingReq, raiseGotExpr); - b.set(hordeDoorReq, hordeDoorExpr); + b.set(runtimeReqs.open_got, openGotExpr); + b.set(runtimeReqs.raise_got, raiseGotExpr); + b.set(runtimeReqs.horde_door, hordeDoorExpr); const mapConnection = (from: string, to: string) => { const exitArea = logic.areaGraph.areasByExit[from]; @@ -439,6 +436,30 @@ export function mapInventory(logic: Logic, itemCounts: Record) { } } + if (logic.counters) { + for (const [target, threshold] of Object.entries(logic.counterThresholds)) { + const counter = logic.counters[threshold.item]; + let result = 0; + for (const addend of counter.targets) { + const itemCount = itemCounts[addend.item]; + switch (addend.expression.type) { + case 'mul': + result += addend.expression.factor * itemCount; + break; + case 'lookup': + result += addend.expression.dict[itemCount]; + break; + default: + console.warn('unknown counter expression', addend.expression) + break; + } + } + if (result >= threshold.count) { + b.set(target, b.true()); + } + } + } + return requirements; } From c36f2f3e2b96a6e7d44e2f5f0e1b75fb553aa018 Mon Sep 17 00:00:00 2001 From: robojumper Date: Sat, 18 May 2024 18:32:27 +0200 Subject: [PATCH 3/4] Fix dynamic reqs --- src/logic/Logic.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/logic/Logic.ts b/src/logic/Logic.ts index dc79d6e0..2ab2bde6 100644 --- a/src/logic/Logic.ts +++ b/src/logic/Logic.ts @@ -858,9 +858,14 @@ export function parseLogic(raw: RawLogic, options: OptionDefs): Logic { return acc; }, {}); + const wellKnownReqs = { + ...getLegacyWellKnownRequirements(), + ...raw.well_known_requirements, + }; + // Now map our area graph to BitLogic const newBuilder = new LogicBuilder(rawItems, itemLookup, staticRequirements); - mapAreaToBitLogic(newBuilder, areaGraph, opaqueItems, counterThresholds); + mapAreaToBitLogic(newBuilder, areaGraph, opaqueItems, counterThresholds, Object.values(wellKnownReqs)); // check for orphaned locations. This again should probably not be in here @@ -933,10 +938,7 @@ export function parseLogic(raw: RawLogic, options: OptionDefs): Logic { dungeonCompletionRequirements: raw.dungeon_completion_requirements, areaGraph, optionConditions: raw.options ?? getLegacyOptionConditions(options), - wellKnownRequirements: { - ...getLegacyWellKnownRequirements(), - ...raw.well_known_requirements, - }, + wellKnownRequirements: wellKnownReqs, counters: raw.counters, counterThresholds, }; @@ -947,10 +949,11 @@ function mapAreaToBitLogic( areaGraph: AreaGraph, opaqueItems: BitVector, counters: Logic['counterThresholds'], + wellKnown: string[], area = areaGraph.rootArea, ) { for (const subArea of Object.values(area.subAreas)) { - mapAreaToBitLogic(b, areaGraph, opaqueItems, counters, subArea); + mapAreaToBitLogic(b, areaGraph, opaqueItems, counters, wellKnown, subArea); } if (area.canSleep) { @@ -971,7 +974,10 @@ function mapAreaToBitLogic( !locName.endsWith('Gratitude Crystals') && !locName.includes("\\Gondo's Upgrades\\Upgrade to") && // Counters are populated at runtime - !counters[location.id] + !counters[location.id] && + // Some requirements are populated at runtime + // FIXME these should be identified via "Unknown" + !wellKnown.includes(location.id) ) { opaqueItems.clearBit(b.bit(locName)); } From 76c1bf1f4fc30e871fad789af34d49362cfa9122 Mon Sep 17 00:00:00 2001 From: robojumper Date: Sat, 18 May 2024 19:49:14 +0200 Subject: [PATCH 4/4] Add missed combination queries --- src/logic/UpstreamTypes.ts | 8 +++++++- src/tracker/selectors.ts | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/logic/UpstreamTypes.ts b/src/logic/UpstreamTypes.ts index f9987cd9..b2842865 100644 --- a/src/logic/UpstreamTypes.ts +++ b/src/logic/UpstreamTypes.ts @@ -51,6 +51,12 @@ export interface ExitLink { exit_from_inside: string; } +export interface CombinationQuery { + type: 'combination'; + op: 'and' | 'or'; + args: SettingsQuery[]; +} + export interface OptionQuery { type: 'query'; option: OptionsCommand; @@ -65,7 +71,7 @@ export interface DungeonQuery { negation: boolean; } -export type SettingsQuery = DungeonQuery | OptionQuery; +export type SettingsQuery = CombinationQuery | DungeonQuery | OptionQuery; export type CounterExpression = | { diff --git a/src/tracker/selectors.ts b/src/tracker/selectors.ts index 38e9278a..9dbd79cf 100644 --- a/src/tracker/selectors.ts +++ b/src/tracker/selectors.ts @@ -279,7 +279,18 @@ function evalCondition( condition: SettingsQuery, settings: TypedOptions, requiredDungeons: string[], -) { +): boolean { + + if (condition.type === 'combination') { + const results = condition.args.map((c) => evalCondition(c, settings, requiredDungeons)); + if (condition.op === 'and') { + return results.every(_.identity); + } else if (condition.op === 'or') { + return results.some(_.identity); + } else { + throw new Error("unreachable"); + } + } const evalInner = () => { const ty = condition.type; @@ -297,7 +308,7 @@ function evalCondition( case 'gt': return (settingsValue as number) > (condition.value as number); default: - console.warn('unknown op', condition.op) + console.warn('unknown op', condition); return false; } }