@terrafirmagreg-team/game 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.
@@ -0,0 +1,181 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import { useState } from 'react';
5
+ import { useEntryContext } from '@terrafirmagreg-team/game/hooks/use-entry-context';
6
+ import {
7
+ Sprite,
8
+ SpriteFrame,
9
+ } from '@terrafirmagreg-team/ui/components/blocks/sprite';
10
+ import { AtlasSprite } from '@terrafirmagreg-team/game/components/entry/atlas-sprite';
11
+ import { useIconSource } from '@terrafirmagreg-team/game/hooks/use-icon-source';
12
+ import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
13
+ import type {
14
+ DumpNbt,
15
+ ReferenceKind,
16
+ TaggedReference,
17
+ } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
18
+ import type { EntrySize } from '@terrafirmagreg-team/game/types/entry';
19
+
20
+ export { ENTRY_CYCLE_MS } from '@terrafirmagreg-team/game/hooks/use-icon-source';
21
+
22
+ const ENTRY_SIZE_PX = { sm: 16, md: 24, lg: 32, xl: 40 } as const;
23
+
24
+ export function entrySizePx(size: EntrySize): number {
25
+ return typeof size === 'number' ? size : ENTRY_SIZE_PX[size];
26
+ }
27
+
28
+ export function EntryIcon({
29
+ reference,
30
+ id,
31
+ kind,
32
+ tag,
33
+ dataset: datasetProp,
34
+ size: sizeProp = 'md',
35
+ labeled = false,
36
+ label,
37
+ missing = 'placeholder',
38
+ chip = false,
39
+ preferVariant = false,
40
+ entityView,
41
+ badge,
42
+ badgeClassName,
43
+ children,
44
+ }: {
45
+
46
+ reference?: TaggedReference;
47
+ id?: string;
48
+ kind?: ReferenceKind;
49
+ tag?: DumpNbt;
50
+ dataset?: DatasetRef;
51
+ size?: EntrySize;
52
+ labeled?: boolean;
53
+ label?: string;
54
+ missing?: 'placeholder' | 'item' | 'hide';
55
+ chip?: boolean;
56
+
57
+ preferVariant?: boolean;
58
+ entityView?: 'auto' | 'texture' | 'egg';
59
+ badge?: React.ReactNode;
60
+ badgeClassName?: string;
61
+ children?: React.ReactNode;
62
+ }) {
63
+ const size = entrySizePx(sizeProp);
64
+ const ctx = useEntryContext();
65
+ const dataset = datasetProp ?? ctx?.dataset;
66
+ const source = useIconSource({
67
+ dataset,
68
+ reference,
69
+ id,
70
+ kind,
71
+ tag,
72
+ missing,
73
+ preferVariant,
74
+ entityView,
75
+ });
76
+ const caption = label || undefined;
77
+ const name = caption ?? source.rawId;
78
+ const [fileFailed, setFileFailed] = useState<{
79
+ key: string;
80
+ failed: boolean;
81
+ }>({ key: '', failed: false });
82
+ const hideMissing = missing === 'hide';
83
+ const imgSrc = source.imgSrc;
84
+
85
+ const failed =
86
+ fileFailed.failed && fileFailed.key === (imgSrc ?? source.sourceKey);
87
+
88
+ const boxStyle = { width: size, height: size };
89
+ const pendingBox = (
90
+ <Skeleton
91
+ className="inline-block shrink-0 rounded-sm"
92
+ style={boxStyle}
93
+ />
94
+ );
95
+ const missingBox = (
96
+ <span
97
+ className="font-mc inline-flex shrink-0 items-center justify-center rounded-sm border border-border bg-secondary text-muted-foreground"
98
+ style={{ ...boxStyle, fontSize: Math.max(7, Math.round(size * 0.6)) }}
99
+ >
100
+ ?
101
+ </span>
102
+ );
103
+ const img = (src: string) => (
104
+ <Sprite
105
+ src={src}
106
+ alt={name}
107
+ size={size}
108
+ onError={() =>
109
+ setFileFailed({ key: imgSrc ?? source.sourceKey, failed: true })
110
+ }
111
+ />
112
+ );
113
+
114
+ let icon: React.ReactNode = null;
115
+ if (source.cycleRef && dataset) {
116
+ icon = (
117
+ <EntryIcon
118
+ reference={source.cycleRef}
119
+ dataset={dataset}
120
+ size={size}
121
+ missing={missing}
122
+ />
123
+ );
124
+ } else if (source.isEntity && imgSrc && !failed) {
125
+ icon = img(imgSrc);
126
+ } else if (source.isEntity && source.awaitingTexture) {
127
+ icon = hideMissing ? null : pendingBox;
128
+ } else if (source.lookup && dataset) {
129
+ icon = <AtlasSprite dataset={dataset} lookup={source.lookup} size={size} />;
130
+ } else if (source.pending || source.awaitingAtlas) {
131
+ icon = hideMissing ? null : pendingBox;
132
+ } else if (source.awaitingTexture) {
133
+ icon = hideMissing ? null : pendingBox;
134
+ } else if (source.usesAtlas || !imgSrc || failed) {
135
+ icon = hideMissing
136
+ ? null
137
+ : missing === 'item' && source.missingLookup && dataset
138
+ ? (
139
+ <AtlasSprite
140
+ dataset={dataset}
141
+ lookup={source.missingLookup}
142
+ size={size}
143
+ />
144
+ )
145
+ : missing === 'item' && source.missingPending
146
+ ? pendingBox
147
+ : missingBox;
148
+ } else {
149
+ icon = img(imgSrc);
150
+ }
151
+
152
+ if (!icon && !labeled && !children) return null;
153
+
154
+ const framed = (
155
+ <SpriteFrame
156
+ size={size}
157
+ chip={chip}
158
+ badge={badge}
159
+ badgeClassName={badgeClassName}
160
+ >
161
+ {icon}
162
+ </SpriteFrame>
163
+ );
164
+
165
+ if (!labeled && !children) return framed;
166
+
167
+ return (
168
+ <span className="inline-flex items-center gap-2 align-middle not-prose">
169
+ {framed}
170
+ {labeled ? (
171
+ caption && caption !== source.rawId ? (
172
+ <span className="text-xs">{caption}</span>
173
+ ) : (
174
+ <code className="text-xs">{source.rawId}</code>
175
+ )
176
+ ) : null}
177
+ {children}
178
+ </span>
179
+ );
180
+ }
181
+
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
5
+ import { SpriteList } from '@terrafirmagreg-team/ui/components/blocks/sprite';
6
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
7
+ import type { TaggedReference } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
8
+
9
+ export function EntryList({
10
+ items,
11
+ dataset,
12
+ lang,
13
+ langMap,
14
+ tab,
15
+ type,
16
+ variant = 'grid',
17
+ labeled,
18
+ href,
19
+ className,
20
+ }: {
21
+ items: TaggedReference[];
22
+ dataset?: DatasetRef;
23
+ lang?: Lang;
24
+ langMap?: Record<string, string>;
25
+ tab?: string;
26
+ type?: string;
27
+ variant?: 'grid' | 'row';
28
+
29
+ labeled?: boolean;
30
+
31
+ href?: string | false;
32
+ className?: string;
33
+ }) {
34
+ return (
35
+ <SpriteList
36
+ variant={variant}
37
+ className={className}
38
+ items={items.map((item, index) => ({
39
+ key: `${item.kind}:${item.id}:${index}`,
40
+ node:
41
+ variant === 'grid' ? (
42
+ <Entry
43
+ reference={item}
44
+ dataset={dataset}
45
+ lang={lang}
46
+ langMap={langMap}
47
+ tab={tab}
48
+ type={type}
49
+ href={href}
50
+ variant="chip"
51
+ size="lg"
52
+ className="size-9"
53
+ />
54
+ ) : (
55
+ <Entry
56
+ reference={item}
57
+ dataset={dataset}
58
+ langMap={langMap}
59
+ labeled={labeled}
60
+ href={href}
61
+ />
62
+ ),
63
+ }))}
64
+ />
65
+ );
66
+ }
67
+
@@ -0,0 +1,6 @@
1
+ import { FormatText } from '@terrafirmagreg-team/ui/components/blocks/text/format-text';
2
+ import { MC_FORMAT } from '@terrafirmagreg-team/dump/lib/dump/contract/text';
3
+
4
+ export function McText({ text }: { text: string }) {
5
+ return <FormatText text={text} format={MC_FORMAT} />;
6
+ }
@@ -0,0 +1,18 @@
1
+ import { SpriteCount } from '@terrafirmagreg-team/ui/components/blocks/sprite';
2
+
3
+ export function StackCount({
4
+ count,
5
+ limit,
6
+ }: {
7
+ count: number;
8
+ limit?: number;
9
+ }) {
10
+ return (
11
+ <SpriteCount
12
+ count={count}
13
+ limit={limit}
14
+ className="font-mc pointer-events-none leading-none text-white"
15
+ style={{ fontSize: 9, textShadow: '1px 1px 0 var(--color-mc-arrow)' }}
16
+ />
17
+ );
18
+ }
@@ -0,0 +1,137 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import { useState, type ReactElement, type ReactNode } from 'react';
5
+ import { useModifierKeys } from '@terrafirmagreg-team/ui/hooks/use-modifier-keys';
6
+ import { LazyTooltip } from '@terrafirmagreg-team/ui/components/blocks/tooltip/lazy';
7
+ import { stackTooltip } from '@terrafirmagreg-team/game/lib/entry/tooltip';
8
+ import { formulaRuns } from '@terrafirmagreg-team/game/lib/entry/formula';
9
+ import { McText } from '@terrafirmagreg-team/game/components/entry/mc-text';
10
+ import { stripMcFormat } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
11
+
12
+ const ENCHANTED_TEXT_COLOR = 'var(--color-mc-enchant)';
13
+
14
+ export type EntryTooltipLevel =
15
+ | 'compact'
16
+ | 'extended'
17
+ | 'full'
18
+ | 'adaptive'
19
+ | 'none';
20
+
21
+ function TooltipLine({ text }: { text: string }) {
22
+ const runs = formulaRuns(stripMcFormat(text));
23
+ if (!runs) return <McText text={text} />;
24
+ return (
25
+ <>
26
+ {runs.map((run, index) =>
27
+ run.color ? (
28
+ <span key={index} style={{ color: run.color }}>
29
+ {run.text}
30
+ </span>
31
+ ) : (
32
+ run.text
33
+ ),
34
+ )}
35
+ </>
36
+ );
37
+ }
38
+
39
+ export function EntryTooltip({
40
+ dataset,
41
+ id,
42
+ name,
43
+ kind,
44
+ lang,
45
+ langMap,
46
+ level = 'full',
47
+ footer,
48
+ children,
49
+ }: {
50
+ dataset: DatasetRef;
51
+ id: string;
52
+ name: string;
53
+ kind?: 'item' | 'fluid' | 'entity';
54
+
55
+ lang?: string;
56
+ langMap?: Record<string, string>;
57
+
58
+ level?: EntryTooltipLevel;
59
+ footer?: ReactNode;
60
+ children: ReactElement;
61
+ }) {
62
+ const [open, setOpen] = useState(false);
63
+ const held = useModifierKeys(open && (level === 'full' || level === 'adaptive'));
64
+
65
+ const renderLines = (value?: {
66
+ lines: string[];
67
+ mods: Record<string, string[]>;
68
+ }) => {
69
+ const tooltip = value ?? { lines: [], mods: {} };
70
+ const base = tooltip.lines;
71
+ const modLine =
72
+ base.length > 1 && !base[base.length - 1].includes('§')
73
+ ? base[base.length - 1]
74
+ : undefined;
75
+
76
+ let extra: string[];
77
+ if (level === 'compact') extra = modLine ? [modLine] : [];
78
+ else if (level === 'extended') extra = base;
79
+ else if (level === 'full') {
80
+ const activeMod = held.shift
81
+ ? 'shift'
82
+ : held.ctrl
83
+ ? 'ctrl'
84
+ : held.alt
85
+ ? 'alt'
86
+ : null;
87
+ extra = (activeMod ? tooltip.mods[activeMod] : undefined) ?? base;
88
+ } else if (held.ctrl || held.alt)
89
+ extra = (held.ctrl ? tooltip.mods.ctrl : tooltip.mods.alt) ?? base;
90
+ else if (held.shift) extra = base;
91
+ else extra = modLine ? [modLine] : [];
92
+
93
+ const lines = extra.length
94
+ ? [name, ...extra.filter((line) => line !== name)]
95
+ : [name];
96
+ const lastIsMod =
97
+ lines.length > 1 && !lines[lines.length - 1].includes('§');
98
+
99
+ return (
100
+ <>
101
+ {lines.map((line, index) => (
102
+ <p
103
+ key={`${index}:${line}`}
104
+ className={
105
+ index === 0
106
+ ? 'font-medium'
107
+ : lastIsMod && index === lines.length - 1
108
+ ? 'italic'
109
+ : 'text-muted-foreground'
110
+ }
111
+ style={
112
+ lastIsMod && index === lines.length - 1
113
+ ? { color: ENCHANTED_TEXT_COLOR }
114
+ : undefined
115
+ }
116
+ >
117
+ <TooltipLine text={line} />
118
+ </p>
119
+ ))}
120
+ {footer}
121
+ </>
122
+ );
123
+ };
124
+
125
+ return (
126
+ <LazyTooltip
127
+ load={() => stackTooltip(dataset, id, lang, langMap, kind)}
128
+ deps={[dataset, id, lang, langMap, kind]}
129
+ onOpenChange={setOpen}
130
+ trigger={children}
131
+ contentClassName="flex-col items-start gap-0.5 p-3 text-sm break-words whitespace-pre-wrap"
132
+ >
133
+ {renderLines}
134
+ </LazyTooltip>
135
+ );
136
+ }
137
+
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+
3
+ import { usePathname } from 'next/navigation';
4
+ import type { ComponentProps } from 'react';
5
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
6
+ import { useEntryContext } from '@terrafirmagreg-team/game/hooks/use-entry-context';
7
+ import { useLangMap } from '@terrafirmagreg-team/game/hooks/use-lang-map';
8
+ import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
9
+ import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
10
+ import {
11
+ type DatasetRef,
12
+ parseSectionLocation,
13
+ } from '@terrafirmagreg-team/sites/location';
14
+ import { lookupSpriteInBundle } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/lookup';
15
+ import { stackDisplayName } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
16
+ import { loadClientModAtlases } from '@terrafirmagreg-team/dump/lib/dump/client/atlases';
17
+ import { fetchTagItem } from '@terrafirmagreg-team/game/lib/entry/icon-load';
18
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
19
+
20
+ export type ItemIconProps = {
21
+ id?: string;
22
+ dataset?: DatasetRef;
23
+ lang?: Lang;
24
+ label?: string;
25
+ children?: React.ReactNode;
26
+ } & ComponentProps<'div'>;
27
+
28
+ export function ItemIcon({
29
+ id,
30
+ dataset: datasetProp,
31
+ lang: langProp,
32
+ label,
33
+ children,
34
+ }: ItemIconProps) {
35
+ const ctx = useEntryContext();
36
+ const pathname = usePathname();
37
+ const locId = parseSectionLocation(pathname).dataset;
38
+ const dataset =
39
+ datasetProp ?? ctx?.dataset ?? (locId ? { id: locId } : undefined);
40
+ const resolvedLang = useResolvedLang();
41
+ const lang = langProp ?? ctx?.lang ?? resolvedLang;
42
+ const iconId = useAsyncValue(
43
+ dataset && id?.startsWith('#')
44
+ ? () => fetchTagItem(dataset, id, 'item').then((item) => item ?? id)
45
+ : false,
46
+ [dataset, id],
47
+ ) ?? id;
48
+ const langKey = useAsyncValue(
49
+ dataset && iconId?.includes(':') && !iconId.startsWith('#')
50
+ ? () =>
51
+ loadClientModAtlases(dataset, iconId.split(':', 2)[0]!).then(
52
+ (bundle) => lookupSpriteInBundle(bundle, iconId)?.sprite.langKey,
53
+ )
54
+ : false,
55
+ [dataset, iconId],
56
+ );
57
+ const fetchedMap = useLangMap(dataset, lang, !ctx?.langMap);
58
+ const map = ctx?.langMap ?? fetchedMap ?? {};
59
+ const resolved =
60
+ label ?? (iconId ? stackDisplayName(map, iconId, undefined, langKey) : undefined);
61
+ if (!iconId) return children ? <>{children}</> : null;
62
+ return (
63
+ <Entry
64
+ kind="item"
65
+ id={iconId}
66
+ dataset={dataset}
67
+ lang={lang}
68
+ name={resolved ?? iconId}
69
+ labeled
70
+ label={resolved}
71
+ href={false}
72
+ >
73
+ {children}
74
+ </Entry>
75
+ );
76
+ }
77
+
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import { DynamicIcon, type IconName } from 'lucide-react/dynamic';
4
+ import { EntryIcon } from '@terrafirmagreg-team/game/components/entry/icon';
5
+ import type { ReferenceKind } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
6
+ import type { PreviewIcon } from '@terrafirmagreg-team/ui/types/preview';
7
+
8
+ export function PagePreviewIcon({ icon }: { icon: PreviewIcon }) {
9
+ if (icon.type === 'entry') {
10
+ return (
11
+ <EntryIcon
12
+ reference={{ kind: icon.kind as ReferenceKind, id: icon.id }}
13
+ dataset={icon.dataset}
14
+ size={16}
15
+ missing="placeholder"
16
+ />
17
+ );
18
+ }
19
+ return (
20
+ <DynamicIcon
21
+ name={icon.name as IconName}
22
+ className="size-4 shrink-0"
23
+ />
24
+ );
25
+ }
26
+
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import { useSyncExternalStore } from 'react';
5
+ import {
6
+ dumpLocation,
7
+ subscribeDumpLocations,
8
+ type DumpLocation,
9
+ } from '@terrafirmagreg-team/dump/lib/dump/paths';
10
+
11
+ export function useDumpLocation(
12
+ dataset: DatasetRef | undefined,
13
+ ): DumpLocation | undefined {
14
+ return useSyncExternalStore(
15
+ subscribeDumpLocations,
16
+ () => (dataset ? dumpLocation(dataset) : undefined),
17
+ () => undefined,
18
+ );
19
+ }
20
+
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+ import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
+
4
+ import {
5
+ createContext,
6
+ createElement,
7
+ useContext,
8
+ useMemo,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
12
+
13
+ type EntryContextValue = {
14
+ dataset: DatasetRef;
15
+ lang?: Lang;
16
+ langMap?: Record<string, string>;
17
+ };
18
+
19
+ const EntryContext = createContext<EntryContextValue | null>(null);
20
+
21
+ export function EntryProvider({
22
+ dataset,
23
+ lang,
24
+ langMap,
25
+ children,
26
+ }: EntryContextValue & { children: ReactNode }) {
27
+ const value = useMemo(
28
+ () => ({ dataset, lang, langMap }),
29
+ [dataset, lang, langMap],
30
+ );
31
+ return createElement(EntryContext.Provider, { value }, children);
32
+ }
33
+
34
+ export function useEntryContext(): EntryContextValue | null {
35
+ return useContext(EntryContext);
36
+ }
37
+