@terrafirmagreg-team/recipe 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/package.json +62 -3
  2. package/src/components/button-copy.tsx +43 -0
  3. package/src/components/cards/card/index.tsx +214 -0
  4. package/src/components/cards/card/scene.tsx +107 -0
  5. package/src/components/cards/card/strip.tsx +198 -0
  6. package/src/components/cards/card/text.tsx +46 -0
  7. package/src/components/cards/card/tips.tsx +63 -0
  8. package/src/components/cards/default-item.tsx +47 -0
  9. package/src/components/cards/glare-filter.tsx +29 -0
  10. package/src/components/cards/paged-card.tsx +222 -0
  11. package/src/components/cards/slot-hitbox.tsx +213 -0
  12. package/src/components/cards/strip-card/buttons.tsx +106 -0
  13. package/src/components/cards/strip-card/index.tsx +214 -0
  14. package/src/components/cards/strip-card/member-cell.tsx +167 -0
  15. package/src/components/cards/strip-card/sprite.tsx +36 -0
  16. package/src/components/embed-card.tsx +44 -0
  17. package/src/components/mdx/recipe.tsx +129 -0
  18. package/src/components/tool-list.tsx +49 -0
  19. package/src/components/tree.tsx +98 -0
  20. package/src/components/views/alloy/index.tsx +112 -0
  21. package/src/components/views/alloy/list.tsx +85 -0
  22. package/src/components/views/alloy/mix/index.tsx +35 -0
  23. package/src/components/views/alloy/mix/ratio.tsx +94 -0
  24. package/src/components/views/alloy/mix/rows.tsx +145 -0
  25. package/src/components/views/alloy/mix/status.tsx +122 -0
  26. package/src/components/views/alloy/mix/total.tsx +132 -0
  27. package/src/components/views/alloy/range-bar.tsx +65 -0
  28. package/src/components/views/alloy/stack-bar.tsx +88 -0
  29. package/src/components/views/browser/axis-switcher.tsx +54 -0
  30. package/src/components/views/browser/card-grid.tsx +275 -0
  31. package/src/components/views/browser/favorite-button.tsx +46 -0
  32. package/src/components/views/browser/frame.tsx +112 -0
  33. package/src/components/views/browser/index.tsx +114 -0
  34. package/src/components/views/browser/item-panel/index.tsx +229 -0
  35. package/src/components/views/browser/item-panel/skeleton.tsx +26 -0
  36. package/src/components/views/browser/item-panel-grid/cell.tsx +178 -0
  37. package/src/components/views/browser/item-panel-grid/favorite-cell.tsx +129 -0
  38. package/src/components/views/browser/item-panel-grid/index.tsx +105 -0
  39. package/src/components/views/browser/item-view/index.tsx +186 -0
  40. package/src/components/views/browser/item-view/tags.tsx +85 -0
  41. package/src/components/views/browser/item-view-header.tsx +131 -0
  42. package/src/components/views/browser/pane-tabs.tsx +61 -0
  43. package/src/components/views/browser/place-header.tsx +72 -0
  44. package/src/components/views/browser/query-controls.tsx +105 -0
  45. package/src/components/views/browser/recipe-item-header.tsx +60 -0
  46. package/src/components/views/browser/settings.tsx +52 -0
  47. package/src/components/views/browser/toolbar.tsx +54 -0
  48. package/src/components/views/browser/type-tabs.tsx +71 -0
  49. package/src/components/views/browser/type-view.tsx +184 -0
  50. package/src/components/views/browser/workstation-rail.tsx +43 -0
  51. package/src/components/views/calc/calculator/actions.tsx +47 -0
  52. package/src/components/views/calc/calculator/help.tsx +189 -0
  53. package/src/components/views/calc/calculator/index.tsx +172 -0
  54. package/src/components/views/calc/dot-overlay.tsx +80 -0
  55. package/src/components/views/calc/edge.tsx +152 -0
  56. package/src/components/views/calc/flow/index.tsx +305 -0
  57. package/src/components/views/calc/flow/picker-dialogs.tsx +146 -0
  58. package/src/components/views/calc/flow/toolbar.tsx +60 -0
  59. package/src/components/views/calc/flow/view-menu.tsx +117 -0
  60. package/src/components/views/calc/member-picker.tsx +120 -0
  61. package/src/components/views/calc/node-card/actions.tsx +90 -0
  62. package/src/components/views/calc/node-card/badges.tsx +59 -0
  63. package/src/components/views/calc/node-card/chips.tsx +187 -0
  64. package/src/components/views/calc/node-card/far.tsx +123 -0
  65. package/src/components/views/calc/node-card/full.tsx +332 -0
  66. package/src/components/views/calc/node-card/index.tsx +27 -0
  67. package/src/components/views/calc/node-card/menu.tsx +282 -0
  68. package/src/components/views/calc/node-card/port-cells.tsx +186 -0
  69. package/src/components/views/calc/node-card/port-rails.tsx +251 -0
  70. package/src/components/views/calc/node-card/ports.tsx +86 -0
  71. package/src/components/views/calc/recipe-picker/body.tsx +207 -0
  72. package/src/components/views/calc/recipe-picker/footer.tsx +131 -0
  73. package/src/components/views/calc/recipe-picker/index.tsx +52 -0
  74. package/src/components/views/calc/sidebar.tsx +171 -0
  75. package/src/components/views/calc/trees/icon-picker.tsx +97 -0
  76. package/src/components/views/calc/trees/index.tsx +134 -0
  77. package/src/components/views/calc/trees/label.tsx +67 -0
  78. package/src/components/views/calc/trees/rows.tsx +140 -0
  79. package/src/components/views/forge/gui.tsx +131 -0
  80. package/src/components/views/forge/index.tsx +112 -0
  81. package/src/components/views/forge/list.tsx +113 -0
  82. package/src/components/views/forge/workbench/controls.tsx +103 -0
  83. package/src/components/views/forge/workbench/index.tsx +85 -0
  84. package/src/components/views/forge/workbench/panel.tsx +228 -0
  85. package/src/components/views/forge/workbench/ruler.tsx +133 -0
  86. package/src/components/views/forge/workbench/status.tsx +55 -0
  87. package/src/components/views/forge/workbench/tape.tsx +70 -0
  88. package/src/hooks/alloy/use-fluid-colors.ts +125 -0
  89. package/src/hooks/alloy/use-mix-state.ts +240 -0
  90. package/src/hooks/calc/flow/use-engage.ts +129 -0
  91. package/src/hooks/calc/flow/use-handlers.ts +192 -0
  92. package/src/hooks/calc/flow/use-layout.ts +105 -0
  93. package/src/hooks/calc/flow/use-mount.ts +97 -0
  94. package/src/hooks/calc/flow/use-nodes.ts +260 -0
  95. package/src/hooks/calc/flow/use-pickers.ts +57 -0
  96. package/src/hooks/calc/flow/use-viewport.ts +141 -0
  97. package/src/hooks/calc/flow/use-window.ts +87 -0
  98. package/src/hooks/calc/use-actions.ts +355 -0
  99. package/src/hooks/calc/use-amounts.ts +64 -0
  100. package/src/hooks/calc/use-data.ts +203 -0
  101. package/src/hooks/calc/use-defaults.ts +40 -0
  102. package/src/hooks/calc/use-far-mode.ts +37 -0
  103. package/src/hooks/calc/use-focus.ts +31 -0
  104. package/src/hooks/calc/use-maps.ts +76 -0
  105. package/src/hooks/calc/use-reset.ts +15 -0
  106. package/src/hooks/calc/use-resolver.ts +70 -0
  107. package/src/hooks/calc/use-session.ts +13 -0
  108. package/src/hooks/calc/use-share.ts +64 -0
  109. package/src/hooks/calc/use-storage.ts +126 -0
  110. package/src/hooks/calc/use-tier.ts +13 -0
  111. package/src/hooks/calc/use-view-mode.ts +24 -0
  112. package/src/hooks/calc/use-worker.ts +204 -0
  113. package/src/hooks/forge/use-anvil-gui.ts +16 -0
  114. package/src/hooks/forge/use-forge-state.ts +135 -0
  115. package/src/hooks/forge/use-ruler.ts +100 -0
  116. package/src/hooks/use-add-to-calc.ts +51 -0
  117. package/src/hooks/use-categories-by-id.ts +18 -0
  118. package/src/hooks/use-favorite-hotkey.ts +39 -0
  119. package/src/hooks/use-favorites.ts +135 -0
  120. package/src/hooks/use-group-outlines.ts +65 -0
  121. package/src/hooks/use-hide-unused.ts +12 -0
  122. package/src/hooks/use-index.ts +72 -0
  123. package/src/hooks/use-item-feeds.ts +46 -0
  124. package/src/hooks/use-item-pane.ts +106 -0
  125. package/src/hooks/use-item-press.ts +89 -0
  126. package/src/hooks/use-item-tag-groups.ts +54 -0
  127. package/src/hooks/use-item-tooltips.ts +18 -0
  128. package/src/hooks/use-left-sidebar-tree.ts +34 -0
  129. package/src/hooks/use-mod-names.ts +17 -0
  130. package/src/hooks/use-mount-limit.ts +22 -0
  131. package/src/hooks/use-panel-cells.ts +117 -0
  132. package/src/hooks/use-panel-page.ts +54 -0
  133. package/src/hooks/use-panel-tabs.ts +43 -0
  134. package/src/hooks/use-query.ts +186 -0
  135. package/src/hooks/use-session.ts +88 -0
  136. package/src/hooks/use-sidebar-group.ts +16 -0
  137. package/src/hooks/use-tag-members.ts +16 -0
  138. package/src/hooks/use-tool-recipes.ts +26 -0
  139. package/src/hooks/use-type-filter.ts +73 -0
  140. package/src/lib/alloy/mix.ts +210 -0
  141. package/src/lib/alloy/parse.ts +37 -0
  142. package/src/lib/calc/defaults.ts +47 -0
  143. package/src/lib/calc/flow.ts +315 -0
  144. package/src/lib/calc/focus.ts +15 -0
  145. package/src/lib/calc/meta.ts +17 -0
  146. package/src/lib/calc/pack.ts +77 -0
  147. package/src/lib/calc/ports.ts +43 -0
  148. package/src/lib/calc/resolve.ts +198 -0
  149. package/src/lib/calc/route/buckets.ts +49 -0
  150. package/src/lib/calc/route/geometry.ts +51 -0
  151. package/src/lib/calc/route/index.ts +3 -0
  152. package/src/lib/calc/route/polyline.ts +103 -0
  153. package/src/lib/calc/route/router.ts +394 -0
  154. package/src/lib/calc/scheme.ts +226 -0
  155. package/src/lib/calc/storage/entry.ts +97 -0
  156. package/src/lib/calc/storage/index.ts +20 -0
  157. package/src/lib/calc/storage/keys.ts +38 -0
  158. package/src/lib/calc/storage/store.ts +198 -0
  159. package/src/lib/calc/tree/index.ts +4 -0
  160. package/src/lib/calc/tree/materials.ts +29 -0
  161. package/src/lib/calc/tree/recipe.ts +104 -0
  162. package/src/lib/calc/tree/share.ts +135 -0
  163. package/src/lib/calc/worker/api.ts +25 -0
  164. package/src/lib/calc/worker/fetch.ts +66 -0
  165. package/src/lib/calc/worker/flush.ts +39 -0
  166. package/src/lib/calc/worker/index.ts +8 -0
  167. package/src/lib/calc/worker/protocol.ts +35 -0
  168. package/src/lib/calc/worker/resolve.ts +49 -0
  169. package/src/lib/calc/worker/state.ts +48 -0
  170. package/src/lib/card-model.ts +65 -0
  171. package/src/lib/client/recipe-categories.ts +14 -0
  172. package/src/lib/client/recipe.ts +85 -0
  173. package/src/lib/client-index/catalog.ts +91 -0
  174. package/src/lib/client-index/chrome.ts +180 -0
  175. package/src/lib/client-index/index.ts +20 -0
  176. package/src/lib/client-index/registry.ts +121 -0
  177. package/src/lib/client-index/rows.ts +260 -0
  178. package/src/lib/client-index/tooltips.ts +74 -0
  179. package/src/lib/config.ts +18 -0
  180. package/src/lib/data/category/dto.ts +25 -0
  181. package/src/lib/data/category/load.ts +47 -0
  182. package/src/lib/data/category/parse.ts +71 -0
  183. package/src/lib/data/dto.ts +152 -0
  184. package/src/lib/data/grid.ts +40 -0
  185. package/src/lib/data/parse/entry.ts +85 -0
  186. package/src/lib/data/parse/index.ts +3 -0
  187. package/src/lib/data/parse/layout.ts +138 -0
  188. package/src/lib/data/parse/slots.ts +85 -0
  189. package/src/lib/data/parse/widgets.ts +81 -0
  190. package/src/lib/data/scene.ts +103 -0
  191. package/src/lib/data/slot-io.ts +44 -0
  192. package/src/lib/dump.ts +6 -0
  193. package/src/lib/embed.ts +22 -0
  194. package/src/lib/favorite-hover.ts +13 -0
  195. package/src/lib/fluid-outputs.ts +70 -0
  196. package/src/lib/forge/actions.ts +59 -0
  197. package/src/lib/forge/data.ts +51 -0
  198. package/src/lib/forge/solve.ts +80 -0
  199. package/src/lib/group.ts +23 -0
  200. package/src/lib/index-store.ts +70 -0
  201. package/src/lib/links.ts +131 -0
  202. package/src/lib/lookup.ts +119 -0
  203. package/src/lib/panel-outline.ts +86 -0
  204. package/src/lib/panel.ts +105 -0
  205. package/src/lib/preview.ts +32 -0
  206. package/src/lib/recipe-slots.ts +8 -0
  207. package/src/lib/search.ts +139 -0
  208. package/src/lib/sidebar.ts +47 -0
  209. package/src/lib/used-items.ts +25 -0
  210. package/src/locales/en_us.json +172 -0
  211. package/src/locales/ru_ru.json +172 -0
  212. package/src/locales.ts +6 -0
  213. package/src/types/alloy.ts +20 -0
  214. package/src/types/calc-flow-props.ts +44 -0
  215. package/src/types/calc.ts +92 -0
  216. package/src/types/forge.ts +21 -0
  217. package/src/types/index.ts +136 -0
  218. package/styles/calc.css +308 -0
  219. package/styles/strip.css +7 -0
  220. package/styles.css +2 -0
  221. package/README.md +0 -3
