@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,204 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import {
5
+ startTransition,
6
+ useCallback,
7
+ useEffect,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+ import { proxy, wrap, type Remote } from 'comlink';
12
+ import type {
13
+ CalcWorkerApi,
14
+ CalcWorkerFlush,
15
+ } from '@terrafirmagreg-team/recipe/lib/calc/worker';
16
+ import { api as localCalcApi } from '@terrafirmagreg-team/recipe/lib/calc/worker/api';
17
+ import type { RecipeCompactIndex } from '@terrafirmagreg-team/recipe/types';
18
+ import type { CalcMapsSlice } from './use-maps';
19
+
20
+ type CalcWorkerHandle = {
21
+ worker: Worker | null;
22
+ api: Remote<CalcWorkerApi>;
23
+ inFlight: number;
24
+ rejecters: Set<(error: Error) => void>;
25
+ };
26
+
27
+ export function useCalcWorker({
28
+ dataset,
29
+ catalog,
30
+ activeTabId,
31
+ applyMaps,
32
+ resetMaps,
33
+ }: {
34
+ dataset: DatasetRef;
35
+ catalog: RecipeCompactIndex | null;
36
+ activeTabId: string | null;
37
+ applyMaps: (src: CalcMapsSlice, extra?: () => void) => void;
38
+ resetMaps: () => void;
39
+ }) {
40
+ const workerRef = useRef<CalcWorkerHandle | null>(null);
41
+ const [workerState, setWorkerState] = useState<'pending' | 'ready'>(
42
+ 'pending',
43
+ );
44
+ const [generation, setGeneration] = useState(0);
45
+ const [workerSettled, setWorkerSettled] = useState(false);
46
+ const resolvedKeyRef = useRef<string | null>(null);
47
+
48
+ const minFlushEpochRef = useRef(0);
49
+ const [prevTab, setPrevTab] = useState(activeTabId);
50
+ if (prevTab !== activeTabId) {
51
+ setPrevTab(activeTabId);
52
+ setWorkerSettled(false);
53
+ }
54
+ useEffect(() => {
55
+ resolvedKeyRef.current = null;
56
+ minFlushEpochRef.current += 1;
57
+ const handle = workerRef.current;
58
+ if (handle) void handle.api.invalidate().catch(() => {});
59
+ }, [activeTabId]);
60
+
61
+ const applyWorkerFlush = useCallback(
62
+ (msg: CalcWorkerFlush) => {
63
+ if (msg.epoch < minFlushEpochRef.current) return;
64
+ applyMaps({
65
+ out: new Map(msg.out),
66
+ tag: new Map(msg.tag),
67
+ recipes: new Map(msg.recipes),
68
+ bodies: new Map(msg.bodies),
69
+ dead: new Set(msg.dead),
70
+ });
71
+ },
72
+ [applyMaps],
73
+ );
74
+
75
+ const bufferedFlush = useRef<CalcWorkerFlush | null>(null);
76
+ const flushTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
77
+ const drainFlush = useCallback(() => {
78
+ flushTimer.current = null;
79
+ const msg = bufferedFlush.current;
80
+ bufferedFlush.current = null;
81
+ if (msg) applyWorkerFlush(msg);
82
+ }, [applyWorkerFlush]);
83
+ const scheduleWorkerFlush = useCallback(
84
+ (msg: CalcWorkerFlush) => {
85
+ const prev = bufferedFlush.current;
86
+ bufferedFlush.current = prev
87
+ ? {
88
+ out: [...prev.out, ...msg.out],
89
+ tag: [...prev.tag, ...msg.tag],
90
+ recipes: [...prev.recipes, ...msg.recipes],
91
+ bodies: [...prev.bodies, ...msg.bodies],
92
+ dead: [...prev.dead, ...msg.dead],
93
+ epoch: Math.max(prev.epoch, msg.epoch),
94
+ }
95
+ : msg;
96
+ if (flushTimer.current) return;
97
+ flushTimer.current = setTimeout(drainFlush, 350);
98
+ },
99
+ [drainFlush],
100
+ );
101
+
102
+ useEffect(() => {
103
+ if (!catalog) return;
104
+ const handle: CalcWorkerHandle = {
105
+ worker: null,
106
+ api: localCalcApi as Remote<CalcWorkerApi>,
107
+ inFlight: 0,
108
+ rejecters: new Set(),
109
+ };
110
+ const swapToLocal = () => {
111
+ handle.worker = null;
112
+ handle.api = localCalcApi as Remote<CalcWorkerApi>;
113
+ const error = new Error('calc worker failed');
114
+ for (const reject of handle.rejecters) reject(error);
115
+ handle.rejecters.clear();
116
+ bufferedFlush.current = null;
117
+ if (flushTimer.current) {
118
+ clearTimeout(flushTimer.current);
119
+ flushTimer.current = null;
120
+ }
121
+ resolvedKeyRef.current = null;
122
+ minFlushEpochRef.current = 0;
123
+ setGeneration((g) => g + 1);
124
+ void handle.api.init(dataset, catalog, scheduleWorkerFlush).catch(() => {});
125
+ };
126
+ if (typeof Worker !== 'undefined') {
127
+ try {
128
+ const worker = new Worker(
129
+ new URL('../../lib/calc/worker/index.ts', import.meta.url),
130
+ { type: 'module' },
131
+ );
132
+ worker.onerror = () => swapToLocal();
133
+ handle.worker = worker;
134
+ handle.api = wrap<CalcWorkerApi>(worker);
135
+ } catch {
136
+ }
137
+ }
138
+ handle.api
139
+ .init(
140
+ dataset,
141
+ catalog,
142
+ handle.worker ? proxy(scheduleWorkerFlush) : scheduleWorkerFlush,
143
+ )
144
+ .then(() => setWorkerState('ready'))
145
+ .catch(() => {});
146
+ workerRef.current = handle;
147
+ resolvedKeyRef.current = null;
148
+ minFlushEpochRef.current = 0;
149
+ resetMaps();
150
+ startTransition(() => setWorkerSettled(false));
151
+ return () => {
152
+ if (flushTimer.current) {
153
+ clearTimeout(flushTimer.current);
154
+ flushTimer.current = null;
155
+ }
156
+ bufferedFlush.current = null;
157
+ workerRef.current = null;
158
+ handle.worker?.terminate();
159
+ };
160
+ }, [dataset, catalog, scheduleWorkerFlush, resetMaps]);
161
+
162
+ const call = useCallback(
163
+ <T,>(fn: (api: Remote<CalcWorkerApi>) => Promise<T>): Promise<T> => {
164
+ const handle = workerRef.current;
165
+ if (!handle) {
166
+ return Promise.reject(new Error('calc worker unavailable'));
167
+ }
168
+ handle.inFlight += 1;
169
+ setWorkerSettled(false);
170
+ return new Promise<T>((resolve, reject) => {
171
+ handle.rejecters.add(reject);
172
+ fn(handle.api)
173
+ .then(resolve)
174
+ .catch(reject)
175
+ .finally(() => {
176
+ handle.rejecters.delete(reject);
177
+ handle.inFlight -= 1;
178
+ if (handle.inFlight === 0) {
179
+ if (flushTimer.current) {
180
+ clearTimeout(flushTimer.current);
181
+ flushTimer.current = null;
182
+ }
183
+ drainFlush();
184
+ setWorkerSettled(true);
185
+ }
186
+ });
187
+ });
188
+ },
189
+ [drainFlush],
190
+ );
191
+
192
+ return {
193
+ ref: workerRef,
194
+ state: workerState,
195
+ generation,
196
+ settled: workerSettled,
197
+ call,
198
+ isResolved: useCallback((key: string) => resolvedKeyRef.current === key, []),
199
+ markResolved: useCallback((key: string) => {
200
+ resolvedKeyRef.current = key;
201
+ }, []),
202
+ };
203
+ }
204
+
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+ import { useSyncExternalStore } from 'react';
4
+ import {
5
+ dumpLocation,
6
+ dumpPublicUrl,
7
+ subscribeDumpLocations,
8
+ } from '@terrafirmagreg-team/dump/lib/dump/paths';
9
+
10
+ export const ANVIL_GUI_REL = 'assets/tfc/textures/gui/anvil.webp';
11
+
12
+ export function useAnvilGui(dataset: DatasetRef): string | undefined {
13
+ useSyncExternalStore(subscribeDumpLocations, () => dumpLocation(dataset));
14
+ return dumpPublicUrl(dataset, ANVIL_GUI_REL);
15
+ }
16
+
@@ -0,0 +1,135 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import {
5
+ FORGE_ACTIONS,
6
+ forgeRulesMatch,
7
+ } from '@terrafirmagreg-team/recipe/lib/forge/actions';
8
+ import { solveForgeSteps } from '@terrafirmagreg-team/recipe/lib/forge/solve';
9
+ import type { ForgeRecipe, ForgeRuleAction } from '@terrafirmagreg-team/recipe/types/forge';
10
+
11
+ export type ForgeStep = {
12
+ id: string;
13
+ action: ForgeRuleAction;
14
+ delta: number;
15
+ work: number;
16
+ };
17
+
18
+ function lastActions(applied: readonly number[]): ForgeRuleAction[] {
19
+ return applied
20
+ .slice(-3)
21
+ .map((index) => FORGE_ACTIONS[index].action)
22
+ .reverse();
23
+ }
24
+
25
+ function parseWork(value: string): number | null {
26
+ const parsed = Number(value);
27
+ return Number.isInteger(parsed) && parsed >= 0 && parsed < 150
28
+ ? parsed
29
+ : null;
30
+ }
31
+
32
+ export function useForgeState(recipe: ForgeRecipe) {
33
+ const [startInput, setStartInput] = useState('0');
34
+ const [targetInput, setTargetInput] = useState('');
35
+ const [applied, setApplied] = useState<number[]>([]);
36
+ const [expanded, setExpanded] = useState(false);
37
+ const [failed, setFailed] = useState(false);
38
+ const [solving, setSolving] = useState(false);
39
+ const replayTimer = useRef<ReturnType<typeof setInterval> | null>(null);
40
+ const startWork = parseWork(startInput) ?? 0;
41
+ const target = targetInput === '' ? null : (parseWork(targetInput) ?? 0);
42
+
43
+ const { work, steps } = useMemo(() => {
44
+ let work = startWork;
45
+ const steps: ForgeStep[] = [];
46
+ for (const index of applied) {
47
+ const action = FORGE_ACTIONS[index];
48
+ work += action.delta;
49
+ steps.push({ ...action, work });
50
+ }
51
+ return { work, steps };
52
+ }, [startWork, applied]);
53
+ const last = lastActions(applied);
54
+ const finished = work === target && forgeRulesMatch(recipe.rules, last);
55
+
56
+ useEffect(
57
+ () => () => {
58
+ if (replayTimer.current) clearInterval(replayTimer.current);
59
+ },
60
+ [],
61
+ );
62
+
63
+ const push = (index: number) => {
64
+ const next = work + FORGE_ACTIONS[index].delta;
65
+ if (next < 0 || next >= 150 || solving) return;
66
+ setFailed(false);
67
+ setApplied((prev) => [...prev, index]);
68
+ };
69
+ const solve = () => {
70
+ if (solving) return;
71
+ const found = solveForgeSteps(
72
+ { work: startWork, last: [] },
73
+ target ?? 0,
74
+ recipe.rules,
75
+ );
76
+ if (!found) return setFailed(true);
77
+ setFailed(false);
78
+ setApplied([]);
79
+ setExpanded(false);
80
+ setSolving(true);
81
+ let i = 0;
82
+ replayTimer.current = setInterval(() => {
83
+ const step = found[i];
84
+ i += 1;
85
+ if (step === undefined) {
86
+ if (replayTimer.current) clearInterval(replayTimer.current);
87
+ replayTimer.current = null;
88
+ setSolving(false);
89
+ return;
90
+ }
91
+ setApplied((prev) => [...prev, step]);
92
+ }, 170);
93
+ };
94
+
95
+ const ruleMet = useMemo(() => {
96
+ const last = lastActions(applied);
97
+ return recipe.rules.map(
98
+ (rule) => applied.length > 0 && forgeRulesMatch([rule], last),
99
+ );
100
+ }, [recipe, applied]);
101
+
102
+ const setStart = (value: string | number) => {
103
+ setStartInput(String(value));
104
+ setApplied([]);
105
+ };
106
+ const setTarget = (value: string | number) => {
107
+ setTargetInput(String(value));
108
+ setFailed(false);
109
+ };
110
+
111
+ return {
112
+ startInput,
113
+ targetInput,
114
+ applied,
115
+ expanded,
116
+ failed,
117
+ solving,
118
+ target,
119
+ work,
120
+ steps,
121
+ last,
122
+ finished,
123
+ ruleMet,
124
+ push,
125
+ solve,
126
+ setStart,
127
+ setTarget,
128
+ setTargetInput,
129
+ setApplied,
130
+ setExpanded,
131
+ };
132
+ }
133
+
134
+ export type ForgeState = ReturnType<typeof useForgeState>;
135
+
@@ -0,0 +1,100 @@
1
+ 'use client';
2
+
3
+ import { useRef, useState } from 'react';
4
+
5
+ export const RULER = { numbersTop: 88, markerX: 13, trackBottom: 110 } as const;
6
+
7
+ export function useRuler({
8
+ panelRef,
9
+ scale,
10
+ work,
11
+ target,
12
+ solving,
13
+ onWork,
14
+ onTarget,
15
+ }: {
16
+ panelRef: React.RefObject<HTMLDivElement | null>;
17
+ scale: number;
18
+ work: number;
19
+ target: number | null;
20
+ solving: boolean;
21
+ onWork: (value: number) => void;
22
+ onTarget: (value: number) => void;
23
+ }) {
24
+ const [dragging, setDragging] = useState(false);
25
+ const dragKind = useRef<'work' | 'target' | null>(null);
26
+ const applyRuler = (value: number) => {
27
+ if (dragKind.current === 'work') {
28
+ onWork(value);
29
+ } else {
30
+ onTarget(value);
31
+ }
32
+ };
33
+ const rulerValue = (clientX: number): number => {
34
+ const panel = panelRef.current?.getBoundingClientRect();
35
+ if (!panel) return 0;
36
+ const atlasX = (clientX - panel.left) / scale;
37
+ return Math.max(0, Math.min(149, Math.round(atlasX - RULER.markerX)));
38
+ };
39
+ const beginDrag = (
40
+ event: React.PointerEvent<HTMLDivElement>,
41
+ kind: 'work' | 'target',
42
+ ) => {
43
+ if (solving) return;
44
+ dragKind.current = kind;
45
+ applyRuler(rulerValue(event.clientX));
46
+ try {
47
+ event.currentTarget.setPointerCapture(event.pointerId);
48
+ } catch {
49
+
50
+ }
51
+ setDragging(true);
52
+ };
53
+
54
+ const onRulerDown = (event: React.PointerEvent<HTMLDivElement>) => {
55
+ const value = rulerValue(event.clientX);
56
+ const nearWork = Math.abs(value - work) <= 4;
57
+ const nearTarget = target !== null && Math.abs(value - target) <= 4;
58
+ beginDrag(
59
+ event,
60
+ nearWork &&
61
+ (!nearTarget || Math.abs(value - work) <= Math.abs(value - target))
62
+ ? 'work'
63
+ : 'target',
64
+ );
65
+ };
66
+ const onRulerMove = (event: React.PointerEvent<HTMLDivElement>) => {
67
+ if (!dragKind.current) return;
68
+ applyRuler(rulerValue(event.clientX));
69
+ };
70
+ const onRulerUp = () => {
71
+ dragKind.current = null;
72
+ setDragging(false);
73
+ };
74
+ const onHandleKey =
75
+ (kind: 'work' | 'target') =>
76
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
77
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
78
+ event.preventDefault();
79
+ const step = event.shiftKey ? 10 : 1;
80
+ const base = kind === 'work' ? work : (target ?? 0);
81
+ const value = Math.max(
82
+ 0,
83
+ Math.min(149, base + (event.key === 'ArrowRight' ? step : -step)),
84
+ );
85
+ if (kind === 'work') {
86
+ onWork(value);
87
+ } else {
88
+ onTarget(value);
89
+ }
90
+ };
91
+ return {
92
+ dragging,
93
+ beginDrag,
94
+ onRulerDown,
95
+ onRulerMove,
96
+ onRulerUp,
97
+ onHandleKey,
98
+ };
99
+ }
100
+
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import {
5
+ decodeCalcState,
6
+ encodeCalcState,
7
+ } from '@terrafirmagreg-team/recipe/lib/calc/tree';
8
+ import { recipeCalcHref } from '@terrafirmagreg-team/recipe/lib/links';
9
+ import { replaceUrlState } from '@terrafirmagreg-team/ui/lib/utils/history';
10
+ import type { RecipeSession } from '@terrafirmagreg-team/recipe/types';
11
+
12
+ export function useAddToCalc(session: RecipeSession) {
13
+ const { dataset, lang, tree } = session;
14
+ return useCallback(
15
+ (item: string, kind: 'item' | 'fluid') => {
16
+ const state = decodeCalcState(tree ?? '') ?? {
17
+ cards: [],
18
+ wires: [],
19
+ tagSelections: {},
20
+ };
21
+ const existing = state.cards.find(
22
+ (card) => card.materialId === item && card.kind === kind,
23
+ );
24
+ const cards = existing
25
+ ? state.cards.map((card) =>
26
+ card === existing
27
+ ? { ...card, crafts: card.crafts + 1 }
28
+ : card,
29
+ )
30
+ : [
31
+ ...state.cards,
32
+ {
33
+ id: `c${Date.now().toString(36)}${Math.random().toString(36).slice(2, 5)}`,
34
+ materialId: item,
35
+ kind,
36
+ recipeId: null,
37
+ crafts: 1,
38
+ },
39
+ ];
40
+ replaceUrlState(
41
+ recipeCalcHref(
42
+ dataset,
43
+ lang,
44
+ encodeCalcState({ ...state, cards }),
45
+ ),
46
+ );
47
+ },
48
+ [tree, dataset, lang],
49
+ );
50
+ }
51
+
@@ -0,0 +1,18 @@
1
+ import { useMemo } from 'react';
2
+ import type {
3
+ RecipeCategory,
4
+ RecipeCompactIndex,
5
+ } from '@terrafirmagreg-team/recipe/types';
6
+
7
+ export function useCategoriesById(
8
+ index: RecipeCompactIndex | null | undefined,
9
+ ): Map<string, RecipeCategory> {
10
+ return useMemo(
11
+ () =>
12
+ new Map<string, RecipeCategory>(
13
+ (index?.categories ?? []).map((row) => [row.id, row]),
14
+ ),
15
+ [index],
16
+ );
17
+ }
18
+
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { useFavorites } from '@terrafirmagreg-team/recipe/hooks/use-favorites';
5
+ import { favoriteHoverTarget } from '@terrafirmagreg-team/recipe/lib/favorite-hover';
6
+
7
+ export function useFavoriteHotkey() {
8
+ const { toggleItem, toggleRecipe } = useFavorites();
9
+ useEffect(() => {
10
+ const onKey = (event: KeyboardEvent) => {
11
+ const key = event.key.toLowerCase();
12
+ if (key !== 'a' && key !== 'ф') return;
13
+ const el = document.activeElement;
14
+ if (
15
+ el instanceof HTMLElement &&
16
+ (el.tagName === 'INPUT' ||
17
+ el.tagName === 'TEXTAREA' ||
18
+ el.isContentEditable)
19
+ ) {
20
+ return;
21
+ }
22
+ const target = favoriteHoverTarget();
23
+ if (!target) return;
24
+ event.preventDefault();
25
+ if (target.kind === 'recipe') {
26
+ toggleRecipe({
27
+ id: target.id,
28
+ type: target.type,
29
+ ...(target.icon ? { icon: target.icon } : {}),
30
+ });
31
+ } else {
32
+ toggleItem(target.id, target.ref);
33
+ }
34
+ };
35
+ window.addEventListener('keydown', onKey);
36
+ return () => window.removeEventListener('keydown', onKey);
37
+ }, [toggleItem, toggleRecipe]);
38
+ }
39
+
@@ -0,0 +1,135 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import {
5
+ createStoredValue,
6
+ type StoredCodec,
7
+ } from '@terrafirmagreg-team/ui/lib/utils/stored-value';
8
+ import type { FavoriteEntry, RecipeFavorite } from '@terrafirmagreg-team/recipe/types';
9
+
10
+ export type { FavoriteEntry, RecipeFavorite };
11
+
12
+ const LEGACY_ITEMS_KEY = 'wiki.recipe.favorites';
13
+ const LEGACY_RECIPES_KEY = 'wiki.recipe.favorites.recipes';
14
+
15
+ function legacyEntries(): FavoriteEntry[] {
16
+ try {
17
+ const items: unknown = JSON.parse(
18
+ localStorage.getItem(LEGACY_ITEMS_KEY) ?? '[]',
19
+ );
20
+ const recipes: unknown = JSON.parse(
21
+ localStorage.getItem(LEGACY_RECIPES_KEY) ?? '[]',
22
+ );
23
+ return [
24
+ ...(Array.isArray(items) ? items : []).flatMap((id) =>
25
+ typeof id === 'string' && id
26
+ ? [{ kind: 'stack' as const, id }]
27
+ : [],
28
+ ),
29
+ ...(Array.isArray(recipes) ? recipes : []).flatMap((row) =>
30
+ row &&
31
+ typeof row === 'object' &&
32
+ typeof (row as RecipeFavorite).id === 'string' &&
33
+ typeof (row as RecipeFavorite).type === 'string'
34
+ ? [{ kind: 'recipe' as const, ...(row as RecipeFavorite) }]
35
+ : [],
36
+ ),
37
+ ];
38
+ } catch {
39
+ return [];
40
+ }
41
+ }
42
+
43
+ const codec: StoredCodec<FavoriteEntry[]> = {
44
+ read: (raw) => {
45
+ if (!raw) return legacyEntries();
46
+ const parsed: unknown = JSON.parse(raw);
47
+ if (!Array.isArray(parsed)) return [];
48
+ return parsed.filter((entry): entry is FavoriteEntry => {
49
+ if (!entry || typeof entry !== 'object') return false;
50
+ const row = entry as FavoriteEntry;
51
+ if (row.kind === 'stack') return typeof row.id === 'string' && !!row.id;
52
+ if (row.kind === 'recipe')
53
+ return (
54
+ typeof row.id === 'string' &&
55
+ !!row.id &&
56
+ typeof row.type === 'string' &&
57
+ !!row.type
58
+ );
59
+ return false;
60
+ });
61
+ },
62
+ write: (entries) => JSON.stringify(entries),
63
+ };
64
+
65
+ const useStoredFavorites = createStoredValue('wiki.recipe.favorites.v2', codec);
66
+
67
+ export function useFavorites() {
68
+ const [entries, setEntries] = useStoredFavorites();
69
+
70
+ const toggleItem = useCallback(
71
+ (itemId: string, ref?: 'item' | 'fluid') => {
72
+ setEntries((prev) =>
73
+ prev.some(
74
+ (entry) => entry.kind === 'stack' && entry.id === itemId,
75
+ )
76
+ ? prev.filter(
77
+ (entry) => !(entry.kind === 'stack' && entry.id === itemId),
78
+ )
79
+ : [
80
+ ...prev,
81
+ {
82
+ kind: 'stack',
83
+ id: itemId,
84
+ ...(ref && ref !== 'item' ? { ref } : {}),
85
+ },
86
+ ],
87
+ );
88
+ },
89
+ [setEntries],
90
+ );
91
+
92
+ const isFavorite = useCallback(
93
+ (itemId: string) =>
94
+ entries.some((entry) => entry.kind === 'stack' && entry.id === itemId),
95
+ [entries],
96
+ );
97
+
98
+ const toggleRecipe = useCallback(
99
+ (row: RecipeFavorite) => {
100
+ setEntries((prev) =>
101
+ prev.some(
102
+ (entry) =>
103
+ entry.kind === 'recipe' &&
104
+ entry.id === row.id &&
105
+ entry.type === row.type,
106
+ )
107
+ ? prev.filter(
108
+ (entry) =>
109
+ !(
110
+ entry.kind === 'recipe' &&
111
+ entry.id === row.id &&
112
+ entry.type === row.type
113
+ ),
114
+ )
115
+ : [...prev, { kind: 'recipe', ...row }],
116
+ );
117
+ },
118
+ [setEntries],
119
+ );
120
+
121
+ const isRecipeFavorite = useCallback(
122
+ (id: string) =>
123
+ entries.some((entry) => entry.kind === 'recipe' && entry.id === id),
124
+ [entries],
125
+ );
126
+
127
+ return {
128
+ entries,
129
+ toggleItem,
130
+ isFavorite,
131
+ toggleRecipe,
132
+ isRecipeFavorite,
133
+ };
134
+ }
135
+