@terrafirmagreg-team/game 0.1.0 → 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 (86) hide show
  1. package/package.json +15 -14
  2. package/src/components/entry/atlas-sprite.tsx +22 -35
  3. package/src/components/entry/copy-id-menu.tsx +22 -29
  4. package/src/components/entry/entry.tsx +99 -20
  5. package/src/components/entry/group.tsx +27 -46
  6. package/src/components/entry/icon.tsx +21 -27
  7. package/src/components/entry/list.tsx +15 -18
  8. package/src/components/entry/mc-text.tsx +3 -89
  9. package/src/components/entry/stack-count.tsx +12 -8
  10. package/src/components/entry/tooltip.tsx +52 -44
  11. package/src/components/mdx/item-icon.tsx +23 -15
  12. package/src/components/preview-icon.tsx +2 -1
  13. package/src/hooks/use-dump-location.ts +4 -2
  14. package/src/hooks/use-entry-context.ts +3 -1
  15. package/src/hooks/use-icon-source.ts +22 -13
  16. package/src/hooks/use-lang-map.ts +4 -2
  17. package/src/lib/entry/display.ts +2 -1
  18. package/src/lib/entry/entry.ts +5 -3
  19. package/src/lib/entry/formula.ts +1 -0
  20. package/src/lib/entry/href.ts +20 -12
  21. package/src/lib/entry/icon-load.ts +67 -31
  22. package/src/lib/entry/icon.ts +3 -2
  23. package/src/lib/entry/tooltip.ts +12 -10
  24. package/src/lib/preview.ts +4 -2
  25. package/src/lib/search.ts +18 -19
  26. package/src/locales/en_us.json +3 -7
  27. package/src/locales/ru_ru.json +3 -7
  28. package/src/locales.ts +1 -0
  29. package/src/types/entry.ts +41 -2
  30. package/styles/fonts.css +18 -2
  31. package/src/components/entry/frame.tsx +0 -32
  32. package/src/components/entry/icon/tabs.tsx +0 -118
  33. package/src/lib/dump/assets/atlas/dto.ts +0 -49
  34. package/src/lib/dump/assets/atlas/lookup.ts +0 -206
  35. package/src/lib/dump/assets/atlas/sprites.ts +0 -130
  36. package/src/lib/dump/assets/lang/load.ts +0 -17
  37. package/src/lib/dump/assets/lang/names.ts +0 -244
  38. package/src/lib/dump/assets/urls.ts +0 -143
  39. package/src/lib/dump/client/atlases.ts +0 -83
  40. package/src/lib/dump/client/dump.ts +0 -188
  41. package/src/lib/dump/client/groups.ts +0 -31
  42. package/src/lib/dump/client/keybinds.ts +0 -11
  43. package/src/lib/dump/client/mods.ts +0 -16
  44. package/src/lib/dump/client/registry.ts +0 -125
  45. package/src/lib/dump/client/tabs.ts +0 -27
  46. package/src/lib/dump/client/tags.ts +0 -64
  47. package/src/lib/dump/contract/kind.ts +0 -61
  48. package/src/lib/dump/contract/read.ts +0 -77
  49. package/src/lib/dump/contract/ref.ts +0 -262
  50. package/src/lib/dump/contract/text.ts +0 -86
  51. package/src/lib/dump/data/registry/dto.ts +0 -56
  52. package/src/lib/dump/data/registry/parse.ts +0 -291
  53. package/src/lib/dump/data/tab/dto.ts +0 -13
  54. package/src/lib/dump/data/tab/parse.ts +0 -26
  55. package/src/lib/dump/data/tag/dto.ts +0 -7
  56. package/src/lib/dump/data/tag/parse.ts +0 -57
  57. package/src/lib/dump/gen/data/guides/book.ts +0 -56
  58. package/src/lib/dump/gen/data/guides/category.ts +0 -30
  59. package/src/lib/dump/gen/data/guides/entry.ts +0 -48
  60. package/src/lib/dump/gen/data/models/model.ts +0 -3
  61. package/src/lib/dump/gen/data/quests/chapter.ts +0 -126
  62. package/src/lib/dump/gen/data/quests/chapter_groups.ts +0 -11
  63. package/src/lib/dump/gen/data/quests/data.ts +0 -40
  64. package/src/lib/dump/gen/data/recipes/recipe.ts +0 -78
  65. package/src/lib/dump/gen/index.ts +0 -20
  66. package/src/lib/dump/gen/keybinds.ts +0 -3
  67. package/src/lib/dump/gen/lang.ts +0 -3
  68. package/src/lib/dump/gen/manifest.ts +0 -59
  69. package/src/lib/dump/gen/registry/blocks.ts +0 -60
  70. package/src/lib/dump/gen/registry/entities.ts +0 -49
  71. package/src/lib/dump/gen/registry/fluids.ts +0 -48
  72. package/src/lib/dump/gen/registry/groups.ts +0 -14
  73. package/src/lib/dump/gen/registry/items.ts +0 -51
  74. package/src/lib/dump/gen/registry/recipe_categories.ts +0 -64
  75. package/src/lib/dump/gen/registry/recipes.ts +0 -3
  76. package/src/lib/dump/gen/registry/tabs.ts +0 -46
  77. package/src/lib/dump/gen/registry/tags.ts +0 -11
  78. package/src/lib/dump/json-reader.ts +0 -231
  79. package/src/lib/dump/manifest/dto.ts +0 -11
  80. package/src/lib/dump/manifest/parse.ts +0 -167
  81. package/src/lib/dump/mods/dto.ts +0 -40
  82. package/src/lib/dump/mods/index.ts +0 -81
  83. package/src/lib/dump/mods/load.ts +0 -61
  84. package/src/lib/dump/paths.ts +0 -171
  85. package/src/lib/dump/reader.ts +0 -15
  86. package/src/lib/snbt.ts +0 -246
