@rpg-engine/long-bow 0.7.96 → 0.7.98
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.
- package/dist/components/DPad/JoystickDPad.d.ts +21 -0
- package/dist/components/InformationCenter/InformationCenter.d.ts +29 -0
- package/dist/components/InformationCenter/InformationCenterCell.d.ts +14 -0
- package/dist/components/InformationCenter/InformationCenterTabView.d.ts +19 -0
- package/dist/components/InformationCenter/InformationCenterTypes.d.ts +79 -0
- package/dist/components/InformationCenter/sections/bestiary/BestiarySection.d.ts +12 -0
- package/dist/components/InformationCenter/sections/bestiary/InformationCenterNPCDetails.d.ts +12 -0
- package/dist/components/InformationCenter/sections/bestiary/InformationCenterNPCTooltip.d.ts +9 -0
- package/dist/components/InformationCenter/sections/faq/FaqSection.d.ts +8 -0
- package/dist/components/InformationCenter/sections/items/InformationCenterItemDetails.d.ts +11 -0
- package/dist/components/InformationCenter/sections/items/InformationCenterItemTooltip.d.ts +7 -0
- package/dist/components/InformationCenter/sections/items/ItemsSection.d.ts +11 -0
- package/dist/components/InformationCenter/sections/tutorials/TutorialsSection.d.ts +8 -0
- package/dist/components/InformationCenter/shared/BaseInformationDetails.d.ts +10 -0
- package/dist/components/shared/BaseTooltip.d.ts +12 -0
- package/dist/components/shared/Collapsible/Collapsible.d.ts +9 -0
- package/dist/components/shared/Portal/Portal.d.ts +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +250 -12
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +251 -14
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/mocks/informationCenter.mocks.d.ts +6 -0
- package/dist/stories/Features/craftbook/CraftBook.stories.d.ts +2 -0
- package/dist/stories/UI/info/InformationCenter.stories.d.ts +7 -0
- package/dist/stories/UI/joystick/JoystickDPad.stories.d.ts +6 -0
- package/package.json +1 -1
- package/src/components/CraftBook/CraftBook.tsx +70 -31
- package/src/components/CraftBook/CraftingRecipe.tsx +2 -1
- package/src/components/CraftBook/CraftingTooltip.tsx +4 -3
- package/src/components/DPad/JoystickDPad.tsx +318 -0
- package/src/components/InformationCenter/InformationCenter.tsx +155 -0
- package/src/components/InformationCenter/InformationCenterCell.tsx +96 -0
- package/src/components/InformationCenter/InformationCenterTabView.tsx +121 -0
- package/src/components/InformationCenter/InformationCenterTypes.ts +87 -0
- package/src/components/InformationCenter/sections/bestiary/BestiarySection.tsx +170 -0
- package/src/components/InformationCenter/sections/bestiary/InformationCenterNPCDetails.tsx +366 -0
- package/src/components/InformationCenter/sections/bestiary/InformationCenterNPCTooltip.tsx +204 -0
- package/src/components/InformationCenter/sections/faq/FaqSection.tsx +71 -0
- package/src/components/InformationCenter/sections/items/InformationCenterItemDetails.tsx +323 -0
- package/src/components/InformationCenter/sections/items/InformationCenterItemTooltip.tsx +88 -0
- package/src/components/InformationCenter/sections/items/ItemsSection.tsx +180 -0
- package/src/components/InformationCenter/sections/tutorials/TutorialsSection.tsx +144 -0
- package/src/components/InformationCenter/shared/BaseInformationDetails.tsx +162 -0
- package/src/components/InternalTabs/InternalTabs.tsx +1 -3
- package/src/components/shared/BaseTooltip.tsx +60 -0
- package/src/components/shared/Collapsible/Collapsible.tsx +70 -0
- package/src/components/shared/Portal/Portal.tsx +19 -0
- package/src/index.tsx +1 -0
- package/src/mocks/informationCenter.mocks.ts +562 -0
- package/src/stories/Features/craftbook/CraftBook.stories.tsx +15 -1
- package/src/stories/UI/info/InformationCenter.stories.tsx +58 -0
- package/src/stories/UI/joystick/JoystickDPad.stories.tsx +52 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { IFaqItem, IVideoGuide } from '../components/InformationCenter/InformationCenter';
|
|
2
|
+
import { IInformationCenterItem, IInformationCenterNPC } from '../components/InformationCenter/InformationCenterTypes';
|
|
3
|
+
export declare const mockBestiaryItems: IInformationCenterNPC[];
|
|
4
|
+
export declare const mockItems: IInformationCenterItem[];
|
|
5
|
+
export declare const mockFaqItems: IFaqItem[];
|
|
6
|
+
export declare const mockTutorials: IVideoGuide[];
|
|
@@ -4,3 +4,5 @@ export default meta;
|
|
|
4
4
|
export declare const Default: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
5
5
|
export declare const WithSearch: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
6
6
|
export declare const WithCategory: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
7
|
+
export declare const Empty: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
8
|
+
export declare const NoSearchResults: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Meta } from '@storybook/react';
|
|
2
|
+
declare const meta: Meta;
|
|
3
|
+
export default meta;
|
|
4
|
+
export declare const Default: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
5
|
+
export declare const Loading: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
6
|
+
export declare const Error: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
7
|
+
export declare const Empty: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare const _default: import("@storybook/csf").ComponentAnnotations<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
2
|
+
export default _default;
|
|
3
|
+
export declare const Default: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
4
|
+
export declare const WithBackground: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
5
|
+
export declare const WithCustomOptions: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
|
6
|
+
export declare const Disabled: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '@rpg-engine/shared';
|
|
8
8
|
import React, { useEffect, useState } from 'react';
|
|
9
9
|
import {
|
|
10
|
+
FaBoxOpen,
|
|
10
11
|
FaChevronLeft,
|
|
11
12
|
FaChevronRight,
|
|
12
13
|
FaSearch,
|
|
@@ -183,35 +184,42 @@ export const CraftBook: React.FC<IItemCraftSelectorProps> = ({
|
|
|
183
184
|
)}
|
|
184
185
|
|
|
185
186
|
<ContentContainer>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
<PinButton
|
|
193
|
-
onClick={e => {
|
|
194
|
-
e.stopPropagation();
|
|
195
|
-
togglePinItem(item.key);
|
|
196
|
-
}}
|
|
197
|
-
isPinned={pinnedItems.includes(item.key)}
|
|
187
|
+
{paginatedItems.length > 0 ? (
|
|
188
|
+
<RadioInputScroller className="inputRadioCraftBook">
|
|
189
|
+
{paginatedItems?.map(item => (
|
|
190
|
+
<CraftingRecipeWrapper
|
|
191
|
+
key={item.key}
|
|
192
|
+
isSelected={pinnedItems.includes(item.key)}
|
|
198
193
|
>
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
194
|
+
<PinButton
|
|
195
|
+
onClick={e => {
|
|
196
|
+
e.stopPropagation();
|
|
197
|
+
togglePinItem(item.key);
|
|
198
|
+
}}
|
|
199
|
+
isPinned={pinnedItems.includes(item.key)}
|
|
200
|
+
>
|
|
201
|
+
<FaThumbtack size={14} />
|
|
202
|
+
</PinButton>
|
|
203
|
+
<CraftingRecipe
|
|
204
|
+
atlasIMG={atlasIMG}
|
|
205
|
+
atlasJSON={atlasJSON}
|
|
206
|
+
equipmentSet={equipmentSet}
|
|
207
|
+
recipe={item}
|
|
208
|
+
scale={scale}
|
|
209
|
+
handleRecipeSelect={setCraftItemKey.bind(null, item.key)}
|
|
210
|
+
selectedCraftItemKey={craftItemKey}
|
|
211
|
+
inventory={inventory}
|
|
212
|
+
skills={skills}
|
|
213
|
+
/>
|
|
214
|
+
</CraftingRecipeWrapper>
|
|
215
|
+
))}
|
|
216
|
+
</RadioInputScroller>
|
|
217
|
+
) : (
|
|
218
|
+
<EmptyState>
|
|
219
|
+
<FaBoxOpen size={48} />
|
|
220
|
+
<p>No craftable items found</p>
|
|
221
|
+
</EmptyState>
|
|
222
|
+
)}
|
|
215
223
|
</ContentContainer>
|
|
216
224
|
|
|
217
225
|
{totalPages > 1 && (
|
|
@@ -353,17 +361,21 @@ const SearchContainer = styled.div`
|
|
|
353
361
|
|
|
354
362
|
const ContentContainer = styled.div`
|
|
355
363
|
flex: 1;
|
|
356
|
-
|
|
364
|
+
display: flex;
|
|
365
|
+
flex-direction: column;
|
|
357
366
|
padding: 16px;
|
|
358
367
|
padding-right: 0;
|
|
359
368
|
padding-bottom: 0;
|
|
360
|
-
overflow: hidden;
|
|
361
369
|
width: 100%;
|
|
370
|
+
position: relative;
|
|
371
|
+
min-height: 300px;
|
|
372
|
+
overflow: hidden;
|
|
362
373
|
`;
|
|
363
374
|
|
|
364
375
|
const RadioInputScroller = styled.div`
|
|
365
376
|
height: 100%;
|
|
366
|
-
|
|
377
|
+
min-height: 300px;
|
|
378
|
+
overflow-y: auto;
|
|
367
379
|
overflow-x: hidden;
|
|
368
380
|
padding: 8px 16px;
|
|
369
381
|
padding-right: 24px;
|
|
@@ -464,3 +476,30 @@ const PageInfo = styled.div`
|
|
|
464
476
|
font-size: 0.8rem;
|
|
465
477
|
font-family: 'Press Start 2P', cursive;
|
|
466
478
|
`;
|
|
479
|
+
|
|
480
|
+
const EmptyState = styled.div`
|
|
481
|
+
position: absolute;
|
|
482
|
+
top: 50%;
|
|
483
|
+
left: 50%;
|
|
484
|
+
transform: translate(-50%, -50%);
|
|
485
|
+
display: flex;
|
|
486
|
+
flex-direction: column;
|
|
487
|
+
align-items: center;
|
|
488
|
+
justify-content: center;
|
|
489
|
+
text-align: center;
|
|
490
|
+
color: ${uiColors.lightGray};
|
|
491
|
+
width: 100%;
|
|
492
|
+
padding: 2rem;
|
|
493
|
+
|
|
494
|
+
svg {
|
|
495
|
+
font-size: 3rem;
|
|
496
|
+
margin-bottom: 1rem;
|
|
497
|
+
opacity: 0.7;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
p {
|
|
501
|
+
font-family: 'Press Start 2P', cursive;
|
|
502
|
+
font-size: 0.9rem;
|
|
503
|
+
margin: 0;
|
|
504
|
+
}
|
|
505
|
+
`;
|
|
@@ -51,15 +51,15 @@ const MinCraftingRequirementsText = styled.div<{ levelIsOk: boolean }>`
|
|
|
51
51
|
font-size: 0.55rem;
|
|
52
52
|
margin: 0;
|
|
53
53
|
margin-bottom: 12px;
|
|
54
|
+
margin-left: 0.5rem;
|
|
54
55
|
color: ${({ levelIsOk }) =>
|
|
55
56
|
levelIsOk ? uiColors.lightGreen : uiColors.lightGray} !important;
|
|
56
57
|
`;
|
|
57
58
|
|
|
58
|
-
const
|
|
59
|
+
const TooltipTitle = styled.div`
|
|
59
60
|
color: ${uiColors.yellow};
|
|
60
61
|
font-size: 0.6rem;
|
|
61
62
|
margin-bottom: 12px;
|
|
62
|
-
text-transform: uppercase;
|
|
63
63
|
letter-spacing: 0.5px;
|
|
64
64
|
`;
|
|
65
65
|
|
|
@@ -105,12 +105,13 @@ export const CraftingTooltip: React.FC<ICraftingTooltipProps> = ({
|
|
|
105
105
|
|
|
106
106
|
return (
|
|
107
107
|
<TooltipContainer x={x} y={y}>
|
|
108
|
+
<TooltipTitle>Skill Requirements</TooltipTitle>
|
|
108
109
|
<MinCraftingRequirementsText levelIsOk={levelIsOk}>
|
|
109
110
|
{modifyString(`${recipe?.minCraftingRequirements?.[0] ?? ''}`)} lvl{' '}
|
|
110
111
|
{recipe?.minCraftingRequirements?.[1] ?? 0} ({levelInSkill})
|
|
111
112
|
</MinCraftingRequirementsText>
|
|
112
113
|
|
|
113
|
-
<
|
|
114
|
+
<TooltipTitle>Ingredients</TooltipTitle>
|
|
114
115
|
{recipe.ingredients.map((ingredient, index) => {
|
|
115
116
|
const itemQtyInInventory = !inventory
|
|
116
117
|
? 0
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
interface IDPadContainerProps {
|
|
5
|
+
opacity?: number;
|
|
6
|
+
showBackground?: boolean;
|
|
7
|
+
size?: number;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface IDPadOptions {
|
|
12
|
+
/** Opacity of the entire component (0-1) */
|
|
13
|
+
opacity?: number;
|
|
14
|
+
/** Show the silver background behind the controller (default: false) */
|
|
15
|
+
showBackground?: boolean;
|
|
16
|
+
/** Size in pixels (default: 100) */
|
|
17
|
+
size?: number;
|
|
18
|
+
/** Interval in ms for continuous press events (default: 500) */
|
|
19
|
+
pressInterval?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface IDPadProps {
|
|
23
|
+
/** Callback fired when a direction is pressed */
|
|
24
|
+
onDirectionPress?: (direction: 'up' | 'down' | 'left' | 'right') => void;
|
|
25
|
+
/** Whether the component is disabled */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/** Additional options for customizing the D-pad */
|
|
28
|
+
options?: IDPadOptions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const JoystickDPad = ({
|
|
32
|
+
onDirectionPress,
|
|
33
|
+
disabled = false,
|
|
34
|
+
options = {},
|
|
35
|
+
}: IDPadProps): JSX.Element => {
|
|
36
|
+
const {
|
|
37
|
+
opacity = 1,
|
|
38
|
+
showBackground = false,
|
|
39
|
+
size = 100,
|
|
40
|
+
pressInterval = 500,
|
|
41
|
+
} = options;
|
|
42
|
+
|
|
43
|
+
const [pressedButtons, setPressedButtons] = useState<Set<string>>(new Set());
|
|
44
|
+
const intervalRef = useRef<number | null>(null);
|
|
45
|
+
const activeDirectionRef = useRef<'up' | 'down' | 'left' | 'right' | null>(
|
|
46
|
+
null
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const clearPressInterval = useCallback(() => {
|
|
50
|
+
if (intervalRef.current) {
|
|
51
|
+
window.clearInterval(intervalRef.current);
|
|
52
|
+
intervalRef.current = null;
|
|
53
|
+
}
|
|
54
|
+
activeDirectionRef.current = null;
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const handleDirectionPress = useCallback(
|
|
58
|
+
(direction: 'up' | 'down' | 'left' | 'right') => {
|
|
59
|
+
if (disabled) return;
|
|
60
|
+
|
|
61
|
+
// Clear any existing interval
|
|
62
|
+
clearPressInterval();
|
|
63
|
+
|
|
64
|
+
// Set the active direction
|
|
65
|
+
activeDirectionRef.current = direction;
|
|
66
|
+
setPressedButtons(prev => new Set(prev).add(direction));
|
|
67
|
+
|
|
68
|
+
// Trigger first press immediately
|
|
69
|
+
onDirectionPress?.(direction);
|
|
70
|
+
|
|
71
|
+
// Set up the interval for continuous press
|
|
72
|
+
intervalRef.current = window.setInterval(() => {
|
|
73
|
+
if (activeDirectionRef.current === direction) {
|
|
74
|
+
onDirectionPress?.(direction);
|
|
75
|
+
}
|
|
76
|
+
}, pressInterval);
|
|
77
|
+
},
|
|
78
|
+
[disabled, onDirectionPress, pressInterval, clearPressInterval]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const handleDirectionRelease = useCallback(
|
|
82
|
+
(direction: 'up' | 'down' | 'left' | 'right') => {
|
|
83
|
+
setPressedButtons(prev => {
|
|
84
|
+
const next = new Set(prev);
|
|
85
|
+
next.delete(direction);
|
|
86
|
+
return next;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (activeDirectionRef.current === direction) {
|
|
90
|
+
clearPressInterval();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[clearPressInterval]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Cleanup on unmount
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
return () => {
|
|
99
|
+
clearPressInterval();
|
|
100
|
+
};
|
|
101
|
+
}, [clearPressInterval]);
|
|
102
|
+
|
|
103
|
+
const preventDefault = (e: React.MouseEvent | React.TouchEvent) => {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
e.stopPropagation();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<DPadContainer
|
|
110
|
+
opacity={opacity}
|
|
111
|
+
showBackground={showBackground}
|
|
112
|
+
size={size}
|
|
113
|
+
disabled={disabled}
|
|
114
|
+
onContextMenu={preventDefault}
|
|
115
|
+
>
|
|
116
|
+
<DPadButton
|
|
117
|
+
className="up"
|
|
118
|
+
onMouseDown={() => handleDirectionPress('up')}
|
|
119
|
+
onMouseUp={() => handleDirectionRelease('up')}
|
|
120
|
+
onMouseLeave={() => handleDirectionRelease('up')}
|
|
121
|
+
onTouchStart={() => handleDirectionPress('up')}
|
|
122
|
+
onTouchEnd={() => handleDirectionRelease('up')}
|
|
123
|
+
onContextMenu={preventDefault}
|
|
124
|
+
size={size}
|
|
125
|
+
isPressed={pressedButtons.has('up')}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
/>
|
|
128
|
+
<DPadButton
|
|
129
|
+
className="right"
|
|
130
|
+
onMouseDown={() => handleDirectionPress('right')}
|
|
131
|
+
onMouseUp={() => handleDirectionRelease('right')}
|
|
132
|
+
onMouseLeave={() => handleDirectionRelease('right')}
|
|
133
|
+
onTouchStart={() => handleDirectionPress('right')}
|
|
134
|
+
onTouchEnd={() => handleDirectionRelease('right')}
|
|
135
|
+
onContextMenu={preventDefault}
|
|
136
|
+
size={size}
|
|
137
|
+
isPressed={pressedButtons.has('right')}
|
|
138
|
+
disabled={disabled}
|
|
139
|
+
/>
|
|
140
|
+
<DPadButton
|
|
141
|
+
className="down"
|
|
142
|
+
onMouseDown={() => handleDirectionPress('down')}
|
|
143
|
+
onMouseUp={() => handleDirectionRelease('down')}
|
|
144
|
+
onMouseLeave={() => handleDirectionRelease('down')}
|
|
145
|
+
onTouchStart={() => handleDirectionPress('down')}
|
|
146
|
+
onTouchEnd={() => handleDirectionRelease('down')}
|
|
147
|
+
onContextMenu={preventDefault}
|
|
148
|
+
size={size}
|
|
149
|
+
isPressed={pressedButtons.has('down')}
|
|
150
|
+
disabled={disabled}
|
|
151
|
+
/>
|
|
152
|
+
<DPadButton
|
|
153
|
+
className="left"
|
|
154
|
+
onMouseDown={() => handleDirectionPress('left')}
|
|
155
|
+
onMouseUp={() => handleDirectionRelease('left')}
|
|
156
|
+
onMouseLeave={() => handleDirectionRelease('left')}
|
|
157
|
+
onTouchStart={() => handleDirectionPress('left')}
|
|
158
|
+
onTouchEnd={() => handleDirectionRelease('left')}
|
|
159
|
+
onContextMenu={preventDefault}
|
|
160
|
+
size={size}
|
|
161
|
+
isPressed={pressedButtons.has('left')}
|
|
162
|
+
disabled={disabled}
|
|
163
|
+
/>
|
|
164
|
+
<DPadCenter
|
|
165
|
+
size={size}
|
|
166
|
+
disabled={disabled}
|
|
167
|
+
onContextMenu={preventDefault}
|
|
168
|
+
/>
|
|
169
|
+
</DPadContainer>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const DPadContainer = styled.div<IDPadContainerProps>`
|
|
174
|
+
width: ${props => props.size ?? 100}px;
|
|
175
|
+
height: ${props => props.size ?? 100}px;
|
|
176
|
+
position: relative;
|
|
177
|
+
background: ${props => (props.showBackground ? '#b8b8b8' : 'transparent')};
|
|
178
|
+
border-radius: 50%;
|
|
179
|
+
box-shadow: ${props =>
|
|
180
|
+
props.showBackground
|
|
181
|
+
? 'inset 0 0 10px rgba(0, 0, 0, 0.3), 0 4px 8px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2)'
|
|
182
|
+
: 'none'};
|
|
183
|
+
opacity: ${props => (props.disabled ? 0.5 : props.opacity ?? 1)};
|
|
184
|
+
user-select: none;
|
|
185
|
+
cursor: ${props => (props.disabled ? 'not-allowed' : 'default')};
|
|
186
|
+
transition: opacity 0.2s ease;
|
|
187
|
+
touch-action: none;
|
|
188
|
+
-webkit-tap-highlight-color: transparent;
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
interface IDPadButtonProps {
|
|
192
|
+
size?: number;
|
|
193
|
+
isPressed?: boolean;
|
|
194
|
+
disabled?: boolean;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const DPadButton = styled.div<IDPadButtonProps>`
|
|
198
|
+
position: absolute;
|
|
199
|
+
background: ${props => (props.isPressed ? '#363636' : '#424242')};
|
|
200
|
+
box-shadow: ${props =>
|
|
201
|
+
props.isPressed
|
|
202
|
+
? 'inset 0 0 12px rgba(0, 0, 0, 0.8), 0 0 2px rgba(0, 0, 0, 0.3)'
|
|
203
|
+
: 'inset 0 0 5px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.2)'};
|
|
204
|
+
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
|
|
205
|
+
user-select: none;
|
|
206
|
+
transition: all 0.08s cubic-bezier(0.4, 0, 0.2, 1);
|
|
207
|
+
touch-action: none;
|
|
208
|
+
-webkit-tap-highlight-color: transparent;
|
|
209
|
+
transform-origin: center center;
|
|
210
|
+
|
|
211
|
+
&:hover:not(:active) {
|
|
212
|
+
@media (hover: hover) {
|
|
213
|
+
filter: ${props =>
|
|
214
|
+
!props.disabled && !props.isPressed ? 'brightness(1.1)' : 'none'};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
&::after {
|
|
219
|
+
content: '';
|
|
220
|
+
position: absolute;
|
|
221
|
+
width: 0;
|
|
222
|
+
height: 0;
|
|
223
|
+
border: ${props => (props.size ?? 100) * 0.05}px solid transparent;
|
|
224
|
+
pointer-events: none;
|
|
225
|
+
opacity: ${props => (props.isPressed ? 0.7 : 1)};
|
|
226
|
+
transition: all 0.08s cubic-bezier(0.4, 0, 0.2, 1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
&.up,
|
|
230
|
+
&.down {
|
|
231
|
+
width: ${props => (props.size ?? 100) * 0.3}px;
|
|
232
|
+
height: ${props => (props.size ?? 100) * 0.4}px;
|
|
233
|
+
left: 50%;
|
|
234
|
+
transform: translateX(-50%) scale(${props => (props.isPressed ? 0.95 : 1)});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
&.left,
|
|
238
|
+
&.right {
|
|
239
|
+
width: ${props => (props.size ?? 100) * 0.4}px;
|
|
240
|
+
height: ${props => (props.size ?? 100) * 0.3}px;
|
|
241
|
+
top: 50%;
|
|
242
|
+
transform: translateY(-50%) scale(${props => (props.isPressed ? 0.95 : 1)});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
&.up {
|
|
246
|
+
top: 0;
|
|
247
|
+
border-radius: 5px 5px 0 0;
|
|
248
|
+
&::after {
|
|
249
|
+
border-bottom-color: #2a2a2a;
|
|
250
|
+
top: 45%;
|
|
251
|
+
left: 50%;
|
|
252
|
+
transform: translate(-50%, -50%);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
&.down {
|
|
257
|
+
bottom: 0;
|
|
258
|
+
border-radius: 0 0 5px 5px;
|
|
259
|
+
&::after {
|
|
260
|
+
border-top-color: #2a2a2a;
|
|
261
|
+
bottom: 45%;
|
|
262
|
+
left: 50%;
|
|
263
|
+
transform: translate(-50%, 50%);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
&.left {
|
|
268
|
+
left: 0;
|
|
269
|
+
border-radius: 5px 0 0 5px;
|
|
270
|
+
&::after {
|
|
271
|
+
border-right-color: #2a2a2a;
|
|
272
|
+
left: 45%;
|
|
273
|
+
top: 50%;
|
|
274
|
+
transform: translate(-50%, -50%);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
&.right {
|
|
279
|
+
right: 0;
|
|
280
|
+
border-radius: 0 5px 5px 0;
|
|
281
|
+
&::after {
|
|
282
|
+
border-left-color: #2a2a2a;
|
|
283
|
+
right: 45%;
|
|
284
|
+
top: 50%;
|
|
285
|
+
transform: translate(50%, -50%);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
`;
|
|
289
|
+
|
|
290
|
+
const DPadCenter = styled.div<IDPadButtonProps>`
|
|
291
|
+
position: absolute;
|
|
292
|
+
width: ${props => (props.size ?? 100) * 0.3}px;
|
|
293
|
+
height: ${props => (props.size ?? 100) * 0.3}px;
|
|
294
|
+
background: #424242;
|
|
295
|
+
top: 50%;
|
|
296
|
+
left: 50%;
|
|
297
|
+
transform: translate(-50%, -50%);
|
|
298
|
+
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.6);
|
|
299
|
+
border-radius: 50%;
|
|
300
|
+
user-select: none;
|
|
301
|
+
cursor: ${props => (props.disabled ? 'not-allowed' : 'default')};
|
|
302
|
+
touch-action: none;
|
|
303
|
+
-webkit-tap-highlight-color: transparent;
|
|
304
|
+
|
|
305
|
+
&::after {
|
|
306
|
+
content: '';
|
|
307
|
+
position: absolute;
|
|
308
|
+
width: ${props => (props.size ?? 100) * 0.08}px;
|
|
309
|
+
height: ${props => (props.size ?? 100) * 0.08}px;
|
|
310
|
+
background: #2a2a2a;
|
|
311
|
+
border-radius: 50%;
|
|
312
|
+
top: 50%;
|
|
313
|
+
left: 50%;
|
|
314
|
+
transform: translate(-50%, -50%);
|
|
315
|
+
pointer-events: none;
|
|
316
|
+
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.8);
|
|
317
|
+
}
|
|
318
|
+
`;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import { InternalTabs } from '../InternalTabs/InternalTabs';
|
|
5
|
+
import {
|
|
6
|
+
IInformationCenterItem,
|
|
7
|
+
IInformationCenterNPC,
|
|
8
|
+
} from './InformationCenterTypes';
|
|
9
|
+
import { BestiarySection } from './sections/bestiary/BestiarySection';
|
|
10
|
+
import { FaqSection } from './sections/faq/FaqSection';
|
|
11
|
+
import { InformationCenterItemDetails } from './sections/items/InformationCenterItemDetails';
|
|
12
|
+
import { ItemsSection } from './sections/items/ItemsSection';
|
|
13
|
+
import { TutorialsSection } from './sections/tutorials/TutorialsSection';
|
|
14
|
+
|
|
15
|
+
export interface IFaqItem {
|
|
16
|
+
id: string;
|
|
17
|
+
question: string;
|
|
18
|
+
answer: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IVideoGuide {
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
thumbnailUrl: string;
|
|
26
|
+
videoUrl: string;
|
|
27
|
+
category: 'Combat' | 'Crafting' | 'Exploration' | 'General';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IInformationCenterProps {
|
|
31
|
+
itemsAtlasJSON: Record<string, any>;
|
|
32
|
+
itemsAtlasIMG: string;
|
|
33
|
+
entitiesAtlasJSON: Record<string, any>;
|
|
34
|
+
entitiesAtlasIMG: string;
|
|
35
|
+
faqItems?: IFaqItem[];
|
|
36
|
+
bestiaryItems?: IInformationCenterNPC[];
|
|
37
|
+
videoGuides?: IVideoGuide[];
|
|
38
|
+
items?: IInformationCenterItem[];
|
|
39
|
+
loading?: boolean;
|
|
40
|
+
error?: string;
|
|
41
|
+
initialSearchQuery?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const InformationCenter: React.FC<IInformationCenterProps> = ({
|
|
45
|
+
itemsAtlasJSON,
|
|
46
|
+
itemsAtlasIMG,
|
|
47
|
+
entitiesAtlasJSON,
|
|
48
|
+
entitiesAtlasIMG,
|
|
49
|
+
faqItems = [],
|
|
50
|
+
bestiaryItems = [],
|
|
51
|
+
videoGuides = [],
|
|
52
|
+
items = [],
|
|
53
|
+
loading = false,
|
|
54
|
+
error,
|
|
55
|
+
initialSearchQuery = '',
|
|
56
|
+
}) => {
|
|
57
|
+
const [
|
|
58
|
+
selectedItem,
|
|
59
|
+
setSelectedItem,
|
|
60
|
+
] = useState<IInformationCenterItem | null>(null);
|
|
61
|
+
|
|
62
|
+
if (loading) {
|
|
63
|
+
return <LoadingMessage>Loading...</LoadingMessage>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (error) {
|
|
67
|
+
return <ErrorMessage>{error}</ErrorMessage>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const tabs = [
|
|
71
|
+
{
|
|
72
|
+
id: 'bestiary',
|
|
73
|
+
title: 'Bestiary',
|
|
74
|
+
content: (
|
|
75
|
+
<BestiarySection
|
|
76
|
+
bestiaryItems={bestiaryItems}
|
|
77
|
+
itemsAtlasJSON={itemsAtlasJSON}
|
|
78
|
+
itemsAtlasIMG={itemsAtlasIMG}
|
|
79
|
+
entitiesAtlasJSON={entitiesAtlasJSON}
|
|
80
|
+
entitiesAtlasIMG={entitiesAtlasIMG}
|
|
81
|
+
initialSearchQuery={initialSearchQuery}
|
|
82
|
+
/>
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'items',
|
|
87
|
+
title: 'Items',
|
|
88
|
+
content: (
|
|
89
|
+
<ItemsSection
|
|
90
|
+
items={items}
|
|
91
|
+
bestiaryItems={bestiaryItems}
|
|
92
|
+
itemsAtlasJSON={itemsAtlasJSON}
|
|
93
|
+
itemsAtlasIMG={itemsAtlasIMG}
|
|
94
|
+
initialSearchQuery={initialSearchQuery}
|
|
95
|
+
/>
|
|
96
|
+
),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'faq',
|
|
100
|
+
title: 'FAQ',
|
|
101
|
+
content: (
|
|
102
|
+
<FaqSection
|
|
103
|
+
faqItems={faqItems}
|
|
104
|
+
initialSearchQuery={initialSearchQuery}
|
|
105
|
+
/>
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'tutorials',
|
|
110
|
+
title: 'Tutorials',
|
|
111
|
+
content: (
|
|
112
|
+
<TutorialsSection
|
|
113
|
+
videoGuides={videoGuides}
|
|
114
|
+
initialSearchQuery={initialSearchQuery}
|
|
115
|
+
/>
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Container>
|
|
122
|
+
<InternalTabs tabs={tabs} activeTextColor="#000000" />
|
|
123
|
+
{selectedItem && (
|
|
124
|
+
<InformationCenterItemDetails
|
|
125
|
+
item={selectedItem}
|
|
126
|
+
itemsAtlasJSON={itemsAtlasJSON}
|
|
127
|
+
itemsAtlasIMG={itemsAtlasIMG}
|
|
128
|
+
droppedBy={bestiaryItems.filter(npc =>
|
|
129
|
+
npc.loots?.some(loot => loot.itemBlueprintKey === selectedItem.key)
|
|
130
|
+
)}
|
|
131
|
+
onBack={() => setSelectedItem(null)}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
</Container>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const Container = styled.div`
|
|
139
|
+
width: 100%;
|
|
140
|
+
max-width: 800px;
|
|
141
|
+
margin: 0 auto;
|
|
142
|
+
padding: 1rem;
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
const LoadingMessage = styled.div`
|
|
146
|
+
text-align: center;
|
|
147
|
+
color: #ffffff;
|
|
148
|
+
padding: 2rem;
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const ErrorMessage = styled.div`
|
|
152
|
+
text-align: center;
|
|
153
|
+
color: #ef4444;
|
|
154
|
+
padding: 2rem;
|
|
155
|
+
`;
|