@rpg-engine/long-bow 0.7.97 → 0.7.99

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 (53) hide show
  1. package/dist/components/DPad/JoystickDPad.d.ts +21 -0
  2. package/dist/components/InformationCenter/InformationCenter.d.ts +29 -0
  3. package/dist/components/InformationCenter/InformationCenterCell.d.ts +14 -0
  4. package/dist/components/InformationCenter/InformationCenterTabView.d.ts +19 -0
  5. package/dist/components/InformationCenter/InformationCenterTypes.d.ts +79 -0
  6. package/dist/components/InformationCenter/sections/bestiary/BestiarySection.d.ts +12 -0
  7. package/dist/components/InformationCenter/sections/bestiary/InformationCenterNPCDetails.d.ts +12 -0
  8. package/dist/components/InformationCenter/sections/bestiary/InformationCenterNPCTooltip.d.ts +9 -0
  9. package/dist/components/InformationCenter/sections/faq/FaqSection.d.ts +8 -0
  10. package/dist/components/InformationCenter/sections/items/InformationCenterItemDetails.d.ts +11 -0
  11. package/dist/components/InformationCenter/sections/items/InformationCenterItemTooltip.d.ts +7 -0
  12. package/dist/components/InformationCenter/sections/items/ItemsSection.d.ts +11 -0
  13. package/dist/components/InformationCenter/sections/tutorials/TutorialsSection.d.ts +8 -0
  14. package/dist/components/InformationCenter/shared/BaseInformationDetails.d.ts +10 -0
  15. package/dist/components/shared/BaseTooltip.d.ts +12 -0
  16. package/dist/components/shared/Collapsible/Collapsible.d.ts +9 -0
  17. package/dist/components/shared/Portal/Portal.d.ts +6 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/long-bow.cjs.development.js +271 -5
  20. package/dist/long-bow.cjs.development.js.map +1 -1
  21. package/dist/long-bow.cjs.production.min.js +1 -1
  22. package/dist/long-bow.cjs.production.min.js.map +1 -1
  23. package/dist/long-bow.esm.js +273 -8
  24. package/dist/long-bow.esm.js.map +1 -1
  25. package/dist/mocks/informationCenter.mocks.d.ts +6 -0
  26. package/dist/stories/Features/craftbook/CraftBook.stories.d.ts +2 -0
  27. package/dist/stories/UI/info/InformationCenter.stories.d.ts +7 -0
  28. package/dist/stories/UI/joystick/JoystickDPad.stories.d.ts +6 -0
  29. package/package.json +1 -1
  30. package/src/components/CraftBook/CraftBook.tsx +70 -31
  31. package/src/components/DPad/JoystickDPad.tsx +417 -0
  32. package/src/components/InformationCenter/InformationCenter.tsx +155 -0
  33. package/src/components/InformationCenter/InformationCenterCell.tsx +96 -0
  34. package/src/components/InformationCenter/InformationCenterTabView.tsx +121 -0
  35. package/src/components/InformationCenter/InformationCenterTypes.ts +87 -0
  36. package/src/components/InformationCenter/sections/bestiary/BestiarySection.tsx +170 -0
  37. package/src/components/InformationCenter/sections/bestiary/InformationCenterNPCDetails.tsx +366 -0
  38. package/src/components/InformationCenter/sections/bestiary/InformationCenterNPCTooltip.tsx +204 -0
  39. package/src/components/InformationCenter/sections/faq/FaqSection.tsx +71 -0
  40. package/src/components/InformationCenter/sections/items/InformationCenterItemDetails.tsx +323 -0
  41. package/src/components/InformationCenter/sections/items/InformationCenterItemTooltip.tsx +88 -0
  42. package/src/components/InformationCenter/sections/items/ItemsSection.tsx +180 -0
  43. package/src/components/InformationCenter/sections/tutorials/TutorialsSection.tsx +144 -0
  44. package/src/components/InformationCenter/shared/BaseInformationDetails.tsx +162 -0
  45. package/src/components/InternalTabs/InternalTabs.tsx +1 -3
  46. package/src/components/shared/BaseTooltip.tsx +60 -0
  47. package/src/components/shared/Collapsible/Collapsible.tsx +70 -0
  48. package/src/components/shared/Portal/Portal.tsx +19 -0
  49. package/src/index.tsx +1 -0
  50. package/src/mocks/informationCenter.mocks.ts +562 -0
  51. package/src/stories/Features/craftbook/CraftBook.stories.tsx +15 -1
  52. package/src/stories/UI/info/InformationCenter.stories.tsx +58 -0
  53. package/src/stories/UI/joystick/JoystickDPad.stories.tsx +52 -0
