@terrafirmagreg-team/recipe 0.1.0 → 1.2.1

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 (240) hide show
  1. package/package.json +16 -16
  2. package/src/components/button-copy.tsx +1 -0
  3. package/src/components/cards/card/index.tsx +5 -3
  4. package/src/components/cards/card/scene.tsx +4 -2
  5. package/src/components/cards/card/strip.tsx +4 -2
  6. package/src/components/cards/card/text.tsx +2 -1
  7. package/src/components/cards/card/tips.tsx +1 -0
  8. package/src/components/cards/default-item.tsx +6 -2
  9. package/src/components/cards/glare-filter.tsx +1 -0
  10. package/src/components/cards/paged-card.tsx +1 -0
  11. package/src/components/cards/slot-hitbox.tsx +4 -2
  12. package/src/components/cards/strip-card/buttons.tsx +7 -2
  13. package/src/components/cards/strip-card/index.tsx +3 -1
  14. package/src/components/cards/strip-card/member-cell.tsx +4 -2
  15. package/src/components/cards/strip-card/sprite.tsx +1 -0
  16. package/src/components/embed-card.tsx +44 -0
  17. package/src/components/mdx/recipe.tsx +34 -16
  18. package/src/components/tool-list.tsx +1 -0
  19. package/src/{lib/tree.ts → components/tree.tsx} +20 -12
  20. package/src/components/views/alloy/index.tsx +14 -7
  21. package/src/components/views/alloy/list.tsx +3 -2
  22. package/src/components/views/alloy/mix/index.tsx +1 -0
  23. package/src/components/views/alloy/mix/ratio.tsx +7 -6
  24. package/src/components/views/alloy/mix/rows.tsx +6 -7
  25. package/src/components/views/alloy/mix/status.tsx +8 -7
  26. package/src/components/views/alloy/mix/total.tsx +13 -12
  27. package/src/components/views/alloy/range-bar.tsx +2 -1
  28. package/src/components/views/alloy/stack-bar.tsx +4 -2
  29. package/src/components/views/browser/axis-switcher.tsx +13 -12
  30. package/src/components/views/browser/card-grid.tsx +2 -1
  31. package/src/components/views/browser/favorite-button.tsx +2 -1
  32. package/src/components/views/browser/frame.tsx +3 -2
  33. package/src/components/views/browser/index.tsx +20 -2
  34. package/src/components/views/browser/item-panel/index.tsx +11 -9
  35. package/src/components/views/browser/item-panel/skeleton.tsx +1 -0
  36. package/src/components/views/browser/item-panel-grid/cell.tsx +5 -3
  37. package/src/components/views/browser/item-panel-grid/favorite-cell.tsx +4 -2
  38. package/src/components/views/browser/item-panel-grid/index.tsx +4 -2
  39. package/src/components/views/browser/item-view/index.tsx +7 -12
  40. package/src/components/views/browser/item-view/tags.tsx +7 -5
  41. package/src/components/views/browser/item-view-header.tsx +10 -9
  42. package/src/components/views/browser/pane-tabs.tsx +1 -0
  43. package/src/components/views/browser/place-header.tsx +4 -3
  44. package/src/components/views/browser/query-controls.tsx +3 -1
  45. package/src/components/views/browser/recipe-item-header.tsx +5 -4
  46. package/src/components/views/browser/settings.tsx +4 -3
  47. package/src/components/views/browser/toolbar.tsx +3 -2
  48. package/src/components/views/browser/type-tabs.tsx +5 -3
  49. package/src/components/views/browser/type-view.tsx +7 -12
  50. package/src/components/views/browser/workstation-rail.tsx +4 -3
  51. package/src/components/views/calc/calculator/actions.tsx +4 -14
  52. package/src/components/views/calc/calculator/help.tsx +69 -28
  53. package/src/components/views/calc/calculator/index.tsx +34 -82
  54. package/src/components/views/calc/dot-overlay.tsx +1 -0
  55. package/src/components/views/calc/edge.tsx +134 -32
  56. package/src/components/views/calc/flow/index.tsx +92 -54
  57. package/src/components/views/calc/flow/picker-dialogs.tsx +84 -6
  58. package/src/components/views/calc/flow/toolbar.tsx +9 -21
  59. package/src/components/views/calc/flow/view-menu.tsx +16 -120
  60. package/src/components/views/calc/member-picker.tsx +8 -6
  61. package/src/components/views/calc/node-card/actions.tsx +28 -106
  62. package/src/components/views/calc/node-card/badges.tsx +10 -28
  63. package/src/components/views/calc/node-card/chips.tsx +8 -40
  64. package/src/components/views/calc/node-card/far.tsx +13 -65
  65. package/src/components/views/calc/node-card/full.tsx +133 -83
  66. package/src/components/views/calc/node-card/index.tsx +1 -0
  67. package/src/components/views/calc/node-card/menu.tsx +93 -22
  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 +17 -14
  72. package/src/components/views/calc/recipe-picker/footer.tsx +5 -3
  73. package/src/components/views/calc/recipe-picker/index.tsx +5 -0
  74. package/src/components/views/calc/sidebar.tsx +19 -10
  75. package/src/components/views/calc/trees/icon-picker.tsx +3 -2
  76. package/src/components/views/calc/trees/index.tsx +5 -4
  77. package/src/components/views/calc/trees/label.tsx +6 -4
  78. package/src/components/views/calc/trees/rows.tsx +6 -5
  79. package/src/components/views/forge/gui.tsx +2 -1
  80. package/src/components/views/forge/index.tsx +14 -7
  81. package/src/components/views/forge/list.tsx +3 -2
  82. package/src/components/views/forge/workbench/controls.tsx +13 -6
  83. package/src/components/views/forge/workbench/index.tsx +3 -2
  84. package/src/components/views/forge/workbench/panel.tsx +1 -0
  85. package/src/components/views/forge/workbench/ruler.tsx +5 -4
  86. package/src/components/views/forge/workbench/status.tsx +6 -5
  87. package/src/components/views/forge/workbench/tape.tsx +5 -4
  88. package/src/hooks/alloy/use-fluid-colors.ts +12 -9
  89. package/src/hooks/alloy/use-mix-state.ts +3 -2
  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/{mount.ts → use-mount.ts} +1 -0
  94. package/src/hooks/calc/flow/{nodes.ts → use-nodes.ts} +46 -37
  95. package/src/hooks/calc/flow/use-pickers.ts +57 -0
  96. package/src/hooks/calc/flow/{viewport.ts → use-viewport.ts} +13 -26
  97. package/src/hooks/calc/flow/{window.ts → use-window.ts} +1 -0
  98. package/src/hooks/calc/use-actions.ts +208 -229
  99. package/src/hooks/calc/use-amounts.ts +23 -60
  100. package/src/hooks/calc/use-data.ts +106 -192
  101. package/src/hooks/calc/use-defaults.ts +4 -2
  102. package/src/hooks/calc/use-far-mode.ts +1 -0
  103. package/src/hooks/calc/use-focus.ts +8 -7
  104. package/src/hooks/calc/use-maps.ts +1 -0
  105. package/src/hooks/calc/use-reset.ts +1 -0
  106. package/src/hooks/calc/use-resolver.ts +32 -196
  107. package/src/hooks/calc/use-session.ts +1 -0
  108. package/src/hooks/calc/use-share.ts +5 -2
  109. package/src/hooks/calc/use-storage.ts +5 -4
  110. package/src/hooks/calc/use-tier.ts +1 -0
  111. package/src/hooks/calc/use-view-mode.ts +1 -0
  112. package/src/hooks/calc/use-worker.ts +56 -121
  113. package/src/hooks/forge/use-anvil-gui.ts +4 -2
  114. package/src/hooks/forge/use-forge-state.ts +1 -0
  115. package/src/hooks/forge/use-ruler.ts +1 -0
  116. package/src/hooks/use-add-to-calc.ts +23 -19
  117. package/src/hooks/use-categories-by-id.ts +18 -0
  118. package/src/hooks/use-favorite-hotkey.ts +1 -0
  119. package/src/hooks/use-favorites.ts +1 -0
  120. package/src/hooks/use-group-outlines.ts +1 -0
  121. package/src/hooks/use-hide-unused.ts +2 -1
  122. package/src/hooks/use-index.ts +6 -2
  123. package/src/hooks/use-item-feeds.ts +3 -1
  124. package/src/hooks/use-item-pane.ts +6 -4
  125. package/src/hooks/use-item-press.ts +27 -23
  126. package/src/hooks/use-item-tag-groups.ts +4 -2
  127. package/src/hooks/use-item-tooltips.ts +3 -1
  128. package/src/hooks/use-left-sidebar-tree.ts +11 -3
  129. package/src/hooks/use-mod-names.ts +4 -2
  130. package/src/hooks/use-mount-limit.ts +1 -0
  131. package/src/hooks/use-panel-cells.ts +3 -1
  132. package/src/hooks/use-panel-page.ts +9 -6
  133. package/src/hooks/use-panel-tabs.ts +2 -1
  134. package/src/hooks/use-query.ts +8 -6
  135. package/src/hooks/use-session.ts +6 -2
  136. package/src/hooks/use-sidebar-group.ts +3 -7
  137. package/src/hooks/use-tag-members.ts +3 -1
  138. package/src/hooks/use-tool-recipes.ts +1 -0
  139. package/src/hooks/use-type-filter.ts +1 -0
  140. package/src/lib/alloy/mix.ts +1 -0
  141. package/src/lib/alloy/parse.ts +1 -0
  142. package/src/lib/calc/defaults.ts +6 -4
  143. package/src/lib/calc/flow.ts +143 -107
  144. package/src/lib/calc/focus.ts +1 -0
  145. package/src/lib/calc/meta.ts +3 -1
  146. package/src/lib/calc/pack.ts +47 -154
  147. package/src/lib/calc/ports.ts +43 -0
  148. package/src/lib/calc/resolve.ts +6 -38
  149. package/src/lib/calc/route/buckets.ts +1 -0
  150. package/src/lib/calc/route/geometry.ts +13 -24
  151. package/src/lib/calc/route/index.ts +1 -0
  152. package/src/lib/calc/route/polyline.ts +3 -3
  153. package/src/lib/calc/route/router.ts +181 -57
  154. package/src/lib/calc/scheme.ts +226 -0
  155. package/src/lib/calc/storage/entry.ts +27 -19
  156. package/src/lib/calc/storage/index.ts +2 -0
  157. package/src/lib/calc/storage/keys.ts +4 -8
  158. package/src/lib/calc/storage/store.ts +11 -36
  159. package/src/lib/calc/tree/index.ts +2 -16
  160. package/src/lib/calc/tree/materials.ts +1 -0
  161. package/src/lib/calc/tree/recipe.ts +1 -0
  162. package/src/lib/calc/tree/share.ts +86 -149
  163. package/src/lib/calc/worker/api.ts +3 -11
  164. package/src/lib/calc/worker/fetch.ts +1 -0
  165. package/src/lib/calc/worker/flush.ts +7 -20
  166. package/src/lib/calc/worker/index.ts +1 -1
  167. package/src/lib/calc/worker/protocol.ts +16 -13
  168. package/src/lib/calc/worker/resolve.ts +32 -84
  169. package/src/lib/calc/worker/state.ts +5 -6
  170. package/src/{components/cards/card/model.ts → lib/card-model.ts} +1 -0
  171. package/src/lib/client/recipe-categories.ts +11 -93
  172. package/src/lib/client/recipe.ts +13 -11
  173. package/src/lib/client-index/catalog.ts +33 -27
  174. package/src/lib/client-index/chrome.ts +30 -30
  175. package/src/lib/client-index/index.ts +1 -0
  176. package/src/lib/client-index/registry.ts +70 -59
  177. package/src/lib/client-index/rows.ts +63 -57
  178. package/src/lib/client-index/tooltips.ts +28 -34
  179. package/src/lib/config.ts +2 -1
  180. package/src/lib/data/category/dto.ts +4 -3
  181. package/src/lib/data/category/load.ts +9 -8
  182. package/src/lib/data/category/parse.ts +4 -3
  183. package/src/lib/data/dto.ts +4 -3
  184. package/src/lib/data/grid.ts +2 -1
  185. package/src/lib/data/parse/entry.ts +3 -2
  186. package/src/lib/data/parse/index.ts +1 -0
  187. package/src/lib/data/parse/layout.ts +5 -2
  188. package/src/lib/data/parse/slots.ts +3 -2
  189. package/src/lib/data/parse/widgets.ts +3 -2
  190. package/src/lib/data/scene.ts +1 -1
  191. package/src/lib/data/slot-io.ts +1 -0
  192. package/src/lib/dump.ts +4 -3
  193. package/src/lib/embed.ts +22 -0
  194. package/src/lib/favorite-hover.ts +1 -0
  195. package/src/lib/fluid-outputs.ts +10 -12
  196. package/src/lib/forge/actions.ts +2 -1
  197. package/src/lib/forge/data.ts +2 -1
  198. package/src/lib/forge/solve.ts +1 -0
  199. package/src/lib/group.ts +1 -0
  200. package/src/lib/index-store.ts +15 -9
  201. package/src/lib/links.ts +15 -9
  202. package/src/lib/lookup.ts +10 -8
  203. package/src/lib/panel-outline.ts +1 -0
  204. package/src/lib/panel.ts +3 -2
  205. package/src/lib/preview.ts +12 -8
  206. package/src/lib/recipe-slots.ts +1 -0
  207. package/src/lib/search.ts +1 -0
  208. package/src/lib/sidebar.ts +1 -0
  209. package/src/lib/used-items.ts +1 -0
  210. package/src/locales/en_us.json +170 -226
  211. package/src/locales/ru_ru.json +170 -226
  212. package/src/locales.ts +1 -0
  213. package/src/types/alloy.ts +1 -0
  214. package/src/{hooks/calc/flow/props.ts → types/calc-flow-props.ts} +18 -14
  215. package/src/types/calc.ts +41 -44
  216. package/src/types/forge.ts +1 -0
  217. package/src/types/index.ts +4 -2
  218. package/styles/calc.css +308 -47
  219. package/src/components/cards/fit-card.tsx +0 -26
  220. package/src/components/views/calc/target.tsx +0 -271
  221. package/src/hooks/calc/flow/engage.ts +0 -89
  222. package/src/hooks/calc/flow/handlers.ts +0 -115
  223. package/src/hooks/calc/flow/layout.ts +0 -149
  224. package/src/hooks/calc/flow/pickers.ts +0 -50
  225. package/src/hooks/calc/use-calc-fill.ts +0 -91
  226. package/src/hooks/calc/use-density.ts +0 -28
  227. package/src/hooks/calc/use-derived.ts +0 -258
  228. package/src/hooks/calc/use-direction.ts +0 -23
  229. package/src/hooks/calc/use-edge-style.ts +0 -40
  230. package/src/hooks/calc/use-fill.ts +0 -213
  231. package/src/lib/alloy/alloy.test.ts +0 -186
  232. package/src/lib/calc/tree/build.ts +0 -196
  233. package/src/lib/calc/tree/context.ts +0 -42
  234. package/src/lib/calc/tree/goal.ts +0 -64
  235. package/src/lib/calc/tree/ideal.ts +0 -36
  236. package/src/lib/calc/tree/pick.ts +0 -68
  237. package/src/lib/calc/tree/tree.test.ts +0 -169
  238. package/src/lib/calc/worker/fill.ts +0 -123
  239. package/src/lib/client-index/cache.ts +0 -13
  240. package/src/lib/forge/forge.test.ts +0 -123
