Skip to content

Commit a4bccc1

Browse files
committed
feat: add 1.2 power and recipe multipliers settings
1 parent 1b41c5e commit a4bccc1

17 files changed

Lines changed: 246 additions & 18 deletions

src/codex/components/StatCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function SectionCard({
4343
title,
4444
children,
4545
}: {
46-
title: string;
46+
title: React.ReactNode;
4747
children: React.ReactNode;
4848
}) {
4949
return (

src/codex/items/CodexItemDetail.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import {
2020
import { useMemo } from 'react';
2121
import { Link, Navigate, useParams } from 'react-router-dom';
2222

23+
import { useRecipeMultiplier } from '@/games/gamesSlice';
2324
import {
2425
AllFactoryBuildings,
2526
AllFactoryBuildingsMap,
2627
} from '@/recipes/FactoryBuilding';
2728
import { AllFactoryItemsMap } from '@/recipes/FactoryItem';
2829
import { AllFactoryRecipes, type FactoryRecipe } from '@/recipes/FactoryRecipe';
2930
import { isDefaultRecipe, isMAMRecipe } from '@/recipes/graph/SchematicGraph';
31+
import { applyRecipeMultiplier } from '@/recipes/recipeMultiplier';
3032
import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage';
3133
import { SectionCard, StatCard } from '../components/StatCard';
3234
import { getEarliestTierForItem } from '../tiers/tierUnlocks';
@@ -201,6 +203,7 @@ function RecipeTable({
201203
highlightResource: string;
202204
type: 'product' | 'ingredient';
203205
}) {
206+
const recipeMultiplier = useRecipeMultiplier();
204207
return (
205208
<Table striped highlightOnHover>
206209
<Table.Thead>
@@ -221,9 +224,12 @@ function RecipeTable({
221224
type === 'product'
222225
? recipe.products.find(p => p.resource === highlightResource)
223226
: recipe.ingredients.find(i => i.resource === highlightResource);
224-
const rate = relevantPart
225-
? (relevantPart.amount * 60) / recipe.time
226-
: 0;
227+
const baseAmount = relevantPart ? relevantPart.amount : 0;
228+
const effectiveAmount =
229+
type === 'ingredient' && relevantPart
230+
? applyRecipeMultiplier(baseAmount, recipeMultiplier)
231+
: baseAmount;
232+
const rate = relevantPart ? (effectiveAmount * 60) / recipe.time : 0;
227233

228234
return (
229235
<Table.Tr key={recipe.id}>

src/codex/recipes/CodexRecipeDetail.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '@tabler/icons-react';
2121
import { Link, Navigate, useParams } from 'react-router-dom';
2222

23+
import { useRecipeMultiplier } from '@/games/gamesSlice';
2324
import { AllFactoryBuildingsMap } from '@/recipes/FactoryBuilding';
2425
import { AllFactoryItemsMap } from '@/recipes/FactoryItem';
2526
import { AllFactoryRecipesMap } from '@/recipes/FactoryRecipe';
@@ -28,13 +29,16 @@ import {
2829
UnlockedByMap,
2930
} from '@/recipes/FactorySchematic';
3031
import { isDefaultRecipe, isMAMRecipe } from '@/recipes/graph/SchematicGraph';
32+
import { applyRecipeMultiplier } from '@/recipes/recipeMultiplier';
3133
import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage';
3234
import { SectionCard, StatCard } from '../components/StatCard';
3335

3436
export function CodexRecipeDetail() {
3537
const { id } = useParams<{ id: string }>();
3638
const recipe = id ? AllFactoryRecipesMap[id] : undefined;
3739

40+
const recipeMultiplier = useRecipeMultiplier();
41+
3842
if (!recipe) return <Navigate to="/codex/recipes" replace />;
3943

4044
const building = AllFactoryBuildingsMap[recipe.producedIn];
@@ -119,7 +123,20 @@ export function CodexRecipeDetail() {
119123
)}
120124
</SimpleGrid>
121125

122-
<SectionCard title="Recipe Flow">
126+
<SectionCard
127+
title={
128+
recipeMultiplier !== 1 ? (
129+
<Group gap="xs">
130+
Recipe Flow
131+
<Badge variant="light" color="cyan" size="sm">
132+
{recipeMultiplier}x ingredients
133+
</Badge>
134+
</Group>
135+
) : (
136+
'Recipe Flow'
137+
)
138+
}
139+
>
123140
<Group
124141
gap="md"
125142
align="center"
@@ -130,7 +147,16 @@ export function CodexRecipeDetail() {
130147
<Stack gap="xs" align="center" miw={120}>
131148
{recipe.ingredients.map(ing => {
132149
const item = AllFactoryItemsMap[ing.resource];
133-
const rate = (ing.amount * 60) / recipe.time;
150+
const effectiveAmount = applyRecipeMultiplier(
151+
ing.amount,
152+
recipeMultiplier,
153+
);
154+
const effectiveDisplayAmount = applyRecipeMultiplier(
155+
ing.displayAmount,
156+
recipeMultiplier,
157+
);
158+
const rate = (effectiveAmount * 60) / recipe.time;
159+
const isModified = effectiveDisplayAmount !== ing.displayAmount;
134160
return (
135161
<Anchor
136162
key={ing.resource}
@@ -150,8 +176,26 @@ export function CodexRecipeDetail() {
150176
{item?.displayName ?? ing.resource}
151177
</Text>
152178
<Text size="xs" c="dimmed">
153-
{ing.displayAmount} (
154-
{rate % 1 === 0 ? rate : rate.toFixed(2)}/min)
179+
{isModified && (
180+
<Text
181+
span
182+
size="xs"
183+
td="line-through"
184+
c="dimmed"
185+
mr={4}
186+
>
187+
{ing.displayAmount}
188+
</Text>
189+
)}
190+
<Text
191+
span
192+
size="xs"
193+
c={isModified ? 'cyan' : undefined}
194+
fw={isModified ? 600 : undefined}
195+
>
196+
{effectiveDisplayAmount}
197+
</Text>{' '}
198+
({rate % 1 === 0 ? rate : rate.toFixed(2)}/min)
155199
</Text>
156200
</Stack>
157201
</Group>

src/games/Game.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export interface GameSettings {
5656
maxPipeline?: string;
5757
orthogonalEdges?: boolean;
5858
disableEdgeAnimation?: boolean;
59+
recipeMultiplier?: number;
60+
powerConsumptionMultiplier?: number;
5961
/**
6062
* Controls whether the solver graph displays "Output to factory X" nodes
6163
* representing this factory's outputs flowing to downstream consumer

src/games/gamesSlice.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
FactoryPipelinesExclAlternates,
1010
} from '@/recipes/FactoryBuilding';
1111
import { AllFactoryRecipes } from '@/recipes/FactoryRecipe';
12+
import {
13+
DEFAULT_POWER_CONSUMPTION_MULTIPLIER,
14+
DEFAULT_RECIPE_MULTIPLIER,
15+
} from '@/recipes/recipeMultiplier';
1216
import type { ParsedSatisfactorySave } from '@/recipes/savegame/ParseSavegameMessages';
1317
import { STATIC_COLLECTIBLE_IDS } from '@/recipes/WorldCollectibles';
1418
import type {
@@ -419,6 +423,22 @@ export function setShowOutputFactoriesNodes(
419423
});
420424
}
421425

426+
export function useRecipeMultiplier(): number {
427+
return useStore(
428+
state =>
429+
state.games.games[state.games.selected ?? '']?.settings
430+
?.recipeMultiplier ?? DEFAULT_RECIPE_MULTIPLIER,
431+
);
432+
}
433+
434+
export function usePowerConsumptionMultiplier(): number {
435+
return useStore(
436+
state =>
437+
state.games.games[state.games.selected ?? '']?.settings
438+
?.powerConsumptionMultiplier ?? DEFAULT_POWER_CONSUMPTION_MULTIPLIER,
439+
);
440+
}
441+
422442
export function useGameSettingMaxPipeline() {
423443
const maxPipeline = useGameSetting('maxPipeline');
424444
if (!maxPipeline) return null;

src/games/settings/GameSettingsModal.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useGameAllowedBuildings, useGameSettings } from '@/games/gamesSlice';
2121
import { SettingSectionNavButton } from './SettingSectionNavButton';
2222
import { AvailableBuildingsSection } from './sections/AvailableBuildingsSection';
2323
import { GraphDisplaySection } from './sections/GraphDisplaySection';
24+
import { RecipeMultiplierSection } from './sections/RecipeMultiplierSection';
2425
import { TransportLimitsSection } from './sections/TransportLimitsSection';
2526
import { UsageHighlightingSection } from './sections/UsageHighlightingSection';
2627
import { SETTINGS_SECTIONS, type SectionId } from './settingsSections';
@@ -45,6 +46,7 @@ export function GameSettingsModal() {
4546
const [activeSection, setActiveSection] = useState<SectionId>('highlighting');
4647

4748
const sectionRefs = useRef<Record<SectionId, HTMLDivElement | null>>({
49+
recipes: null,
4850
highlighting: null,
4951
transport: null,
5052
graph: null,
@@ -125,6 +127,11 @@ export function GameSettingsModal() {
125127
}}
126128
>
127129
<Stack gap="md">
130+
<RecipeMultiplierSection
131+
ref={setRef('recipes')}
132+
settings={settings}
133+
onChange={onChange}
134+
/>
128135
<UsageHighlightingSection
129136
ref={setRef('highlighting')}
130137
settings={settings}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Select, Stack } from '@mantine/core';
2+
import {
3+
DEFAULT_POWER_CONSUMPTION_MULTIPLIER,
4+
DEFAULT_RECIPE_MULTIPLIER,
5+
POWER_CONSUMPTION_MULTIPLIER_OPTIONS,
6+
RECIPE_MULTIPLIER_OPTIONS,
7+
} from '@/recipes/recipeMultiplier';
8+
import { SettingSectionCard } from '../SettingSectionCard';
9+
import {
10+
SETTINGS_SECTIONS,
11+
type SectionComponentProps,
12+
} from '../settingsSections';
13+
14+
const section = SETTINGS_SECTIONS.find(s => s.id === 'recipes')!;
15+
16+
export function RecipeMultiplierSection({
17+
ref,
18+
settings,
19+
onChange,
20+
}: SectionComponentProps) {
21+
return (
22+
<SettingSectionCard section={section} ref={ref}>
23+
<Stack gap="md">
24+
<Select
25+
label="Recipe Parts Cost Multiplier"
26+
description="Changes ingredient amounts in all recipes. Matches the in-game World Settings from Satisfactory 1.2."
27+
data={RECIPE_MULTIPLIER_OPTIONS}
28+
value={String(
29+
settings?.recipeMultiplier ?? DEFAULT_RECIPE_MULTIPLIER,
30+
)}
31+
allowDeselect={false}
32+
onChange={value => {
33+
if (value == null) return;
34+
onChange('recipeMultiplier')(Number(value));
35+
}}
36+
/>
37+
<Select
38+
label="Power Consumption Multiplier"
39+
description="Scales power consumption of all buildings. Matches the in-game World Settings from Satisfactory 1.2."
40+
data={POWER_CONSUMPTION_MULTIPLIER_OPTIONS}
41+
value={String(
42+
settings?.powerConsumptionMultiplier ??
43+
DEFAULT_POWER_CONSUMPTION_MULTIPLIER,
44+
)}
45+
allowDeselect={false}
46+
onChange={value => {
47+
if (value == null) return;
48+
onChange('powerConsumptionMultiplier')(Number(value));
49+
}}
50+
/>
51+
</Stack>
52+
</SettingSectionCard>
53+
);
54+
}

src/games/settings/settingsSections.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import {
22
IconBuildingFactory2,
33
IconHighlight,
4+
IconPercentage,
45
IconRoute,
56
IconVectorBezier2,
67
} from '@tabler/icons-react';
78
import type { Ref } from 'react';
89
import type { FormOnChangeHandler } from '@/core/form/useFormOnChange';
910
import type { GameSettings } from '@/games/Game';
1011

11-
export type SectionId = 'highlighting' | 'transport' | 'graph' | 'buildings';
12+
export type SectionId =
13+
| 'recipes'
14+
| 'highlighting'
15+
| 'transport'
16+
| 'graph'
17+
| 'buildings';
1218

1319
export interface SectionComponentProps {
1420
ref?: Ref<HTMLDivElement>;
@@ -25,6 +31,13 @@ export interface Section {
2531
}
2632

2733
export const SETTINGS_SECTIONS: Section[] = [
34+
{
35+
id: 'recipes',
36+
label: 'Cost Multipliers',
37+
description: 'Recipe & power scaling',
38+
icon: IconPercentage,
39+
color: 'cyan',
40+
},
2841
{
2942
id: 'highlighting',
3043
label: 'Usage Highlighting',

src/recipes/recipeMultiplier.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export function applyRecipeMultiplier(
2+
baseAmount: number,
3+
multiplier: number,
4+
): number {
5+
if (multiplier === 1) return baseAmount;
6+
return Math.max(1, Math.round(baseAmount * multiplier));
7+
}
8+
9+
export const RECIPE_MULTIPLIER_OPTIONS = [
10+
{ value: '0.25', label: '0.25x' },
11+
{ value: '0.5', label: '0.5x' },
12+
{ value: '0.75', label: '0.75x' },
13+
{ value: '1', label: '1x (Default)' },
14+
{ value: '1.25', label: '1.25x' },
15+
{ value: '1.5', label: '1.5x' },
16+
{ value: '1.75', label: '1.75x' },
17+
{ value: '2', label: '2x' },
18+
];
19+
20+
export const POWER_CONSUMPTION_MULTIPLIER_OPTIONS = [
21+
{ value: '0.25', label: '0.25x' },
22+
{ value: '0.5', label: '0.5x' },
23+
{ value: '0.75', label: '0.75x' },
24+
{ value: '1', label: '1x (Default)' },
25+
{ value: '2', label: '2x' },
26+
{ value: '5', label: '5x' },
27+
];
28+
29+
export const DEFAULT_RECIPE_MULTIPLIER = 1;
30+
export const DEFAULT_POWER_CONSUMPTION_MULTIPLIER = 1;

src/recipes/ui/RecipeTooltip.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { Stack, Table, Text, Tooltip } from '@mantine/core';
22
import type * as React from 'react';
33
import { useCallback, useState } from 'react';
44
import { RepeatingNumber } from '@/core/intl/NumberFormatter';
5+
import { useRecipeMultiplier } from '@/games/gamesSlice';
56
import { AllFactoryItemsMap } from '@/recipes/FactoryItem';
67
import { AllFactoryRecipesMap } from '@/recipes/FactoryRecipe';
8+
import { applyRecipeMultiplier } from '@/recipes/recipeMultiplier';
79
import { FactoryItemImage } from './FactoryItemImage';
810

911
export interface IRecipeTooltipProps {
@@ -13,6 +15,7 @@ export interface IRecipeTooltipProps {
1315

1416
export function RecipeTooltip(props: IRecipeTooltipProps) {
1517
const recipe = AllFactoryRecipesMap[props.recipeId];
18+
const recipeMultiplier = useRecipeMultiplier();
1619
const [label, setLabel] = useState<React.ReactNode>(null);
1720

1821
const handleMouseEnter = useCallback(() => {
@@ -38,7 +41,14 @@ export function RecipeTooltip(props: IRecipeTooltipProps) {
3841
</Table.Td>
3942
<Table.Td>
4043
<RepeatingNumber
41-
value={(ingredient.displayAmount * 60) / recipe.time}
44+
value={
45+
(applyRecipeMultiplier(
46+
ingredient.displayAmount,
47+
recipeMultiplier,
48+
) *
49+
60) /
50+
recipe.time
51+
}
4252
/>
4353
/min
4454
</Table.Td>
@@ -69,7 +79,7 @@ export function RecipeTooltip(props: IRecipeTooltipProps) {
6979
</Table>
7080
</Stack>,
7181
);
72-
}, [recipe]);
82+
}, [recipe, recipeMultiplier]);
7383

7484
if (label) {
7585
return (

0 commit comments

Comments
 (0)