diff --git a/src/factories/Factory.ts b/src/factories/Factory.ts index d06109eb..fae6ebd6 100644 --- a/src/factories/Factory.ts +++ b/src/factories/Factory.ts @@ -1,7 +1,16 @@ import type { JSONContent } from '@tiptap/react'; import type { LogisticType } from '@/recipes/logistics/LogisticTypes'; -export type FactoryProgressStatus = 'draft' | 'todo' | 'in_progress' | 'done'; +export type FactoryProgressStatus = + | 'draft' + | 'todo' + | 'in_progress' + | 'done' + | 'disabled'; + +export const isFactoryDisabled = ( + factory: Pick | null | undefined, +) => factory?.progress === 'disabled'; export interface Factory { id: string; diff --git a/src/factories/charts/graph/useFactoriesGraph.tsx b/src/factories/charts/graph/useFactoriesGraph.tsx index 82c99c36..0276cd5b 100644 --- a/src/factories/charts/graph/useFactoriesGraph.tsx +++ b/src/factories/charts/graph/useFactoriesGraph.tsx @@ -12,15 +12,21 @@ export function useFactoriesGraph() { const nodes: Node[] = []; const edges: Edge[] = []; + const disabledIds = new Set( + factories.filter(f => f?.progress === 'disabled').map(f => f!.id), + ); + const maxInputAmount = max( - factories.flatMap( - factory => factory?.inputs?.map(input => input.amount ?? 0) ?? [], - ), + factories + .filter(f => f && f.progress !== 'disabled') + .flatMap( + factory => factory?.inputs?.map(input => input.amount ?? 0) ?? [], + ), ) ?? 1; for (const factory of factories) { - if (!factory) continue; + if (!factory || factory.progress === 'disabled') continue; nodes.push({ id: factory.id, @@ -38,6 +44,7 @@ export function useFactoriesGraph() { for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; if (!input.factoryId) continue; + if (disabledIds.has(input.factoryId)) continue; edges.push({ id: `${factory.id}-i${i}`, diff --git a/src/factories/charts/sankey/FactoriesSankeyChart.tsx b/src/factories/charts/sankey/FactoriesSankeyChart.tsx index bae9308c..a21fef7b 100644 --- a/src/factories/charts/sankey/FactoriesSankeyChart.tsx +++ b/src/factories/charts/sankey/FactoriesSankeyChart.tsx @@ -43,8 +43,12 @@ export function FactoriesSankeyChart(props: IFactoriesSankeyChartProps) { nodes: SankeyNode[]; links: SankeyLink[]; } = useMemo(() => { + const disabledIds = new Set( + factories.filter(f => f.progress === 'disabled').map(f => f.id), + ); + const nodes: SankeyNode[] = factories - .filter(f => f.name) + .filter(f => f.name && f.progress !== 'disabled') .map(f => ({ id: f.name!, _originalId: f.id, @@ -55,17 +59,22 @@ export function FactoriesSankeyChart(props: IFactoriesSankeyChartProps) { _originalId: 'WORLD', }); - const links: SankeyLink[] = factories.flatMap(target => { - return (target.inputs ?? []) - .filter(i => i.factoryId && target.name) - .map(input => ({ - source: nodes.find(n => n._originalId === input.factoryId)?.id ?? '', - target: target.name!, - value: input.amount ?? 0, - resourceLabel: getResourceName(input.resource ?? ''), - })) - .filter(l => l.source !== l.target); - }); + const links: SankeyLink[] = factories + .filter(target => target.progress !== 'disabled') + .flatMap(target => { + return (target.inputs ?? []) + .filter( + i => i.factoryId && target.name && !disabledIds.has(i.factoryId), + ) + .map(input => ({ + source: + nodes.find(n => n._originalId === input.factoryId)?.id ?? '', + target: target.name!, + value: input.amount ?? 0, + resourceLabel: getResourceName(input.resource ?? ''), + })) + .filter(l => l.source !== l.target); + }); return { nodes, links }; }, [factories]); diff --git a/src/factories/components/peek/OutputDependenciesPeekModal.tsx b/src/factories/components/peek/OutputDependenciesPeekModal.tsx index 30c3bcf8..e5ba719f 100644 --- a/src/factories/components/peek/OutputDependenciesPeekModal.tsx +++ b/src/factories/components/peek/OutputDependenciesPeekModal.tsx @@ -1,6 +1,8 @@ import { ActionIcon, Modal, Tooltip } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconEye } from '@tabler/icons-react'; +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; import type { FactoryOutput } from '@/factories/Factory'; import { OutputDependenciesTable } from './OutputDependenciesTable'; @@ -13,6 +15,12 @@ export function OutputDependenciesPeekModal( props: IOutputDependenciesPeekModalProps, ) { const [opened, { open, close }] = useDisclosure(false); + const { pathname } = useLocation(); + + // biome-ignore lint/correctness/useExhaustiveDependencies: close modal on route change + useEffect(() => { + close(); + }, [pathname]); return (
diff --git a/src/factories/components/peek/OutputDependenciesTable.tsx b/src/factories/components/peek/OutputDependenciesTable.tsx index b44c5cf4..6ce949d4 100644 --- a/src/factories/components/peek/OutputDependenciesTable.tsx +++ b/src/factories/components/peek/OutputDependenciesTable.tsx @@ -17,10 +17,12 @@ export function OutputDependenciesTable(props: IOutputDependenciesTableProps) { const factoriesUsingOutput = useShallowStore(state => state.games.games[state.games.selected ?? '']?.factoriesIds .map(id => state.factories.factories[id]) - .filter(factory => - factory?.inputs?.some( - i => i.resource === output.resource && i.factoryId === factoryId, - ), + .filter( + factory => + factory?.progress !== 'disabled' && + factory?.inputs?.some( + i => i.resource === output.resource && i.factoryId === factoryId, + ), ), ); diff --git a/src/factories/components/progressProperties.ts b/src/factories/components/progressProperties.ts index 8b4dbc69..96733eb8 100644 --- a/src/factories/components/progressProperties.ts +++ b/src/factories/components/progressProperties.ts @@ -2,6 +2,7 @@ import type { MantineColor } from '@mantine/core'; import { IconCircle, IconCircleCheck, + IconPlayerPause, IconProgress, IconProgressHelp, type IconProps, @@ -37,4 +38,9 @@ export const progressProperties: Record< label: 'Done', Icon: IconCircleCheck, }, + disabled: { + color: 'red', + label: 'Disabled', + Icon: IconPlayerPause, + }, }; diff --git a/src/factories/components/usage/useOutputUsage.tsx b/src/factories/components/usage/useOutputUsage.tsx index 1a53a15e..e68ad87e 100644 --- a/src/factories/components/usage/useOutputUsage.tsx +++ b/src/factories/components/usage/useOutputUsage.tsx @@ -6,21 +6,26 @@ import type { IFactoryUsageProps } from './FactoryUsage'; export function useOutputUsage( options: Pick, ) { - const producedAmount = useStore(state => - options.factoryId === WORLD_SOURCE_ID - ? getWorldResourceMax(options.output) - : Math.max( - state.factories.factories[options.factoryId ?? '']?.outputs - ?.filter(o => o?.resource === options.output) - .reduce((sum, o) => sum + (o?.amount ?? 0), 0) ?? 0, - 0, - ), - ); + const producedAmount = useStore(state => { + if (options.factoryId === WORLD_SOURCE_ID) { + return getWorldResourceMax(options.output); + } + const source = state.factories.factories[options.factoryId ?? '']; + if (source?.progress === 'disabled') return 0; + return Math.max( + source?.outputs + ?.filter(o => o?.resource === options.output) + .reduce((sum, o) => sum + (o?.amount ?? 0), 0) ?? 0, + 0, + ); + }); const usedAmount = useStore( state => state.games.games[state.games.selected ?? '']?.factoriesIds - .flatMap(id => state.factories.factories[id]?.inputs) + .map(id => state.factories.factories[id]) + .filter(f => f && f.progress !== 'disabled') + .flatMap(f => f!.inputs) .filter( i => i?.resource === options.output && diff --git a/src/factories/details/ProductionView.tsx b/src/factories/details/ProductionView.tsx index b4bb8f15..6ad4f1d9 100644 --- a/src/factories/details/ProductionView.tsx +++ b/src/factories/details/ProductionView.tsx @@ -1,5 +1,6 @@ import { type Path, setByPath } from '@clickbar/dot-diver'; import { + ActionIcon, Alert, Button, Container, @@ -10,7 +11,12 @@ import { Text, TextInput, } from '@mantine/core'; -import { IconBulb, IconCalculator } from '@tabler/icons-react'; +import { + IconBulb, + IconCalculator, + IconPlayerPause, + IconX, +} from '@tabler/icons-react'; import { useCallback } from 'react'; import { Link } from 'react-router-dom'; import { useFormOnChange } from '@/core/form/useFormOnChange'; @@ -46,6 +52,10 @@ const progressValues: { value: FactoryProgressStatus; label: string }[] = [ value: 'done', label: 'Done', }, + { + value: 'disabled', + label: 'Disabled', + }, ]; export const ProductionView = ({ id }: { id: string }) => { @@ -63,6 +73,9 @@ export const ProductionView = ({ id }: { id: string }) => { const hasSolverLayout = useStore( state => !!state.solvers.instances[id]?.layout, ); + const readyToPlanHintDismissed = useStore( + state => state.factoryView.readyToPlanHintDismissed ?? false, + ); const hasConfiguredOutputs = outputs.some( o => o.resource != null && (o.amount ?? 0) > 0, ); @@ -72,30 +85,65 @@ export const ProductionView = ({ id }: { id: string }) => { - {hasConfiguredOutputs && !hasSolverLayout && ( + {factory.progress === 'disabled' && ( } - color="cyan" + icon={} + color="red" variant="light" - title="Ready to plan?" + title="Factory disabled" > - - - Use the Calculator to compute your optimal production chain. - - - + This factory is disabled. Its inputs and outputs are excluded from + global usage totals, dependency tables, and charts. Change the + Progress to re-enable it. )} + {hasConfiguredOutputs && + !hasSolverLayout && + !readyToPlanHintDismissed && ( + } + color="cyan" + variant="light" + title="Ready to plan?" + withCloseButton={false} + > + + + + Use the Calculator to compute your optimal production + chain. + + + + + useStore.getState().updateFactoryView(s => { + s.readyToPlanHintDismissed = true; + }) + } + > + + + + + )} Inputs diff --git a/src/factories/list/FactoryGridCard.tsx b/src/factories/list/FactoryGridCard.tsx index 4c68202c..6e084e9b 100644 --- a/src/factories/list/FactoryGridCard.tsx +++ b/src/factories/list/FactoryGridCard.tsx @@ -37,7 +37,14 @@ export function FactoryGridCard(props: IFactoryGridCard) { } return ( - + + diff --git a/src/factories/store/factoryViewSlice.ts b/src/factories/store/factoryViewSlice.ts index bd8f3acd..612e60fb 100644 --- a/src/factories/store/factoryViewSlice.ts +++ b/src/factories/store/factoryViewSlice.ts @@ -6,6 +6,7 @@ export interface FactoryViewSlice { filterResource: string | null; sortBy: 'name'; viewMode: 'spreadsheet' | 'kanban' | 'grid'; + readyToPlanHintDismissed?: boolean; } export const factoryViewSlice = createSlice({ @@ -15,6 +16,7 @@ export const factoryViewSlice = createSlice({ filterResource: null, sortBy: 'name', viewMode: 'spreadsheet', + readyToPlanHintDismissed: false, } as FactoryViewSlice, actions: { updateFactoryView: diff --git a/src/tutorial/chapters/factoryBasicsChapter.ts b/src/tutorial/chapters/factoryBasicsChapter.ts index b457261a..780be3c9 100644 --- a/src/tutorial/chapters/factoryBasicsChapter.ts +++ b/src/tutorial/chapters/factoryBasicsChapter.ts @@ -71,7 +71,7 @@ export const factoryBasicsChapter: TutorialChapter = { element: '[data-tutorial-id="factory-properties"]', popover: { title: 'Name & build status', - description: `Every factory has a name and a build status. I named this one "${DEMO_NAME}" and set its status to “Todo”. The status tracks whether a factory is just planned, being built, or already running, and it is also what powers the columns in the Kanban view of the Factories list.`, + description: `Every factory has a name and a build status. I named this one "${DEMO_NAME}" and set its status to “Todo”. The status tracks whether a factory is just planned, being built, or already running, and it is also what powers the columns in the Kanban view of the Factories list. Set a factory to “Disabled” to temporarily power it down — it stays here but is excluded from global usage totals and charts, and is hidden from the Kanban board.`, side: 'left', }, onHighlightStarted: () => {