@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,103 @@
1
+ 'use client';
2
+
3
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
4
+ import { Input } from '@terrafirmagreg-team/ui/components/ui/input';
5
+ import { Button } from '@terrafirmagreg-team/ui/components/ui/button';
6
+ import { useCopyButton } from '@terrafirmagreg-team/ui/hooks/use-copy-button';
7
+ import { IconButton } from '@terrafirmagreg-team/ui/components/blocks/button/icon-button';
8
+ import { Check, Copy, RotateCcw, Undo2, Wand2 } from 'lucide-react';
9
+ import { forgeActionLabel } from '@terrafirmagreg-team/recipe/lib/forge/actions';
10
+ import type { ForgeState } from '@terrafirmagreg-team/recipe/hooks/forge/use-forge-state';
11
+
12
+ export function WorkbenchControls({
13
+ state,
14
+ rulesSupported,
15
+ langMap,
16
+ }: {
17
+ state: ForgeState;
18
+ rulesSupported: boolean;
19
+ langMap: Record<string, string>;
20
+ }) {
21
+ const t = useTranslations();
22
+ const { startInput, targetInput, applied, steps, target, solving } = state;
23
+ const { checked: copied, onClick: copy } = useCopyButton(() =>
24
+ navigator.clipboard.writeText(
25
+ steps
26
+ .map(
27
+ (step, i) =>
28
+ `${i + 1}. ${forgeActionLabel(langMap, step.id)} (${step.delta > 0 ? '+' : ''}${step.delta}) = ${step.work}`,
29
+ )
30
+ .join('\n'),
31
+ ),
32
+ );
33
+
34
+ return (
35
+ <div className="flex flex-wrap items-end gap-3">
36
+ <label className="flex w-24 flex-col gap-1 border-l-2 border-green-600 pl-2 text-xs text-green-700 dark:border-green-500 dark:text-green-400">
37
+ {t('Current work', { note: 'recipe.forge.current' })}
38
+ <Input
39
+ inputMode="numeric"
40
+ name="forge-start"
41
+ autoComplete="off"
42
+ value={startInput}
43
+ disabled={solving}
44
+ onChange={(event) => state.setStart(event.target.value)}
45
+ placeholder="0"
46
+ />
47
+ </label>
48
+ <label className="flex w-24 flex-col gap-1 border-l-2 border-red-600 pl-2 text-xs text-red-700 dark:border-red-500 dark:text-red-400">
49
+ {t('Target', { note: 'recipe.forge.target' })}
50
+ <Input
51
+ inputMode="numeric"
52
+ name="forge-target"
53
+ autoComplete="off"
54
+ value={targetInput}
55
+ disabled={solving}
56
+ onChange={(event) => state.setTarget(event.target.value)}
57
+ placeholder="0"
58
+ />
59
+ </label>
60
+ <Button
61
+ onClick={state.solve}
62
+ disabled={target === null || !rulesSupported || solving}
63
+ >
64
+ <Wand2 className="size-4" />
65
+ {solving
66
+ ? t('Solving…', { note: 'recipe.forge.solving' })
67
+ : t('Solve', { note: 'recipe.forge.solve' })}
68
+ </Button>
69
+ <span className="flex items-center gap-1.5">
70
+ <IconButton
71
+ label={t('Undo last action', { note: 'recipe.forge.undo' })}
72
+ disabled={!applied.length || solving}
73
+ onClick={() => state.setApplied((prev) => prev.slice(0, -1))}
74
+ >
75
+ <Undo2 />
76
+ </IconButton>
77
+ <IconButton
78
+ label={t('Reset the workpiece', { note: 'recipe.forge.reset' })}
79
+ disabled={!applied.length || solving}
80
+ onClick={() => state.setApplied([])}
81
+ >
82
+ <RotateCcw />
83
+ </IconButton>
84
+ <IconButton
85
+ label={
86
+ copied
87
+ ? t('Copied', { note: 'recipe.forge.copied' })
88
+ : t('Copy steps', { note: 'recipe.forge.copy' })
89
+ }
90
+ disabled={!steps.length}
91
+ onClick={copy}
92
+ >
93
+ {copied ? (
94
+ <Check className="text-green-600 dark:text-green-400" />
95
+ ) : (
96
+ <Copy />
97
+ )}
98
+ </IconButton>
99
+ </span>
100
+ </div>
101
+ );
102
+ }
103
+
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+
3
+ import { useRef } from 'react';
4
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
5
+ import { useElementSize } from '@terrafirmagreg-team/ui/hooks/use-element-size';
6
+ import { GUI_PANEL, GuiSprite } from '@terrafirmagreg-team/recipe/components/views/forge/gui';
7
+ import { useAnvilGui } from '@terrafirmagreg-team/recipe/hooks/forge/use-anvil-gui';
8
+ import { useForgeState } from '@terrafirmagreg-team/recipe/hooks/forge/use-forge-state';
9
+ import { WorkbenchControls } from './controls';
10
+ import { AnvilPanel } from './panel';
11
+ import { RulerOverlay } from './ruler';
12
+ import { StatusPlate } from './status';
13
+ import { StepTape } from './tape';
14
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
15
+ import type { ForgeRecipe } from '@terrafirmagreg-team/recipe/types/forge';
16
+ import type { RecipeSession } from '@terrafirmagreg-team/recipe/types';
17
+
18
+ const S_MAX = 2.5;
19
+ const S_MIN = 1.5;
20
+
21
+ export function ForgeWorkbench({
22
+ recipe,
23
+ session,
24
+ }: {
25
+ recipe: ForgeRecipe;
26
+ session: RecipeSession;
27
+ }) {
28
+ const t = useTranslations();
29
+ const atlas = useAnvilGui(session.dataset);
30
+ const wrapRef = useRef<HTMLDivElement>(null);
31
+ const panelRef = useRef<HTMLDivElement>(null);
32
+ const { w: wrapW } = useElementSize(wrapRef);
33
+ const S = wrapW
34
+ ? Math.max(S_MIN, Math.min(S_MAX, Math.floor((wrapW / GUI_PANEL.w) * 4) / 4))
35
+ : S_MAX;
36
+ const PX = (v: number) => v * S;
37
+ const state = useForgeState(recipe);
38
+ const { finished } = state;
39
+
40
+ return (
41
+ <section className="space-y-3">
42
+ {}
43
+ <WorkbenchControls state={state} rulesSupported={recipe.rulesSupported} langMap={session.langMap} />
44
+ {!recipe.rulesSupported ? (
45
+ <p role="alert" className="text-sm text-destructive">
46
+ {t('The dump contains missing or unrecognized forging rules. A safe solution cannot be calculated.', { note: 'recipe.forge.unsupported' })}
47
+ </p>
48
+ ) : null}
49
+
50
+ {}
51
+ <div
52
+ ref={wrapRef}
53
+ className="flex w-full flex-col items-center overflow-x-auto"
54
+ >
55
+ <div
56
+ ref={panelRef}
57
+ className={cn(
58
+ 'relative shrink-0 rounded-sm bg-muted shadow-inner transition-shadow duration-300',
59
+ finished && 'shadow-[0_0_0_2px_rgb(63,185,80,0.5)]',
60
+ )}
61
+ style={{ width: PX(GUI_PANEL.w), height: PX(GUI_PANEL.h) }}
62
+ >
63
+ <GuiSprite
64
+ gui={atlas}
65
+ rect={GUI_PANEL}
66
+ scale={S}
67
+ className="absolute inset-0 rounded-sm"
68
+ />
69
+ <AnvilPanel atlas={atlas} scale={S} recipe={recipe} state={state} langMap={session.langMap} />
70
+ <RulerOverlay
71
+ atlas={atlas}
72
+ scale={S}
73
+ panelRef={panelRef}
74
+ state={state}
75
+ />
76
+ </div>
77
+
78
+ <StatusPlate state={state} scale={S} />
79
+ </div>
80
+
81
+ <StepTape atlas={atlas} state={state} />
82
+ </section>
83
+ );
84
+ }
85
+
@@ -0,0 +1,228 @@
1
+ 'use client';
2
+
3
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
4
+ import { FORGE_ACTIONS, forgeActionLabel } from '@terrafirmagreg-team/recipe/lib/forge/actions';
5
+ import {
6
+ PLAN_SLOT,
7
+ RULE_ICON,
8
+ RULE_OVERLAY_Y,
9
+ actionGlyphRect,
10
+ GuiMask,
11
+ GuiSprite,
12
+ type GuiRect,
13
+ } from '@terrafirmagreg-team/recipe/components/views/forge/gui';
14
+ import type { ForgeRecipe } from '@terrafirmagreg-team/recipe/types/forge';
15
+ import type { ForgeState } from '@terrafirmagreg-team/recipe/hooks/forge/use-forge-state';
16
+
17
+ const RULE_TINT = {
18
+ met: 'rgb(0,153,51)',
19
+ pending: 'rgb(255,102,0)',
20
+ } as const;
21
+ const ruleOutline = (color: string) =>
22
+ `drop-shadow(1px 0 0 ${color}) drop-shadow(-1px 0 0 ${color}) drop-shadow(0 1px 0 ${color}) drop-shadow(0 -1px 0 ${color})`;
23
+
24
+ const BUTTON_TILE: Record<string, GuiRect> = {
25
+ light: { x: 53, y: 50, w: 16, h: 16 },
26
+ medium: { x: 71, y: 50, w: 16, h: 16 },
27
+ hard: { x: 53, y: 68, w: 16, h: 16 },
28
+ draw: { x: 71, y: 68, w: 16, h: 16 },
29
+ punch: { x: 89, y: 50, w: 16, h: 16 },
30
+ bend: { x: 107, y: 50, w: 16, h: 16 },
31
+ upset: { x: 89, y: 68, w: 16, h: 16 },
32
+ shrink: { x: 107, y: 68, w: 16, h: 16 },
33
+ };
34
+
35
+ export function AnvilPanel({
36
+ atlas,
37
+ scale,
38
+ recipe,
39
+ state,
40
+ langMap,
41
+ }: {
42
+ atlas: string | undefined;
43
+ scale: number;
44
+ recipe: ForgeRecipe;
45
+ state: ForgeState;
46
+ langMap: Record<string, string>;
47
+ }) {
48
+ const PX = (v: number) => v * scale;
49
+ const { work, solving, last, applied, ruleMet, push } = state;
50
+ const input = recipe.recipe.inputs[0];
51
+ const output = recipe.recipe.outputs[0];
52
+
53
+ return (
54
+ <>
55
+ {}
56
+ {recipe.rules.slice(0, 3).map((rule, i) => {
57
+ const rect = { x: 198, y: RULE_OVERLAY_Y[rule.order], w: 20, h: 22 };
58
+ return (
59
+ <div
60
+ key={i}
61
+ className="absolute"
62
+ style={{
63
+ left: PX(59 + i * 19),
64
+ top: PX(7),
65
+ width: PX(20),
66
+ height: PX(22),
67
+ }}
68
+ >
69
+ <GuiSprite
70
+ gui={atlas}
71
+ rect={rect}
72
+ scale={scale}
73
+ className="absolute inset-0"
74
+ />
75
+ {(['pending', 'met'] as const).map((state) => (
76
+ <GuiMask
77
+ key={state}
78
+ gui={atlas}
79
+ rect={rect}
80
+ scale={scale}
81
+ className="absolute inset-0 transition-opacity duration-200"
82
+ style={{
83
+ backgroundColor: RULE_TINT[state],
84
+ mixBlendMode: 'multiply',
85
+ filter: ruleOutline(RULE_TINT[state]),
86
+ opacity: (state === 'met') === ruleMet[i] ? 1 : 0,
87
+ }}
88
+ />
89
+ ))}
90
+ </div>
91
+ );
92
+ })}
93
+ {recipe.rules.slice(0, 3).map((rule, i) => (
94
+ <GuiSprite
95
+ key={i}
96
+ gui={atlas}
97
+ rect={RULE_ICON[rule.action]}
98
+ scale={scale / 3.2}
99
+ className="absolute"
100
+ style={{
101
+ left: PX(64 + i * 19),
102
+ top: PX(10),
103
+ width: PX(10),
104
+ height: PX(10),
105
+ }}
106
+ />
107
+ ))}
108
+
109
+ {}
110
+ {last.map((action, i) => (
111
+ <GuiSprite
112
+ key={`${i}-${action}-${applied.length}`}
113
+ gui={atlas}
114
+ rect={actionGlyphRect(action)}
115
+ scale={scale / 3.2}
116
+ className="absolute animate-in fade-in-0 zoom-in-50 duration-150"
117
+ style={{
118
+ left: PX(64 + i * 19),
119
+ top: PX(31),
120
+ width: PX(10),
121
+ height: PX(10),
122
+ }}
123
+ />
124
+ ))}
125
+
126
+ {}
127
+ <GuiSprite
128
+ gui={atlas}
129
+ rect={PLAN_SLOT}
130
+ scale={scale}
131
+ className="absolute"
132
+ style={{ left: PX(21), top: PX(40) }}
133
+ />
134
+ {output ? (
135
+ <div
136
+ className="absolute"
137
+ style={{
138
+ left: PX(22),
139
+ top: PX(41),
140
+ width: PX(16),
141
+ height: PX(16),
142
+ }}
143
+ >
144
+ <Entry
145
+ id={output.id}
146
+ kind={output.kind === 'fluid' ? 'fluid' : 'item'}
147
+ size={PX(16)}
148
+ href={false}
149
+ tooltip
150
+ />
151
+ </div>
152
+ ) : null}
153
+
154
+ {}
155
+ {input ? (
156
+ <div
157
+ className="absolute"
158
+ style={{
159
+ left: PX(31),
160
+ top: PX(68),
161
+ width: PX(16),
162
+ height: PX(16),
163
+ }}
164
+ >
165
+ <Entry
166
+ id={input.id}
167
+ kind={input.kind === 'fluid' ? 'fluid' : 'item'}
168
+ size={PX(16)}
169
+ href={false}
170
+ tooltip
171
+ />
172
+ </div>
173
+ ) : null}
174
+
175
+ {}
176
+ {FORGE_ACTIONS.map((action, index) => {
177
+ const disabled =
178
+ solving || work + action.delta < 0 || work + action.delta >= 150;
179
+ const [bx, by] = [BUTTON_TILE[action.id].x, BUTTON_TILE[action.id].y];
180
+ return (
181
+ <button
182
+ key={action.id}
183
+ type="button"
184
+ disabled={disabled}
185
+ title={`${forgeActionLabel(langMap, action.id)} (${action.delta > 0 ? '+' : ''}${action.delta})`}
186
+ aria-label={`${forgeActionLabel(langMap, action.id)} (${action.delta > 0 ? '+' : ''}${action.delta})`}
187
+ onClick={() => push(index)}
188
+ className="group absolute rounded-[6px] outline-none transition-[filter,transform] duration-100 hover:brightness-110 hover:drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)] focus-visible:ring-2 focus-visible:ring-ring active:scale-90 disabled:hover:brightness-100 disabled:hover:drop-shadow-none disabled:active:scale-100"
189
+ style={{
190
+ left: PX(bx),
191
+ top: PX(by),
192
+ width: PX(16),
193
+ height: PX(16),
194
+ }}
195
+ >
196
+ <GuiSprite
197
+ gui={atlas}
198
+ rect={BUTTON_TILE[action.id]}
199
+ scale={scale}
200
+ className="absolute inset-0"
201
+ />
202
+ <GuiSprite
203
+ gui={atlas}
204
+ rect={actionGlyphRect(action.id)}
205
+ scale={scale / 2}
206
+ className="absolute inset-0"
207
+ />
208
+ {disabled ? (
209
+ <GuiMask
210
+ gui={atlas}
211
+ rect={BUTTON_TILE[action.id]}
212
+ scale={scale}
213
+ className="absolute inset-0 bg-black/50"
214
+ />
215
+ ) : null}
216
+ {!atlas ? (
217
+ <span className="absolute inset-0 flex items-center justify-center text-[10px] font-bold tabular-nums">
218
+ {action.delta > 0 ? '+' : ''}
219
+ {action.delta}
220
+ </span>
221
+ ) : null}
222
+ </button>
223
+ );
224
+ })}
225
+ </>
226
+ );
227
+ }
228
+
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+
3
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
4
+ import { MARK_PROGRESS, MARK_TARGET, GuiSprite } from '@terrafirmagreg-team/recipe/components/views/forge/gui';
5
+ import { RULER, useRuler } from '@terrafirmagreg-team/recipe/hooks/forge/use-ruler';
6
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
7
+ import type { ForgeState } from '@terrafirmagreg-team/recipe/hooks/forge/use-forge-state';
8
+
9
+ export function RulerOverlay({
10
+ atlas,
11
+ scale,
12
+ panelRef,
13
+ state,
14
+ }: {
15
+ atlas: string | undefined;
16
+ scale: number;
17
+ panelRef: React.RefObject<HTMLDivElement | null>;
18
+ state: ForgeState;
19
+ }) {
20
+ const t = useTranslations();
21
+ const PX = (v: number) => v * scale;
22
+ const { work, target, solving, setStart, setTarget, setTargetInput } = state;
23
+ const {
24
+ dragging,
25
+ beginDrag,
26
+ onRulerDown,
27
+ onRulerMove,
28
+ onRulerUp,
29
+ onHandleKey,
30
+ } = useRuler({
31
+ panelRef,
32
+ scale,
33
+ work,
34
+ target,
35
+ solving,
36
+ onWork: setStart,
37
+ onTarget: setTarget,
38
+ });
39
+
40
+ const markerHandles = [
41
+ { kind: 'target', value: target ?? 0, label: t('Target', { note: 'recipe.forge.target' }) },
42
+ { kind: 'work', value: work, label: t('Current work', { note: 'recipe.forge.current' }) },
43
+ ] as const;
44
+
45
+ return (
46
+ <>
47
+ <GuiSprite
48
+ gui={atlas}
49
+ rect={MARK_PROGRESS}
50
+ scale={scale}
51
+ className={cn(
52
+ 'pointer-events-none absolute',
53
+ !dragging && 'transition-[left] duration-200',
54
+ )}
55
+ style={{ left: PX(RULER.markerX + work), top: PX(100) }}
56
+ />
57
+ <GuiSprite
58
+ gui={atlas}
59
+ rect={MARK_TARGET}
60
+ scale={scale}
61
+ className={cn(
62
+ 'pointer-events-none absolute',
63
+ !dragging && 'transition-[left] duration-200',
64
+ )}
65
+ style={{ left: PX(RULER.markerX + (target ?? 0)), top: PX(94) }}
66
+ />
67
+ <div
68
+ role="slider"
69
+ aria-label={t('Work ruler', { note: 'recipe.forge.ruler' })}
70
+ aria-valuemin={0}
71
+ aria-valuemax={149}
72
+ aria-valuenow={target ?? work}
73
+ aria-disabled={solving}
74
+ tabIndex={0}
75
+ className={cn(
76
+ 'absolute touch-none',
77
+ solving ? 'cursor-not-allowed' : 'cursor-ew-resize',
78
+ )}
79
+ onKeyDown={(event) => {
80
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
81
+ event.preventDefault();
82
+ const step = event.shiftKey ? 10 : 1;
83
+ const value = Math.max(
84
+ 0,
85
+ Math.min(
86
+ 149,
87
+ (target ?? work) + (event.key === 'ArrowRight' ? step : -step),
88
+ ),
89
+ );
90
+ setTargetInput(String(value));
91
+ }}
92
+ style={{
93
+ left: PX(4),
94
+ top: PX(RULER.numbersTop - 2),
95
+ width: PX(168),
96
+ height: PX(RULER.trackBottom - RULER.numbersTop + 4),
97
+ }}
98
+ onPointerDown={onRulerDown}
99
+ onPointerMove={onRulerMove}
100
+ onPointerUp={onRulerUp}
101
+ onPointerCancel={onRulerUp}
102
+ />
103
+ {markerHandles.map(({ kind, value, label }) => (
104
+ <div
105
+ key={kind}
106
+ role="slider"
107
+ aria-label={label}
108
+ aria-valuemin={0}
109
+ aria-valuemax={149}
110
+ aria-valuenow={value}
111
+ aria-disabled={solving}
112
+ tabIndex={0}
113
+ className={cn(
114
+ 'absolute touch-none rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring',
115
+ solving ? 'cursor-not-allowed' : 'cursor-grab',
116
+ )}
117
+ onKeyDown={onHandleKey(kind)}
118
+ onPointerDown={(event) => beginDrag(event, kind)}
119
+ onPointerMove={onRulerMove}
120
+ onPointerUp={onRulerUp}
121
+ onPointerCancel={onRulerUp}
122
+ style={{
123
+ left: PX(RULER.markerX + value + 2.5 - 6),
124
+ top: PX(RULER.numbersTop - 2),
125
+ width: PX(12),
126
+ height: PX(RULER.trackBottom - RULER.numbersTop + 4),
127
+ }}
128
+ />
129
+ ))}
130
+ </>
131
+ );
132
+ }
133
+
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
4
+ import { Check, X } from 'lucide-react';
5
+ import { GUI_PANEL } from '@terrafirmagreg-team/recipe/components/views/forge/gui';
6
+ import type { ForgeState } from '@terrafirmagreg-team/recipe/hooks/forge/use-forge-state';
7
+
8
+ export function StatusPlate({
9
+ state,
10
+ scale,
11
+ }: {
12
+ state: ForgeState;
13
+ scale: number;
14
+ }) {
15
+ const t = useTranslations();
16
+ const PX = (v: number) => v * scale;
17
+ const { work, target, finished, failed } = state;
18
+
19
+ return (
20
+ <div
21
+ className="mt-2 flex flex-wrap items-center justify-center gap-x-4 gap-y-1 rounded-md border bg-muted/40 px-3 py-1.5 text-xs text-muted-foreground tabular-nums"
22
+ style={{ width: PX(GUI_PANEL.w) }}
23
+ >
24
+ <span>
25
+ {t('Work', { note: 'recipe.forge.work' })}:{' '}
26
+ <span className="font-medium text-green-700 dark:text-green-400">
27
+ {work}
28
+ </span>
29
+ </span>
30
+ <span>
31
+ {t('Target', { note: 'recipe.forge.target' })}:{' '}
32
+ <span className="font-medium text-red-700 dark:text-red-400">
33
+ {target ?? 0}
34
+ </span>
35
+ </span>
36
+ {finished ? (
37
+ <span className="inline-flex items-center gap-1 text-green-600 animate-in fade-in-0">
38
+ <Check className="size-3" /> {t('Done', { note: 'recipe.forge.done' })}
39
+ </span>
40
+ ) : null}
41
+ {failed ? (
42
+ <span
43
+ role="status"
44
+ className="inline-flex items-center gap-1 text-destructive"
45
+ >
46
+ <X className="size-3" /> {t('No sequence reaches this target under these rules. Reset the workpiece and try another target.', { note: 'recipe.forge.no_solution' })}
47
+ </span>
48
+ ) : null}
49
+ {target !== null && work > target && !failed ? (
50
+ <span>{t('Work is above the target, use Draw or lighter hits to come back.', { note: 'recipe.forge.over_target' })}</span>
51
+ ) : null}
52
+ </div>
53
+ );
54
+ }
55
+
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
4
+ import { GuiSprite, actionGlyphRect } from '@terrafirmagreg-team/recipe/components/views/forge/gui';
5
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
6
+ import type { ForgeState } from '@terrafirmagreg-team/recipe/hooks/forge/use-forge-state';
7
+
8
+ const TAPE_MAX = 12;
9
+
10
+ export function StepTape({
11
+ atlas,
12
+ state,
13
+ }: {
14
+ atlas: string | undefined;
15
+ state: ForgeState;
16
+ }) {
17
+ const t = useTranslations();
18
+ const { steps, expanded, setExpanded, setApplied } = state;
19
+ const tapeHidden = Math.max(0, steps.length - TAPE_MAX);
20
+ const tapeShown = expanded ? steps : steps.slice(tapeHidden);
21
+
22
+ if (!steps.length) return null;
23
+ return (
24
+ <ol className="flex flex-wrap gap-1.5">
25
+ {tapeHidden > 0 && !expanded ? (
26
+ <li>
27
+ <button
28
+ type="button"
29
+ title={t('Show all steps', { note: 'recipe.forge.expand_steps' })}
30
+ aria-label={t('Show all steps', { note: 'recipe.forge.expand_steps' })}
31
+ onClick={() => setExpanded(true)}
32
+ className="flex items-center rounded border border-dashed px-1.5 py-0.5 text-xs tabular-nums text-muted-foreground hover:bg-accent"
33
+ >
34
+ …+{tapeHidden}
35
+ </button>
36
+ </li>
37
+ ) : null}
38
+ {tapeShown.map((step, j) => {
39
+ const i = expanded ? j : tapeHidden + j;
40
+ return (
41
+ <li key={i} className="animate-in fade-in-0 zoom-in-95 duration-150">
42
+ <button
43
+ type="button"
44
+ title={t('Trim to this step', { note: 'recipe.forge.trim_to' })}
45
+ onClick={() => setApplied((prev) => prev.slice(0, i + 1))}
46
+ className={cn(
47
+ 'flex items-center gap-1.5 rounded border px-1.5 py-0.5 text-xs tabular-nums hover:bg-accent',
48
+ step.delta < 0 ? 'border-red-500/30' : 'border-green-500/30',
49
+ )}
50
+ >
51
+ <span className="text-muted-foreground">{i + 1}</span>
52
+ <GuiSprite
53
+ gui={atlas}
54
+ rect={actionGlyphRect(step.id)}
55
+ scale={14 / 32}
56
+ className="size-3.5"
57
+ />
58
+ <span>
59
+ {step.delta > 0 ? '+' : ''}
60
+ {step.delta}
61
+ </span>
62
+ <span className="text-muted-foreground">→ {step.work}</span>
63
+ </button>
64
+ </li>
65
+ );
66
+ })}
67
+ </ol>
68
+ );
69
+ }
70
+