Skip to content
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,4 @@
"vitest": "^2.1.4"
},
"packageManager": "npm@10.8.3"
}
}
2 changes: 2 additions & 0 deletions src/factories/Factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export interface Factory {
inputs: FactoryInput[];
outputs: FactoryOutput[];
powerConsumption?: number | null;
/** Factory-specific building overrides. If set, overrides game-level allowedBuildings */
allowedBuildings?: string[] | null;
}

export interface FactoryInput {
Expand Down
26 changes: 26 additions & 0 deletions src/factories/store/factoriesSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ export const factoriesSlice = createSlice({
(factoryId: string, outputIndex: number, amount: number) => state => {
state.factories[factoryId].outputs[outputIndex].amount = amount;
},
setFactoryAllowedBuildings:
(factoryId: string, allowedBuildings: string[] | null) => state => {
state.factories[factoryId].allowedBuildings = allowedBuildings;
},
toggleFactoryBuilding:
(factoryId: string, buildingId: string, enabled?: boolean) => state => {
const factory = state.factories[factoryId];
if (!factory) return;

// Initialize with empty array if not set
if (
factory.allowedBuildings === undefined ||
factory.allowedBuildings === null
) {
factory.allowedBuildings = [];
}

const index = factory.allowedBuildings.indexOf(buildingId);
const shouldAdd = enabled ?? index === -1;

if (shouldAdd && index === -1) {
factory.allowedBuildings.push(buildingId);
} else if (!shouldAdd && index !== -1) {
factory.allowedBuildings.splice(index, 1);
}
},
},
});

Expand Down
1 change: 1 addition & 0 deletions src/games/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Game {
// factories: Factory[];
settings: GameSettings;
allowedRecipes?: string[];
allowedBuildings?: string[];
collapsedFactoriesIds?: string[];
// Only if saved
savedId?: string;
Expand Down
49 changes: 47 additions & 2 deletions src/games/gamesSlice.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useStore } from '@/core/zustand';
import { createSlice } from '@/core/zustand-helpers/slices';
import {
FactoryBuildingsForRecipes,
FactoryConveyorBelts,
FactoryPipelinesExclAlternates,
} from '@/recipes/FactoryBuilding';
import dayjs from 'dayjs';
import { useShallow } from 'zustand/shallow';
import { useStore } from '@/core/zustand';
import { createSlice } from '@/core/zustand-helpers/slices';
import { Game, type GameRemoteData, GameSettings } from './Game';

