@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
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/recipe",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.2.0",
4
+ "type": "module",
5
+ "description": "Recipe viewer and calculator",
6
+ "license": "LGPL-3.0-only",
7
+ "exports": {
8
+ "./components/*": [
9
+ "./src/components/*.tsx",
10
+ "./src/components/*/index.tsx",
11
+ "./src/components/*/index.ts"
12
+ ],
13
+ "./hooks/*": [
14
+ "./src/hooks/*.ts",
15
+ "./src/hooks/*/index.ts"
16
+ ],
17
+ "./lib/*": [
18
+ "./src/lib/*.ts",
19
+ "./src/lib/*/index.ts"
20
+ ],
21
+ "./types/*": "./src/types/*.ts",
22
+ "./types": "./src/types/index.ts",
23
+ "./styles.css": "./styles.css",
24
+ "./locales": "./src/locales.ts"
25
+ },
26
+ "dependencies": {
27
+ "@terrafirmagreg-team/data": "1.2.0",
28
+ "@terrafirmagreg-team/dump": "1.2.0",
29
+ "@terrafirmagreg-team/game": "1.2.0",
30
+ "@terrafirmagreg-team/kit": "1.2.0",
31
+ "@terrafirmagreg-team/mdx": "1.2.0",
32
+ "@terrafirmagreg-team/preview": "1.2.0",
33
+ "@terrafirmagreg-team/sites": "1.2.0",
34
+ "@terrafirmagreg-team/translate": "1.2.0",
35
+ "@terrafirmagreg-team/ui": "1.2.0",
36
+ "comlink": "^4.4.2",
37
+ "fflate": "0.8.3",
38
+ "fumadocs-core": "16.17.8",
39
+ "lucide-react": "^0.577.0",
40
+ "pathfinding": "0.4.18"
41
+ },
42
+ "peerDependencies": {
43
+ "next": ">=15",
44
+ "react": ">=19"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^26.6.5",
48
+ "@types/pathfinding": "^0.1.0",
49
+ "@types/react": "^19.3.0",
50
+ "@types/react-dom": "^19.3.0"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/TerraFirmaGreg-Team/Web-Foundation.git",
55
+ "directory": "packages/recipe"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "files": [
61
+ "src",
62
+ "styles",
63
+ "styles.css"
64
+ ]
6
65
  }
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import { type ComponentProps, type ReactNode } from 'react';
3
+ import { useCopyButton } from '@terrafirmagreg-team/ui/hooks/use-copy-button';
4
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
5
+ import { Button } from '@terrafirmagreg-team/ui/components/ui/button';
6
+ import { Hint } from '@terrafirmagreg-team/ui/components/blocks/tooltip/hint';
7
+
8
+ export function ButtonCopy({
9
+ value,
10
+ copy,
11
+ copied,
12
+ className,
13
+ title,
14
+ ...props
15
+ }: Omit<ComponentProps<typeof Button>, 'value' | 'onCopy' | 'children'> & {
16
+ value: string;
17
+ copy: ReactNode;
18
+ copied: ReactNode;
19
+ }) {
20
+ const { checked: done, onClick } = useCopyButton(() =>
21
+ navigator.clipboard.writeText(value),
22
+ );
23
+ const button = (
24
+ <Button
25
+ variant="ghost"
26
+ size="xs"
27
+ className={cn(
28
+ 'shrink-0 text-2xs text-muted-foreground hover:text-foreground',
29
+ className,
30
+ )}
31
+ onClick={(event) => {
32
+ event.preventDefault();
33
+ event.stopPropagation();
34
+ onClick();
35
+ }}
36
+ {...props}
37
+ >
38
+ {done ? copied : copy}
39
+ </Button>
40
+ );
41
+ return title ? <Hint label={title}>{button}</Hint> : button;
42
+ }
43
+
@@ -0,0 +1,214 @@
1
+ 'use client';
2
+
3
+ import { useMemo, useRef, useState } from 'react';
4
+ import { SlotHitbox } from '@terrafirmagreg-team/recipe/components/cards/slot-hitbox';
5
+ import { recipeGlareFilter } from '@terrafirmagreg-team/recipe/components/cards/glare-filter';
6
+ import {
7
+ cardStripGeo,
8
+ countCoveredText,
9
+ textInStrip,
10
+ } from '@terrafirmagreg-team/recipe/lib/card-model';
11
+ import { CardText } from '@terrafirmagreg-team/recipe/components/cards/card/text';
12
+ import { CardSceneOverlay } from '@terrafirmagreg-team/recipe/components/cards/card/scene';
13
+ import { CardWidgetTips } from '@terrafirmagreg-team/recipe/components/cards/card/tips';
14
+ import { CardStrip } from '@terrafirmagreg-team/recipe/components/cards/card/strip';
15
+ import { recipeSlots } from '@terrafirmagreg-team/recipe/lib/recipe-slots';
16
+ import {
17
+ recipeMultiblock,
18
+ recipeSceneEntities,
19
+ } from '@terrafirmagreg-team/recipe/lib/data/scene';
20
+ import { dumpRelPublicUrl } from '@terrafirmagreg-team/dump/lib/dump/paths';
21
+ import { useDumpLocation } from '@terrafirmagreg-team/game/hooks/use-dump-location';
22
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
23
+ import { useInView } from '@terrafirmagreg-team/ui/hooks/use-in-view';
24
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
25
+ import { GlbEmbedViewer } from '@terrafirmagreg-team/ui/components/blocks/media/glb/viewer';
26
+ import type { EntryTooltipLevel } from '@terrafirmagreg-team/game/components/entry/tooltip';
27
+ import type {
28
+ Recipe,
29
+ RecipeTextWidget,
30
+ } from '@terrafirmagreg-team/recipe/lib/data/dto';
31
+ import type { RecipeHrefOpts } from '@terrafirmagreg-team/recipe/types';
32
+
33
+ const SCALE = 2;
34
+
35
+ export function RecipeCard({
36
+ recipe,
37
+ dataset,
38
+ lang,
39
+ langMap,
40
+ nav,
41
+ title,
42
+ src,
43
+ onFail,
44
+ naturalW,
45
+ naturalH,
46
+ interactive,
47
+ slotTooltip,
48
+ eager,
49
+ frameOnly,
50
+ }: {
51
+ recipe: Recipe;
52
+ dataset: DatasetRef;
53
+ lang: string;
54
+ langMap?: Record<string, string>;
55
+ nav?: Pick<RecipeHrefOpts, 'tab' | 'search'>;
56
+ title: string;
57
+ src: string;
58
+ onFail: () => void;
59
+ naturalW?: number;
60
+ naturalH?: number;
61
+ interactive?: boolean;
62
+
63
+ slotTooltip?: EntryTooltipLevel;
64
+
65
+ eager?: boolean;
66
+
67
+ frameOnly?: boolean;
68
+ }) {
69
+ const scale = recipe.scale ?? SCALE;
70
+ useDumpLocation(dataset);
71
+ const margin = recipe.margin ?? 0;
72
+ const cardW = naturalW ?? recipe.w ?? 0;
73
+ const cardH = naturalH ?? recipe.h ?? 0;
74
+ const rootRef = useRef<HTMLDivElement>(null);
75
+ const observedInView = useInView(rootRef);
76
+ const inView = eager || observedInView;
77
+ const slots = useMemo(() => recipeSlots(recipe), [recipe]);
78
+ const contentGuiW = cardW / scale - margin * 2;
79
+ const innerBottomGui = cardH / scale - margin;
80
+ const strip = cardStripGeo(slots, contentGuiW, innerBottomGui, margin);
81
+ const plainSlots = strip.overflow
82
+ ? slots.filter((slot) => slot.role !== 'renderOnly')
83
+ : slots;
84
+ const entities = recipeSceneEntities(recipe.properties);
85
+ const textWidgets = recipe.widgets.filter(
86
+ (widget): widget is RecipeTextWidget => widget.type === 'text',
87
+ );
88
+ const stripTexts = textWidgets.filter(
89
+ (widget) => textInStrip(widget, strip) && !countCoveredText(widget, strip.slots),
90
+ );
91
+ const multiblock = recipeMultiblock(recipe.properties);
92
+ const partsTop = multiblock?.parts.length
93
+ ? Math.min(...multiblock.parts.map((part) => part.y)) * scale
94
+ : undefined;
95
+ const sceneHeight = Math.max(
96
+ (partsTop ?? cardH - margin * scale) - margin * scale,
97
+ 0,
98
+ );
99
+ const glbUrl = multiblock?.glb
100
+ ? dumpRelPublicUrl(dataset, multiblock.glb)
101
+ : undefined;
102
+
103
+ const [imgReady, setImgReady] = useState(false);
104
+ const [prevImgSrc, setPrevImgSrc] = useState(src);
105
+ if (prevImgSrc !== src) {
106
+ setPrevImgSrc(src);
107
+ setImgReady(false);
108
+ }
109
+ return (
110
+ <div
111
+ ref={rootRef}
112
+ className={cn(
113
+ 'relative w-fit overflow-hidden rounded-sm',
114
+ imgReady && 'bg-mc-panel',
115
+ )}
116
+ >
117
+ {/* eslint-disable-next-line @next/next/no-img-element */}
118
+ <img
119
+ src={src}
120
+ alt={title}
121
+ className="block"
122
+ width={naturalW}
123
+ height={naturalH}
124
+ loading="lazy"
125
+ decoding="async"
126
+ draggable={false}
127
+ onError={onFail}
128
+ onLoad={() => setImgReady(true)}
129
+ style={recipeGlareFilter}
130
+ />
131
+ {inView && interactive && glbUrl ? (
132
+ <div
133
+ className="absolute"
134
+ style={{
135
+ left: margin * scale,
136
+ top: margin * scale,
137
+ width: cardW - margin * 2 * scale,
138
+ height: sceneHeight,
139
+ }}
140
+ >
141
+ <GlbEmbedViewer bare src={glbUrl} title={title} />
142
+ </div>
143
+ ) : null}
144
+ {inView && !frameOnly
145
+ ? entities.map((entity, index) => (
146
+ <CardSceneOverlay
147
+ key={`${entity.id}:${index}`}
148
+ entity={entity}
149
+ dataset={dataset}
150
+ lang={lang}
151
+ langMap={langMap}
152
+ scale={scale}
153
+ margin={margin}
154
+ interactive={interactive}
155
+ />
156
+ ))
157
+ : null}
158
+ {inView && !frameOnly
159
+ ? plainSlots.map((widget, index) => (
160
+ <SlotHitbox
161
+ key={`${widget.id}:${index}`}
162
+ widget={widget}
163
+ dataset={dataset}
164
+ lang={lang}
165
+ langMap={langMap}
166
+ nav={nav}
167
+ scale={scale}
168
+ margin={margin}
169
+ interactive={interactive}
170
+ tooltipLevel={slotTooltip}
171
+ />
172
+ ))
173
+ : null}
174
+ {inView && interactive ? (
175
+ <CardWidgetTips
176
+ widgets={recipe.widgets}
177
+ entities={entities}
178
+ margin={margin}
179
+ scale={scale}
180
+ />
181
+ ) : null}
182
+ {inView && strip.overflow && !frameOnly ? (
183
+ <CardStrip
184
+ geo={strip}
185
+ texts={stripTexts}
186
+ margin={margin}
187
+ scale={scale}
188
+ contentGuiW={contentGuiW}
189
+ dataset={dataset}
190
+ lang={lang}
191
+ langMap={langMap}
192
+ nav={nav}
193
+ interactive={interactive}
194
+ tooltipLevel={slotTooltip}
195
+ />
196
+ ) : null}
197
+ {inView
198
+ ? textWidgets.map((widget, index) =>
199
+ textInStrip(widget, strip) ? null : (
200
+ <CardText
201
+ key={`${widget.x}:${widget.y}:${index}`}
202
+ widget={widget}
203
+ offX={margin}
204
+ offY={margin}
205
+ scale={scale}
206
+ langMap={langMap}
207
+ />
208
+ ),
209
+ )
210
+ : null}
211
+ </div>
212
+ );
213
+ }
214
+
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import { EntryTooltip } from '@terrafirmagreg-team/game/components/entry/tooltip';
5
+ import { fetchEntityTexture } from '@terrafirmagreg-team/game/lib/entry/icon-load';
6
+ import { stackDisplayName } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
7
+ import { useAsyncResult } from '@terrafirmagreg-team/ui/hooks/use-async-value';
8
+ import type {
9
+ RecipeSceneEntity,
10
+ RecipeSceneRect,
11
+ } from '@terrafirmagreg-team/recipe/lib/data/scene';
12
+ import type { RecipeDecorWidget } from '@terrafirmagreg-team/recipe/lib/data/dto';
13
+
14
+ export function sceneEntityHit(
15
+ entities: RecipeSceneEntity[],
16
+ widget: RecipeDecorWidget,
17
+ ): boolean {
18
+ return entities.some((entity) => {
19
+ const region: RecipeSceneRect = entity.clip ?? entity;
20
+ return (
21
+ widget.x < region.x + region.w &&
22
+ widget.x + widget.w > region.x &&
23
+ widget.y < region.y + region.h &&
24
+ widget.y + widget.h > region.y
25
+ );
26
+ });
27
+ }
28
+
29
+ export function CardSceneOverlay({
30
+ entity,
31
+ dataset,
32
+ lang,
33
+ langMap,
34
+ scale,
35
+ margin,
36
+ interactive = true,
37
+ }: {
38
+ entity: RecipeSceneEntity;
39
+ dataset: DatasetRef;
40
+ lang: string;
41
+ langMap?: Record<string, string>;
42
+ scale: number;
43
+ margin: number;
44
+ interactive?: boolean;
45
+ }) {
46
+ const baked = entity.baked === true;
47
+ const tex = useAsyncResult(
48
+ !baked && dataset
49
+ ? async () => ({
50
+ key: entity.id,
51
+ url: await fetchEntityTexture(dataset, entity.id),
52
+ })
53
+ : false,
54
+ [baked, dataset, entity.id],
55
+ );
56
+ const url =
57
+ tex.value && tex.value.key === entity.id ? tex.value.url : undefined;
58
+ const missing = !baked && !tex.pending && url == null;
59
+ const region = entity.clip ?? entity;
60
+ const body = (
61
+ <span
62
+ className="absolute block"
63
+ style={{
64
+ left: (region.x + margin) * scale,
65
+ top: (region.y + margin) * scale,
66
+ width: (region.w || 16) * scale,
67
+ height: (region.h || 16) * scale,
68
+ }}
69
+ >
70
+ {url ? (
71
+ // eslint-disable-next-line @next/next/no-img-element
72
+ <img
73
+ src={url}
74
+ alt=""
75
+ draggable={false}
76
+ className="pointer-events-none absolute inset-0 m-auto h-full object-contain"
77
+ style={{ imageRendering: 'pixelated' }}
78
+ loading="lazy"
79
+ decoding="async"
80
+ />
81
+ ) : null}
82
+ {missing ? (
83
+ <span
84
+ aria-hidden
85
+ className="font-mc pointer-events-none absolute inset-0 flex items-center justify-center text-muted-foreground"
86
+ style={{ fontSize: 8 * scale }}
87
+ >
88
+ ?
89
+ </span>
90
+ ) : null}
91
+ </span>
92
+ );
93
+ if (!interactive) return body;
94
+ return (
95
+ <EntryTooltip
96
+ dataset={dataset}
97
+ id={entity.id}
98
+ name={stackDisplayName(langMap, entity.id, 'entity')}
99
+ kind="entity"
100
+ lang={lang}
101
+ langMap={langMap}
102
+ >
103
+ {body}
104
+ </EntryTooltip>
105
+ );
106
+ }
107
+
@@ -0,0 +1,198 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import { Fragment, useRef, useState } from 'react';
5
+ import { SlotHitbox } from '@terrafirmagreg-team/recipe/components/cards/slot-hitbox';
6
+ import { CardText } from '@terrafirmagreg-team/recipe/components/cards/card/text';
7
+ import { formatCount } from '@terrafirmagreg-team/ui/lib/utils/format';
8
+ import type { CardStripGeo } from '@terrafirmagreg-team/recipe/lib/card-model';
9
+ import type { EntryTooltipLevel } from '@terrafirmagreg-team/game/components/entry/tooltip';
10
+ import type { RecipeTextWidget } from '@terrafirmagreg-team/recipe/lib/data/dto';
11
+ import type { RecipeHrefOpts } from '@terrafirmagreg-team/recipe/types';
12
+
13
+ export function CardStrip({
14
+ geo,
15
+ texts,
16
+ margin,
17
+ scale,
18
+ contentGuiW,
19
+ dataset,
20
+ lang,
21
+ langMap,
22
+ nav,
23
+ interactive,
24
+ tooltipLevel,
25
+ }: {
26
+ geo: CardStripGeo;
27
+ texts: RecipeTextWidget[];
28
+ margin: number;
29
+ scale: number;
30
+ contentGuiW: number;
31
+ dataset: DatasetRef;
32
+ lang: string;
33
+ langMap?: Record<string, string>;
34
+ nav?: Pick<RecipeHrefOpts, 'tab' | 'search'>;
35
+ interactive?: boolean;
36
+ tooltipLevel?: EntryTooltipLevel;
37
+ }) {
38
+ const stripRef = useRef<HTMLDivElement>(null);
39
+ const barDrag = useRef<{ x: number; left: number } | null>(null);
40
+ const [stripScroll, setStripScroll] = useState(0);
41
+ const trackW = Math.max((contentGuiW - 8) * scale, 0);
42
+ const totalW = (geo.right + 2) * scale;
43
+ const maxScroll = Math.max(totalW - contentGuiW * scale, 1);
44
+ const thumbW = Math.max(
45
+ 16,
46
+ trackW * Math.min(1, (contentGuiW * scale) / totalW),
47
+ );
48
+ const thumbX =
49
+ (stripScroll / maxScroll) * Math.max(trackW - thumbW, 0);
50
+ return (
51
+ <>
52
+ <div
53
+ aria-hidden
54
+ className="absolute bg-mc-panel"
55
+ style={{
56
+ left: (margin + 3) * scale,
57
+ top: (geo.top + margin - 1) * scale,
58
+ width: (contentGuiW - 6) * scale,
59
+ height: Math.max(geo.patchBottom - (geo.top + margin - 1), 0) * scale,
60
+ }}
61
+ />
62
+ <div
63
+ ref={stripRef}
64
+ className="recipe-strip absolute overflow-x-auto overflow-y-hidden select-none"
65
+ style={{
66
+ left: margin * scale,
67
+ top: (geo.top + margin) * scale,
68
+ width: contentGuiW * scale,
69
+ height: (geo.bottom - geo.top + 2) * scale,
70
+ touchAction: 'pan-y',
71
+ }}
72
+ onScroll={(event) => setStripScroll(event.currentTarget.scrollLeft)}
73
+ onWheel={(event) => {
74
+ const el = stripRef.current;
75
+ if (!el || event.deltaY === 0) return;
76
+ event.stopPropagation();
77
+ el.scrollLeft += event.deltaY > 0 ? 40 : -40;
78
+ }}
79
+ onDragStart={(event) => event.preventDefault()}
80
+ >
81
+ <div
82
+ className="relative h-full"
83
+ style={{
84
+ width: totalW,
85
+ transform: `translateY(${-geo.top * scale}px)`,
86
+ }}
87
+ >
88
+ {geo.slots.map((widget, index) => (
89
+ <Fragment key={`${widget.id}:${index}`}>
90
+ <span
91
+ aria-hidden
92
+ className="absolute bg-mc-edge"
93
+ style={{
94
+ left: widget.x * scale,
95
+ top: widget.y * scale,
96
+ width: widget.w * scale,
97
+ height: widget.h * scale,
98
+ }}
99
+ />
100
+ <SlotHitbox
101
+ widget={widget}
102
+ dataset={dataset}
103
+ lang={lang}
104
+ langMap={langMap}
105
+ nav={nav}
106
+ scale={scale}
107
+ margin={0}
108
+ interactive={interactive}
109
+ tooltipLevel={tooltipLevel}
110
+ />
111
+ {widget.count !== undefined && widget.count > 1 ? (
112
+ <span
113
+ aria-hidden
114
+ className="font-mc pointer-events-none absolute leading-none text-white"
115
+ style={{
116
+ left: widget.x * scale,
117
+ top: (widget.y + widget.h - 8) * scale,
118
+ width: widget.w * scale,
119
+ paddingRight: scale,
120
+ fontSize: 7 * scale,
121
+ textAlign: 'right',
122
+ textShadow: `${scale}px ${scale}px 0 var(--color-mc-arrow)`,
123
+ }}
124
+ >
125
+ {formatCount(widget.count)}
126
+ </span>
127
+ ) : null}
128
+ </Fragment>
129
+ ))}
130
+ {texts.map((widget, index) => (
131
+ <CardText
132
+ key={`${widget.x}:${widget.y}:${index}`}
133
+ widget={widget}
134
+ scale={scale}
135
+ langMap={langMap}
136
+ />
137
+ ))}
138
+ </div>
139
+ </div>
140
+ <div
141
+ className="absolute"
142
+ style={{
143
+ left: (margin + 3) * scale,
144
+ top: geo.barTop * scale,
145
+ width: (contentGuiW - 8) * scale,
146
+ height: 4 * scale,
147
+ background: 'var(--color-mc-inset)',
148
+ borderTop: '1px solid var(--color-mc-bevel-dark)',
149
+ borderBottom: '1px solid var(--color-mc-bevel-light)',
150
+ }}
151
+ onPointerDown={(event) => {
152
+ const el = stripRef.current;
153
+ if (!el || event.button !== 0) return;
154
+ if (event.target !== event.currentTarget) return;
155
+ event.preventDefault();
156
+ const thumbRange = Math.max(trackW - thumbW, 1);
157
+ const trackX =
158
+ event.clientX - event.currentTarget.getBoundingClientRect().left;
159
+ el.scrollLeft =
160
+ ((trackX - thumbW / 2) / thumbRange) * maxScroll;
161
+ }}
162
+ >
163
+ <div
164
+ className="absolute cursor-ew-resize"
165
+ style={{
166
+ left: thumbX,
167
+ top: 0,
168
+ bottom: 0,
169
+ width: thumbW,
170
+ background: 'var(--color-mc-paper)',
171
+ boxShadow: `inset 0 ${scale}px 0 var(--color-mc-shadow), inset 0 -${scale}px 0 var(--color-mc-bevel-dark)`,
172
+ touchAction: 'none',
173
+ }}
174
+ onPointerDown={(event) => {
175
+ const el = stripRef.current;
176
+ if (!el || event.button !== 0) return;
177
+ event.preventDefault();
178
+ event.stopPropagation();
179
+ event.currentTarget.setPointerCapture(event.pointerId);
180
+ barDrag.current = { x: event.clientX, left: el.scrollLeft };
181
+ }}
182
+ onPointerMove={(event) => {
183
+ const drag = barDrag.current;
184
+ const el = stripRef.current;
185
+ if (!drag || !el) return;
186
+ const thumbRange = Math.max(trackW - thumbW, 1);
187
+ el.scrollLeft =
188
+ drag.left +
189
+ ((event.clientX - drag.x) / thumbRange) * maxScroll;
190
+ }}
191
+ onPointerUp={() => (barDrag.current = null)}
192
+ onPointerCancel={() => (barDrag.current = null)}
193
+ />
194
+ </div>
195
+ </>
196
+ );
197
+ }
198
+
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { McText } from '@terrafirmagreg-team/game/components/entry/mc-text';
4
+ import { dumpTextString } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
5
+ import type { RecipeTextWidget } from '@terrafirmagreg-team/recipe/lib/data/dto';
6
+
7
+ export function CardText({
8
+ widget,
9
+ offX = 0,
10
+ offY = 0,
11
+ scale,
12
+ langMap,
13
+ }: {
14
+ widget: RecipeTextWidget;
15
+ offX?: number;
16
+ offY?: number;
17
+ scale: number;
18
+ langMap?: Record<string, string>;
19
+ }) {
20
+ const content = widget.component
21
+ ? dumpTextString(langMap ?? {}, widget.component) || widget.text
22
+ : widget.text;
23
+ if (!content) return null;
24
+ const rgb = widget.color === undefined ? 0xffffff : widget.color & 0xffffff;
25
+ const shadowRgb = ((rgb & 0xfcfcfc) >> 2) & 0xffffff;
26
+ const shadowed = widget.shadow === true;
27
+ return (
28
+ <span
29
+ className="font-mc pointer-events-none absolute leading-none whitespace-pre"
30
+ style={{
31
+ left: (widget.x + offX) * scale,
32
+ top: (widget.y + offY) * scale,
33
+ fontSize: Math.max(6, (widget.h - 1 || 8) * scale),
34
+ color: `#${rgb.toString(16).padStart(6, '0')}`,
35
+ ...(shadowed
36
+ ? {
37
+ textShadow: `${scale}px ${scale}px 0 #${shadowRgb.toString(16).padStart(6, '0')}`,
38
+ }
39
+ : {}),
40
+ }}
41
+ >
42
+ <McText text={content} />
43
+ </span>
44
+ );
45
+ }
46
+