@@ -0,0 +1,44 @@
1
+ import type { DumpRecipeLayout } from '@terrafirmagreg-team/recipe/lib/data/category/dto';
2
+ import type { RecipeSlotType } from '@terrafirmagreg-team/recipe/lib/data/dto';
3
+
4
+ export function outputBoundary(
5
+ slots: Readonly<
6
+ Record<string, { type?: unknown; x?: unknown } | undefined>
7
+ >,
8
+ layout?: DumpRecipeLayout,
9
+ ): number {
10
+ const byId = layout
11
+ ? new Map(layout.slots.map((slot) => [slot.id, slot]))
12
+ : undefined;
13
+ let bound = Number.POSITIVE_INFINITY;
14
+ for (const [id, slot] of Object.entries(slots)) {
15
+ if (slot?.type !== 'output') continue;
16
+ const x = typeof slot.x === 'number' ? slot.x : byId?.get(id)?.x;
17
+ if (typeof x === 'number') bound = Math.min(bound, x);
18
+ }
19
+ if (!Number.isFinite(bound) && byId) {
20
+ for (const slot of byId.values()) {
21
+ if (slot.role === 'output' && typeof slot.x === 'number') {
22
+ bound = Math.min(bound, slot.x);
23
+ }
24
+ }
25
+ }
26
+ if (!Number.isFinite(bound)) {
27
+ bound = layout?.w ? layout.w / 2 : Number.POSITIVE_INFINITY;
28
+ }
29
+ return bound;
30
+ }
31
+
32
+ export function slotIoRole(
33
+ type: RecipeSlotType,
34
+ x: number | undefined,
35
+ boundary: number,
36
+ ): 'input' | 'output' | 'catalyst' {
37
+ if (type === 'output') return 'output';
38
+ if (type === 'catalyst') return 'catalyst';
39
+ if (type === 'tank') {
40
+ return typeof x === 'number' && x >= boundary ? 'output' : 'input';
41
+ }
42
+ return 'input';
43
+ }
44
+
@@ -0,0 +1,6 @@
1
+ export function recipeDumpHasContent(dump: {
2
+ mods: readonly { recipeIndex: string | null }[];
3
+ }): boolean {
4
+ return dump.mods.some((mod) => mod.recipeIndex != null);
5
+ }
6
+
@@ -0,0 +1,22 @@
1
+ import { RecipeEmbedCard } from '@terrafirmagreg-team/recipe/components/embed-card';
2
+ import { loadClientRecipe } from '@terrafirmagreg-team/recipe/lib/client/recipe';
3
+ import {
4
+ recipeCardImageUrl,
5
+ recipeHref,
6
+ } from '@terrafirmagreg-team/recipe/lib/links';
7
+ import { recipeTitle } from '@terrafirmagreg-team/recipe/lib/data/parse';
8
+ import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
9
+ import type {
10
+ EmbedRecipe,
11
+ SectionEmbeds,
12
+ } from '@terrafirmagreg-team/kit/types/embeds';
13
+
14
+ export const recipeSectionEmbed: NonNullable<SectionEmbeds['recipe']> = {
15
+ href: (dataset, lang, ref) => recipeHref(dataset, lang, { id: ref.id }),
16
+ cardImageUrl: recipeCardImageUrl,
17
+ load: async (dataset, id) =>
18
+ (await loadClientRecipe(dataset, id)) as EmbedRecipe | undefined,
19
+ title: (recipe, langMap) =>
20
+ recipeTitle(recipe as unknown as Recipe, langMap),
21
+ Card: RecipeEmbedCard,
22
+ };
@@ -0,0 +1,13 @@
1
+ export type FavoriteHoverTarget =
2
+ | { kind: 'stack'; id: string; ref?: 'item' | 'fluid' }
3
+ | { kind: 'recipe'; id: string; type: string; icon?: string };
4
+
5
+ export const favoriteHover: {
6
+ base?: FavoriteHoverTarget;
7
+ slot?: FavoriteHoverTarget;
8
+ } = {};
9
+
10
+ export function favoriteHoverTarget(): FavoriteHoverTarget | undefined {
11
+ return favoriteHover.slot ?? favoriteHover.base;
12
+ }
13
+
@@ -0,0 +1,70 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import type { RecipeCompactIndex } from '@terrafirmagreg-team/recipe/types';
3
+ import { outputBoundary } from '@terrafirmagreg-team/recipe/lib/data/slot-io';
4
+ import { loadTypeRows } from '@terrafirmagreg-team/recipe/lib/client-index';
5
+ import { createDatasetCache } from '@terrafirmagreg-team/dump/lib/dump/reader';
6
+
7
+ export type FluidIoIndex = {
8
+ inputs: Map<string, string[]>;
9
+ outputs: Map<string, string[]>;
10
+ };
11
+
12
+ const cache = createDatasetCache(
13
+ build,
14
+ (dataset) => `${dataset.id}/${dataset.version ?? ''}`,
15
+ );
16
+
17
+ function tankCategories(index: RecipeCompactIndex, role: 'input' | 'output') {
18
+ return (index.categories ?? []).filter((cat) => {
19
+ const layout = cat.layout;
20
+ if (!layout) return false;
21
+ const bound = outputBoundary({}, layout);
22
+ return layout.slots.some((slot) =>
23
+ slot.role === 'tank' &&
24
+ (role === 'output' ? slot.x >= bound : slot.x < bound),
25
+ );
26
+ });
27
+ }
28
+
29
+ async function build(
30
+ dataset: DatasetRef,
31
+ index: RecipeCompactIndex,
32
+ ): Promise<FluidIoIndex> {
33
+ const fluids = new Set(index.fluids ?? []);
34
+ const inputs = new Map<string, string[]>();
35
+ const outputs = new Map<string, string[]>();
36
+ const typeIds = new Set([
37
+ ...tankCategories(index, 'input').map((cat) => cat.id),
38
+ ...tankCategories(index, 'output').map((cat) => cat.id),
39
+ ]);
40
+ const perType = await Promise.all(
41
+ [...typeIds].map((typeId) =>
42
+ loadTypeRows(dataset, index, typeId).catch(() => []),
43
+ ),
44
+ );
45
+ for (const rows of perType) {
46
+ for (const row of rows) {
47
+ for (const id of row.in) {
48
+ if (!fluids.has(id)) continue;
49
+ const list = inputs.get(id);
50
+ if (list) list.push(row.id);
51
+ else inputs.set(id, [row.id]);
52
+ }
53
+ for (const id of row.out) {
54
+ if (!fluids.has(id)) continue;
55
+ const list = outputs.get(id);
56
+ if (list) list.push(row.id);
57
+ else outputs.set(id, [row.id]);
58
+ }
59
+ }
60
+ }
61
+ return { inputs, outputs };
62
+ }
63
+
64
+ export function loadFluidIoIndex(
65
+ dataset: DatasetRef,
66
+ index: RecipeCompactIndex,
67
+ ): Promise<FluidIoIndex> {
68
+ return cache(dataset, index);
69
+ }
70
+
@@ -0,0 +1,59 @@
1
+ import { langString } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
2
+ import type { ForgeRule, ForgeRuleAction } from '@terrafirmagreg-team/recipe/types/forge';
3
+
4
+ const FORGE_STEP_LANG_PREFIX = 'tfc.enum.forgestep.';
5
+ const HIT_VARIANTS = new Set(['light', 'medium', 'hard']);
6
+
7
+ export function forgeActionLabel(
8
+ langMap: Record<string, string>,
9
+ id: string,
10
+ ): string {
11
+ const key = HIT_VARIANTS.has(id) ? `hit_${id}` : id;
12
+ return langString(langMap, `${FORGE_STEP_LANG_PREFIX}${key}`) ?? id;
13
+ }
14
+
15
+ export const FORGE_ACTIONS = [
16
+ { id: 'light', action: 'hit', delta: -3 },
17
+ { id: 'medium', action: 'hit', delta: -6 },
18
+ { id: 'hard', action: 'hit', delta: -9 },
19
+ { id: 'draw', action: 'draw', delta: -15 },
20
+ { id: 'punch', action: 'punch', delta: 2 },
21
+ { id: 'bend', action: 'bend', delta: 7 },
22
+ { id: 'upset', action: 'upset', delta: 13 },
23
+ { id: 'shrink', action: 'shrink', delta: 16 },
24
+ ] as const satisfies readonly {
25
+ id: string;
26
+ action: ForgeRuleAction;
27
+ delta: number;
28
+ }[];
29
+
30
+ export function cleanForgeText(text: string): string {
31
+ return text.replace(/§./g, '').trim();
32
+ }
33
+
34
+ export function parseForgeRuleTooltip(text: string): ForgeRule | undefined {
35
+ const match =
36
+ /^(Hit|Draw|Punch|Bend|Upset|Shrink) (Last|Second Last|Third Last|Not Last|Any)$/i.exec(
37
+ cleanForgeText(text),
38
+ );
39
+ if (!match) return undefined;
40
+ return {
41
+ action: match[1].toLowerCase() as ForgeRule['action'],
42
+ order: match[2].toLowerCase().replaceAll(' ', '_') as ForgeRule['order'],
43
+ };
44
+ }
45
+
46
+ export function forgeRulesMatch(
47
+ rules: readonly ForgeRule[],
48
+ last: readonly (ForgeRuleAction | '')[],
49
+ ): boolean {
50
+ return rules.every(({ action, order }) => {
51
+ if (order === 'last') return last[0] === action;
52
+ if (order === 'second_last') return last[1] === action;
53
+ if (order === 'third_last') return last[2] === action;
54
+ return (order === 'not_last' ? last.slice(1, 3) : last.slice(0, 3)).includes(
55
+ action,
56
+ );
57
+ });
58
+ }
59
+
@@ -0,0 +1,51 @@
1
+ import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
2
+ import type { ForgeRecipe } from '@terrafirmagreg-team/recipe/types/forge';
3
+ import { stackIdFromRef } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
4
+ import { parseForgeRuleTooltip } from './actions';
5
+
6
+ export function toForgeRecipe(recipe: Recipe): ForgeRecipe {
7
+ const tooltips = recipe.widgets.flatMap((widget) =>
8
+ 'tooltip' in widget ? (widget.tooltip ?? []) : [],
9
+ );
10
+ const parsed = tooltips.map(parseForgeRuleTooltip);
11
+ return {
12
+ recipe,
13
+ rules: parsed.filter((rule) => rule !== undefined),
14
+ rulesSupported:
15
+ tooltips.length > 0 && parsed.every((rule) => rule !== undefined),
16
+ };
17
+ }
18
+
19
+ const ANVIL_TIER_ORDER = [
20
+ 'rock',
21
+ 'stone',
22
+ 'copper',
23
+ 'bronze',
24
+ 'wrought_iron',
25
+ 'steel',
26
+ 'black_steel',
27
+ 'red_or_blue',
28
+ ];
29
+
30
+ export function anvilGroup(recipe: Recipe): { id: string; icon?: string } {
31
+ const catalyst = Object.values(recipe.slots).find(
32
+ (slot) => slot.type === 'catalyst',
33
+ );
34
+ const ref = catalyst?.ref;
35
+ if (!ref) return { id: '' };
36
+ const id = ref.kind === 'itemTag' ? `#${ref.id}` : stackIdFromRef(ref);
37
+ return { id, icon: catalyst?.alternatives?.[0]?.id ?? ref.id };
38
+ }
39
+
40
+ export function anvilGroupRank(groupId: string): number {
41
+ const last = groupId
42
+ .replace(/^#/, '')
43
+ .split('/')
44
+ .pop()!
45
+ .split(':')
46
+ .pop()!
47
+ .replace(/_anvils?$/, '');
48
+ const index = ANVIL_TIER_ORDER.indexOf(last);
49
+ return index < 0 ? ANVIL_TIER_ORDER.length : index;
50
+ }
51
+
@@ -0,0 +1,80 @@
1
+ import type { ForgeRule, ForgeRuleAction } from '@terrafirmagreg-team/recipe/types/forge';
2
+ import { FORGE_ACTIONS, forgeRulesMatch } from './actions';
3
+
4
+ const KINDS: (ForgeRuleAction | '')[] = [
5
+ '',
6
+ 'hit',
7
+ 'draw',
8
+ 'punch',
9
+ 'bend',
10
+ 'upset',
11
+ 'shrink',
12
+ ];
13
+ const HISTORIES = 343;
14
+ const STATES = 150 * HISTORIES;
15
+
16
+ export function solveForgeSteps(
17
+ start: { work: number; last: readonly ForgeRuleAction[] },
18
+ target: number,
19
+ rules: readonly ForgeRule[],
20
+ ): number[] | null {
21
+ if (
22
+ !Number.isInteger(target) ||
23
+ target < 0 ||
24
+ target >= 150 ||
25
+ !Number.isInteger(start.work) ||
26
+ start.work < 0 ||
27
+ start.work >= 150
28
+ ) {
29
+ return null;
30
+ }
31
+ const last = [...start.last.slice(0, 3)].reverse();
32
+ while (last.length < 3) last.push('' as ForgeRuleAction);
33
+ const startHistory =
34
+ KINDS.indexOf(last[0]) +
35
+ KINDS.indexOf(last[1]) * 7 +
36
+ KINDS.indexOf(last[2]) * 49;
37
+ const parent = new Int32Array(STATES).fill(-1);
38
+ const via = new Uint8Array(STATES);
39
+ const queue = new Int32Array(STATES);
40
+ const startState = start.work * HISTORIES + startHistory;
41
+ queue[0] = startState;
42
+ parent[startState] = startState;
43
+ let end = 1;
44
+ for (let head = 0; head < end; head++) {
45
+ const state = queue[head];
46
+ const work = Math.floor(state / HISTORIES);
47
+ const history = state % HISTORIES;
48
+ if (
49
+ work === target &&
50
+ forgeRulesMatch(rules, [
51
+ KINDS[history % 7],
52
+ KINDS[Math.floor(history / 7) % 7],
53
+ KINDS[Math.floor(history / 49)],
54
+ ])
55
+ ) {
56
+ const steps: number[] = [];
57
+ for (
58
+ let cursor = state;
59
+ cursor !== startState;
60
+ cursor = parent[cursor]
61
+ ) {
62
+ steps.push(via[cursor]);
63
+ }
64
+ return steps.reverse();
65
+ }
66
+ for (let i = 0; i < FORGE_ACTIONS.length; i++) {
67
+ const action = FORGE_ACTIONS[i];
68
+ const nextWork = work + action.delta;
69
+ if (nextWork < 0 || nextWork >= 150) continue;
70
+ const nextHistory = KINDS.indexOf(action.action) + (history % 49) * 7;
71
+ const next = nextWork * HISTORIES + nextHistory;
72
+ if (parent[next] !== -1) continue;
73
+ parent[next] = state;
74
+ via[next] = i;
75
+ queue[end++] = next;
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
@@ -0,0 +1,23 @@
1
+ import type {
2
+ CategoryGroup,
3
+ RecipeCategory,
4
+ RecipeCompactRow,
5
+ } from '@terrafirmagreg-team/recipe/types';
6
+
7
+ export function groupByCategory(
8
+ rows: RecipeCompactRow[],
9
+ types: string[],
10
+ categories?: RecipeCategory[],
11
+ ): CategoryGroup[] {
12
+ const groups = [...Map.groupBy(rows, (row) => types[row.type] ?? '')].map(
13
+ ([category, rows]) => ({ category, rows }),
14
+ );
15
+ if (!categories?.length) return groups;
16
+ const order = new Map(categories.map((row, index) => [row.id, index]));
17
+ return groups.sort(
18
+ (a, b) =>
19
+ (order.get(a.category) ?? Number.MAX_SAFE_INTEGER) -
20
+ (order.get(b.category) ?? Number.MAX_SAFE_INTEGER),
21
+ );
22
+ }
23
+
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import { useSyncExternalStore } from 'react';
5
+ import type { RecipeCompactIndex } from '@terrafirmagreg-team/recipe/types';
6
+
7
+ const indexes = new Map<string, RecipeCompactIndex>();
8
+ const listeners = new Set<() => void>();
9
+
10
+ function refKey(dataset: DatasetRef) {
11
+ return `${dataset.id}/${dataset.version ?? ''}`;
12
+ }
13
+
14
+ function key(dataset: DatasetRef, lang: string) {
15
+ return `${refKey(dataset)} ${lang}`;
16
+ }
17
+
18
+ const namesSnapshots = new Map<
19
+ string,
20
+ Record<string, Record<string, string>>
21
+ >();
22
+
23
+ function rebuildNames(dataset: DatasetRef) {
24
+ const merged: Record<string, Record<string, string>> = {};
25
+ for (const [k, index] of indexes) {
26
+ if (!k.startsWith(`${refKey(dataset)} `)) continue;
27
+ for (const [lang, names] of Object.entries(index.names ?? {})) {
28
+ merged[lang] = { ...merged[lang], ...names };
29
+ }
30
+ }
31
+ namesSnapshots.set(refKey(dataset), merged);
32
+ }
33
+
34
+ export function publishRecipeIndex(
35
+ dataset: DatasetRef,
36
+ lang: string,
37
+ index: RecipeCompactIndex,
38
+ ) {
39
+ if (indexes.get(key(dataset, lang)) === index) return;
40
+ indexes.set(key(dataset, lang), index);
41
+ rebuildNames(dataset);
42
+ for (const listener of listeners) listener();
43
+ }
44
+
45
+ function subscribe(listener: () => void) {
46
+ listeners.add(listener);
47
+ return () => listeners.delete(listener);
48
+ }
49
+
50
+ export function useRecipeIndexStore(
51
+ dataset: DatasetRef,
52
+ lang: string,
53
+ ): RecipeCompactIndex | null {
54
+ return useSyncExternalStore(
55
+ subscribe,
56
+ () => indexes.get(key(dataset, lang)) ?? null,
57
+ () => null,
58
+ );
59
+ }
60
+
61
+ export function useRecipeIndexNames(
62
+ dataset: DatasetRef,
63
+ ): Record<string, Record<string, string>> | null {
64
+ return useSyncExternalStore(
65
+ subscribe,
66
+ () => namesSnapshots.get(refKey(dataset)) ?? null,
67
+ () => null,
68
+ );
69
+ }
70
+
@@ -0,0 +1,131 @@
1
+ import { encodeQueryValue, withLang } from '@terrafirmagreg-team/translate/lib/url';
2
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
3
+ import {
4
+ type DatasetRef,
5
+ sectionUrl,
6
+ } from '@terrafirmagreg-team/sites/location';
7
+ import { dumpRelPublicUrl, splitDumpId, tagPseudoId } from '@terrafirmagreg-team/dump/lib/dump/paths';
8
+ import type { RecipeHrefOpts, RecipePlace } from '@terrafirmagreg-team/recipe/types';
9
+
10
+ export type { RecipeHrefOpts, RecipePlace } from '@terrafirmagreg-team/recipe/types';
11
+
12
+ function recipeQuery(
13
+ entries: Array<readonly [string, string | number | undefined]>,
14
+ ): string {
15
+ const parts: string[] = [];
16
+ for (const [key, value] of entries) {
17
+ if (value === undefined || value === '') continue;
18
+ parts.push(`${key}=${encodeQueryValue(String(value))}`);
19
+ }
20
+ return parts.join('&');
21
+ }
22
+
23
+ export function recipeBasePath(dataset: DatasetRef): string {
24
+ return sectionUrl('recipe', dataset.id);
25
+ }
26
+
27
+ export const RECIPE_ALL_TYPE = 'all';
28
+
29
+ export function recipePlaceFromParams(params: {
30
+ get(name: string): string | null;
31
+ }): RecipePlace {
32
+ const tool = params.get('tool');
33
+ if (tool === 'calc' || tool === 'forge' || tool === 'alloy') {
34
+ return { kind: tool };
35
+ }
36
+ const type = params.get('type');
37
+ return { kind: 'type', id: type || RECIPE_ALL_TYPE };
38
+ }
39
+
40
+ export function recipePlaceOpts(
41
+ place: RecipePlace | undefined,
42
+ ): Pick<RecipeHrefOpts, 'type' | 'tool'> {
43
+ if (place?.kind === 'type') {
44
+ return place.id === RECIPE_ALL_TYPE ? {} : { type: place.id };
45
+ }
46
+ return place ? { tool: place.kind } : {};
47
+ }
48
+
49
+ export function recipeHref(
50
+ dataset: DatasetRef,
51
+ lang: Lang | undefined,
52
+ opts?: RecipeHrefOpts,
53
+ ): string {
54
+ const query = recipeQuery([
55
+ ['type', opts?.type],
56
+ ['tool', opts?.tool],
57
+ ['item', opts?.item],
58
+ ['fluid', opts?.item ? undefined : opts?.fluid],
59
+ ['id', opts?.id],
60
+ ['solo', opts?.id ? opts?.solo : undefined],
61
+ ['relation', opts?.relation],
62
+ ['unit', opts?.tool === 'alloy' ? opts?.unit : undefined],
63
+ ['mix', opts?.tool === 'alloy' ? opts?.mix : undefined],
64
+ ['tab', opts?.tab],
65
+ ['search', opts?.search],
66
+ [
67
+ 'rpage',
68
+ opts?.rpage && opts.rpage > 1 ? Math.floor(opts.rpage) : undefined,
69
+ ],
70
+ ['version', dataset.version],
71
+ ]);
72
+ const base = recipeBasePath(dataset);
73
+ const href = query ? `${base}?${query}` : base;
74
+ return lang ? withLang(href, lang) : href;
75
+ }
76
+
77
+ export function recipeCalcHref(
78
+ dataset: DatasetRef,
79
+ lang: Lang | undefined,
80
+ tree?: string,
81
+ ): string {
82
+ const query = recipeQuery([
83
+ ['tool', 'calc'],
84
+ ['tree', tree],
85
+ ['version', dataset.version],
86
+ ]);
87
+ const href = `${recipeBasePath(dataset)}?${query}`;
88
+ return lang ? withLang(href, lang) : href;
89
+ }
90
+
91
+ export function recipeStackHref(
92
+ dataset: DatasetRef,
93
+ lang: Lang,
94
+ id: string,
95
+ opts?: Pick<RecipeHrefOpts, 'tab' | 'search'> & {
96
+ kind?: 'item' | 'fluid' | 'tag';
97
+ tagKind?: string;
98
+ },
99
+ ): string {
100
+ const kept = {
101
+ tab: opts?.tab,
102
+ search: opts?.search,
103
+ };
104
+ if (opts?.kind === 'tag' || id.startsWith('#')) {
105
+ const bare = id.startsWith('#') ? id.slice(1) : id;
106
+ const tagKind = opts?.tagKind ?? 'item';
107
+ return recipeHref(dataset, lang, {
108
+ type: 'emi:tag',
109
+ id: tagPseudoId(bare, tagKind),
110
+ ...kept,
111
+ });
112
+ }
113
+ return opts?.kind === 'fluid'
114
+ ? recipeHref(dataset, lang, { fluid: id, ...kept })
115
+ : recipeHref(dataset, lang, { item: id, ...kept });
116
+ }
117
+
118
+ export { recipeImageUrl } from '@terrafirmagreg-team/dump/lib/dump/assets/urls';
119
+
120
+ export function recipeCardImageUrl(
121
+ dataset: DatasetRef,
122
+ recipeId: string | undefined,
123
+ ): string | undefined {
124
+ const parts = recipeId ? splitDumpId(recipeId) : undefined;
125
+ if (!parts) return undefined;
126
+ return dumpRelPublicUrl(
127
+ dataset,
128
+ `assets/${parts.ns}/textures/recipe/${parts.path}.webp`,
129
+ );
130
+ }
131
+