Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project are documented in this file. It mirrors the

## Unreleased

- **The Recipe picker shows each recipe's ingredients.** Rows in the picker are taller, with a picture of every ingredient under the Alt and tier badges. Hovering one names it and says how many a minute one building uses at 100%.
- **Check your plan against your game save.** A new **Game options** button beside **Options** opens a dialog where you load a Satisfactory save. The planner reads it in your browser and keeps what it has unlocked: every recipe, every building, and the tiers reached. That list stays with the plan and syncs with it, and **Forget save** clears it. A file it cannot read is refused as not compatible, with the reason. The **Import world [WIP]** button is gone.
- **Locked recipes get a padlock.** Once a save is loaded, the Recipe picker shows a padlock on each recipe and greys out the ones the save has not unlocked, standard recipes included. Clicking a padlock marks the recipe unlocked or locked by hand, and the mark survives loading a newer save. The tick beside the selected recipe is gone.
- **Every alternate in one list.** Game options lists every alternate recipe with its rating, searchable and filterable by unlocked or locked. Clicking one marks it by hand.
Expand Down
10 changes: 10 additions & 0 deletions web/src/components/planner/products/RecipePicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,14 @@ describe('Component: RecipePicker', () => {
expect(subject.find('[data-testid="recipe-selection-locked"]').exists()).toBe(true)
})
})

it('lists each recipe\'s ingredients with their per-minute rate for one building', () => {
const items = mountPicker('Wire', 'Wire').findComponent(VAutocomplete).props('items') as any[]

expect(items.find(item => item.value === 'Wire')?.ingredients).toEqual([
{ part: 'CopperIngot', label: 'Copper Ingot: 15/min' },
])
expect(items.find(item => item.value === 'Alternate_FusedWire')?.ingredients.map((i: any) => i.part))
.toEqual(['CopperIngot', 'GoldIngot'])
})
})
84 changes: 67 additions & 17 deletions web/src/components/planner/products/RecipePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
hide-details
:items="items"
label="Recipe"
:menu-props="{ maxHeight: 440 }"
:model-value="modelValue"
variant="outlined"
@update:focused="focused = $event"
Expand All @@ -24,25 +25,40 @@
</span>
</template>
<template #item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps" :data-recipe="item.value" title="">
<v-list-item v-bind="itemProps" class="recipe-picker-item" :data-recipe="item.value" title="">
<span class="recipe-picker-row" :class="{ locked: item.locked }">
<span class="recipe-picker-name">{{ item.name }}</span>
<recipe-picker-tags :alternate="item.alternate" :tier="item.tier" />
<!-- Only once a save is loaded: until then nothing is locked, so there is nothing to say. -->
<tooltip v-if="save && item.lockable" :text="lockTooltip(item)">
<button
class="recipe-picker-lock"
:class="{ locked: item.locked, overridden: item.overridden }"
:data-testid="`recipe-lock-${item.value}`"
type="button"
@click.stop="toggle(item.value)"
@mousedown.stop
>
<!-- Wrapped: Font Awesome swaps the <i> for an <svg> Vue no longer owns. -->
<span v-if="item.locked" key="locked"><i class="fas fa-lock" /></span>
<span v-else key="unlocked"><i class="fas fa-lock-open" /></span>
</button>
</tooltip>
<span class="recipe-picker-meta">
<span v-if="item.alternate || item.tier || (save && item.lockable)" class="recipe-picker-tags-line">
<recipe-picker-tags :alternate="item.alternate" :tier="item.tier" />
<!-- Only once a save is loaded: until then nothing is locked, so there is nothing to say. -->
<tooltip v-if="save && item.lockable" :text="lockTooltip(item)">
<button
class="recipe-picker-lock"
:class="{ locked: item.locked, overridden: item.overridden }"
:data-testid="`recipe-lock-${item.value}`"
type="button"
@click.stop="toggle(item.value)"
@mousedown.stop
>
<!-- Wrapped: Font Awesome swaps the <i> for an <svg> Vue no longer owns. -->
<span v-if="item.locked" key="locked"><i class="fas fa-lock" /></span>
<span v-else key="unlocked"><i class="fas fa-lock-open" /></span>
</button>
</tooltip>
</span>
<span v-if="item.ingredients.length" class="recipe-picker-ingredients" data-testid="recipe-ingredients">
<game-asset
v-for="ingredient in item.ingredients"
:key="ingredient.part"
height="24px"
:subject="ingredient.part"
:tooltip="ingredient.label"
type="item"
width="24px"
/>
</span>
</span>
</span>
</v-list-item>
</template>
Expand All @@ -66,6 +82,8 @@
import { getRecipeRating, matchesRecipeQuery, RECIPE_RATINGS_SOURCE, RecipeTier, TIER_ORDER } from '@/utils/recipe-ratings'
import { useRecipeUnlocks } from '@/composables/useRecipeUnlocks'
import { isLockableRecipe } from '@/utils/recipe-unlocks'
import { getPartDisplayName } from '@/utils/helpers'
import { formatNumber } from '@/utils/numberFormatter'
import type { FilterFunction } from 'vuetify'

defineOptions({ inheritAttrs: false })
Expand All @@ -92,6 +110,11 @@
lockable: isLockableRecipe(recipe.id),
locked: !isUnlocked(recipe.id),
overridden: isOverridden(recipe.id),
// Per minute for one building at 100%, as the game's own recipe screen counts it.
ingredients: recipe.ingredients.map(ingredient => ({
part: ingredient.part,
label: `${getPartDisplayName(ingredient.part)}: ${formatNumber(ingredient.perMin)}/min`,
})),
}))
.sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name)))

Expand Down Expand Up @@ -128,6 +151,33 @@
width: 100%;
}

.recipe-picker-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0;
gap: 4px;
padding: 4px 0;
}

.recipe-picker-tags-line {
display: flex;
align-items: center;
gap: 6px;
}

// Every row the same height and ruled off, so close-set ingredient pictures read as one recipe's.
// On a row with no tags, the ingredients centre in that height.
.recipe-picker-item {
min-height: 64px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.recipe-picker-ingredients {
display: flex;
gap: 4px;
}

.recipe-picker :deep(.v-field__input) {
flex-wrap: nowrap;
}
Expand Down
Loading