Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Filter mods menu with search pills #9304

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions config/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,27 @@
"Unknown": "Challenge",
"Weekly": "Weekly Challenge"
},
"ModEffect": {
"Finder": "Finder",
"Targeting": "Targeting",
"Dexterity": "Dexterity",
"Loader": "Loader",
"Reserves": "Reserves",
"Unflinching": "Unflinching",
"Holster": "Holster",
"Scavenger": "Scavenger",
"Siphon": "Siphon",
"Scout": "Scout",
"Stat": "Stat",
"Resistance": "Resistance",
"Super": "Super",
"ClassAbility": "Class Ability",
"Grenade": "Grenade",
"Orbs": "Orbs",
"Finisher": "Finisher",
"Champion": "Champion",
"ArmorCharge": "Armor Charge"
},
"MoveAmount": {
"Amount": "Amount:"
},
Expand Down
21 changes: 21 additions & 0 deletions i18next-scanner.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ module.exports = {
progress: { list: ['Bounties', 'Items', 'Quests'] },
sockets: { list: ['Mod', 'Ability', 'Shader', 'Ornament', 'Fragment', 'Aspect', 'Projection', 'Transmat', 'Super'] },
unsupported: { list: ['Unsupported', 'Steam'] },
modeffect: {list: [
"Finder",
"Targeting",
"Dexterity",
"Loader",
"Reserves",
"Unflinching",
"Holster",
"Scavenger",
"Siphon",
"Scout",
"Stat",
"Resistance",
"Super",
"ClassAbility",
"Grenade",
"Orbs",
"Finisher",
"Champion",
"ArmorCharge",
]}
};