export interface GamesSlice {
Expand All @@ -31,6 +32,7 @@ export const gamesSlice = createSlice({
createdAt: dayjs().toISOString(),
...game,
factoriesIds: [],
allowedBuildings: [],
settings: {
noHighlight100PercentUsage: false,
highlight100PercentColor: '#339af0',
Expand Down Expand Up @@ -69,6 +71,41 @@ export const gamesSlice = createSlice({
}
state.games[targetId].allowedRecipes = allowedRecipes;
},
setGameAllowedBuildings:
(gameId: string | undefined, allowedBuildings: string[]) => state => {
const targetId = gameId ?? state.selected;
if (!targetId) {
throw new Error('No game selected');
}
state.games[targetId].allowedBuildings = allowedBuildings;
},
toggleGameBuilding: (buildingId: string, enabled?: boolean) => state => {
const game = state.games[state.selected ?? ''];
if (!game) return;

if (!game.allowedBuildings) {
game.allowedBuildings = [];
}

const index = game.allowedBuildings.indexOf(buildingId);
const shouldAdd = enabled ?? index === -1;

if (shouldAdd && index === -1) {
game.allowedBuildings.push(buildingId);
} else if (!shouldAdd && index !== -1) {
game.allowedBuildings.splice(index, 1);
}
},
enableAllGameBuildings: () => state => {
const game = state.games[state.selected ?? ''];
if (!game) return;
game.allowedBuildings = FactoryBuildingsForRecipes.map(b => b.id);
},
disableAllGameBuildings: () => state => {
const game = state.games[state.selected ?? ''];
if (!game) return;
game.allowedBuildings = [];
},
setRemoteGameData: (gameId: string, data: GameRemoteData) => state => {
state.games[gameId].authorId = data.author_id;
state.games[gameId].createdAt = data.created_at;
Expand Down Expand Up @@ -131,6 +168,14 @@ export function useGameSettings() {
);
}

export function useGameAllowedBuildings() {
return useStore(
useShallow(
state => state.games.games[state.games.selected ?? '']?.allowedBuildings,
),
);
}

export function useGameSetting(
key: keyof GameSettings,
defaultValue?: GameSettings[keyof GameSettings],
Expand Down
67 changes: 63 additions & 4 deletions src/games/settings/GameSettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { SelectIconInput } from '@/core/form/SelectIconInput';
import { useFormOnChange } from '@/core/form/useFormOnChange';
import { useStore } from '@/core/zustand';
import { GameSettings } from '@/games/Game';
import { useGameAllowedBuildings, useGameSettings } from '@/games/gamesSlice';
import {
FactoryBuildingsForRecipes,
FactoryConveyorBelts,
FactoryPipelinesExclAlternates,
} from '@/recipes/FactoryBuilding';
Expand All @@ -8,18 +13,16 @@ import {
Button,
Checkbox,
ColorInput,
Group,
Image,
Modal,
Space,
Stack,
Text,
Title,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconSettings } from '@tabler/icons-react';
import { useFormOnChange } from '@/core/form/useFormOnChange';
import { useStore } from '@/core/zustand';
import { GameSettings } from '@/games/Game';
import { useGameSettings } from '@/games/gamesSlice';

export interface IGameSettingsModalProps {
withLabel?: boolean;
Expand Down Expand Up @@ -54,6 +57,7 @@ const PipelinesOptions = FactoryPipelinesExclAlternates.map(
export function GameSettingsModal(props: IGameSettingsModalProps) {
const [opened, { open, close }] = useDisclosure(false);
const settings = useGameSettings();
const allowedBuildings = useGameAllowedBuildings();
const onChangeHandler = useFormOnChange<GameSettings>(updateGameSettings);

return (
Expand Down Expand Up @@ -110,6 +114,61 @@ export function GameSettingsModal(props: IGameSettingsModalProps) {
onChange={onChangeHandler('maxPipeline')}
placeholder="No pipeline selected"
/>
<Title order={3} mt="md" mb="md">
Available Buildings
</Title>
<Text size="sm" c="dimmed" mb="xs">
Select which buildings you have unlocked in your save. When you
create a new factory, only these buildings will be available. This
mirrors how Satisfactory unlocks work.
</Text>
<Group gap="xs" mb="sm">
<Button
size="xs"
variant="light"
onClick={() => {
useStore.getState().enableAllGameBuildings();
useStore.getState().syncGameBuildingsToFactories();
}}
>
Enable All
</Button>
<Button
size="xs"
variant="light"
color="red"
onClick={() => {
useStore.getState().disableAllGameBuildings();
useStore.getState().syncGameBuildingsToFactories();
}}
>
Disable All
</Button>
</Group>
<Stack gap="xs">
{FactoryBuildingsForRecipes.map(building => (
<Checkbox
key={building.id}
label={
<Group gap="xs">
<Image
src={building.imagePath.replace('_256', '_64')}
width={24}
height={24}
/>
{building.name}
</Group>
}
checked={allowedBuildings?.includes(building.id) ?? false}
onChange={e => {
useStore
.getState()
.toggleGameBuilding(building.id, e.currentTarget.checked);
useStore.getState().syncGameBuildingsToFactories();
}}
/>
))}
</Stack>
</Stack>
<Space h={50} />
</Modal>
Expand Down
33 changes: 33 additions & 0 deletions src/games/store/gameFactoriesActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useStore } from '@/core/zustand';
import { createActions } from '@/core/zustand-helpers/actions';
import { Factory } from '@/factories/Factory';
import { Game } from '@/games/Game';
import { FactoryBuildingsForRecipes } from '@/recipes/FactoryBuilding';
import type { SolverInstance } from '@/solver/store/Solver';
import dayjs from 'dayjs';
import { cloneDeep, omit } from 'lodash';
Expand Down Expand Up @@ -89,6 +90,38 @@ export const gameFactoriesActions = createActions({
state.games.selected = null;
}
},
/**
* Syncs the game's allowedBuildings to all factory solvers' blockedBuildings
*/
syncGameBuildingsToFactories: (gameId?: string | null) => state => {
const targetId = gameId ?? state.games.selected;
if (!targetId) return;

const game = state.games.games[targetId];
if (!game) return;

const allowedBuildings = game.allowedBuildings ?? [];
const blockedBuildings = FactoryBuildingsForRecipes.filter(
b => !allowedBuildings.includes(b.id),
).map(b => b.id);

game.factoriesIds.forEach(factoryId => {
const factory = state.factories.factories[factoryId];
const solver = state.solvers.instances[factoryId];

// Skip if factory has its own building overrides
if (
factory?.allowedBuildings !== undefined &&
factory?.allowedBuildings !== null
) {
return;
}

if (solver) {
solver.request.blockedBuildings = blockedBuildings;
}
});
},
});

export type SerializedGame = {
Expand Down
Loading