@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.
Files changed (55) 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 +250 -12
  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 +251 -14
  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/CraftBook/CraftingRecipe.tsx +2 -1
  32. package/src/components/CraftBook/CraftingTooltip.tsx +4 -3
  33. package/src/components/DPad/JoystickDPad.tsx +318 -0
  34. package/src/components/InformationCenter/InformationCenter.tsx +155 -0
  35. package/src/components/InformationCenter/InformationCenterCell.tsx +96 -0
  36. package/src/components/InformationCenter/InformationCenterTabView.tsx +121 -0
  37. package/src/components/InformationCenter/InformationCenterTypes.ts +87 -0
  38. package/src/components/InformationCenter/sections/bestiary/BestiarySection.tsx +170 -0
  39. package/src/components/InformationCenter/sections/bestiary/InformationCenterNPCDetails.tsx +366 -0
  40. package/src/components/InformationCenter/sections/bestiary/InformationCenterNPCTooltip.tsx +204 -0
  41. package/src/components/InformationCenter/sections/faq/FaqSection.tsx +71 -0
  42. package/src/components/InformationCenter/sections/items/InformationCenterItemDetails.tsx +323 -0
  43. package/src/components/InformationCenter/sections/items/InformationCenterItemTooltip.tsx +88 -0
  44. package/src/components/InformationCenter/sections/items/ItemsSection.tsx +180 -0
  45. package/src/components/InformationCenter/sections/tutorials/TutorialsSection.tsx +144 -0
  46. package/src/components/InformationCenter/shared/BaseInformationDetails.tsx +162 -0
  47. package/src/components/InternalTabs/InternalTabs.tsx +1 -3
  48. package/src/components/shared/BaseTooltip.tsx +60 -0
  49. package/src/components/shared/Collapsible/Collapsible.tsx +70 -0
  50. package/src/components/shared/Portal/Portal.tsx +19 -0
  51. package/src/index.tsx +1 -0
  52. package/src/mocks/informationCenter.mocks.ts +562 -0
  53. package/src/stories/Features/craftbook/CraftBook.stories.tsx +15 -1
  54. package/src/stories/UI/info/InformationCenter.stories.tsx +58 -0
  55. package/src/stories/UI/joystick/JoystickDPad.stories.tsx +52 -0
