diff --git a/src/components/RelicFilterBar.jsx b/src/components/RelicFilterBar.jsx index 5eefc0491..460c77107 100644 --- a/src/components/RelicFilterBar.jsx +++ b/src/components/RelicFilterBar.jsx @@ -103,6 +103,15 @@ export default function RelicFilterBar(props) { }) } + function generateRestrictedTags(arr) { + return arr.map((x) => { + return { + key: x, + display: Renderer.renderFilter(x), + } + }) + } + function generateEquippedTags(arr) { return arr.map((x) => { return { @@ -114,6 +123,7 @@ export default function RelicFilterBar(props) { const gradeData = generateGradeTags([2, 3, 4, 5]) const verifiedData = generateVerifiedTags([true, false]) + const restrictedData = generateRestrictedTags([false, true]) const setsData = generateImageTags(Object.values(Constants.SetsRelics).concat(Object.values(Constants.SetsOrnaments)).filter((x) => !UnreleasedSets[x]), (x) => Assets.getSetImage(x, Constants.Parts.PlanarSphere), true) const partsData = generateImageTags(Object.values(Constants.Parts), (x) => Assets.getPart(x), false) @@ -238,6 +248,10 @@ export default function RelicFilterBar(props) { Verified + + Restricted + + Equipped diff --git a/src/components/RelicModal.tsx b/src/components/RelicModal.tsx index ec4acf037..f9652d9f4 100644 --- a/src/components/RelicModal.tsx +++ b/src/components/RelicModal.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' -import { Button, Flex, Form, Image, InputNumber, Modal, Radio, Select, theme } from 'antd' +import { Button, Flex, Form, Image, InputNumber, Modal, Radio, Select, Switch, theme } from 'antd' import React, { ReactElement, useEffect, useMemo, useState } from 'react' import { Constants } from 'lib/constants' import { HeaderText } from './HeaderText' @@ -15,6 +15,7 @@ import { calculateUpgradeValues, RelicForm, RelicUpgradeValues, validateRelic } import { CaretRightOutlined } from '@ant-design/icons' import { FormInstance } from 'antd/es/form/hooks/useForm' import { generateCharacterList } from 'lib/displayUtils' +import CharacterSelect from './optimizerTab/optimizerForm/CharacterSelect' const { useToken } = theme @@ -92,6 +93,8 @@ export default function RelicModal(props: { const equippedBy: string = Form.useWatch('equippedBy', relicForm) const [upgradeValues, setUpgradeValues] = useState([]) + const [restrictionList, setRestrictionList] = useState([]) + useEffect(() => { let defaultValues = { grade: 5, @@ -103,6 +106,8 @@ export default function RelicModal(props: { const relic = props.selectedRelic if (!relic || props.type != 'edit') { + relicForm.setFieldValue('restrictionEnabled', false) + relicForm.setFieldValue('restrictionList', []) // Ignore } else { defaultValues = { @@ -121,7 +126,10 @@ export default function RelicModal(props: { substatValue2: renderSubstat(relic, 2).value, substatType3: renderSubstat(relic, 3).stat, substatValue3: renderSubstat(relic, 3).value, + restrictionEnabled: relic.restriction.enabled, + restrictionList: relic.restriction.list, } + setRestrictionList(relic.restriction.list) } onValuesChange(defaultValues) relicForm.setFieldsValue(defaultValues) @@ -363,6 +371,32 @@ export default function RelicModal(props: { + + Restrict in Optimiser + + + { + const excludedCharacterIds = Array.from(x || new Map()) + .filter((entry) => entry[1] == true) + .map((entry) => entry[0]) + relicForm.setFieldValue('restrictionList', excludedCharacterIds) + setRestrictionList(excludedCharacterIds) + }} + multipleSelect={true} + /> + + + + + + diff --git a/src/components/RelicPreview.jsx b/src/components/RelicPreview.jsx index fae5d4151..4ccf09492 100644 --- a/src/components/RelicPreview.jsx +++ b/src/components/RelicPreview.jsx @@ -76,6 +76,7 @@ const RelicPreview = ({ + {relic.restriction && Renderer.renderFilter(relic.restriction.enabled)} { + const [model, setModel] = useState(null) + + const isFilterActive = useCallback(() => { + return model != null && (model.restricted.length > 0) + }, [model]) + + // expose AG Grid Filter Lifecycle callbacks + useImperativeHandle(ref, () => { + return { + doesFilterPass(params) { + if ([0, 2].includes(model.restricted.length)) return true + if ((model.restricted[0] == params.data.restriction.enabled)) return true + return false + }, + + isFilterActive, + + getModel() { + return model + }, + + setModel(model) { + setModel(model) + }, + } + }) + + useEffect(() => { + props.filterChangedCallback() + }, [model, props]) +}) + +RestrictedFilter.displayName = 'RestrictedFilter' +RestrictedFilter.propTypes = { + filterChangedCallback: PropTypes.func, +} const PLOT_ALL = 'PLOT_ALL' const PLOT_CUSTOM = 'PLOT_CUSTOM' @@ -175,6 +213,10 @@ export default function RelicsTab() { operator: 'OR', } + filterModel.restricted = { + restricted: relicTabFilters.restricted, + } + filterModel.part = { conditions: relicTabFilters.part.map((x) => ({ filterType: 'text', @@ -295,6 +337,12 @@ export default function RelicsTab() { } }, }, + { + field: 'restricted', + width: 55, + cellRenderer: Renderer.renderFilterCell, + filter: RestrictedFilter, + }, { field: 'part', valueFormatter: Renderer.readablePart, width: 55, filter: 'agTextColumnFilter' }, { field: 'enhance', width: 55, filter: 'agNumberColumnFilter' }, { field: 'main.stat', valueFormatter: Renderer.readableStat, headerName: 'Main\nStat', width: 70, filter: 'agTextColumnFilter' }, diff --git a/src/components/optimizerTab/optimizerForm/CharacterSelect.tsx b/src/components/optimizerTab/optimizerForm/CharacterSelect.tsx index bb4a9b076..a604bdab1 100644 --- a/src/components/optimizerTab/optimizerForm/CharacterSelect.tsx +++ b/src/components/optimizerTab/optimizerForm/CharacterSelect.tsx @@ -35,7 +35,6 @@ const CharacterSelect: React.FC = ({ value, onChange, sele const [currentFilters, setCurrentFilters] = useState(Utils.clone(defaultFilters)) const characterOptions = useMemo(() => Utils.generateCharacterOptions(), []) const [selected, setSelected] = useState>(new Map()) - const excludedRelicPotentialCharacters = window.store((s) => s.excludedRelicPotentialCharacters) const labelledOptions: { value: string; label }[] = [] for (const option of characterOptions) { @@ -58,7 +57,7 @@ const CharacterSelect: React.FC = ({ value, onChange, sele setTimeout(() => inputRef?.current?.focus(), 100) if (multipleSelect) { - const newSelected = new Map(excludedRelicPotentialCharacters.map((characterId: string) => [characterId, true])) + const newSelected = new Map(value.map((characterId: string) => [characterId, true])) setSelected(newSelected) } } @@ -114,7 +113,7 @@ const CharacterSelect: React.FC = ({ value, onChange, sele allowClear maxTagCount={0} maxTagPlaceholder={() => ( - {excludedRelicPotentialCharacters.length ? `${excludedRelicPotentialCharacters.length} characters excluded` : 'All characters enabled'} + {value.length ? `${value.length} characters excluded` : 'All characters enabled'} )} onClear={() => { if (onChange) onChange(null) diff --git a/src/components/optimizerTab/optimizerForm/OptimizerOptionsDisplay.tsx b/src/components/optimizerTab/optimizerForm/OptimizerOptionsDisplay.tsx index d38f4f805..758b97e9a 100644 --- a/src/components/optimizerTab/optimizerForm/OptimizerOptionsDisplay.tsx +++ b/src/components/optimizerTab/optimizerForm/OptimizerOptionsDisplay.tsx @@ -89,6 +89,17 @@ const OptimizerOptionsDisplay = (): JSX.Element => { Keep current relics + + + } + unCheckedChildren={} + style={{ width: 45, marginRight: 5 }} + /> + + Ignore wearer restrictions + + diff --git a/src/lib/db.js b/src/lib/db.js index 994ad786c..8171facb6 100644 --- a/src/lib/db.js +++ b/src/lib/db.js @@ -128,6 +128,7 @@ window.store = create((set) => ({ subStats: [], grade: [], verified: [], + restricted: [], equipped: [], }, characterTabFilters: { @@ -410,6 +411,12 @@ export const DB = { } else { relic.equippedBy = undefined } + if (!relic.restriction) { + relic.restriction = { + enabled: false, + list: [], + } + } } if (x.scoringMetadataOverrides) { @@ -733,14 +740,17 @@ export const DB = { found.equippedBy = newRelic.equippedBy newRelic = found } + // Fix metadata if field not present + if (!found.restriction) found.restriction = { enabled: false, list: [] } // Save the old relic because it may have edited speed values, delete the hash to prevent duplicates replacementRelics.push(found) stableRelicId = found.id delete oldRelicHashes[hash] } else { - // No match found - save the new relic + // No match found - add the restriction field - save the new relic stableRelicId = newRelic.id + newRelic.restriction = { enabled: false, list: [] } replacementRelics.push(newRelic) } @@ -884,6 +894,7 @@ export const DB = { match.verified = true updatedOldRelics.push(match) } else { + newRelic.restriction = { enabled: false, list: [] } oldRelics.push(newRelic) newRelic.verified = true diff --git a/src/lib/hint.jsx b/src/lib/hint.jsx index a9dd361a6..ba0cdf925 100644 --- a/src/lib/hint.jsx +++ b/src/lib/hint.jsx @@ -128,14 +128,14 @@ export const Hint = { content: (

- Character priority filter + Allow equipped relics {' '} - - When this option is enabled, the character may only steal relics from lower priority characters. The optimizer will ignore relics equipped by higher priority characters on the list. Change character ranks from the priority selector or by dragging them on the Characters page. + - When enabled, the optimizer will allow using currently equipped by a character for the search. Otherwise equipped relics are excluded

- Boost main stat + Character priority filter {' '} - - Calculates relic mains stats as if they were this level (or their max if they can't reach this level) if they are currently below it. Substats are not changed accordingly, so builds with lower level relics may be stronger once you level them. + - When this option is enabled, the character may only steal relics from lower priority characters. The optimizer will ignore relics equipped by higher priority characters on the list. Change character ranks from the priority selector or by dragging them on the Characters page.

Keep current relics @@ -143,9 +143,9 @@ export const Hint = { - The character must use its currently equipped items, and the optimizer will try to fill in empty slots

- Include equipped relics + Ignore wearer restrictions {' '} - - When enabled, the optimizer will allow using currently equipped by a character for the search. Otherwise equipped relics are excluded + - When this option is enabled, the character will ignore the wearer restrictions of relics

Priority @@ -158,10 +158,15 @@ export const Hint = { - Select specific characters' equipped relics to exclude for the search. This setting overrides the priority filter

- Enhance / grade + Enhance / Rarity {' '} - Select the minimum enhance to search for and minimum stars for relics to include

+

+ Boost main stat + {' '} + - Calculates relic mains stats as if they were this level (or their max if they can't reach this level) if they are currently below it. Substats are not changed accordingly, so builds with lower level relics may be stronger once you level them. +

), } diff --git a/src/lib/optimizer/optimizer.js b/src/lib/optimizer/optimizer.js index 5a28fb8b2..2b9a5e279 100644 --- a/src/lib/optimizer/optimizer.js +++ b/src/lib/optimizer/optimizer.js @@ -46,6 +46,8 @@ export const Optimizer = { relics = RelicFilters.applyRankFilter(request, relics) relics = RelicFilters.applyExcludeFilter(request, relics) + relics = RelicFilters.applyRestrictionFilter(request, relics) + // Pre-split filters const preFilteredRelicsByPart = RelicFilters.splitRelicsByPart(relics) @@ -62,7 +64,7 @@ export const Optimizer = { return [relics, preFilteredRelicsByPart] }, - optimize: function(request) { + optimize: function (request) { CANCEL = false window.store.getState().setPermutationsSearched(0) diff --git a/src/lib/relicFilters.js b/src/lib/relicFilters.js index bff8589ca..c382080c0 100644 --- a/src/lib/relicFilters.js +++ b/src/lib/relicFilters.js @@ -135,6 +135,12 @@ export const RelicFilters = { return ret }, + applyRestrictionFilter: (request, relics) => { + if (request.ignoreRestrictions) return relics + const ret = relics.filter((x) => !(x.restriction.list).includes(request.characterId) || !x.restriction.enabled) + return ret + }, + applyGradeFilter: (request, relics) => { return relics.filter((x) => x.grade ? x.grade >= request.grade : true) }, diff --git a/src/lib/relicModalController.ts b/src/lib/relicModalController.ts index 25a44cbf8..9e6f68ce6 100644 --- a/src/lib/relicModalController.ts +++ b/src/lib/relicModalController.ts @@ -45,6 +45,8 @@ export type RelicForm = { substatValue2: number substatValue3: number equippedBy: string + restrictionEnabled: boolean + restrictionList: string[] } export function validateRelic(relicForm: RelicForm): Relic | void { @@ -170,6 +172,10 @@ export function validateRelic(relicForm: RelicForm): Relic | void { } relic.substats = substats RelicAugmenter.augment(relic) + relic.restriction = { + enabled: relicForm.restrictionEnabled, + list: relicForm.restrictionList, + } return relic } diff --git a/src/lib/renderer.jsx b/src/lib/renderer.jsx index 5981d0d31..65eb0cd13 100644 --- a/src/lib/renderer.jsx +++ b/src/lib/renderer.jsx @@ -1,5 +1,5 @@ import { Flex, Image, Tooltip } from 'antd' -import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons' +import { CheckCircleFilled, CloseCircleFilled, FilterTwoTone } from '@ant-design/icons' import { Constants, StatsToReadableShort } from './constants.ts' import { Assets } from './assets' import { Utils } from './utils' @@ -193,6 +193,17 @@ export const Renderer = { :
) }, + renderFilterCell: (x) => { + const enabled = x.data.restriction.enabled + return Renderer.renderFilter(enabled) + }, + renderFilter: (enabled) => { + return ( + enabled + ? + : + ) + }, renderEquipped: ({ equipped }) => { return ( equipped diff --git a/src/types/Relic.d.ts b/src/types/Relic.d.ts index eb8edfff4..10e35fede 100644 --- a/src/types/Relic.d.ts +++ b/src/types/Relic.d.ts @@ -21,6 +21,7 @@ export type Relic = { grade: RelicGrade id: GUID verified?: boolean + restriction: RelicOptimizerRestriction main: { stat: MainStats @@ -50,3 +51,8 @@ export type Stat = { stat: string value: number } + +type RelicOptimizerRestriction = { + enabled: boolean + list: string[] +} diff --git a/src/types/store.ts b/src/types/store.ts index d008110c2..0cccda9b0 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -25,6 +25,7 @@ type RelicTabFilters = { subStats: unknown[] grade: unknown[] verified: unknown[] + restricted: unknown[] equipped: unknown[] } diff --git a/tests/RelicModal/3-edit-from-relics-tab.spec.ts b/tests/RelicModal/3-edit-from-relics-tab.spec.ts index 651f7663c..83a727db8 100644 --- a/tests/RelicModal/3-edit-from-relics-tab.spec.ts +++ b/tests/RelicModal/3-edit-from-relics-tab.spec.ts @@ -10,7 +10,7 @@ test('Open RelicModal in edit mode from the CharacterPreview tab', async ({ page // got relics tab await page.getByRole('menuitem', { name: 'Relics' }).click() - await page.getByRole('row', { name: 'Hunter of Glacial Forest Head 15 HP 705 11.0 10.3 3.4 5.1 32.4' }).click() + await page.getByRole('row', { name: 'Hunter of Glacial Forest filter Head 15 HP 705 11.0 10.3 3.4 5.1 32.4' }).click() await page.getByText('+15HP705CRIT Rate11.0%CRIT').click() await expect(page.getByRole('dialog')).toContainText('Equipped by') diff --git a/tests/RelicsTab/delete-relic.spec.ts b/tests/RelicsTab/delete-relic.spec.ts index 4326088fe..e5c996153 100644 --- a/tests/RelicsTab/delete-relic.spec.ts +++ b/tests/RelicsTab/delete-relic.spec.ts @@ -9,10 +9,10 @@ test('Delete relic from RelicsTab', async ({ page }) => { // nav to RelicsTab await page.getByRole('menuitem', { name: 'Relics' }).click() - await page.getByRole('row', { name: 'Hunter of Glacial Forest Head 15 HP 705 11.0 10.3 3.4 5.1 32.4' }).click() + await page.getByRole('row', { name: 'Hunter of Glacial Forest filter Head 15 HP 705 11.0 10.3 3.4 5.1 32.4' }).click() await page.getByRole('button', { name: 'Delete Relic' }).click() await page.getByRole('button', { name: 'Yes' }).click() - await expect(page.getByRole('row', { name: 'Hunter of Glacial Forest Head 15 HP 705 11.0 10.3 3.4 5.1 32.4' })).toHaveCount(0) + await expect(page.getByRole('row', { name: 'Hunter of Glacial Forest filter Head 15 HP 705 11.0 10.3 3.4 5.1 32.4' })).toHaveCount(0) await expect(page.locator('body')).toContainText('Successfully deleted relic') })