package/package.json CHANGED
@@ -1,25 +1,22 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/game",
3
- "version": "0.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
+ "description": "Game-data components and catalogs",
6
+ "license": "LGPL-3.0-only",
5
7
  "exports": {
6
8
  "./components/*": [
7
9
  "./src/components/*.tsx",
8
- "./src/components/*.ts",
9
10
  "./src/components/*/index.tsx",
10
11
  "./src/components/*/index.ts"
11
12
  ],
12
13
  "./hooks/*": [
13
14
  "./src/hooks/*.ts",
14
- "./src/hooks/*.tsx",
15
- "./src/hooks/*/index.ts",
16
- "./src/hooks/*/index.tsx"
15
+ "./src/hooks/*/index.ts"
17
16
  ],
18
17
  "./lib/*": [
19
18
  "./src/lib/*.ts",
20
- "./src/lib/*.tsx",
21
- "./src/lib/*/index.ts",
22
- "./src/lib/*/index.tsx"
19
+ "./src/lib/*/index.ts"
23
20
  ],
24
21
  "./types/*": [
25
22
  "./src/types/*.ts",
@@ -29,12 +26,16 @@
29
26
  "./locales": "./src/locales.ts"
30
27
  },
31
28
  "dependencies": {
32
- "lucide-react": "^1.63.0",
33
- "server-only": "^0.0.1",
34
- "@terrafirmagreg-team/translate": "0.1.0",
35
- "@terrafirmagreg-team/ui": "0.1.0",
36
- "@terrafirmagreg-team/kit": "0.1.0",
37
- "fumadocs-core": "16.17.8"
29
+ "@terrafirmagreg-team/data": "1.2.0",
30
+ "@terrafirmagreg-team/dump": "1.2.0",
31
+ "@terrafirmagreg-team/kit": "1.2.0",
32
+ "@terrafirmagreg-team/search": "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
+ "fumadocs-core": "16.17.8",
37
+ "lucide-react": "^0.577.0",
38
+ "server-only": "^0.0.1"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "next": ">=15",
@@ -1,7 +1,9 @@
1
1
  'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
3
 
3
- import { atlasPagePublicUrl } from '@terrafirmagreg-team/game/lib/dump/assets/atlas/lookup';
4
- import type { AtlasLookup } from '@terrafirmagreg-team/game/lib/dump/assets/atlas/dto';
4
+ import { atlasPagePublicUrl } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/lookup';
5
+ import type { AtlasLookup } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/dto';
6
+ import { Sprite } from '@terrafirmagreg-team/ui/components/blocks/sprite';
5
7
  import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
6
8
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
7
9
 
@@ -11,7 +13,7 @@ export function AtlasSprite({
11
13
  size = 16,
12
14
  className,
13
15
  }: {
14
- dataset: string;
16
+ dataset: DatasetRef;
15
17
  lookup: AtlasLookup | null;
16
18
  size?: number;
17
19
  className?: string;
@@ -19,39 +21,24 @@ export function AtlasSprite({
19
21
  const url = lookup
20
22
  ? atlasPagePublicUrl(dataset, lookup.atlas, lookup.page)
21
23
  : undefined;
22
- if (!lookup || !url) {
23
- return <Skeleton className={cn('inline-block shrink-0', className)} style={{ width: size, height: size }} />;
24
- }
25
- const { sprite, page } = lookup;
26
- const scale = size / Math.max(sprite.w, 1);
27
- const pos = `-${sprite.x * scale}px -${sprite.y * scale}px`;
28
- const bgSize = `${page.w * scale}px ${page.h * scale}px`;
29
- const tint = sprite.tintColor && sprite.tint !== 'none' ? sprite.tintColor : undefined;
30
24
  return (
31
- <span
32
- className={className ?? 'relative inline-block shrink-0'}
33
- style={{
34
- width: size,
35
- height: size,
36
- backgroundImage: `url(${url})`,
37
- backgroundPosition: pos,
38
- backgroundSize: bgSize,
39
- imageRendering: 'pixelated',
40
- ...(tint
41
- ? {
42
- backgroundColor: tint,
43
- backgroundBlendMode: 'multiply',
44
- WebkitMaskImage: `url(${url})`,
45
- maskImage: `url(${url})`,
46
- WebkitMaskPosition: pos,
47
- maskPosition: pos,
48
- WebkitMaskSize: bgSize,
49
- maskSize: bgSize,
50
- WebkitMaskRepeat: 'no-repeat',
51
- maskRepeat: 'no-repeat',
52
- }
53
- : {}),
54
- }}
25
+ <Sprite
26
+ src={url}
27
+ rect={lookup ? lookup.sprite : undefined}
28
+ page={lookup ? lookup.page : undefined}
29
+ tint={
30
+ lookup?.sprite.tintColor && lookup.sprite.tint !== 'none'
31
+ ? lookup.sprite.tintColor
32
+ : undefined
33
+ }
34
+ size={size}
35
+ className={className}
36
+ fallback={
37
+ <Skeleton
38
+ className={cn('inline-block shrink-0', className)}
39
+ style={{ width: size, height: size }}
40
+ />
41
+ }
55
42
  />
56
43
  );
57
44
  }
@@ -2,12 +2,7 @@
2
2
 
3
3
  import { Copy } from 'lucide-react';
4
4
  import type { ReactNode } from 'react';
5
- import {
6
- ContextMenu,
7
- ContextMenuContent,
8
- ContextMenuItem,
9
- ContextMenuTrigger,
10
- } from '@terrafirmagreg-team/ui/components/ui/context-menu';
5
+ import { CopyMenu } from '@terrafirmagreg-team/ui/components/blocks/nav/copy-menu';
11
6
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
12
7
 
13
8
  export function CopyIdMenu({
@@ -27,29 +22,27 @@ export function CopyIdMenu({
27
22
  children: ReactNode;
28
23
  }) {
29
24
  const t = useTranslations();
30
- const copy = (value: string) => () => {
31
- void navigator.clipboard.writeText(value);
32
- };
33
25
  return (
34
- <ContextMenu>
35
- <ContextMenuTrigger className="contents">{children}</ContextMenuTrigger>
36
- <ContextMenuContent>
37
- <ContextMenuItem onClick={copy(id)}>
38
- <Copy />
39
- {t(label ?? 'ui.entry.copy')}
40
- </ContextMenuItem>
41
- {nbt ? (
42
- <ContextMenuItem onClick={copy(`${id}${nbt}`)}>
43
- {t('ui.entry.copy_stack')}
44
- </ContextMenuItem>
45
- ) : null}
46
- {nbt ? (
47
- <ContextMenuItem onClick={copy(nbt)}>
48
- {t('ui.entry.copy_nbt')}
49
- </ContextMenuItem>
50
- ) : null}
51
- {extra}
52
- </ContextMenuContent>
53
- </ContextMenu>
26
+ <CopyMenu
27
+ items={[
28
+ {
29
+ icon: <Copy />,
30
+ label: label ?? t('Copy id', { note: 'ui.entry.copy' }),
31
+ value: id,
32
+ },
33
+ ...(nbt
34
+ ? [
35
+ {
36
+ label: t('Copy stack id', { note: 'ui.entry.copy_stack' }),
37
+ value: `${id}${nbt}`,
38
+ },
39
+ { label: t('Copy NBT', { note: 'ui.entry.copy_nbt' }), value: nbt },
40
+ ]
41
+ : []),
42
+ ]}
43
+ extra={extra}
44
+ >
45
+ {children}
46
+ </CopyMenu>
54
47
  );
55
48
  }
@@ -1,12 +1,16 @@
1
1
  'use client';
2
2
 
3
3
  import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { parseSectionLocation } from '@terrafirmagreg-team/sites/location';
4
6
  import type { ReactElement } from 'react';
5
7
  import { EntryIcon } from '@terrafirmagreg-team/game/components/entry/icon';
6
8
  import { EntryTooltip } from '@terrafirmagreg-team/game/components/entry/tooltip';
7
9
  import { CopyIdMenu } from '@terrafirmagreg-team/game/components/entry/copy-id-menu';
8
10
  import { useEntryContext } from '@terrafirmagreg-team/game/hooks/use-entry-context';
9
11
  import { useLangMap } from '@terrafirmagreg-team/game/hooks/use-lang-map';
12
+ import { ResolvedLangContext } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
13
+ import { useContext, useMemo } from 'react';
10
14
  import {
11
15
  Tooltip,
12
16
  TooltipContent,
@@ -17,9 +21,13 @@ import {
17
21
  isIconKind,
18
22
  isTagKind,
19
23
  refId,
20
- } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
21
- import { formatNbt } from '@terrafirmagreg-team/game/lib/snbt';
22
- import { stackIdFromRef } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
24
+ } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
25
+ import { formatNbt } from '@terrafirmagreg-team/dump/lib/snbt';
26
+ import { fetchEntityModel } from '@terrafirmagreg-team/game/lib/entry/icon-load';
27
+ import { useDumpLocation } from '@terrafirmagreg-team/game/hooks/use-dump-location';
28
+ import { createDumpAssetResolver } from '@terrafirmagreg-team/dump/lib/dump/assets/urls';
29
+ import { GlbViewer } from '@terrafirmagreg-team/ui/components/blocks/media/glb/viewer';
30
+ import { stackIdFromRef } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
23
31
  import { stackDisplay } from '@terrafirmagreg-team/game/lib/entry/display';
24
32
  import { loadStackDisplay } from '@terrafirmagreg-team/game/lib/entry/tooltip';
25
33
  import { useAsyncResult } from '@terrafirmagreg-team/ui/hooks/use-async-value';
@@ -29,7 +37,16 @@ import {
29
37
  usesStackTooltip,
30
38
  } from '@terrafirmagreg-team/game/lib/entry/entry';
31
39
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
32
- import type { KindEntryProps, EntryProps } from '@terrafirmagreg-team/game/types/entry';
40
+ import type {
41
+ BlockDirectiveProps,
42
+ EntityDirectiveProps,
43
+ EntryProps,
44
+ FluidDirectiveProps,
45
+ ItemDirectiveProps,
46
+ KindEntryProps,
47
+ RecipeDirectiveProps,
48
+ TagDirectiveProps,
49
+ } from '@terrafirmagreg-team/game/types/entry';
33
50
 
34
51
  export function Entry({
35
52
  reference: referenceProp,
@@ -51,11 +68,17 @@ export function Entry({
51
68
  missing,
52
69
  tooltip,
53
70
  children,
71
+ entityView,
72
+ icon: iconProp,
54
73
  ...hrefOpts
55
74
  }: EntryProps) {
56
75
  const ctx = useEntryContext();
57
- const dataset = datasetProp ?? ctx?.dataset;
58
- const lang = langProp ?? ctx?.lang;
76
+ const pathname = usePathname();
77
+ const locId = parseSectionLocation(pathname).dataset;
78
+ const dataset =
79
+ datasetProp ?? ctx?.dataset ?? (locId ? { id: locId } : undefined);
80
+ const resolvedLang = useContext(ResolvedLangContext)?.lang;
81
+ const lang = langProp ?? ctx?.lang ?? resolvedLang;
59
82
  const fetchedLangMap = useLangMap(
60
83
  dataset,
61
84
  lang,
@@ -77,7 +100,7 @@ export function Entry({
77
100
  refKind === 'block' ||
78
101
  refKind === 'fluid' ||
79
102
  refKind === 'entity')
80
- ? `${dataset}:${stackId}`
103
+ ? `${dataset.id}/${dataset.version ?? ''}:${stackId}`
81
104
  : undefined;
82
105
  const displayPeek =
83
106
  displayKey && dataset
@@ -144,9 +167,15 @@ export function Entry({
144
167
 
145
168
  const icon = (
146
169
  <EntryIcon
147
- reference={reference}
148
- id={refIdStr}
149
- kind={iconKind}
170
+ reference={iconProp ?? reference}
171
+ id={iconProp ? refId(iconProp) : refIdStr}
172
+ kind={
173
+ iconProp
174
+ ? isIconKind(iconProp.kind)
175
+ ? iconProp.kind
176
+ : undefined
177
+ : iconKind
178
+ }
150
179
  tag={tag}
151
180
  dataset={dataset}
152
181
  size={size}
@@ -155,6 +184,7 @@ export function Entry({
155
184
  missing={missing}
156
185
  chip={variant === 'chip'}
157
186
  preferVariant={preferVariant}
187
+ entityView={entityView}
158
188
  badge={badge}
159
189
  badgeClassName={badgeClassName}
160
190
  >
@@ -212,18 +242,67 @@ export function Entry({
212
242
  );
213
243
  }
214
244
 
215
- function kindEntry(kind: EntryProps['kind']) {
216
- function KindEntry(props: KindEntryProps) {
245
+ function kindEntry<P extends KindEntryProps>(kind: EntryProps['kind']) {
246
+ function KindEntry(props: P) {
217
247
  return <Entry kind={kind} {...props} />;
218
248
  }
219
249
  return KindEntry;
220
250
  }
221
251
 
222
- export const ItemEntry = kindEntry('item');
223
- export const BlockEntry = kindEntry('block');
224
- export const FluidEntry = kindEntry('fluid');
225
- export const EntityEntry = kindEntry('entity');
226
- export const TagEntry = kindEntry('itemTag');
227
- export const RecipeEntry = kindEntry('recipe');
228
- export const QuestEntry = kindEntry('quest');
229
- export const GuideEntry = kindEntry('guide');
252
+ export const ItemEntry = kindEntry<ItemDirectiveProps>('item');
253
+ export const BlockEntry = kindEntry<BlockDirectiveProps>('block');
254
+ export const FluidEntry = kindEntry<FluidDirectiveProps>('fluid');
255
+ export function EntityEntry({
256
+ view = 'auto',
257
+ id,
258
+ dataset: datasetProp,
259
+ name,
260
+ ...rest
261
+ }: EntityDirectiveProps) {
262
+ const ctx = useEntryContext();
263
+ const pathname = usePathname();
264
+ const locId = parseSectionLocation(pathname).dataset;
265
+ const dataset = useMemo(
266
+ () =>
267
+ datasetProp ?? ctx?.dataset ?? (locId ? { id: locId } : undefined),
268
+ [datasetProp, ctx, locId],
269
+ );
270
+ const modelKey = dataset && id ? `${dataset.id}/${dataset.version ?? ''}:${id}` : undefined;
271
+ const modelRes = useAsyncResult(
272
+ view === 'model' && modelKey
273
+ ? async () => ({
274
+ key: modelKey,
275
+ url: await fetchEntityModel(dataset!, id!),
276
+ })
277
+ : false,
278
+ [view, dataset, id, modelKey],
279
+ );
280
+ useDumpLocation(dataset);
281
+ const resolveResource = useMemo(
282
+ () => (dataset ? createDumpAssetResolver(dataset) : undefined),
283
+ [dataset],
284
+ );
285
+ if (view === 'model') {
286
+ const model = modelRes.value;
287
+ const src = model && model.key === modelKey ? model.url : undefined;
288
+ if (src === undefined) return null;
289
+ return src ? (
290
+ <GlbViewer src={src} title={name ?? id} resolveResource={resolveResource} />
291
+ ) : (
292
+ <Entry kind="entity" id={id} dataset={dataset} name={name} {...rest} />
293
+ );
294
+ }
295
+ return (
296
+ <Entry
297
+ kind="entity"
298
+ id={id}
299
+ dataset={dataset}
300
+ name={name}
301
+ entityView={view}
302
+ {...rest}
303
+ />
304
+ );
305
+ }
306
+ export const TagEntry = kindEntry<TagDirectiveProps>('itemTag');
307
+ export const RecipeEntry = kindEntry<RecipeDirectiveProps>('recipe');
308
+
@@ -1,6 +1,11 @@
1
1
  'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
3
 
3
- import { EntryFrame } from '@terrafirmagreg-team/game/components/entry/frame';
4
+ import {
5
+ SPRITE_CLUSTER_INSET,
6
+ SPRITE_CLUSTER_UNITS,
7
+ SpriteCluster,
8
+ } from '@terrafirmagreg-team/ui/components/blocks/sprite';
4
9
  import { EntryIcon, entrySizePx, ENTRY_CYCLE_MS } from '@terrafirmagreg-team/game/components/entry/icon';
5
10
  import { useCycleIndex } from '@terrafirmagreg-team/ui/hooks/use-cycle-index';
6
11
  import { useAsyncResult } from '@terrafirmagreg-team/ui/hooks/use-async-value';
@@ -12,26 +17,11 @@ import {
12
17
  tagKindOf,
13
18
  type ReferenceKind,
14
19
  type TaggedReference,
15
- } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
20
+ } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
16
21
  import type { EntrySize } from '@terrafirmagreg-team/game/types/entry';
17
22
 
18
- const GAME_SLOT = 18;
19
23
  const GAME_ICON = 12.8;
20
24
  const STACK_SCALE = 1.15;
21
- const FRAME_INSET = 6;
22
- const POSITIONS: readonly (readonly [number, number])[][] = [
23
- [],
24
- [[1.6, 1.6]],
25
- [
26
- [0.4, 0.8],
27
- [2.8, 2.4],
28
- ],
29
- [
30
- [-0.8, 0],
31
- [1.6, 1.6],
32
- [4.0, 3.2],
33
- ],
34
- ];
35
25
 
36
26
  function memberRef(
37
27
  member: string | TaggedReference,
@@ -47,7 +37,7 @@ function memberKey(member: string | TaggedReference, kind: ReferenceKind): strin
47
37
 
48
38
  async function expandTagRefs(
49
39
  refs: readonly TaggedReference[],
50
- dataset: string,
40
+ dataset: DatasetRef,
51
41
  ): Promise<TaggedReference[]> {
52
42
  const groups = await Promise.all(
53
43
  refs.map((ref) =>
@@ -90,7 +80,7 @@ export function EntryGroup({
90
80
  kind?: ReferenceKind;
91
81
  size?: EntrySize;
92
82
  chip?: boolean;
93
- dataset?: string;
83
+ dataset?: DatasetRef;
94
84
  limit?: number;
95
85
  cycle?: number;
96
86
  missing?: 'placeholder' | 'item' | 'hide';
@@ -103,7 +93,7 @@ export function EntryGroup({
103
93
  cycle !== undefined &&
104
94
  dataset &&
105
95
  references.some((ref) => isTagKind(ref.kind))
106
- ? `${dataset}|${references
96
+ ? `${dataset.id}/${dataset.version ?? ''}|${references
107
97
  .map((ref) => `${ref.kind}:${stackIdFromRef(ref)}`)
108
98
  .join('|')}`
109
99
  : null;
@@ -149,32 +139,23 @@ export function EntryGroup({
149
139
  </span>
150
140
  );
151
141
  }
152
- const inner = sizePx - (chip ? FRAME_INSET : 0);
153
- const scale = inner / GAME_SLOT;
154
- const icons = members.slice(0, Math.min(limit, POSITIONS.length - 1));
155
- const positions = POSITIONS[icons.length];
142
+ const scale =
143
+ (sizePx - (chip ? SPRITE_CLUSTER_INSET : 0)) / SPRITE_CLUSTER_UNITS;
156
144
  return (
157
- <EntryFrame size={sizePx} chip={chip}>
158
- <span className="relative block" style={{ width: inner, height: inner }}>
159
- {icons.map((member, i) => (
160
- <span
161
- key={memberKey(member, kind)}
162
- className="absolute"
163
- style={{
164
- left: positions[i][0] * STACK_SCALE * scale,
165
- top: positions[i][1] * STACK_SCALE * scale,
166
- zIndex: icons.length - i,
167
- }}
168
- >
169
- <EntryIcon
170
- reference={memberRef(member, kind)}
171
- dataset={dataset}
172
- size={GAME_ICON * STACK_SCALE * scale}
173
- missing={missing}
174
- />
175
- </span>
176
- ))}
177
- </span>
178
- </EntryFrame>
145
+ <SpriteCluster
146
+ size={sizePx}
147
+ chip={chip}
148
+ limit={limit}
149
+ items={members.slice(0, limit).map((member) => (
150
+ <EntryIcon
151
+ key={memberKey(member, kind)}
152
+ reference={memberRef(member, kind)}
153
+ dataset={dataset}
154
+ size={GAME_ICON * STACK_SCALE * scale}
155
+ missing={missing}
156
+ />
157
+ ))}
158
+ />
179
159
  );
180
160
  }
161
+
@@ -1,17 +1,20 @@
1
1
  'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
2
3
 
3
4
  import { useState } from 'react';
4
5
  import { useEntryContext } from '@terrafirmagreg-team/game/hooks/use-entry-context';
5
- import { EntryFrame } from '@terrafirmagreg-team/game/components/entry/frame';
6
+ import {
7
+ Sprite,
8
+ SpriteFrame,
9
+ } from '@terrafirmagreg-team/ui/components/blocks/sprite';
6
10
  import { AtlasSprite } from '@terrafirmagreg-team/game/components/entry/atlas-sprite';
7
11
  import { useIconSource } from '@terrafirmagreg-team/game/hooks/use-icon-source';
8
12
  import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
9
- import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
10
13
  import type {
11
14
  DumpNbt,
12
15
  ReferenceKind,
13
16
  TaggedReference,
14
- } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
17
+ } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
15
18
  import type { EntrySize } from '@terrafirmagreg-team/game/types/entry';
16
19
 
17
20
  export { ENTRY_CYCLE_MS } from '@terrafirmagreg-team/game/hooks/use-icon-source';
@@ -34,6 +37,7 @@ export function EntryIcon({
34
37
  missing = 'placeholder',
35
38
  chip = false,
36
39
  preferVariant = false,
40
+ entityView,
37
41
  badge,
38
42
  badgeClassName,
39
43
  children,
@@ -43,7 +47,7 @@ export function EntryIcon({
43
47
  id?: string;
44
48
  kind?: ReferenceKind;
45
49
  tag?: DumpNbt;
46
- dataset?: string;
50
+ dataset?: DatasetRef;
47
51
  size?: EntrySize;
48
52
  labeled?: boolean;
49
53
  label?: string;
@@ -51,6 +55,7 @@ export function EntryIcon({
51
55
  chip?: boolean;
52
56
 
53
57
  preferVariant?: boolean;
58
+ entityView?: 'auto' | 'texture' | 'egg';
54
59
  badge?: React.ReactNode;
55
60
  badgeClassName?: string;
56
61
  children?: React.ReactNode;
@@ -66,6 +71,7 @@ export function EntryIcon({
66
71
  tag,
67
72
  missing,
68
73
  preferVariant,
74
+ entityView,
69
75
  });
70
76
  const caption = label || undefined;
71
77
  const name = caption ?? source.rawId;
@@ -95,16 +101,10 @@ export function EntryIcon({
95
101
  </span>
96
102
  );
97
103
  const img = (src: string) => (
98
- // eslint-disable-next-line @next/next/no-img-element
99
- <img
104
+ <Sprite
100
105
  src={src}
101
106
  alt={name}
102
- width={size}
103
- height={size}
104
- className="rounded-sm"
105
- style={{ width: size, height: size, imageRendering: 'pixelated' }}
106
- loading="lazy"
107
- decoding="async"
107
+ size={size}
108
108
  onError={() =>
109
109
  setFileFailed({ key: imgSrc ?? source.sourceKey, failed: true })
110
110
  }
@@ -121,12 +121,14 @@ export function EntryIcon({
121
121
  missing={missing}
122
122
  />
123
123
  );
124
+ } else if (source.isEntity && imgSrc && !failed) {
125
+ icon = img(imgSrc);
126
+ } else if (source.isEntity && source.awaitingTexture) {
127
+ icon = hideMissing ? null : pendingBox;
124
128
  } else if (source.lookup && dataset) {
125
129
  icon = <AtlasSprite dataset={dataset} lookup={source.lookup} size={size} />;
126
130
  } else if (source.pending || source.awaitingAtlas) {
127
131
  icon = hideMissing ? null : pendingBox;
128
- } else if (source.isEntity && imgSrc && !failed) {
129
- icon = img(imgSrc);
130
132
  } else if (source.awaitingTexture) {
131
133
  icon = hideMissing ? null : pendingBox;
132
134
  } else if (source.usesAtlas || !imgSrc || failed) {
@@ -150,23 +152,14 @@ export function EntryIcon({
150
152
  if (!icon && !labeled && !children) return null;
151
153
 
152
154
  const framed = (
153
- <EntryFrame
155
+ <SpriteFrame
154
156
  size={size}
155
157
  chip={chip}
156
- className={badge !== undefined ? 'relative' : undefined}
158
+ badge={badge}
159
+ badgeClassName={badgeClassName}
157
160
  >
158
161
  {icon}
159
- {badge !== undefined ? (
160
- <span
161
- className={cn(
162
- 'pointer-events-none absolute -right-0.5 -bottom-0.5 z-10',
163
- badgeClassName,
164
- )}
165
- >
166
- {badge}
167
- </span>
168
- ) : null}
169
- </EntryFrame>
162
+ </SpriteFrame>
170
163
  );
171
164
 
172
165
  if (!labeled && !children) return framed;
@@ -185,3 +178,4 @@ export function EntryIcon({
185
178
  </span>
186
179
  );
187
180
  }
181
+