parser.parseFuncFromString(content, { list: ['t', 'tl', 'DimError'] }, (key, options) => {
Expand Down
8 changes: 7 additions & 1 deletion src/app/item-picker/ItemPicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
// These classes are used in item-picker, mod-picker, exotic-picker and search results

.item-picker {
.sheet-contents > div {
.sheet-contents {
padding: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.sub-bucket {
padding: 0;
}
.item {
cursor: pointer;
Expand Down
4 changes: 4 additions & 0 deletions src/app/loadout/plug-drawer/PlugDrawer.m.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.elementIcon {
height: 14px;
width: 14px;
}
7 changes: 7 additions & 0 deletions src/app/loadout/plug-drawer/PlugDrawer.m.scss.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

149 changes: 146 additions & 3 deletions src/app/loadout/plug-drawer/PlugDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { D2ManifestDefinitions } from 'app/destiny2/d2-definitions';
import { languageSelector } from 'app/dim-api/selectors';
import ElementIcon from 'app/dim-ui/ElementIcon';
import FilterPills, { Option } from 'app/dim-ui/FilterPills';
import { PluggableInventoryItemDefinition } from 'app/inventory/item-types';
import { useD2Definitions } from 'app/manifest/selectors';
import { createPlugSearchPredicate } from 'app/search/plug-search';
import { SearchInput } from 'app/search/SearchInput';
import { createPlugSearchPredicate } from 'app/search/plug-search';
import { useIsPhonePortrait } from 'app/shell/selectors';
import { isiOSBrowser } from 'app/utils/browsers';
import { Comparator } from 'app/utils/comparators';
import { DestinyClass } from 'bungie-api-ts/destiny2';
import { HashLookup } from 'app/utils/util-types';
import { DamageType, DestinyClass } from 'bungie-api-ts/destiny2';
import modsInfoFile from 'data/d2/mods.json';
import { t } from 'i18next';
import { produce } from 'immer';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import Sheet from '../../dim-ui/Sheet';
import '../../item-picker/ItemPicker.scss';
import Footer from './Footer';
import styles from './PlugDrawer.m.scss';
import PlugSection from './PlugSection';
import { PlugSet } from './types';

Expand Down Expand Up @@ -70,6 +77,7 @@ export default function PlugDrawer({
const defs = useD2Definitions()!;
const language = useSelector(languageSelector);
const [query, setQuery] = useState(initialQuery || '');
const [selectedFilters, setSelectedFilters] = useState<Option[]>([]);
const [internalPlugSets, setInternalPlugSets] = useState(() =>
plugSets
.map((plugSet) => ({ ...plugSet, plugs: Array.from(plugSet.plugs).sort(sortPlugs) }))
Expand Down Expand Up @@ -183,6 +191,11 @@ export default function PlugDrawer({
/>
);

const [filterPillOptions, mapping] = useMemo(
() => modFilterPillOptions(defs, queryFilteredPlugSets),
[defs, queryFilteredPlugSets]
);

// On iOS at least, focusing the keyboard pushes the content off the screen
const nativeAutoFocus = !isPhonePortrait && !isiOSBrowser();

Expand All @@ -197,9 +210,25 @@ export default function PlugDrawer({
autoFocus={nativeAutoFocus}
/>
</div>
{filterPillOptions.length > 0 && (
<FilterPills
darkBackground
options={filterPillOptions}
selectedOptions={selectedFilters}
onOptionsSelected={setSelectedFilters}
/>
)}
</div>
);

const finalFilteredPlugSets =
selectedFilters.length > 0
? queryFilteredPlugSets.map((plugSet) => ({
...plugSet,
plugs: filterPlugsFromPills(plugSet.plugs, selectedFilters, mapping),
}))
: queryFilteredPlugSets;

return (
<Sheet
onClose={onClose}
Expand All @@ -208,7 +237,7 @@ export default function PlugDrawer({
sheetClassName="item-picker"
freezeInitialHeight={true}
>
{queryFilteredPlugSets.map((plugSet) => (
{finalFilteredPlugSets.map((plugSet) => (
<PlugSection
key={plugSet.plugSetHash}
plugSet={plugSet}
Expand All @@ -221,3 +250,117 @@ export default function PlugDrawer({
</Sheet>
);
}

export enum ModEffect {
None = 0, // Reserve 0
// Standardized per-weapon mods
Finder,
Targeting,
Dexterity,
Loader,
Reserves,
Unflinching,
Holster,
Scavenger,
Siphon,
Scout,
// More like effects that any mod can have
Stat,
Resistance,
Super,
ClassAbility,
Grenade,
Orbs,
Finisher,
Champion,
ArmorCharge,
}

/** Interesting things about mods that we can't figure out from the translated defs */
export interface ModInfo {
effects?: ModEffect[];
element?: DamageType[];
}

export type DefType = keyof ModInfo;

export interface BountyFilter {
type: DefType;
hash: number;
}

function modFilterPillOptions(
defs: D2ManifestDefinitions,
plugSets: PlugSet[]
): [options: Option[], mapping: { [type in DefType]: { [key: number]: number[] } }] {
const mapped: { [type in DefType]: { [key: number]: number[] } } = {
effects: {},
element: {},
};

for (const plugSet of plugSets) {
for (const plug of plugSet.plugs) {
const info = (modsInfoFile as unknown as HashLookup<{ [type in DefType]: number[] }>)[
plug.hash
];
if (info) {
for (const key in info) {
for (const value of info[key as 'effects' | 'element']) {
(mapped[key as 'effects' | 'element'][value] ??= []).push(plug.hash);
}
}
}
}
}

const flattened = Object.entries(mapped).flatMap(([type, mapping]) =>
Object.entries(mapping).map(([value, plugHashes]) => ({
type: type as DefType,
value: parseInt(value, 10),
plugHashes,
}))
);

const options = flattened.map(({ type, value }) => {
let content: React.ReactNode;
switch (type) {
case 'effects':
content = t(`ModEffect.${ModEffect[value]}`, { metadata: { keys: 'modeffect' } });
break;
case 'element': {
const damageType = Object.values(defs.DamageType.getAll()).find(
(d) => d.enumValue === value
)!;
content = (
<>
<ElementIcon className={styles.elementIcon} element={damageType} />
{damageType.displayProperties.name}
</>
);
break;
}
}

return {
key: `${type}.${value}`,
content,
};
});

return [options, mapped];
}

function filterPlugsFromPills(
plugs: PluggableInventoryItemDefinition[],
options: Option[],
mapping: { [type in DefType]: { [key: number]: number[] } }
) {
let allHashes = new Set(plugs.map((p) => p.hash));
for (const { key } of options) {
const [type, value] = key.split('.');
const hashes = new Set(mapping[type as DefType][value as unknown as number] ?? []);
console.log({ type, value, result: mapping[type as DefType][value as unknown as number] });
allHashes = new Set([...allHashes].filter((h) => hashes.has(h)));
}
return plugs.filter((p) => allHashes.has(p.hash));
}
15 changes: 3 additions & 12 deletions src/app/loadout/plug-drawer/PlugSection.m.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,12 @@
font-weight: 600;
font-size: 20px;
border-bottom: 1px solid white;
margin: 8px 0;
margin: 4px 0 8px 0;
padding-bottom: 4px;
}

.items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 10px;
margin: 0 4px 16px 4px;
}

.bucket {
h3 {
text-transform: uppercase;
letter-spacing: 1px;
margin: 0 0 4px 0;
}
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 4px;
}
1 change: 0 additions & 1 deletion src/app/loadout/plug-drawer/PlugSection.m.scss.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions src/app/loadout/plug-drawer/SelectablePlug.m.scss
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
@import '../../variables';

.plug {
> * {
margin-right: 8px;
}

border: 1px solid #666;
cursor: pointer;
padding: 8px;
display: flex;
flex-direction: row;
gap: 8px;
height: 100%;
box-sizing: border-box;
&:hover {
Expand Down
Loading