@@ -1,186 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { test } from 'node:test';
3
- import { parseAlloyPercent, toAlloyRecipe } from './parse';
4
- import {
5
- INGOT_MB,
6
- checkMix,
7
- ingotRatioMix,
8
- minShareMix,
9
- minimalAdditions,
10
- scaleAmounts,
11
- snapToIngots,
12
- suggestAmounts,
13
- } from './mix';
14
- import { parseRecipe } from '@terrafirmagreg-team/recipe/lib/data/parse/index';
15
- import type { RecipeSlotFill } from '@terrafirmagreg-team/recipe/lib/data/dto';
16
- import type { AlloyComponent } from '@terrafirmagreg-team/recipe/types/alloy';
17
-
18
- const component = (
19
- amount: number,
20
- range?: { min: number; max: number },
21
- ): AlloyComponent => ({
22
- slot: { amount, id: `test:m${amount}`, ref: { kind: 'fluid', id: `test:m${amount}` } } as RecipeSlotFill,
23
- ...(range ? { range } : {}),
24
- });
25
-
26
- test('matches multi-row alloy ranges in reading order', () => {
27
- const raw = {
28
- category: 'tfc:alloying',
29
- slots: Object.fromEntries(
30
- [1, 1, 3].map((amount, i) => [
31
- `input${i}`,
32
- {
33
- type: 'input',
34
- amount,
35
- ref: { kind: 'fluid', id: `test:metal${i}` },
36
- },
37
- ]),
38
- ),
39
- widgets: [
40
- [24, 27, '10-20%'],
41
- [90, 27, '20-30%'],
42
- [24, 47, '50-65%'],
43
- ].map(([x, y, text]) => ({ type: 'text', x, y, text, w: 36, h: 9 })),
44
- };
45
- const recipe = parseRecipe(raw, {
46
- id: 'test:alloy',
47
- modId: 'test',
48
- bodyUrl: '/test',
49
- });
50
- assert.ok(recipe);
51
- const model = toAlloyRecipe(recipe);
52
- assert.deepEqual(
53
- model.components.map((row) => row.range?.min),
54
- [10, 20, 50],
55
- );
56
- });
57
-
58
- test('parses percent ranges from dump text widgets', () => {
59
- assert.deepEqual(parseAlloyPercent('§x§0§0§0§0§0§070-85%'), {
60
- min: 70,
61
- max: 85,
62
- });
63
- assert.equal(parseAlloyPercent('85-70%'), undefined);
64
- assert.equal(parseAlloyPercent('Copper'), undefined);
65
- });
66
-
67
- test('recipe-ratio split scales to a total', () => {
68
- const components = [component(4), component(1)];
69
- assert.deepEqual(suggestAmounts(components, 100), [80, 20]);
70
- assert.equal(suggestAmounts(components, 0), null);
71
- assert.equal(suggestAmounts(components, Infinity), null);
72
- assert.equal(suggestAmounts([], 100), null);
73
- });
74
-
75
- test('scaleAmounts rescales to a new total keeping proportions', () => {
76
- assert.deepEqual(scaleAmounts([60, 40], 200), [120, 80]);
77
- assert.deepEqual(scaleAmounts([3, 1], 100), [75, 25]);
78
- assert.equal(scaleAmounts([60, 40], 0), null);
79
- assert.equal(scaleAmounts([0, 0], 100), null);
80
- assert.equal(scaleAmounts([60, 40], Infinity), null);
81
- });
82
-
83
- test('mix check flags out-of-range shares', () => {
84
- const components = [
85
- component(4, { min: 70, max: 85 }),
86
- component(1, { min: 15, max: 30 }),
87
- ];
88
- const ok = checkMix(components, [80, 20]);
89
- assert.equal(ok.valid, true);
90
- assert.equal(ok.output, 100);
91
- const bad = checkMix(components, [50, 50]);
92
- assert.equal(bad.valid, false);
93
- assert.equal(bad.rows[0].valid, false);
94
- assert.equal(bad.rows[1].valid, false);
95
- assert.equal(checkMix(components, [0, 0]).valid, false);
96
- });
97
-
98
- test('recipe ratio renders as whole ingots', () => {
99
- const components = [
100
- component(9, { min: 88, max: 92 }),
101
- component(1, { min: 8, max: 12 }),
102
- ];
103
- const ratio = ingotRatioMix(components);
104
- assert.deepEqual(ratio, { counts: [9, 1], total: 10 });
105
- const mix = checkMix(components, ratio!.counts.map((c) => c * INGOT_MB));
106
- assert.equal(mix.valid, true);
107
- });
108
-
109
- test('ingot ratio reduces amounts to lowest terms', () => {
110
- assert.deepEqual(ingotRatioMix([component(4), component(1)]), {
111
- counts: [4, 1],
112
- total: 5,
113
- });
114
- assert.deepEqual(ingotRatioMix([component(700), component(300)]), {
115
- counts: [7, 3],
116
- total: 10,
117
- });
118
- assert.deepEqual(ingotRatioMix([component(1.5), component(0.5)]), {
119
- counts: [3, 1],
120
- total: 4,
121
- });
122
- });
123
-
124
- test('ingot ratio is null without usable amounts', () => {
125
- assert.equal(ingotRatioMix([]), null);
126
- assert.equal(ingotRatioMix([component(0), component(1)]), null);
127
- });
128
-
129
- test('minimal additions restore a valid mix without removing metal', () => {
130
- const components = [
131
- component(4, { min: 70, max: 85 }),
132
- component(1, { min: 15, max: 30 }),
133
- ];
134
- const add = minimalAdditions(components, [50, 50]);
135
- assert.ok(add);
136
- assert.equal(add[1], 0);
137
- assert.ok(add[0] > 0);
138
- const fixed = checkMix(components, [50 + add[0], 50]);
139
- assert.equal(fixed.valid, true);
140
-
141
- assert.deepEqual(minimalAdditions(components, [80, 20]), [0, 0]);
142
-
143
- const impossible = minimalAdditions(
144
- [component(1, { min: 10, max: 20 }), component(1, { min: 10, max: 20 })],
145
- [50, 50],
146
- );
147
- assert.equal(impossible, null);
148
- });
149
-
150
- test('snapToIngots finds the closest valid whole-ingot mix', () => {
151
- const components = [
152
- component(9, { min: 88, max: 92 }),
153
- component(1, { min: 8, max: 12 }),
154
- ];
155
-
156
- assert.deepEqual(snapToIngots(components, [1296, 144]), [1296, 144]);
157
-
158
- assert.deepEqual(snapToIngots(components, [1150, 150]), [1152, 144]);
159
-
160
- assert.equal(snapToIngots(components, [144, 144]), null);
161
-
162
- assert.deepEqual(
163
- snapToIngots([component(4), component(1)], [500, 130]),
164
- [432, 144],
165
- );
166
-
167
- assert.deepEqual(
168
- snapToIngots([component(4), component(1)], [432, 0]),
169
- [432, 0],
170
- );
171
- });
172
-
173
- test('minShareMix pins a metal to its min bound', () => {
174
- const components = [
175
- component(9, { min: 88, max: 92 }),
176
- component(1, { min: 8, max: 12 }),
177
- ];
178
- const mix = minShareMix(components, 1, 1440);
179
- assert.ok(mix);
180
- assert.equal(mix[1], Math.ceil((1440 * 8) / 100));
181
- assert.equal(mix[0] + mix[1], 1440);
182
- assert.equal(checkMix(components, mix).valid, true);
183
-
184
- assert.equal(minShareMix([component(4), component(1)], 0, 100), null);
185
- assert.equal(minShareMix(components, 1, 0), null);
186
- });
@@ -1,196 +0,0 @@
1
- import type { BuildTreeInput, CalcKind, CalcNode } from '@terrafirmagreg-team/recipe/types/calc';
2
- import { AUTO_MAX_DEPTH, type BuildContext, type RemainderPool } from './context';
3
- import { materialKey } from './materials';
4
- import { calcAutoPick } from './pick';
5
- import { recipeDivisor } from './recipe';
6
-
7
- const MAX_BUILD_DEPTH = 64;
8
-
9
- function poolTake(pool: RemainderPool, kind: CalcKind, id: string, want: number) {
10
- const key = materialKey(kind, id);
11
- const have = pool.get(key) ?? 0;
12
- const taken = Math.min(have, want);
13
- const left = have - taken;
14
- if (left > 0) pool.set(key, left);
15
- else pool.delete(key);
16
- return taken;
17
- }
18
-
19
- function poolAdd(pool: RemainderPool, kind: CalcKind, id: string, amount: number) {
20
- if (amount <= 0) return;
21
- const key = materialKey(kind, id);
22
- pool.set(key, (pool.get(key) ?? 0) + amount);
23
- }
24
-
25
- function resolveTagMember(ctx: BuildContext, tagId: string) {
26
- const tag = ctx.tagMembers(tagId);
27
- if (!tag || !tag.members.length) return undefined;
28
- const picked = ctx.tagSelections[tagId];
29
- const memberId =
30
- picked && tag.members.includes(picked) ? picked : tag.members[0]!;
31
- return { memberId, members: tag.members, kind: tag.kind };
32
- }
33
-
34
- export function buildTree(input: BuildTreeInput, ctx: BuildContext): CalcNode {
35
- const { materialId, kind, amount, depth, tagId, catalyst, consumeChance, path } =
36
- input;
37
- const matKey = materialKey(kind, materialId);
38
- const availableRecipes = ctx.getRecipesForItem(materialId, kind);
39
- const tag = tagId ? resolveTagMember(ctx, tagId) : undefined;
40
-
41
- const base: CalcNode = {
42
- materialId,
43
- kind,
44
- amount,
45
- satisfied: 0,
46
- need: amount,
47
- recipe: null,
48
- batches: 0,
49
- produced: 0,
50
- cycle: false,
51
- catalyst: catalyst === true,
52
- children: [],
53
- availableRecipes,
54
- depth,
55
- ...(path ? { path } : {}),
56
- ...(consumeChance !== undefined && consumeChance < 1
57
- ? { consumeChance }
58
- : {}),
59
- ...(tagId ? { tagId, members: tag?.members } : {}),
60
- };
61
-
62
- const userCollapsed =
63
- path != null && (ctx.collapsed?.has(path) ?? false);
64
- const underAuto =
65
- path != null &&
66
- (ctx.autoRoots?.some(
67
- (root) => path === root || path.startsWith(`${root}/`),
68
- ) ??
69
- false);
70
- const auto =
71
- underAuto &&
72
- depth < AUTO_MAX_DEPTH &&
73
- (ctx.autoBudget ? ctx.autoBudget.left-- > 0 : true);
74
- const pickOnly = !auto || userCollapsed;
75
- let selectedRecipeId = pickOnly
76
- ? path
77
- ? ctx.selections[path]
78
- : undefined
79
- : ((path ? ctx.selections[path] : undefined) ??
80
- ctx.selections[materialId]);
81
- let pickPending = false;
82
- if (!availableRecipes.some((row) => row.recipeId === selectedRecipeId)) {
83
- if (pickOnly) {
84
- selectedRecipeId = undefined;
85
- } else {
86
- const autoPick = calcAutoPick(
87
- availableRecipes,
88
- materialId,
89
- ctx.usedMaterials,
90
- ctx,
91
- );
92
- selectedRecipeId = autoPick.pick;
93
- pickPending = !autoPick.pick;
94
- }
95
- }
96
- const recipe = selectedRecipeId ? ctx.recipeMap.get(selectedRecipeId) : null;
97
- base.recipe = recipe ?? null;
98
- if (underAuto) base.auto = auto;
99
- if (!recipe && !catalyst && availableRecipes.length > 0 && path != null) {
100
- if (pickPending) base.loading = true;
101
- else base.expandable = true;
102
- } else if (
103
- !recipe &&
104
- !availableRecipes.length &&
105
- path != null &&
106
- !(ctx.recipesResolved?.(materialId) ?? true)
107
- ) {
108
- base.loading = true;
109
- }
110
- if (userCollapsed && !recipe) base.collapsed = true;
111
- if (catalyst && !recipe) return base;
112
- if (depth >= MAX_BUILD_DEPTH) return base;
113
-
114
- const satisfied = ctx.pool
115
- ? poolTake(ctx.pool, kind, materialId, amount)
116
- : 0;
117
- const need = amount - satisfied;
118
- base.satisfied = satisfied;
119
- base.need = need;
120
- if (need <= 0) return base;
121
-
122
- if (!recipe) return base;
123
- if (
124
- ctx.used.has(recipe.recipeId) ||
125
- ctx.usedMaterials.has(matKey)
126
- ) {
127
- base.cycle = true;
128
- return base;
129
- }
130
-
131
- const divisor = recipeDivisor(recipe, materialId, kind);
132
- const batches = Math.ceil(need / divisor);
133
- const produced = batches * divisor;
134
- base.batches = batches;
135
- base.produced = produced;
136
-
137
- ctx.used.add(recipe.recipeId);
138
- ctx.usedMaterials.add(matKey);
139
- for (const inp of recipe.inputs) {
140
- if (inp.catalyst) {
141
- if (inp.id) {
142
- const resolved = inp.tagId
143
- ? resolveTagMember(ctx, inp.tagId)?.memberId
144
- : undefined;
145
- base.children.push(
146
- buildTree(
147
- {
148
- materialId: resolved ?? inp.id,
149
- kind: inp.kind,
150
- amount: inp.amount,
151
- depth: depth + 1,
152
- tagId: inp.tagId,
153
- catalyst: true,
154
- path: path ? `${path}/${base.children.length}` : undefined,
155
- },
156
- ctx,
157
- ),
158
- );
159
- }
160
- continue;
161
- }
162
-
163
- const childAmount = inp.amount * batches * (inp.chance ?? 1);
164
- if (childAmount <= 0 || !inp.id) continue;
165
- const resolved = inp.tagId ? resolveTagMember(ctx, inp.tagId) : undefined;
166
- base.children.push(
167
- buildTree(
168
- {
169
- materialId: resolved?.memberId ?? inp.id,
170
- kind: resolved?.kind ?? inp.kind,
171
- amount: childAmount,
172
- depth: depth + 1,
173
- tagId: inp.tagId,
174
- consumeChance: inp.chance,
175
- path: path ? `${path}/${base.children.length}` : undefined,
176
- },
177
- ctx,
178
- ),
179
- );
180
- }
181
- ctx.used.delete(recipe.recipeId);
182
- ctx.usedMaterials.delete(matKey);
183
-
184
- poolAdd(ctx.pool, kind, materialId, produced - need);
185
- for (const out of recipe.outputs) {
186
- if (out.id !== materialId || out.kind !== kind) {
187
- poolAdd(
188
- ctx.pool,
189
- out.kind,
190
- out.id,
191
- out.amount * batches * (out.chance ?? 1),
192
- );
193
- }
194
- }
195
- return base;
196
- }
@@ -1,42 +0,0 @@
1
- import type {
2
- CalcKind,
3
- CalcNode,
4
- CalcMaterial,
5
- CalcRecipe,
6
- CalcRecipeSummary,
7
- } from '@terrafirmagreg-team/recipe/types/calc';
8
-
9
- export type RemainderPool = Map<string, number>;
10
-
11
- export type BuildContext = {
12
- recipeMap: Map<string, CalcRecipe>;
13
- getRecipesForItem: (
14
- materialId: string,
15
- kind: CalcKind,
16
- ) => CalcRecipeSummary[];
17
- selections: Record<string, string>;
18
- tagSelections: Record<string, string>;
19
- tagMembers: (tagId: string) => { members: string[]; kind: CalcKind } | undefined;
20
- pool: RemainderPool;
21
- used: Set<string>;
22
-
23
- usedMaterials: Set<string>;
24
-
25
- collapsed?: Set<string>;
26
-
27
- autoRoots?: readonly string[];
28
-
29
- autoBudget?: { left: number };
30
-
31
- recipesResolved?: (materialId: string) => boolean;
32
-
33
- deadIds?: ReadonlySet<string>;
34
- };
35
-
36
- export const AUTO_MAX_DEPTH = 32;
37
- export const AUTO_NODE_BUDGET = 800;
38
-
39
- export type CalcGoal = {
40
- node: CalcNode;
41
- remainders: CalcMaterial[];
42
- };
@@ -1,64 +0,0 @@
1
- import type {
2
- CalcKind,
3
- CalcMaterial,
4
- CalcNode,
5
- CalculatorTarget,
6
- } from '@terrafirmagreg-team/recipe/types/calc';
7
- import { buildTree } from './build';
8
- import type { BuildContext, CalcGoal, RemainderPool } from './context';
9
- import { deduplicateMaterials, mergeAmounts } from './materials';
10
-
11
- export function buildGoal(
12
- target: CalculatorTarget,
13
- ctx: Omit<BuildContext, 'pool' | 'used' | 'usedMaterials'>,
14
- path?: string,
15
- inventory?: readonly CalcMaterial[],
16
- ): CalcGoal {
17
-
18
- const pool: RemainderPool = new Map();
19
- for (const row of inventory ?? []) {
20
- if (row.amount > 0) {
21
- const key = `${row.kind}:${row.id}`;
22
- pool.set(key, (pool.get(key) ?? 0) + row.amount);
23
- }
24
- }
25
- const node = buildTree(
26
- {
27
- materialId: target.id,
28
- kind: target.kind,
29
- amount: target.amount,
30
- depth: 0,
31
- path,
32
- },
33
- { ...ctx, pool, used: new Set(), usedMaterials: new Set() },
34
- );
35
- const remainders: CalcMaterial[] = [...pool.entries()].map(([key, amount]) => {
36
- const sep = key.indexOf(':');
37
- return { kind: key.slice(0, sep) as CalcKind, id: key.slice(sep + 1), amount };
38
- });
39
- return { node, remainders };
40
- }
41
-
42
- export function flattenTree(node: CalcNode): {
43
- cost: CalcMaterial[];
44
- catalysts: CalcMaterial[];
45
- } {
46
- const cost: CalcMaterial[] = [];
47
- const catalysts: CalcMaterial[] = [];
48
-
49
- function recurse(n: CalcNode) {
50
- if (n.catalyst) {
51
- catalysts.push({ id: n.materialId, kind: n.kind, amount: n.amount });
52
- return;
53
- }
54
- if (n.need <= 0) return;
55
- if (n.recipe === null || n.cycle) {
56
- cost.push({ id: n.materialId, kind: n.kind, amount: n.need });
57
- return;
58
- }
59
- for (const child of n.children) recurse(child);
60
- }
61
-
62
- recurse(node);
63
- return { cost: mergeAmounts(cost), catalysts: deduplicateMaterials(catalysts) };
64
- }
@@ -1,36 +0,0 @@
1
- import type { CalcNode } from '@terrafirmagreg-team/recipe/types/calc';
2
- import { recipeDivisor } from './recipe';
3
-
4
- const SCALE = 1000;
5
- const CAP = 1_000_000;
6
-
7
- function gcd(a: number, b: number): number {
8
- a = Math.round(Math.abs(a));
9
- b = Math.round(Math.abs(b));
10
- while (b) {
11
- const t = a % b;
12
- a = b;
13
- b = t;
14
- }
15
- return a;
16
- }
17
-
18
- export function idealBatchMultiplier(node: CalcNode): number {
19
- let acc = 1;
20
- const walk = (n: CalcNode): void => {
21
- if (!Number.isFinite(acc)) return;
22
- if (n.recipe && n.need > 0 && !n.cycle && n.batches > 0) {
23
- const divisor = recipeDivisor(n.recipe, n.materialId, n.kind);
24
- const mod =
25
- (divisor * SCALE) / gcd(divisor * SCALE, n.need * SCALE);
26
- acc = (acc * mod) / gcd(acc, mod);
27
- if (!Number.isFinite(acc) || acc > CAP) {
28
- acc = Infinity;
29
- return;
30
- }
31
- }
32
- for (const child of n.children) walk(child);
33
- };
34
- walk(node);
35
- return acc;
36
- }
@@ -1,68 +0,0 @@
1
- import type { CalcRecipeSummary } from '@terrafirmagreg-team/recipe/types/calc';
2
- import type { BuildContext } from './context';
3
-
4
- export type AutoPickResult = {
5
-
6
- pick?: string;
7
-
8
- wantRecipes: string[];
9
-
10
- wantOut: string[];
11
- };
12
-
13
- const TIER_WEIGHT = 2;
14
-
15
- export function calcAutoPick(
16
- rows: CalcRecipeSummary[],
17
- materialId: string,
18
- ancestors: ReadonlySet<string>,
19
- ctx: Pick<
20
- BuildContext,
21
- 'recipeMap' | 'getRecipesForItem' | 'recipesResolved' | 'deadIds'
22
- >,
23
- ): AutoPickResult {
24
- const alive = rows.filter((row) => !ctx.deadIds?.has(row.recipeId));
25
-
26
- const crafted = alive.filter((row) => !/(^|_)loot(_|$)/.test(row.category));
27
- const pool = crafted.length ? crafted : alive;
28
- const isCycle = (input: string) =>
29
- input === materialId ||
30
- ancestors.has(`item:${input}`) ||
31
- ancestors.has(`fluid:${input}`);
32
- const wantRecipes: string[] = [];
33
- const wantOut = new Set<string>();
34
- for (const row of pool) {
35
- if (!ctx.recipeMap.has(row.recipeId)) wantRecipes.push(row.recipeId);
36
- for (const input of row.row.in) {
37
-
38
- if (input.startsWith('#') || isCycle(input)) continue;
39
- if (!(ctx.recipesResolved?.(input) ?? true)) wantOut.add(input);
40
- }
41
- }
42
- if (wantRecipes.length || wantOut.size) {
43
- return { wantRecipes, wantOut: [...wantOut] };
44
- }
45
- const inputGuess = (input: string): number => {
46
- if (isCycle(input)) return Infinity;
47
- if (input.startsWith('#')) return 1;
48
- return ctx.getRecipesForItem(input, 'item').length ? 1 : 0;
49
- };
50
- let best: CalcRecipeSummary | undefined;
51
- let bestCost = Infinity;
52
- for (const row of pool) {
53
- let cost = 1 + TIER_WEIGHT * (ctx.recipeMap.get(row.recipeId)?.tier ?? 0);
54
- for (const input of new Set(row.row.in)) {
55
- cost += inputGuess(input);
56
- if (cost === Infinity) break;
57
- }
58
- if (cost < bestCost) {
59
- best = row;
60
- bestCost = cost;
61
- }
62
- }
63
- return {
64
- pick: best?.recipeId ?? pool[0]?.recipeId ?? rows[0]?.recipeId,
65
- wantRecipes,
66
- wantOut: [],
67
- };
68
- }