@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,20 @@
1
+ export {
2
+ migrateCalcState,
3
+ newCalcEntry,
4
+ type CalcTreeEntry,
5
+ } from './entry';
6
+ export { calcStorageKey } from './keys';
7
+ export {
8
+ readCalcActive,
9
+ readCalcHistory,
10
+ resolveCalcActive,
11
+ readCalcStorage,
12
+ readCalcTabs,
13
+ removeCalcHistory,
14
+ subscribeCalcStorage,
15
+ updateCalcTab,
16
+ writeCalcActive,
17
+ writeCalcStorage,
18
+ writeCalcTabs,
19
+ } from './store';
20
+
@@ -0,0 +1,38 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ export const CALC_STATE_EVENT = 'tfg:calc-state';
3
+ const TAB_KEY = 'tfg:calc-tab';
4
+ const ACTIVE_PREFIX = 'calc:active:';
5
+ const HIST_PREFIX = 'calc:hist:';
6
+
7
+ let tabId: string | null = null;
8
+
9
+ function calcTabId(): string {
10
+ if (tabId) return tabId;
11
+ try {
12
+ tabId = window.sessionStorage.getItem(TAB_KEY);
13
+ if (!tabId) {
14
+ tabId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
15
+ window.sessionStorage.setItem(TAB_KEY, tabId);
16
+ }
17
+ } catch {
18
+ tabId ??= `tab-${Math.random().toString(36).slice(2, 8)}`;
19
+ }
20
+ return tabId;
21
+ }
22
+
23
+ export function calcStorageKey(section: string, dataset: DatasetRef): string {
24
+ void calcTabId();
25
+ return `calc:tabs:${section}:${dataset.id}/${dataset.version ?? ''}`;
26
+ }
27
+
28
+ export function activeKey(key: string): string {
29
+ return `${ACTIVE_PREFIX}${key}@${calcTabId()}`;
30
+ }
31
+
32
+ export function historyKey(key: string): string {
33
+ return `${HIST_PREFIX}${key}`;
34
+ }
35
+
36
+ export const emitCalcStorage = () =>
37
+ window.dispatchEvent(new Event(CALC_STATE_EVENT));
38
+
@@ -0,0 +1,198 @@
1
+ import type { CalculatorState } from '@terrafirmagreg-team/recipe/types/calc';
2
+ import {
3
+ capHistory,
4
+ EMPTY_ENTRIES,
5
+ isCalcHistory,
6
+ isCalcTabs,
7
+ migrateCalcState,
8
+ newCalcEntry,
9
+ type CalcTreeEntry,
10
+ } from './entry';
11
+ import {
12
+ activeKey,
13
+ CALC_STATE_EVENT,
14
+ emitCalcStorage,
15
+ historyKey,
16
+ } from './keys';
17
+
18
+ function readJSON<T>(raw: string | null, guard: (v: unknown) => v is T): T | null {
19
+ if (raw === null) return null;
20
+ try {
21
+ const value = JSON.parse(raw) as unknown;
22
+ return guard(value) ? value : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ const snapshots = new Map<string, { raw: string | null; value: unknown }>();
29
+
30
+ function snap<T>(
31
+ storeKey: string,
32
+ raw: string | null,
33
+ guard: (v: unknown) => v is T,
34
+ normalize?: (value: T) => void,
35
+ ): T | null {
36
+ const hit = snapshots.get(storeKey);
37
+ if (hit && hit.raw === raw) return hit.value as T | null;
38
+ const value = readJSON(raw, guard);
39
+ if (value) normalize?.(value);
40
+ snapshots.set(storeKey, { raw, value });
41
+ return value;
42
+ }
43
+
44
+ const migrateEntries = (entries: CalcTreeEntry[]): void => {
45
+ for (const entry of entries) {
46
+ entry.state = migrateCalcState(entry.state);
47
+ }
48
+ };
49
+
50
+ export function readCalcTabs(key: string): CalcTreeEntry[] {
51
+ if (typeof window === 'undefined') return [];
52
+ return (
53
+ snap(key, window.localStorage.getItem(key), isCalcTabs, migrateEntries) ??
54
+ EMPTY_ENTRIES
55
+ );
56
+ }
57
+
58
+ export function readCalcActive(key: string): string | null {
59
+ if (typeof window === 'undefined') return null;
60
+ try {
61
+ return window.sessionStorage.getItem(activeKey(key)) || null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ export function readCalcHistory(key: string): CalcTreeEntry[] {
68
+ if (typeof window === 'undefined') return [];
69
+ const hKey = historyKey(key);
70
+ return (
71
+ snap(hKey, window.localStorage.getItem(hKey), isCalcHistory, migrateEntries) ??
72
+ EMPTY_ENTRIES
73
+ );
74
+ }
75
+
76
+ function writeTabsRaw(key: string, tabs: CalcTreeEntry[]): void {
77
+ window.localStorage.setItem(key, JSON.stringify(tabs));
78
+ }
79
+
80
+ function writeHistoryRaw(key: string, entries: CalcTreeEntry[]): void {
81
+ window.localStorage.setItem(
82
+ historyKey(key),
83
+ JSON.stringify(capHistory(entries)),
84
+ );
85
+ }
86
+
87
+ function syncHistory(key: string, tabs: CalcTreeEntry[]): void {
88
+ const live = tabs.filter((tab) => tab.state.cards.length);
89
+ if (!live.length) return;
90
+ const prev = readCalcHistory(key);
91
+ const ids = new Set(live.map((tab) => tab.id));
92
+ writeHistoryRaw(key, [...live, ...prev.filter((e) => !ids.has(e.id))]);
93
+ }
94
+
95
+ export function writeCalcTabs(
96
+ key: string,
97
+ tabs: CalcTreeEntry[],
98
+ active: string | null,
99
+ ): void {
100
+ if (typeof window === 'undefined') return;
101
+ try {
102
+ writeTabsRaw(key, tabs);
103
+ syncHistory(key, tabs);
104
+ window.sessionStorage.setItem(activeKey(key), active ?? '');
105
+ } catch {
106
+ return;
107
+ }
108
+ emitCalcStorage();
109
+ }
110
+
111
+ export function writeCalcActive(key: string, id: string | null): void {
112
+ if (typeof window === 'undefined') return;
113
+ try {
114
+ window.sessionStorage.setItem(activeKey(key), id ?? '');
115
+ } catch {
116
+ return;
117
+ }
118
+ emitCalcStorage();
119
+ }
120
+
121
+ export function updateCalcTab(
122
+ key: string,
123
+ id: string,
124
+ update: (entry: CalcTreeEntry) => CalcTreeEntry,
125
+ ): void {
126
+ if (typeof window === 'undefined') return;
127
+ const tabs = readCalcTabs(key);
128
+ const index = tabs.findIndex((tab) => tab.id === id);
129
+ if (index < 0) return;
130
+ const next = tabs.slice();
131
+ next[index] = update(next[index]!);
132
+ try {
133
+ writeTabsRaw(key, next);
134
+ syncHistory(key, next);
135
+ } catch {
136
+ return;
137
+ }
138
+ emitCalcStorage();
139
+ }
140
+
141
+ export function removeCalcHistory(key: string, id: string): void {
142
+ if (typeof window === 'undefined') return;
143
+ try {
144
+ writeHistoryRaw(
145
+ key,
146
+ readCalcHistory(key).filter((entry) => entry.id !== id),
147
+ );
148
+ const tabs = readCalcTabs(key).filter((tab) => tab.id !== id);
149
+ writeTabsRaw(key, tabs);
150
+ if (readCalcActive(key) === id) {
151
+ window.sessionStorage.setItem(activeKey(key), '');
152
+ }
153
+ } catch {
154
+ return;
155
+ }
156
+ emitCalcStorage();
157
+ }
158
+
159
+ export function resolveCalcActive(
160
+ tabs: readonly CalcTreeEntry[],
161
+ raw: string | null,
162
+ ): string | null {
163
+ return (tabs.find((tab) => tab.id === raw) ?? tabs[0])?.id ?? null;
164
+ }
165
+
166
+ export function readCalcStorage(key: string): CalculatorState | null {
167
+ const tabs = readCalcTabs(key);
168
+ const active = resolveCalcActive(tabs, readCalcActive(key));
169
+ return tabs.find((tab) => tab.id === active)?.state ?? null;
170
+ }
171
+
172
+ export function writeCalcStorage(
173
+ key: string,
174
+ state: CalculatorState,
175
+ ): void {
176
+ const tabs = readCalcTabs(key);
177
+ const active = resolveCalcActive(tabs, readCalcActive(key));
178
+ const index = tabs.findIndex((tab) => tab.id === active);
179
+ if (index >= 0) {
180
+ const next = tabs.slice();
181
+ next[index] = { ...next[index]!, savedAt: Date.now(), state };
182
+ writeCalcTabs(key, next, active);
183
+ return;
184
+ }
185
+ const entry = newCalcEntry(state);
186
+ writeCalcTabs(key, [...tabs, entry], entry.id);
187
+ }
188
+
189
+ export function subscribeCalcStorage(listener: () => void): () => void {
190
+ if (typeof window === 'undefined') return () => {};
191
+ window.addEventListener(CALC_STATE_EVENT, listener);
192
+ window.addEventListener('storage', listener);
193
+ return () => {
194
+ window.removeEventListener(CALC_STATE_EVENT, listener);
195
+ window.removeEventListener('storage', listener);
196
+ };
197
+ }
198
+
@@ -0,0 +1,4 @@
1
+ export { mergeAmounts } from './materials';
2
+ export { recipeDivisor, toCalcRecipe } from './recipe';
3
+ export { decodeCalcState, encodeCalcState } from './share';
4
+
@@ -0,0 +1,29 @@
1
+ import type { CalcKind, CalcMaterial } from '@terrafirmagreg-team/recipe/types/calc';
2
+
3
+ export function materialKey(kind: CalcKind, id: string): string {
4
+ return `${kind}:${id}`;
5
+ }
6
+
7
+ export function mergeAmounts(list: CalcMaterial[]): CalcMaterial[] {
8
+ const map = new Map<string, CalcMaterial>();
9
+ for (const row of list) {
10
+ const key = materialKey(row.kind, row.id);
11
+ const existing = map.get(key);
12
+ if (existing) existing.amount += row.amount;
13
+ else map.set(key, { ...row });
14
+ }
15
+ return [...map.values()];
16
+ }
17
+
18
+ export function deduplicateMaterials(list: CalcMaterial[]): CalcMaterial[] {
19
+ const seen = new Set<string>();
20
+ const result: CalcMaterial[] = [];
21
+ for (const row of list) {
22
+ const key = materialKey(row.kind, row.id);
23
+ if (seen.has(key)) continue;
24
+ seen.add(key);
25
+ result.push({ ...row });
26
+ }
27
+ return result;
28
+ }
29
+
@@ -0,0 +1,104 @@
1
+ import type { Recipe, RecipeIngredient } from '@terrafirmagreg-team/recipe/lib/data/dto';
2
+ import type {
3
+ CalcKind,
4
+ CalcMaterial,
5
+ CalcRecipe,
6
+ CalcRecipeInput,
7
+ } from '@terrafirmagreg-team/recipe/types/calc';
8
+
9
+ function slotKind(slot: { kind: RecipeIngredient['kind'] }): CalcKind {
10
+ return slot.kind === 'fluid' ? 'fluid' : 'item';
11
+ }
12
+
13
+ const GT_TIERS: Record<string, number> = {
14
+ ULV: 0,
15
+ LV: 1,
16
+ MV: 2,
17
+ HV: 3,
18
+ EV: 4,
19
+ IV: 5,
20
+ LuV: 6,
21
+ ZPM: 7,
22
+ UV: 8,
23
+ UHV: 9,
24
+ UEV: 10,
25
+ UIV: 11,
26
+ UXV: 12,
27
+ OpV: 13,
28
+ MAX: 14,
29
+ };
30
+
31
+ function recipeTier(recipe: Recipe): number | undefined {
32
+ for (const widget of recipe.widgets) {
33
+ if (widget.type !== 'text' || !widget.component) continue;
34
+ for (const part of [
35
+ widget.component,
36
+ ...(widget.component.extra ?? []),
37
+ ]) {
38
+ if (part.translate !== 'gtceu.recipe.eu') continue;
39
+ for (const arg of part.with ?? []) {
40
+ if (arg.text && arg.text in GT_TIERS) return GT_TIERS[arg.text];
41
+ }
42
+ }
43
+ }
44
+ return undefined;
45
+ }
46
+
47
+ export function toCalcRecipe(recipe: Recipe): CalcRecipe {
48
+ const inputs: CalcRecipeInput[] = recipe.inputs.map((slot) => ({
49
+ id: slot.id,
50
+ kind: slotKind(slot),
51
+ amount: slot.count && slot.count > 0 ? slot.count : 1,
52
+ catalyst: false,
53
+ ...(slot.chance !== undefined ? { chance: slot.chance } : {}),
54
+ ...(slot.kind === 'tag' ? { tagId: slot.id } : {}),
55
+ }));
56
+ for (const widget of recipe.widgets) {
57
+ if (widget.type !== 'slot' || widget.role !== 'catalyst') continue;
58
+ inputs.push({
59
+ id: widget.id,
60
+ kind: widget.kind === 'fluid' ? 'fluid' : 'item',
61
+ amount: 1,
62
+ catalyst: true,
63
+ ...(widget.chance !== undefined ? { chance: widget.chance } : {}),
64
+ ...(widget.kind === 'tag' ? { tagId: widget.id } : {}),
65
+ });
66
+ }
67
+ const merged: CalcRecipeInput[] = [];
68
+ for (const input of inputs) {
69
+ const key = `${input.kind}:${input.id}:${input.catalyst}:${input.tagId ?? ''}:${input.chance ?? ''}`;
70
+ const existing = merged.find(
71
+ (row) =>
72
+ `${row.kind}:${row.id}:${row.catalyst}:${row.tagId ?? ''}:${row.chance ?? ''}` ===
73
+ key,
74
+ );
75
+ if (existing) existing.amount += input.amount;
76
+ else merged.push({ ...input });
77
+ }
78
+ const outputs: CalcMaterial[] = recipe.outputs.map((slot) => ({
79
+ id: slot.id,
80
+ kind: slotKind(slot),
81
+ amount: slot.count && slot.count > 0 ? slot.count : 1,
82
+ ...(slot.chance !== undefined ? { chance: slot.chance } : {}),
83
+ }));
84
+ const tier = recipeTier(recipe);
85
+ return {
86
+ recipeId: recipe.id,
87
+ category: recipe.category ?? recipe.type,
88
+ inputs: merged,
89
+ outputs,
90
+ ...(tier !== undefined ? { tier } : {}),
91
+ };
92
+ }
93
+
94
+ export function recipeDivisor(
95
+ recipe: CalcRecipe,
96
+ materialId: string,
97
+ kind: CalcKind,
98
+ ): number {
99
+ const amount = recipe.outputs
100
+ .filter((out) => out.id === materialId && out.kind === kind)
101
+ .reduce((sum, out) => sum + out.amount * (out.chance ?? 1), 0);
102
+ return amount > 0 ? amount : 1;
103
+ }
104
+
@@ -0,0 +1,135 @@
1
+ import { deflateSync, inflateSync, strFromU8, strToU8 } from 'fflate';
2
+ import type {
3
+ CalcCard,
4
+ CalculatorState,
5
+ CalcWire,
6
+ } from '@terrafirmagreg-team/recipe/types/calc';
7
+
8
+ function encodeUrlSafe(base64: string): string {
9
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
10
+ }
11
+
12
+ function decodeUrlSafe(encoded: string): string {
13
+ let result = encoded;
14
+ const pad = result.length % 4;
15
+ if (pad) result += '='.repeat(4 - pad);
16
+ return result.replace(/-/g, '+').replace(/_/g, '/');
17
+ }
18
+
19
+ function u8ToBinary(u8: Uint8Array): string {
20
+ let bin = '';
21
+ for (let i = 0; i < u8.length; i += 8192) {
22
+ bin += String.fromCharCode(...u8.subarray(i, i + 8192));
23
+ }
24
+ return bin;
25
+ }
26
+
27
+ export function encodeCalcState(state: CalculatorState): string {
28
+ const dict = new Map<string, number>();
29
+ const ref = (id: string): number => {
30
+ const hit = dict.get(id);
31
+ if (hit !== undefined) return hit;
32
+ const idx = dict.size;
33
+ dict.set(id, idx);
34
+ return idx;
35
+ };
36
+ const indexOf = new Map<string, number>();
37
+ const cards = state.cards.map((card, index) => {
38
+ indexOf.set(card.id, index);
39
+ const row = [
40
+ ref(card.materialId),
41
+ card.kind === 'fluid' ? 1 : 0,
42
+ card.recipeId ? ref(card.recipeId) : -1,
43
+ card.crafts,
44
+ Math.round(card.pos?.x ?? 0),
45
+ Math.round(card.pos?.y ?? 0),
46
+ card.pos ? 1 : 0,
47
+ ];
48
+ return row;
49
+ });
50
+ const wires = state.wires.flatMap((wire) => {
51
+ const from = indexOf.get(wire.from);
52
+ const to = indexOf.get(wire.to);
53
+ return from === undefined || to === undefined
54
+ ? []
55
+ : [[from, to, wire.input, wire.out ?? 0, wire.amount ?? 0]];
56
+ });
57
+ const tagSelections: Record<string, number> = {};
58
+ for (const [tagId, memberId] of Object.entries(state.tagSelections)) {
59
+ if (memberId) tagSelections[String(ref(tagId))] = ref(memberId);
60
+ }
61
+ const payload = {
62
+ v: 2,
63
+ d: [...dict.keys()],
64
+ c: cards,
65
+ w: wires,
66
+ g: tagSelections,
67
+ };
68
+ const packed = deflateSync(strToU8(JSON.stringify(payload)));
69
+ return `x${encodeUrlSafe(btoa(u8ToBinary(packed)))}`;
70
+ }
71
+
72
+ export function decodeCalcState(encoded: string): CalculatorState | null {
73
+ const text = encoded.trim();
74
+ if (!text.startsWith('x')) return null;
75
+ try {
76
+ const bin = atob(decodeUrlSafe(text.slice(1)));
77
+ const packed = Uint8Array.from(bin, (c) => c.charCodeAt(0));
78
+ const payload = JSON.parse(strFromU8(inflateSync(packed))) as {
79
+ v?: number;
80
+ d?: string[];
81
+ c?: number[][];
82
+ w?: number[][];
83
+ g?: Record<string, number>;
84
+ };
85
+ if (payload.v !== 2 || !Array.isArray(payload.d)) return null;
86
+ const dict = payload.d;
87
+ const id = (idx: unknown): string | null =>
88
+ typeof idx === 'number' && idx >= 0 && idx < dict.length
89
+ ? dict[idx]!
90
+ : null;
91
+ const cards: CalcCard[] = [];
92
+ for (const [index, row] of (payload.c ?? []).entries()) {
93
+ if (!Array.isArray(row)) continue;
94
+ const materialId = id(row[0]);
95
+ if (!materialId) continue;
96
+ const kind = row[1] === 1 ? 'fluid' : 'item';
97
+ const recipeId = row[2] === -1 ? null : id(row[2]);
98
+ cards.push({
99
+ id: `c${index}`,
100
+ materialId,
101
+ kind,
102
+ recipeId: recipeId ?? null,
103
+ crafts: Number(row[3]) || 1,
104
+ pos:
105
+ row[6] === 1
106
+ ? { x: Number(row[4]) || 0, y: Number(row[5]) || 0 }
107
+ : undefined,
108
+ });
109
+ }
110
+ const wires: CalcWire[] = [];
111
+ for (const row of payload.w ?? []) {
112
+ if (!Array.isArray(row)) continue;
113
+ const from = cards[row[0]!];
114
+ const to = cards[row[1]!];
115
+ if (!from || !to) continue;
116
+ wires.push({
117
+ from: from.id,
118
+ to: to.id,
119
+ input: Number(row[2]) || 0,
120
+ out: Number(row[3]) || undefined,
121
+ amount: Number(row[4]) || undefined,
122
+ });
123
+ }
124
+ const tagSelections: Record<string, string> = {};
125
+ for (const [tagIdx, memberIdx] of Object.entries(payload.g ?? {})) {
126
+ const tagId = id(Number(tagIdx));
127
+ const memberId = id(memberIdx);
128
+ if (tagId && memberId) tagSelections[tagId] = memberId;
129
+ }
130
+ return { cards, wires, tagSelections };
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
@@ -0,0 +1,25 @@
1
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
+ import type { RecipeCompactIndex } from '@terrafirmagreg-team/recipe/types';
3
+ import type { FlushHandler, ResolveInput } from './protocol';
4
+ import { runResolve } from './resolve';
5
+ import { resetWorkerState, state } from './state';
6
+
7
+ export const api = {
8
+ async init(
9
+ nextDataset: DatasetRef,
10
+ nextCatalog: RecipeCompactIndex,
11
+ flushHandler: FlushHandler,
12
+ ) {
13
+ resetWorkerState(nextDataset, nextCatalog);
14
+ state.onFlush = flushHandler;
15
+ },
16
+ async resolve(input: ResolveInput) {
17
+ state.resolveGen += 1;
18
+ await runResolve(input);
19
+ },
20
+ async invalidate() {
21
+ state.resolveGen += 1;
22
+ state.epoch += 1;
23
+ },
24
+ };
25
+
@@ -0,0 +1,66 @@
1
+ import type { CalcKind, CalcRecipe, CalcRecipeSummary } from '@terrafirmagreg-team/recipe/types/calc';
2
+ import { fetchSlices, summarizeRecipes } from '@terrafirmagreg-team/recipe/lib/calc/resolve';
3
+ import type { Pending } from './flush';
4
+ import { state } from './state';
5
+
6
+ export function createWorkerSummaries(
7
+ localRecipes: ReadonlyMap<string, CalcRecipe>,
8
+ ) {
9
+ const cache = new Map<string, CalcRecipeSummary[]>();
10
+ return {
11
+ get: (materialId: string, kind: CalcKind): CalcRecipeSummary[] =>
12
+ summarizeRecipes(
13
+ state.outMap,
14
+ localRecipes,
15
+ state.catalog?.types ?? [],
16
+ cache,
17
+ materialId,
18
+ kind,
19
+ ),
20
+ clear: () => cache.clear(),
21
+ };
22
+ }
23
+
24
+ export async function fetchWanted(
25
+ pending: Pending,
26
+ wanted: Set<string>,
27
+ wantedOut: Set<string>,
28
+ wantedTag: Set<string>,
29
+ localRecipes: Map<string, CalcRecipe>,
30
+ ): Promise<boolean> {
31
+ if (!state.catalog) return false;
32
+ const slices = await fetchSlices(state.dataset, state.catalog, {
33
+ recipes: wanted,
34
+ outs: wantedOut,
35
+ tags: wantedTag,
36
+ });
37
+ let progressed = false;
38
+ for (const id of slices.dead) {
39
+ if (state.deadIds.has(id)) continue;
40
+ state.deadIds.add(id);
41
+ pending.dead.add(id);
42
+ pending.dirty = true;
43
+ }
44
+ for (const [id, entry] of slices.recipes) {
45
+ localRecipes.set(id, entry.calc);
46
+ state.bodyMap.set(id, entry.body);
47
+ pending.recipes.set(id, entry.calc);
48
+ pending.bodies.set(id, entry.body);
49
+ pending.dirty = true;
50
+ progressed = true;
51
+ }
52
+ for (const [id, list] of slices.outs) {
53
+ state.outMap.set(id, list);
54
+ pending.out.set(id, list);
55
+ pending.dirty = true;
56
+ progressed = true;
57
+ }
58
+ for (const [id, resolution] of slices.tags) {
59
+ state.tagMap.set(id, resolution);
60
+ pending.tag.set(id, resolution);
61
+ pending.dirty = true;
62
+ progressed = true;
63
+ }
64
+ return progressed;
65
+ }
66
+
@@ -0,0 +1,39 @@
1
+ import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
2
+ import type { RecipeCompactRow } from '@terrafirmagreg-team/recipe/types';
3
+ import type {
4
+ CalcRecipe,
5
+ TagResolution,
6
+ } from '@terrafirmagreg-team/recipe/types/calc';
7
+ import { state } from './state';
8
+
9
+ export function createFlush() {
10
+ return {
11
+ out: new Map<string, RecipeCompactRow[]>(),
12
+ tag: new Map<string, TagResolution>(),
13
+ recipes: new Map<string, CalcRecipe>(),
14
+ bodies: new Map<string, Recipe>(),
15
+ dead: new Set<string>(),
16
+ dirty: false,
17
+ };
18
+ }
19
+
20
+ export type Pending = ReturnType<typeof createFlush>;
21
+
22
+ export function postFlush(pending: Pending) {
23
+ if (!pending.dirty) return;
24
+ pending.dirty = false;
25
+ state.onFlush({
26
+ epoch: state.epoch,
27
+ out: [...pending.out],
28
+ tag: [...pending.tag],
29
+ recipes: [...pending.recipes],
30
+ bodies: [...pending.bodies],
31
+ dead: [...pending.dead],
32
+ });
33
+ pending.out.clear();
34
+ pending.tag.clear();
35
+ pending.recipes.clear();
36
+ pending.bodies.clear();
37
+ pending.dead.clear();
38
+ }
39
+
@@ -0,0 +1,8 @@
1
+ import { expose } from 'comlink';
2
+ import { api } from './api';
3
+
4
+ export type CalcWorkerApi = typeof api;
5
+ export type { CalcWorkerFlush } from './protocol';
6
+
7
+ expose(api);
8
+