@@ -0,0 +1,366 @@
1
+ import { isMobileOrTablet, RangeTypes } from '@rpg-engine/shared';
2
+ import React, { useState } from 'react';
3
+ import styled from 'styled-components';
4
+ import { uiColors } from '../../../../constants/uiColors';
5
+ import { Collapsible } from '../../../shared/Collapsible/Collapsible';
6
+ import { Pagination } from '../../../shared/Pagination/Pagination';
7
+ import { SearchBar } from '../../../shared/SearchBar/SearchBar';
8
+ import { SpriteFromAtlas } from '../../../shared/SpriteFromAtlas';
9
+ import {
10
+ IInformationCenterNPC,
11
+ MovementSpeed,
12
+ } from '../../InformationCenterTypes';
13
+ import { BaseInformationDetails } from '../../shared/BaseInformationDetails';
14
+
15
+ interface INPCDetailsProps {
16
+ npc: IInformationCenterNPC;
17
+ itemsAtlasJSON: Record<string, any>;
18
+ itemsAtlasIMG: string;
19
+ entitiesAtlasJSON: Record<string, any>;
20
+ entitiesAtlasIMG: string;
21
+ onBack: () => void;
22
+ }
23
+
24
+ const ITEMS_PER_PAGE = 5;
25
+
26
+ export const InformationCenterNPCDetails: React.FC<INPCDetailsProps> = ({
27
+ npc,
28
+ itemsAtlasJSON,
29
+ itemsAtlasIMG,
30
+ entitiesAtlasJSON,
31
+ entitiesAtlasIMG,
32
+ onBack,
33
+ }) => {
34
+ const isMobile = isMobileOrTablet();
35
+ const [lootSearchQuery, setLootSearchQuery] = useState('');
36
+ const [currentLootPage, setCurrentLootPage] = useState(1);
37
+
38
+ const formatText = (text: string | RangeTypes | MovementSpeed): string => {
39
+ if (typeof text === 'number') {
40
+ return text.toString();
41
+ }
42
+ return text
43
+ .toString()
44
+ .replace(/([A-Z])/g, ' $1')
45
+ .trim()
46
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
47
+ .replace(/\s+/g, ' ');
48
+ };
49
+
50
+ const formatRarity = (rarity: number): string => {
51
+ switch (rarity) {
52
+ case 0.5:
53
+ return 'Very Rare';
54
+ case 1:
55
+ return 'Rare';
56
+ case 10:
57
+ return 'Uncommon';
58
+ case 15:
59
+ return 'Semi Common';
60
+ case 20:
61
+ return 'Common';
62
+ case 35:
63
+ return 'Very Common';
64
+ default:
65
+ return 'Unknown';
66
+ }
67
+ };
68
+
69
+ const filteredLoots =
70
+ npc.loots?.filter(loot =>
71
+ formatText(loot.itemBlueprintKey)
72
+ .toLowerCase()
73
+ .includes(lootSearchQuery.toLowerCase())
74
+ ) || [];
75
+
76
+ const totalLootPages = Math.ceil(filteredLoots.length / ITEMS_PER_PAGE);
77
+ const paginatedLoots = filteredLoots.slice(
78
+ (currentLootPage - 1) * ITEMS_PER_PAGE,
79
+ currentLootPage * ITEMS_PER_PAGE
80
+ );
81
+
82
+ return (
83
+ <BaseInformationDetails
84
+ name={npc.name}
85
+ spriteKey={npc.key}
86
+ atlasJSON={entitiesAtlasJSON}
87
+ atlasIMG={entitiesAtlasIMG}
88
+ onBack={onBack}
89
+ >
90
+ <InfoSection>
91
+ <InfoItem>
92
+ <Label>Type:</Label>
93
+ <Value>{formatText(npc.subType)}</Value>
94
+ </InfoItem>
95
+ <InfoItem>
96
+ <Label>Alignment:</Label>
97
+ <Value>{formatText(npc.alignment)}</Value>
98
+ </InfoItem>
99
+ <InfoItem>
100
+ <Label>Attack Type:</Label>
101
+ <Value>{formatText(npc.attackType)}</Value>
102
+ </InfoItem>
103
+ <InfoItem>
104
+ <Label>Range:</Label>
105
+ <Value>{formatText(npc.maxRangeAttack)}</Value>
106
+ </InfoItem>
107
+ <InfoItem>
108
+ <Label>Speed:</Label>
109
+ <Value>{formatText(npc.speed)}</Value>
110
+ </InfoItem>
111
+ </InfoSection>
112
+
113
+ <StyledCollapsible title="Stats" defaultOpen={!isMobile}>
114
+ <StatGrid>
115
+ <StatItem>HP: {npc.baseHealth}</StatItem>
116
+ <StatItem>Level: {npc.skills.level}</StatItem>
117
+ {npc.skills.strength?.level && (
118
+ <StatItem>Strength: {npc.skills.strength.level}</StatItem>
119
+ )}
120
+ {npc.skills.dexterity?.level && (
121
+ <StatItem>Dexterity: {npc.skills.dexterity.level}</StatItem>
122
+ )}
123
+ {npc.skills.resistance?.level && (
124
+ <StatItem>Resistance: {npc.skills.resistance.level}</StatItem>
125
+ )}
126
+ </StatGrid>
127
+ </StyledCollapsible>
128
+
129
+ {npc.loots && npc.loots.length > 0 && (
130
+ <StyledCollapsible title="Loot" defaultOpen={!isMobile}>
131
+ <LootSearchContainer>
132
+ <StyledSearchBar
133
+ value={lootSearchQuery}
134
+ onChange={setLootSearchQuery}
135
+ placeholder="Search loot..."
136
+ />
137
+ </LootSearchContainer>
138
+ <LootGrid>
139
+ {paginatedLoots.map((loot, index) => (
140
+ <LootItem key={index}>
141
+ <SpriteFromAtlas
142
+ atlasJSON={itemsAtlasJSON}
143
+ atlasIMG={itemsAtlasIMG}
144
+ spriteKey={loot.itemBlueprintKey}
145
+ width={24}
146
+ height={24}
147
+ imgScale={1}
148
+ />
149
+ <LootDetails>
150
+ <LootName>{formatText(loot.itemBlueprintKey)}</LootName>
151
+ <LootInfo>
152
+ <LootChance>{formatRarity(loot.chance)}</LootChance>
153
+ {loot.quantityRange && (
154
+ <LootQuantity>
155
+ x{loot.quantityRange[0]}-{loot.quantityRange[1]}
156
+ </LootQuantity>
157
+ )}
158
+ </LootInfo>
159
+ </LootDetails>
160
+ </LootItem>
161
+ ))}
162
+ </LootGrid>
163
+ {filteredLoots.length > ITEMS_PER_PAGE && (
164
+ <PaginationContainer>
165
+ <StyledPagination
166
+ currentPage={currentLootPage}
167
+ totalPages={totalLootPages}
168
+ onPageChange={setCurrentLootPage}
169
+ />
170
+ </PaginationContainer>
171
+ )}
172
+ </StyledCollapsible>
173
+ )}
174
+
175
+ {npc.entityEffects && npc.entityEffects.length > 0 && (
176
+ <StyledCollapsible title="Effects" defaultOpen={!isMobile}>
177
+ <EffectsList>
178
+ {npc.entityEffects.map((effect, index) => (
179
+ <EffectItem key={index}>{formatText(effect)}</EffectItem>
180
+ ))}
181
+ </EffectsList>
182
+ </StyledCollapsible>
183
+ )}
184
+
185
+ {npc.areaSpells && npc.areaSpells.length > 0 && (
186
+ <StyledCollapsible title="Spells" defaultOpen={!isMobile}>
187
+ <SpellsList>
188
+ {npc.areaSpells.map((spell, index) => (
189
+ <SpellItem key={index}>
190
+ <SpellName>{formatText(spell.spellKey)}</SpellName>
191
+ <SpellDetails>
192
+ Power: <SpellValue>{formatText(spell.power)}</SpellValue>
193
+ <Separator>•</Separator>
194
+ Chance: <SpellValue>{spell.probability}%</SpellValue>
195
+ </SpellDetails>
196
+ </SpellItem>
197
+ ))}
198
+ </SpellsList>
199
+ </StyledCollapsible>
200
+ )}
201
+ </BaseInformationDetails>
202
+ );
203
+ };
204
+
205
+ const InfoSection = styled.div`
206
+ display: grid;
207
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
208
+ gap: 8px;
209
+ background: rgba(255, 255, 255, 0.05);
210
+ padding: 12px;
211
+ border-radius: 4px;
212
+ `;
213
+
214
+ const InfoItem = styled.div`
215
+ display: flex;
216
+ align-items: center;
217
+ gap: 8px;
218
+ `;
219
+
220
+ const Label = styled.span`
221
+ color: ${uiColors.yellow};
222
+ font-size: 0.5rem;
223
+ opacity: 0.8;
224
+ `;
225
+
226
+ const Value = styled.span`
227
+ color: ${uiColors.white};
228
+ font-size: 0.5rem;
229
+ `;
230
+
231
+ const StyledCollapsible = styled(Collapsible)`
232
+ background: rgba(255, 255, 255, 0.05);
233
+ border-radius: 4px;
234
+ overflow: hidden;
235
+ `;
236
+
237
+ const StatGrid = styled.div`
238
+ display: grid;
239
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
240
+ gap: 8px;
241
+ padding: 12px;
242
+ `;
243
+
244
+ const StatItem = styled.div`
245
+ color: ${uiColors.white};
246
+ font-size: 0.5rem;
247
+ background: rgba(255, 255, 255, 0.05);
248
+ padding: 8px;
249
+ border-radius: 4px;
250
+ `;
251
+
252
+ const EffectsList = styled.div`
253
+ display: flex;
254
+ flex-wrap: wrap;
255
+ gap: 8px;
256
+ padding: 12px;
257
+ `;
258
+
259
+ const EffectItem = styled.div`
260
+ color: ${uiColors.white};
261
+ font-size: 0.5rem;
262
+ background: rgba(255, 255, 255, 0.1);
263
+ padding: 4px 8px;
264
+ border-radius: 4px;
265
+ `;
266
+
267
+ const SpellsList = styled.div`
268
+ display: flex;
269
+ flex-direction: column;
270
+ gap: 8px;
271
+ padding: 12px;
272
+ `;
273
+
274
+ const SpellItem = styled.div`
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 4px;
278
+ background: rgba(255, 255, 255, 0.05);
279
+ padding: 8px;
280
+ border-radius: 4px;
281
+ `;
282
+
283
+ const SpellName = styled.div`
284
+ color: ${uiColors.yellow};
285
+ font-size: 0.5rem;
286
+ `;
287
+
288
+ const SpellDetails = styled.div`
289
+ display: flex;
290
+ align-items: center;
291
+ gap: 8px;
292
+ color: ${uiColors.white};
293
+ font-size: 0.45rem;
294
+ opacity: 0.8;
295
+ `;
296
+
297
+ const SpellValue = styled.span`
298
+ color: ${uiColors.yellow};
299
+ `;
300
+
301
+ const Separator = styled.span`
302
+ color: ${uiColors.yellow};
303
+ opacity: 0.5;
304
+ `;
305
+
306
+ const LootSearchContainer = styled.div`
307
+ padding: 12px 12px 0;
308
+ `;
309
+
310
+ const StyledSearchBar = styled(SearchBar)`
311
+ width: 100%;
312
+ `;
313
+
314
+ const LootGrid = styled.div`
315
+ display: grid;
316
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
317
+ gap: 8px;
318
+ padding: 12px;
319
+ `;
320
+
321
+ const LootItem = styled.div`
322
+ display: flex;
323
+ align-items: center;
324
+ gap: 8px;
325
+ background: rgba(255, 255, 255, 0.05);
326
+ padding: 8px;
327
+ border-radius: 4px;
328
+ `;
329
+
330
+ const LootDetails = styled.div`
331
+ flex: 1;
332
+ display: flex;
333
+ flex-direction: column;
334
+ gap: 4px;
335
+ `;
336
+
337
+ const LootName = styled.div`
338
+ color: ${uiColors.white};
339
+ font-size: 0.5rem;
340
+ `;
341
+
342
+ const LootInfo = styled.div`
343
+ display: flex;
344
+ align-items: center;
345
+ gap: 8px;
346
+ `;
347
+
348
+ const LootChance = styled.span`
349
+ color: ${uiColors.yellow};
350
+ font-size: 0.45rem;
351
+ `;
352
+
353
+ const LootQuantity = styled.span`
354
+ color: ${uiColors.lightGray};
355
+ font-size: 0.45rem;
356
+ `;
357
+
358
+ const PaginationContainer = styled.div`
359
+ display: flex;
360
+ justify-content: center;
361
+ padding: 12px;
362
+ `;
363
+
364
+ const StyledPagination = styled(Pagination)`
365
+ font-size: 0.6rem;
366
+ `;
@@ -0,0 +1,204 @@
1
+ import { RangeTypes } from '@rpg-engine/shared';
2
+ import React from 'react';
3
+ import styled from 'styled-components';
4
+ import { uiColors } from '../../../../constants/uiColors';
5
+ import BaseTooltip, {
6
+ Section,
7
+ SectionTitle,
8
+ StatItem,
9
+ StatsContainer,
10
+ TooltipTitle,
11
+ } from '../../../shared/BaseTooltip';
12
+ import { SpriteFromAtlas } from '../../../shared/SpriteFromAtlas';
13
+ import {
14
+ IInformationCenterNPC,
15
+ MovementSpeed,
16
+ } from '../../InformationCenterTypes';
17
+
18
+ interface INPCTooltipProps {
19
+ npc: IInformationCenterNPC;
20
+ itemsAtlasJSON: any;
21
+ itemsAtlasIMG: string;
22
+ }
23
+
24
+ const EffectsList = styled.div`
25
+ display: flex;
26
+ gap: 8px;
27
+ flex-wrap: wrap;
28
+ `;
29
+
30
+ const EffectItem = styled.span`
31
+ font-size: 0.5rem;
32
+ color: ${uiColors.white};
33
+ background: rgba(255, 255, 255, 0.1);
34
+ padding: 2px 6px;
35
+ border-radius: 4px;
36
+ `;
37
+
38
+ const SpellList = styled.div`
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: 2px;
42
+ `;
43
+
44
+ const SpellItem = styled.div`
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ font-size: 0.5rem;
49
+ color: ${uiColors.white};
50
+ background: rgba(255, 255, 255, 0.05);
51
+ padding: 2px 6px;
52
+ border-radius: 4px;
53
+ `;
54
+
55
+ const SpellInfo = styled.span`
56
+ color: ${uiColors.yellow};
57
+ margin-left: 8px;
58
+ opacity: 0.8;
59
+ `;
60
+
61
+ const LootList = styled.div`
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 2px;
65
+ `;
66
+
67
+ const LootItem = styled.div`
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 4px;
71
+ font-size: 0.5rem;
72
+ background: rgba(255, 255, 255, 0.05);
73
+ padding: 4px 6px;
74
+ border-radius: 4px;
75
+
76
+ .sprite-from-atlas-img {
77
+ top: 0px;
78
+ left: 0px;
79
+ }
80
+ `;
81
+
82
+ const LootName = styled.span`
83
+ color: ${uiColors.white};
84
+ flex: 1;
85
+ overflow: hidden;
86
+ text-overflow: ellipsis;
87
+ white-space: nowrap;
88
+ margin-left: 4px;
89
+ `;
90
+
91
+ const LootChance = styled.span`
92
+ color: ${uiColors.yellow};
93
+ font-size: 0.45rem;
94
+ text-transform: lowercase;
95
+ opacity: 0.8;
96
+ `;
97
+
98
+ const MoreIndicator = styled.div`
99
+ color: ${uiColors.yellow};
100
+ font-size: 0.45rem;
101
+ text-align: center;
102
+ margin-top: 2px;
103
+ opacity: 0.7;
104
+ `;
105
+
106
+ const formatText = (text: string | RangeTypes | MovementSpeed): string => {
107
+ if (typeof text === 'number') {
108
+ return text.toString();
109
+ }
110
+ return text
111
+ .toString()
112
+ .replace(/([A-Z])/g, ' $1')
113
+ .trim()
114
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
115
+ .replace(/\s+/g, ' ');
116
+ };
117
+
118
+ const formatRarity = (rarity: number): string => {
119
+ switch (rarity) {
120
+ case 0.5:
121
+ return 'Very Rare';
122
+ case 1:
123
+ return 'Rare';
124
+ case 10:
125
+ return 'Uncommon';
126
+ case 15:
127
+ return 'Semi Common';
128
+ case 20:
129
+ return 'Common';
130
+ case 35:
131
+ return 'Very Common';
132
+ default:
133
+ return 'Unknown';
134
+ }
135
+ };
136
+
137
+ export const InformationCenterNPCTooltip: React.FC<INPCTooltipProps> = ({
138
+ npc,
139
+ itemsAtlasJSON,
140
+ itemsAtlasIMG,
141
+ }) => {
142
+ return (
143
+ <BaseTooltip>
144
+ <TooltipTitle>{npc.name}</TooltipTitle>
145
+
146
+ <StatsContainer>
147
+ <StatItem>HP: {npc.baseHealth}</StatItem>
148
+ <StatItem>LVL: {npc.skills.level}</StatItem>
149
+ <StatItem>STR: {npc.skills.strength?.level || '-'}</StatItem>
150
+ <StatItem>DEX: {npc.skills.dexterity?.level || '-'}</StatItem>
151
+ <StatItem>RES: {npc.skills.resistance?.level || '-'}</StatItem>
152
+ <StatItem>SPD: {formatText(npc.speed)}</StatItem>
153
+ </StatsContainer>
154
+
155
+ {npc.entityEffects && npc.entityEffects.length > 0 && (
156
+ <Section>
157
+ <SectionTitle>Effects</SectionTitle>
158
+ <EffectsList>
159
+ {npc.entityEffects.map(effect => (
160
+ <EffectItem key={effect}>{formatText(effect)}</EffectItem>
161
+ ))}
162
+ </EffectsList>
163
+ </Section>
164
+ )}
165
+
166
+ {npc.areaSpells && npc.areaSpells.length > 0 && (
167
+ <Section>
168
+ <SectionTitle>Spells</SectionTitle>
169
+ <SpellList>
170
+ {npc.areaSpells.map(spell => (
171
+ <SpellItem key={spell.spellKey}>
172
+ {formatText(spell.spellKey)}
173
+ <SpellInfo>
174
+ {formatText(spell.power)}, {spell.probability}%
175
+ </SpellInfo>
176
+ </SpellItem>
177
+ ))}
178
+ </SpellList>
179
+ </Section>
180
+ )}
181
+
182
+ {npc.loots && npc.loots.length > 0 && (
183
+ <Section>
184
+ <SectionTitle>Possible Loot</SectionTitle>
185
+ <LootList>
186
+ {npc.loots.slice(0, 4).map(loot => (
187
+ <LootItem key={loot.itemBlueprintKey}>
188
+ <SpriteFromAtlas
189
+ atlasIMG={itemsAtlasIMG}
190
+ atlasJSON={itemsAtlasJSON}
191
+ spriteKey={loot.itemBlueprintKey}
192
+ imgScale={1}
193
+ />
194
+ <LootName>{formatText(loot.itemBlueprintKey)}</LootName>
195
+ <LootChance>{formatRarity(loot.chance)}</LootChance>
196
+ </LootItem>
197
+ ))}
198
+ {npc.loots.length > 4 && <MoreIndicator>...</MoreIndicator>}
199
+ </LootList>
200
+ </Section>
201
+ )}
202
+ </BaseTooltip>
203
+ );
204
+ };
@@ -0,0 +1,71 @@
1
+ import React, { useState } from 'react';
2
+ import styled from 'styled-components';
3
+ import { IFaqItem } from '../../InformationCenter';
4
+ import { InformationCenterTabView } from '../../InformationCenterTabView';
5
+
6
+ interface IFaqSectionProps {
7
+ faqItems: IFaqItem[];
8
+ initialSearchQuery: string;
9
+ }
10
+
11
+ export const FaqSection: React.FC<IFaqSectionProps> = ({
12
+ faqItems,
13
+ initialSearchQuery,
14
+ }) => {
15
+ const [faqSearchQuery, setFaqSearchQuery] = useState(initialSearchQuery);
16
+
17
+ const filterItems = (items: IFaqItem[]) => {
18
+ return items.filter(
19
+ item =>
20
+ item.question.toLowerCase().includes(faqSearchQuery.toLowerCase()) ||
21
+ item.answer.toLowerCase().includes(faqSearchQuery.toLowerCase())
22
+ );
23
+ };
24
+
25
+ const renderContent = (items: IFaqItem[]) => (
26
+ <FaqContainer>
27
+ {items.map(item => (
28
+ <FaqItem key={item.id}>
29
+ <Question>{item.question}</Question>
30
+ <Answer>{item.answer}</Answer>
31
+ </FaqItem>
32
+ ))}
33
+ </FaqContainer>
34
+ );
35
+
36
+ return (
37
+ <InformationCenterTabView
38
+ items={faqItems}
39
+ searchQuery={faqSearchQuery}
40
+ onSearchChange={setFaqSearchQuery}
41
+ filterItems={filterItems}
42
+ renderContent={renderContent}
43
+ searchPlaceholder="Search FAQ..."
44
+ emptyMessage="No FAQ items found"
45
+ />
46
+ );
47
+ };
48
+
49
+ const FaqContainer = styled.div`
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: 1rem;
53
+ padding: 0 1rem;
54
+ `;
55
+
56
+ const FaqItem = styled.div`
57
+ background: rgba(0, 0, 0, 0.2);
58
+ padding: 1rem;
59
+ border-radius: 4px;
60
+ `;
61
+
62
+ const Question = styled.h3`
63
+ font-size: 1rem;
64
+ color: #fef08a;
65
+ margin-bottom: 0.5rem;
66
+ `;
67
+
68
+ const Answer = styled.p`
69
+ font-size: 0.9rem;
70
+ color: #ffffff;
71
+ `;