diff --git a/components/contexts/GroupProvider.tsx b/components/contexts/GroupProvider.tsx index f4c3b93..16c16c3 100644 --- a/components/contexts/GroupProvider.tsx +++ b/components/contexts/GroupProvider.tsx @@ -9,6 +9,7 @@ interface Props { interface GroupContextType { doNotSuggest: Set + exportedFactories: Set ignoredFactories: string[] setIgnoredFactories(factories: string[]): void @@ -21,13 +22,14 @@ interface GroupContextType { removeGroup(name: string): void renameGroup(name: string, newName: string): void - setFactories(name: string, factories: string[], type: 'inputs'|'intermediates'|'exports'|'malls'): void + setFactories(name: string, factories: string[], type: 'exports'|'malls'): void getInputType(uid: string): 'base'|'produced'|'unknown' } const defaultValues: GroupContextType = { doNotSuggest: new Set(), + exportedFactories: new Set(), ignoredFactories: [], setIgnoredFactories() {}, @@ -53,7 +55,7 @@ export const GroupProvider: FC = ({children}) => { const [groups, setGroups] = useLocalStorage>('serviceGroups', {}) const doNotSuggest = useMemo>(() => { - return new Set([...Object.values(groups).flatMap(group => [...group.exports, ...group.malls, ...(group.malls.length ? group.intermediates : [])]), ...excludedSuggestions, ...basicValues]) + return new Set([...Object.values(groups).flatMap(group => [...group.exports, ...group.malls]), ...excludedSuggestions, ...basicValues]) }, [basicValues, groups, excludedSuggestions]) const exportedFactories = useMemo>(() => { @@ -63,7 +65,7 @@ export const GroupProvider: FC = ({children}) => { const addGroup = useCallback((name: string, exports: string[] = [], malls: string[] = []) => { if (name in groups) return false setGroups(groups => { - groups[name] = { name, inputs: [], intermediates: [], exports, malls } + groups[name] = { name, exports, malls } return groups }) return true @@ -75,6 +77,7 @@ export const GroupProvider: FC = ({children}) => { }) }, [setGroups]) const renameGroup = useCallback((name: string, newName: string) => { + if (name === newName) return setGroups(groups => { groups[newName] = {...groups[name], name: newName} delete groups[name] @@ -96,6 +99,7 @@ export const GroupProvider: FC = ({children}) => { const value: GroupContextType = useMemo(() => ({ doNotSuggest, + exportedFactories, ignoredFactories: excludedSuggestions, setIgnoredFactories: setExcludedSuggestions, @@ -110,6 +114,6 @@ export const GroupProvider: FC = ({children}) => { setFactories, getInputType - }), [addGroup, basicValues, doNotSuggest, excludedSuggestions, getInputType, groups, removeGroup, renameGroup, setBasicValues, setExcludedSuggestions, setFactories]) + }), [addGroup, basicValues, doNotSuggest, excludedSuggestions, exportedFactories, getInputType, groups, removeGroup, renameGroup, setBasicValues, setExcludedSuggestions, setFactories]) return {children} } diff --git a/components/home/EntitySpan/EntitySpan.module.css b/components/home/EntitySpan/EntitySpan.module.css index c878290..bd9d543 100644 --- a/components/home/EntitySpan/EntitySpan.module.css +++ b/components/home/EntitySpan/EntitySpan.module.css @@ -10,6 +10,8 @@ .spanSimple { padding-inline-start: 0.2em; + position: relative; + display: inline-block; } .tooltip { @@ -19,8 +21,8 @@ display: none; position: absolute; left: calc(100% + var(--arrow-width)); - top: -500%; - bottom: -500%; + top: -5000%; + bottom: -5000%; margin: auto 0; width: max-content; height: max-content; @@ -37,8 +39,8 @@ .tooltip::before { content: ""; border-style: solid; - top: -500%; - bottom: -500%; + top: 0; + bottom: 0; margin: auto 0; height: max-content; border-width: var(--arrow-height) var(--arrow-width) var(--arrow-height) 0; @@ -76,6 +78,10 @@ margin-block-start: 0; } +.usedBy { + max-width: 20em; +} + .leftClick { height: 1em; transform: translateY(0.1em); diff --git a/components/home/EntitySpan/EntitySpan.tsx b/components/home/EntitySpan/EntitySpan.tsx index c8ce508..be443ba 100644 --- a/components/home/EntitySpan/EntitySpan.tsx +++ b/components/home/EntitySpan/EntitySpan.tsx @@ -1,25 +1,28 @@ import {FC, HTMLProps, memo, useMemo} from "react" -import {Entity} from "../../../src/types" +import {EnrichedEntity, Entity} from "../../../src/types" import {useFactories} from "../../../src/hooks/useFactories" import styles from './EntitySpan.module.css' import {RecipeSpan} from "../Recipe/Recipe"; import {LeftClickIcon} from "../LeftClickIcon/LeftClickIcon"; import cx from 'classnames'; +import {EntityIcon} from "../EntityIcon/EntityIcon"; interface Props extends Omit, 'value'> { - value: Entity|string + value: EnrichedEntity|string state?: 'base'|'produced'|'unknown' leftClickText?: string rightClickText?: string simpleStyle?: boolean + className?: string } -const EntitySpanUnmemo: FC = ({value, state, leftClickText, rightClickText, simpleStyle, ...rest}) => { +const EntitySpanUnmemo: FC = ({className, value, state, leftClickText, rightClickText, simpleStyle, ...rest}) => { const {findFactory} = useFactories() - const entity = useMemo(() => { + const entity = useMemo(() => { return typeof value === "object" ? value : findFactory(value) ?? { + usedBy: [], href: value, name: value, image: value, @@ -27,7 +30,7 @@ const EntitySpanUnmemo: FC = ({value, state, leftClickText, rightClickTex } }, [findFactory, value]) - return + return {/* eslint-disable-next-line @next/next/no-img-element */} {entity.name}/ {entity.name} @@ -38,6 +41,14 @@ const EntitySpanUnmemo: FC = ({value, state, leftClickText, rightClickTex )} + {entity.usedBy?.length ? ( + <> +
Used By
+
+ {entity.usedBy.map(used => )} +
+ + ) : null} {(leftClickText || rightClickText) && ( <>
Actions
diff --git a/components/home/Group/Group.module.css b/components/home/Group/Group.module.css index 664df4c..da75cdc 100644 --- a/components/home/Group/Group.module.css +++ b/components/home/Group/Group.module.css @@ -1,8 +1,7 @@ .root { position: relative; border: 2px solid black; - padding: 0.5em; - margin: 1em; + padding: 1em 0.5em; } .quit { diff --git a/components/home/Group/Group.tsx b/components/home/Group/Group.tsx index 1d32568..d1415cf 100644 --- a/components/home/Group/Group.tsx +++ b/components/home/Group/Group.tsx @@ -1,7 +1,7 @@ import {FC, memo, useCallback, useEffect, useMemo} from "react"; import {FactorySelect} from "../FactorySelect/FactorySelect"; import {useFactories} from "../../../src/hooks/useFactories"; -import {Entity, Group} from "../../../src/types"; +import {EnrichedEntity, Entity, Group} from "../../../src/types"; import styles from "./Group.module.css" import {EntitySpan} from "../EntitySpan/EntitySpan"; import {useGroups} from "../../contexts/GroupProvider"; @@ -11,11 +11,13 @@ interface Props { } const GroupBoxBase: FC = ({ group }) => { - const {factories} = useFactories() + const {factories, findFactory} = useFactories() console.log("group") const { doNotSuggest, setFactories, + baseFactories, + exportedFactories, ignoredFactories, setIgnoredFactories, renameGroup, @@ -24,42 +26,59 @@ const GroupBoxBase: FC = ({ group }) => { } = useGroups() const { name, - inputs, - intermediates, exports, malls } = group - const calculatedInputs = useMemo(() => { - const allProducingFactories = [...intermediates, ...exports, ...malls] - const newData: string[] = factories - .filter(factory => allProducingFactories.includes(factory.href)) - .flatMap(factory => Object.keys(factory.recipe?.prerequisites ?? {})) - const uniqueInputs = Array.from(new Set(newData)) - return uniqueInputs - .filter(input => !allProducingFactories.includes(input)) - .sort((a, b) => a.localeCompare(b)) - }, [factories, intermediates, exports, malls]) + const [inputs, intermediates] = useMemo<[string[], string[]]>(() => { + const allProducingFactories = [...exports, ...malls] + const prducingSet = new Set(allProducingFactories) + const ignored = new Set(ignoredFactories) + const base = new Set(baseFactories) + const inputs = new Set() + const intermediates = new Set() - const suggestions = useMemo(() => { - const selectedValues = Array.from(new Set([...inputs, ...intermediates, ...exports, ...malls])) - const availableIngredients = Array.from(new Set([...selectedValues, ...calculatedInputs])) + let next: string|undefined + while (next = allProducingFactories.pop()) { + const pres = Object.keys(findFactory(next)?.recipe?.prerequisites ?? {}) + for (const pre of pres) { + if (exportedFactories.has(pre) || base.has(pre)) { + if (!prducingSet.has(pre)) inputs.add(pre) + } else if (!intermediates.has(pre)) { + if (!prducingSet.has(pre)) intermediates.add(pre) + allProducingFactories.push(pre) + } + } + } + return [ + Array.from(inputs).sort((a, b) => a.localeCompare(b)), + Array.from(intermediates).sort((a, b) => a.localeCompare(b)) + ] + }, [exports, malls, ignoredFactories, baseFactories, findFactory, exportedFactories]) + + const [suggestionsExport, suggestionMall] = useMemo<[EnrichedEntity[], EnrichedEntity[]]>(() => { + const selectedValues = Array.from(new Set([...exports, ...malls])) + const availableIngredients = Array.from(new Set([...selectedValues, ...intermediates, ...inputs])) return factories .filter(factory => { if (!factory.recipe) return false if (selectedValues.includes(factory.href)) return false if (doNotSuggest.has(factory.href)) return false - const prerequisites = Object.keys(factory.recipe?.prerequisites ?? {}) + const prerequisites = Object.keys(factory.recipe.prerequisites ?? {}) return prerequisites.every(pre => availableIngredients.includes(pre)) }) - }, [doNotSuggest, factories, calculatedInputs, inputs, intermediates, exports, malls]) + .reduce((acc, factory) => + (factory.usedBy?.length ?? 0) >= 3 + ? [[...acc[0], factory], acc[1]] + : [acc[0], [...acc[1], factory]], + [[], []] as [EnrichedEntity[], EnrichedEntity[]] + ) + }, [exports, malls, intermediates, inputs, factories, doNotSuggest]) const addFactory = (uid: string, type: Parameters[2]) => { setFactories(name, [...group[type], uid], type) } - const setInputFactories = useCallback((factories: string[]) => setFactories(name, factories, 'inputs'), [setFactories, name]) - const setIntermediateFactories = useCallback((factories: string[]) => setFactories(name, factories, 'intermediates'), [setFactories, name]) const setExportFactories = useCallback((factories: string[]) => setFactories(name, factories, 'exports'), [setFactories, name]) const setMallFactories = useCallback((factories: string[]) => setFactories(name, factories, 'malls'), [setFactories, name]) @@ -75,44 +94,47 @@ const GroupBoxBase: FC = ({ group }) => { {name} - - - - - +

Exported Factories

- +

Mall Factories

-

Inputs

+ { inputs.length ? <> +

Input Factories ({inputs.length})

- { calculatedInputs.map(input => addFactory(input, 'intermediates')} state={getInputType(input)} - leftClickText={"Add to intermediate factories"} + onContextMenu={event => { + event.preventDefault() + setIgnoredFactories([...ignoredFactories, input]) + }} + rightClickText={"Exclude this recipe from suggestions"} />) } -
- { suggestions.length ? <> -

Suggestions

+ + : null } + { intermediates.length ? <> +

Intermediate Factories ({intermediates.length})

- { suggestions.map(suggestion => ) } +
+ : null } + { suggestionsExport.length ? <> +

Suggestions (Export)

+
+ { suggestionsExport.map(suggestion => addFactory(suggestion.href, 'exports')} @@ -125,6 +147,22 @@ const GroupBoxBase: FC = ({ group }) => { />) }
: null } + { suggestionMall.length ? <> +

Suggestions (Mall)

+
+ { suggestionMall.map(suggestion => addFactory(suggestion.href, 'malls')} + onContextMenu={event => { + event.preventDefault() + setIgnoredFactories([...ignoredFactories, suggestion.href]) + }} + leftClickText={"Add to mall factories"} + rightClickText={"Exclude this recipe from suggestions"} + />) } +
+ : null } } diff --git a/components/home/Home.module.css b/components/home/Home.module.css index 0ddb197..9876e31 100644 --- a/components/home/Home.module.css +++ b/components/home/Home.module.css @@ -1,6 +1,8 @@ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, max-content)); + gap: 1em; + margin-top: 2em; } .missingFactories { diff --git a/components/home/Home.tsx b/components/home/Home.tsx index 2000672..55a0071 100644 --- a/components/home/Home.tsx +++ b/components/home/Home.tsx @@ -3,7 +3,7 @@ import {GroupBox} from "./Group/Group"; import {FactorySelect} from "./FactorySelect/FactorySelect"; import styles from "./Home.module.css" import {useFactories} from "../../src/hooks/useFactories"; -import {Entity} from "../../src/types"; +import {EnrichedEntity, Entity} from "../../src/types"; import pako from 'pako'; import {EntitySpan} from "./EntitySpan/EntitySpan"; import {useGroups} from "../contexts/GroupProvider"; @@ -16,15 +16,20 @@ export const HomeComponent: FC = () => { groups, addGroup, doNotSuggest, - baseFactories, - setBaseFactories, ignoredFactories, setIgnoredFactories } = useGroups() const [newGroupValue, setNewGroupValue] = useState("New group") - const missingFactories = useMemo(() => { - return factories.filter(factory => !doNotSuggest.has(factory.href) && factory.recipe) + const [missingExport, missingMall] = useMemo<[EnrichedEntity[], EnrichedEntity[]]>(() => { + return factories + .filter(factory => !doNotSuggest.has(factory.href) && factory.recipe) + .reduce((acc, factory) => + (factory.usedBy?.length ?? 0) >= 3 + ? [[...acc[0], factory], acc[1]] + : [acc[0], [...acc[1], factory]], + [[], []] as [EnrichedEntity[], EnrichedEntity[]] + ) }, [factories, doNotSuggest]) const store = () => { @@ -42,9 +47,9 @@ export const HomeComponent: FC = () => {
- Missing factories + Missing export factories
- { missingFactories.map(missing => ( + { missingExport.map(missing => ( { event.preventDefault() setIgnoredFactories([...ignoredFactories, missing.href]) }} - leftClickText={"Create a new factory with this item and name"} + leftClickText={"Create a new group with this name and item as exported factory"} + rightClickText={"Exclude this recipe from suggestions"} + /> + ))} +
+
+
+ Missing mall factories +
+ { missingMall.map(missing => ( + { + addGroup(newGroupValue !== "New group" ? newGroupValue : missing.name, [], [missing.href]) + setNewGroupValue("New group") + }} + onContextMenu={event => { + event.preventDefault() + setIgnoredFactories([...ignoredFactories, missing.href]) + }} + leftClickText={"Create a new group with this name and item as mall factory"} rightClickText={"Exclude this recipe from suggestions"} /> ))} diff --git a/src/hooks/useFactories.ts b/src/hooks/useFactories.ts index 40b1550..3d5013a 100644 --- a/src/hooks/useFactories.ts +++ b/src/hooks/useFactories.ts @@ -1,22 +1,19 @@ import {EnrichedEntity, Entity} from "../types"; import details from "../../res/details.json"; -const factories = details as Entity[] +const factories = (details as Entity[]).map((detail: EnrichedEntity) => { + detail.usedBy = (details as Entity[]) + .filter(f => Object + .keys(f.recipe?.prerequisites ?? {}) + .includes(detail.href) + ) + return detail; +}) -const detailsMap = Object - .fromEntries( - factories.map((detail: EnrichedEntity) => { - detail.usedBy = factories - .filter(f => Object - .keys(f.recipe?.prerequisites ?? {}) - .includes(detail.href) - ) - return [detail.href, detail]; - }) - ) +const detailsMap = Object.fromEntries(factories.map((detail: EnrichedEntity) => [detail.href, detail])) export const useFactories = () => ({ - factories: factories, + factories, findFactory: (uid: string): EnrichedEntity|undefined => { return detailsMap[uid] } diff --git a/src/types.ts b/src/types.ts index 7656701..34c4e33 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,8 +20,6 @@ export interface EnrichedEntity extends Entity { export interface Group { name: string - inputs: string[] - intermediates: string[] exports: string[] malls: string[] } diff --git a/styles/globals.css b/styles/globals.css index 372363a..f6294c2 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -9,7 +9,7 @@ body { body { color: black; background: #FAFAFA; - padding-inline: 2em; + padding: 2em; } a { @@ -21,6 +21,19 @@ a { box-sizing: border-box; } +:is(h1, h2, h3, h4, h5, h6):is(:first-child) { + margin-block-start: 0; +} + +h3 { + margin-block: 1.5em 1em; +} + +h4 { + margin-block: 1em 0.3em; + font-weight: 500; +} + @media (prefers-color-scheme: dark) { html { color-scheme: dark;