@@ -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
+ `;
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
4
+
5
+ interface IInformationCenterCellProps {
6
+ name: string;
7
+ spriteKey: string;
8
+ atlasJSON: any;
9
+ atlasIMG: string;
10
+ onClick?: () => void;
11
+ onMouseEnter?: (e: React.MouseEvent) => void;
12
+ onMouseLeave?: () => void;
13
+ onMouseMove?: (e: React.MouseEvent) => void;
14
+ onTouchStart?: (e: React.TouchEvent) => void;
15
+ }
16
+
17
+ export const InformationCenterCell: React.FC<IInformationCenterCellProps> = ({
18
+ name,
19
+ spriteKey,
20
+ atlasJSON,
21
+ atlasIMG,
22
+ onClick,
23
+ onMouseEnter,
24
+ onMouseLeave,
25
+ onMouseMove,
26
+ onTouchStart,
27
+ }) => {
28
+ return (
29
+ <CellContainer
30
+ onClick={onClick}
31
+ onMouseEnter={onMouseEnter}
32
+ onMouseLeave={onMouseLeave}
33
+ onMouseMove={onMouseMove}
34
+ onTouchStart={onTouchStart}
35
+ >
36
+ <SpriteContainer>
37
+ <SpriteFromAtlas
38
+ atlasJSON={atlasJSON}
39
+ atlasIMG={atlasIMG}
40
+ spriteKey={spriteKey}
41
+ width={32}
42
+ height={32}
43
+ imgScale={1}
44
+ />
45
+ </SpriteContainer>
46
+ <CellName>{name}</CellName>
47
+ </CellContainer>
48
+ );
49
+ };
50
+
51
+ const CellContainer = styled.div`
52
+ position: relative;
53
+ background: rgba(0, 0, 0, 0.2);
54
+ padding: 0.75rem;
55
+ border-radius: 4px;
56
+ display: flex;
57
+ flex-direction: column;
58
+ align-items: center;
59
+ cursor: pointer;
60
+ transition: background-color 0.2s ease;
61
+ min-height: 120px;
62
+
63
+ &:hover {
64
+ background: rgba(0, 0, 0, 0.3);
65
+ }
66
+ `;
67
+
68
+ const SpriteContainer = styled.div`
69
+ margin-bottom: 0.5rem;
70
+ display: flex;
71
+ justify-content: center;
72
+ align-items: center;
73
+ width: 64px;
74
+ height: 64px;
75
+ position: relative;
76
+ background: rgba(0, 0, 0, 0.3);
77
+ border-radius: 4px;
78
+
79
+ .sprite-from-atlas-img {
80
+ position: absolute;
81
+ top: 50%;
82
+ left: 50%;
83
+ transform: translate(-50%, -50%) scale(2);
84
+ image-rendering: pixelated;
85
+ }
86
+ `;
87
+
88
+ const CellName = styled.h3`
89
+ font-size: 0.7rem;
90
+ color: #fef08a;
91
+ margin: 0;
92
+ text-align: center;
93
+ font-family: 'Press Start 2P', cursive;
94
+ line-height: 1.2;
95
+ word-break: break-word;
96
+ `;
@@ -0,0 +1,121 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { usePagination } from '../CraftBook/hooks/usePagination';
4
+ import { Dropdown, IOptionsProps } from '../Dropdown';
5
+ import { Pagination } from '../shared/Pagination/Pagination';
6
+ import { SearchBar } from '../shared/SearchBar/SearchBar';
7
+
8
+ interface IInformationCenterTabViewProps<T> {
9
+ items: T[];
10
+ searchQuery: string;
11
+ onSearchChange: (query: string) => void;
12
+ filterItems: (items: T[]) => T[];
13
+ renderContent: (items: T[]) => React.ReactNode;
14
+ searchPlaceholder: string;
15
+ filterOptions?: {
16
+ options: IOptionsProps[];
17
+ selectedOption: string;
18
+ onOptionChange: (value: string) => void;
19
+ };
20
+ emptyMessage?: string;
21
+ dependencies?: any[];
22
+ }
23
+
24
+ const ITEMS_PER_PAGE = 5;
25
+
26
+ export function InformationCenterTabView<T>({
27
+ items,
28
+ searchQuery,
29
+ onSearchChange,
30
+ filterItems,
31
+ renderContent,
32
+ searchPlaceholder,
33
+ filterOptions,
34
+ emptyMessage = 'No items found',
35
+ dependencies = [],
36
+ }: IInformationCenterTabViewProps<T>): React.ReactElement {
37
+ const filteredItems = filterItems(items);
38
+
39
+ const {
40
+ currentPage,
41
+ setCurrentPage,
42
+ paginatedItems,
43
+ totalPages,
44
+ } = usePagination({
45
+ items: filteredItems,
46
+ itemsPerPage: ITEMS_PER_PAGE,
47
+ dependencies: [searchQuery, ...dependencies],
48
+ });
49
+
50
+ return (
51
+ <Section>
52
+ <HeaderContainer>
53
+ <StyledSearchBar
54
+ value={searchQuery}
55
+ onChange={onSearchChange}
56
+ placeholder={searchPlaceholder}
57
+ />
58
+ {filterOptions && (
59
+ <StyledDropdown
60
+ options={filterOptions.options}
61
+ onChange={filterOptions.onOptionChange}
62
+ width="200px"
63
+ />
64
+ )}
65
+ </HeaderContainer>
66
+
67
+ {paginatedItems.length === 0 ? (
68
+ <EmptyMessage>{emptyMessage}</EmptyMessage>
69
+ ) : (
70
+ <>
71
+ {renderContent(paginatedItems)}
72
+ <StyledPagination
73
+ currentPage={currentPage}
74
+ totalPages={totalPages}
75
+ onPageChange={setCurrentPage}
76
+ />
77
+ </>
78
+ )}
79
+ </Section>
80
+ );
81
+ }
82
+
83
+ const Section = styled.div`
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: 1rem;
87
+ height: 100%;
88
+ min-height: 400px;
89
+ overflow-y: auto;
90
+ `;
91
+
92
+ const HeaderContainer = styled.div`
93
+ display: flex;
94
+ justify-content: space-between;
95
+ align-items: flex-start;
96
+ gap: 1rem;
97
+ padding: 0 1rem;
98
+ margin-top: 1.5rem;
99
+ `;
100
+
101
+ const StyledSearchBar = styled(SearchBar)`
102
+ margin-bottom: 1rem;
103
+ `;
104
+
105
+ const StyledDropdown = styled(Dropdown)`
106
+ min-width: 150px;
107
+ `;
108
+
109
+ const StyledPagination = styled(Pagination)`
110
+ margin-top: 1rem;
111
+ `;
112
+
113
+ const EmptyMessage = styled.div`
114
+ text-align: center;
115
+ color: #9ca3af;
116
+ padding: 2rem;
117
+ flex: 1;
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ `;
@@ -0,0 +1,87 @@
1
+ import {
2
+ EntityAttackType,
3
+ ICharacterPermanentBuff,
4
+ IEquippableItemBlueprint,
5
+ INPCLoot,
6
+ ItemRarities,
7
+ NPCAlignment,
8
+ NPCSubtype,
9
+ RangeTypes,
10
+ } from '@rpg-engine/shared';
11
+
12
+ export enum MovementSpeed {
13
+ ExtraSlow = 1.5,
14
+ Slow = 2.25,
15
+ Standard = 2.6,
16
+ Fast = 3,
17
+ ExtraFast = 3.5,
18
+ }
19
+
20
+ export enum EntityEffectBlueprint {
21
+ Poison = 'poison',
22
+ Bleeding = 'bleeding',
23
+ Freezing = 'freezing',
24
+ Burning = 'burning',
25
+ Corruption = 'corruption',
26
+ VineGrasp = 'vine-grasp',
27
+ Curse = 'curse',
28
+ Drain = 'drain',
29
+ Shadow = 'shadow',
30
+ Stun = 'stun',
31
+ Knockback = 'knockback',
32
+ Rage = 'rage',
33
+ Weakness = 'weakness',
34
+ Cripple = 'cripple',
35
+ Regeneration = 'regeneration',
36
+ }
37
+
38
+ export enum LootProbability {
39
+ VeryRare = 0.5,
40
+ Rare = 1,
41
+ Uncommon = 10,
42
+ SemiCommon = 15,
43
+ Common = 20,
44
+ VeryCommon = 35,
45
+ }
46
+
47
+ interface INPCBlueprintSpellArea {
48
+ spellKey: string;
49
+ probability: number;
50
+ power: string;
51
+ }
52
+
53
+ export interface IInformationCenterNPC {
54
+ id: string;
55
+ name: string;
56
+ key: string;
57
+ subType: NPCSubtype;
58
+ alignment: NPCAlignment;
59
+ attackType: EntityAttackType;
60
+ maxRangeAttack: RangeTypes;
61
+ speed: MovementSpeed;
62
+ baseHealth: number;
63
+ skills: {
64
+ level: number;
65
+ strength?: { level: number };
66
+ dexterity?: { level: number };
67
+ resistance?: { level: number };
68
+ };
69
+ fleeOnLowHealth: boolean;
70
+ entityEffects: EntityEffectBlueprint[];
71
+ areaSpells: INPCBlueprintSpellArea[];
72
+ loots: INPCLoot[];
73
+ }
74
+
75
+ export interface IInformationCenterItem extends IEquippableItemBlueprint {
76
+ tier: number;
77
+ rarity: ItemRarities;
78
+ rangeType?: EntityAttackType;
79
+ basePrice: number;
80
+ canSell: boolean;
81
+ maxStackSize: number;
82
+ usableEffectDescription?: string;
83
+ entityEffects?: [EntityEffectBlueprint];
84
+ entityEffectChance?: number;
85
+ equippedBuff?: ICharacterPermanentBuff[];
86
+ equippedBuffDescription?: string;
87
+ }
@@ -0,0 +1,170 @@
1
+ import React, { useState } from 'react';
2
+ import styled from 'styled-components';
3
+ import { Portal } from '../../../shared/Portal/Portal';
4
+ import { InformationCenterCell } from '../../InformationCenterCell';
5
+ import { InformationCenterTabView } from '../../InformationCenterTabView';
6
+ import { IInformationCenterNPC } from '../../InformationCenterTypes';
7
+ import { InformationCenterNPCDetails } from './InformationCenterNPCDetails';
8
+ import { InformationCenterNPCTooltip } from './InformationCenterNPCTooltip';
9
+
10
+ interface IBestiarySectionProps {
11
+ bestiaryItems: IInformationCenterNPC[];
12
+ itemsAtlasJSON: Record<string, any>;
13
+ itemsAtlasIMG: string;
14
+ entitiesAtlasJSON: Record<string, any>;
15
+ entitiesAtlasIMG: string;
16
+ initialSearchQuery: string;
17
+ }
18
+
19
+ export const BestiarySection: React.FC<IBestiarySectionProps> = ({
20
+ bestiaryItems,
21
+ itemsAtlasJSON,
22
+ itemsAtlasIMG,
23
+ entitiesAtlasJSON,
24
+ entitiesAtlasIMG,
25
+ initialSearchQuery,
26
+ }) => {
27
+ const [bestiarySearchQuery, setBestiarySearchQuery] = useState(
28
+ initialSearchQuery
29
+ );
30
+ const [tooltipData, setTooltipData] = useState<{
31
+ npc: IInformationCenterNPC;
32
+ position: { x: number; y: number };
33
+ } | null>(null);
34
+ const [
35
+ selectedMonster,
36
+ setSelectedMonster,
37
+ ] = useState<IInformationCenterNPC | null>(null);
38
+ const [isTouchDevice] = useState('ontouchstart' in window);
39
+
40
+ const filterItems = (items: IInformationCenterNPC[]) => {
41
+ return items.filter(item =>
42
+ item.name.toLowerCase().includes(bestiarySearchQuery.toLowerCase())
43
+ );
44
+ };
45
+
46
+ const handleMouseEnter = (
47
+ monster: IInformationCenterNPC,
48
+ event: React.MouseEvent
49
+ ) => {
50
+ if (!isTouchDevice && !selectedMonster) {
51
+ setTooltipData({
52
+ npc: monster,
53
+ position: { x: event.clientX, y: event.clientY },
54
+ });
55
+ }
56
+ };
57
+
58
+ const handleMouseLeave = () => {
59
+ if (!isTouchDevice) {
60
+ setTooltipData(null);
61
+ }
62
+ };
63
+
64
+ const handleMouseMove = (event: React.MouseEvent) => {
65
+ if (!isTouchDevice && tooltipData) {
66
+ setTooltipData({
67
+ ...tooltipData,
68
+ position: { x: event.clientX, y: event.clientY },
69
+ });
70
+ }
71
+ };
72
+
73
+ const handleTouchStart = (
74
+ monster: IInformationCenterNPC,
75
+ event: React.TouchEvent
76
+ ) => {
77
+ if (isTouchDevice) {
78
+ event.preventDefault();
79
+ const touch = event.touches[0];
80
+ if (tooltipData?.npc.id === monster.id) {
81
+ setTooltipData(null);
82
+ } else {
83
+ setTooltipData({
84
+ npc: monster,
85
+ position: { x: touch.clientX, y: touch.clientY },
86
+ });
87
+ }
88
+ }
89
+ };
90
+
91
+ const handleMonsterClick = (monster: IInformationCenterNPC) => {
92
+ setSelectedMonster(monster);
93
+ setTooltipData(null);
94
+ };
95
+
96
+ const renderContent = (items: IInformationCenterNPC[]) => (
97
+ <BestiaryGrid>
98
+ {items.map(item => (
99
+ <InformationCenterCell
100
+ key={item.id}
101
+ name={item.name}
102
+ spriteKey={item.key}
103
+ atlasJSON={entitiesAtlasJSON}
104
+ atlasIMG={entitiesAtlasIMG}
105
+ onClick={() => handleMonsterClick(item)}
106
+ onMouseEnter={e => handleMouseEnter(item, e)}
107
+ onMouseLeave={handleMouseLeave}
108
+ onMouseMove={handleMouseMove}
109
+ onTouchStart={e => handleTouchStart(item, e)}
110
+ />
111
+ ))}
112
+ </BestiaryGrid>
113
+ );
114
+
115
+ return (
116
+ <>
117
+ <InformationCenterTabView
118
+ items={bestiaryItems}
119
+ searchQuery={bestiarySearchQuery}
120
+ onSearchChange={setBestiarySearchQuery}
121
+ filterItems={filterItems}
122
+ renderContent={renderContent}
123
+ searchPlaceholder="Search monsters..."
124
+ emptyMessage="No monsters found"
125
+ />
126
+ {tooltipData && (
127
+ <Portal>
128
+ <TooltipWrapper
129
+ style={{
130
+ position: 'fixed',
131
+ left: tooltipData.position.x + 10,
132
+ top: tooltipData.position.y + 10,
133
+ }}
134
+ >
135
+ <InformationCenterNPCTooltip
136
+ npc={tooltipData.npc}
137
+ itemsAtlasJSON={itemsAtlasJSON}
138
+ itemsAtlasIMG={itemsAtlasIMG}
139
+ />
140
+ </TooltipWrapper>
141
+ </Portal>
142
+ )}
143
+ {selectedMonster && (
144
+ <InformationCenterNPCDetails
145
+ npc={selectedMonster}
146
+ itemsAtlasJSON={itemsAtlasJSON}
147
+ itemsAtlasIMG={itemsAtlasIMG}
148
+ entitiesAtlasJSON={entitiesAtlasJSON}
149
+ entitiesAtlasIMG={entitiesAtlasIMG}
150
+ onBack={() => setSelectedMonster(null)}
151
+ />
152
+ )}
153
+ </>
154
+ );
155
+ };
156
+
157
+ const BestiaryGrid = styled.div`
158
+ display: grid;
159
+ grid-template-columns: repeat(4, 1fr);
160
+ gap: 1rem;
161
+ padding: 0 1rem;
162
+ min-height: 300px;
163
+ `;
164
+
165
+ const TooltipWrapper = styled.div`
166
+ position: fixed;
167
+ z-index: 1000;
168
+ pointer-events: none;
169
+ width: 300px;
170
+ `;