@rpg-engine/long-bow 0.8.230 → 0.8.232

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 (30) hide show
  1. package/dist/components/Quests/QuestInfo/QuestInfo.d.ts +3 -1
  2. package/dist/components/Quests/QuestInfo/QuestObjectivesSection.d.ts +8 -0
  3. package/dist/components/Quests/QuestInfo/QuestRequirementsSection.d.ts +8 -0
  4. package/dist/components/Quests/QuestInfo/QuestRewardsSection.d.ts +8 -0
  5. package/dist/components/Quests/QuestInfo/QuestSectionTypes.d.ts +8 -0
  6. package/dist/components/Quests/QuestList.d.ts +4 -5
  7. package/dist/components/Quests/QuestListRow.d.ts +15 -0
  8. package/dist/components/Tutorial/TutorialStepper.d.ts +2 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/long-bow.cjs.development.js +473 -156
  11. package/dist/long-bow.cjs.development.js.map +1 -1
  12. package/dist/long-bow.cjs.production.min.js +1 -1
  13. package/dist/long-bow.cjs.production.min.js.map +1 -1
  14. package/dist/long-bow.esm.js +474 -158
  15. package/dist/long-bow.esm.js.map +1 -1
  16. package/package.json +1 -1
  17. package/src/components/Item/Cards/ItemTooltip.tsx +27 -16
  18. package/src/components/Quests/QuestInfo/QuestInfo.tsx +57 -68
  19. package/src/components/Quests/QuestInfo/QuestObjectivesSection.tsx +114 -0
  20. package/src/components/Quests/QuestInfo/QuestRequirementsSection.tsx +74 -0
  21. package/src/components/Quests/QuestInfo/QuestRewardsSection.tsx +128 -0
  22. package/src/components/Quests/QuestInfo/QuestSectionTypes.ts +8 -0
  23. package/src/components/Quests/QuestList.tsx +12 -87
  24. package/src/components/Quests/QuestListRow.tsx +178 -0
  25. package/src/components/Store/CartView.tsx +3 -2
  26. package/src/components/Store/MetadataCollector.tsx +28 -1
  27. package/src/components/Store/__test__/MetadataCollector.spec.tsx +12 -3
  28. package/src/components/Tutorial/TutorialStepper.tsx +2 -0
  29. package/src/components/shared/SimpleTooltip.tsx +32 -19
  30. package/src/index.tsx +1 -1
@@ -1,10 +1,12 @@
1
- import { IQuest, QuestStatus } from '@rpg-engine/shared';
1
+ import { IQuest } from '@rpg-engine/shared';
2
2
  import React from 'react';
3
3
  import styled, { CSSProperties } from 'styled-components';
4
4
  import { uiColors } from '../../constants/uiColors';
5
+ import { IQuestListAtlasProps, QuestListRow } from './QuestListRow';
5
6
 
6
- export interface IQuestListProps {
7
+ export interface IQuestListProps extends IQuestListAtlasProps {
7
8
  quests?: IQuest[];
9
+ compact?: boolean;
8
10
  styles?: {
9
11
  container?: CSSProperties;
10
12
  card?: CSSProperties;
@@ -13,34 +15,18 @@ export interface IQuestListProps {
13
15
  };
14
16
  }
15
17
 
16
- export const QuestList: React.FC<IQuestListProps> = ({ quests, styles }) => {
18
+ export const QuestList: React.FC<IQuestListProps> = ({ quests, compact, styles, itemsAtlasJSON, itemsAtlasIMG }) => {
17
19
  return (
18
20
  <QuestListContainer style={styles?.container}>
19
21
  {quests && quests.length > 0 ? (
20
22
  quests.map((quest, i) => (
21
- <QuestCard key={i} style={styles?.card}>
22
- <QuestItem>
23
- <Label style={styles?.label}>Title:</Label>
24
- <Value style={styles?.value}>
25
- {formatQuestText(quest.title)}
26
- </Value>
27
- </QuestItem>
28
- <QuestItem>
29
- <Label style={styles?.label}>Status:</Label>
30
- <Value
31
- style={{
32
- ...styles?.value,
33
- color: getQuestStatusColor(quest.status),
34
- }}
35
- >
36
- {formatQuestStatus(quest.status) ?? 'Unknown'}
37
- </Value>
38
- </QuestItem>
39
- <QuestItem>
40
- <Label style={styles?.label}>Description:</Label>
41
- <Value style={styles?.value}>{quest.description}</Value>
42
- </QuestItem>
43
- </QuestCard>
23
+ <QuestListRow
24
+ key={quest._id || i}
25
+ quest={quest}
26
+ compact={compact}
27
+ itemsAtlasJSON={itemsAtlasJSON}
28
+ itemsAtlasIMG={itemsAtlasIMG}
29
+ />
44
30
  ))
45
31
  ) : (
46
32
  <NoQuestContainer>
@@ -58,38 +44,6 @@ const QuestListContainer = styled.div`
58
44
  font-size: 0.7rem;
59
45
  `;
60
46
 
61
- const QuestCard = styled.div`
62
- background-color: ${uiColors.darkGray};
63
- padding: 15px;
64
- margin-bottom: 10px;
65
- border-radius: 10px;
66
- border: 1px solid ${uiColors.gray};
67
- display: flex;
68
- flex-direction: column;
69
- `;
70
-
71
- const QuestItem = styled.div`
72
- display: flex;
73
- margin-bottom: 5px;
74
- flex-wrap: wrap;
75
-
76
- &:last-child {
77
- margin-bottom: 0;
78
- }
79
- `;
80
-
81
- const Label = styled.span`
82
- font-weight: bold;
83
- color: ${uiColors.yellow} !important;
84
- margin-right: 10px;
85
- `;
86
-
87
- const Value = styled.span`
88
- flex-grow: 1;
89
- color: ${uiColors.white};
90
- word-wrap: break-word;
91
- `;
92
-
93
47
  const NoQuestContainer = styled.div`
94
48
  text-align: center;
95
49
  p {
@@ -97,32 +51,3 @@ const NoQuestContainer = styled.div`
97
51
  color: ${uiColors.lightGray};
98
52
  }
99
53
  `;
100
-
101
- export const formatQuestText = (text: string) => {
102
- if (!text) return '';
103
- return text
104
- .split('-')
105
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
106
- .join(' ');
107
- };
108
-
109
- export const getQuestStatusColor = (status?: QuestStatus) => {
110
- switch (status) {
111
- case QuestStatus.Pending:
112
- return uiColors.orange;
113
- case QuestStatus.InProgress:
114
- return uiColors.blue;
115
- case QuestStatus.Completed:
116
- return uiColors.lightGreen;
117
- default:
118
- return uiColors.white;
119
- }
120
- };
121
-
122
- export const formatQuestStatus = (status?: QuestStatus) => {
123
- if (!status) return '';
124
- return status
125
- .split(/(?=[A-Z])/)
126
- .join(' ')
127
- .replace(/^\w/, c => c.toUpperCase());
128
- };
@@ -0,0 +1,178 @@
1
+ import { IQuest, QuestStatus, QuestType } from '@rpg-engine/shared';
2
+ import React from 'react';
3
+ import styled from 'styled-components';
4
+ import { uiColors } from '../../constants/uiColors';
5
+ import { ItemInfoWrapper } from '../Item/Cards/ItemInfoWrapper';
6
+ import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
7
+
8
+ export interface IQuestListAtlasProps {
9
+ itemsAtlasJSON?: any;
10
+ itemsAtlasIMG?: any;
11
+ }
12
+
13
+ interface IProps extends IQuestListAtlasProps {
14
+ quest: IQuest;
15
+ compact?: boolean;
16
+ }
17
+
18
+ const getProgressText = (quest: IQuest): string | null => {
19
+ const objective = (quest.objectives || [])[0] as any;
20
+ if (!objective) {
21
+ return null;
22
+ }
23
+
24
+ if (objective.type === QuestType.Kill) {
25
+ return `${objective.killCount || 0}/${objective.killCountTarget}`;
26
+ }
27
+
28
+ if (objective.type === QuestType.Interaction && objective.items?.length) {
29
+ const item = objective.items[0];
30
+ return `Bring ${item.playerHasQty || 0}/${item.qty}`;
31
+ }
32
+
33
+ return objective.type === QuestType.Interaction && objective.targetNPCkey ? 'Talk' : null;
34
+ };
35
+
36
+ export const QuestListRow: React.FC<IProps> = ({
37
+ quest,
38
+ compact,
39
+ itemsAtlasJSON,
40
+ itemsAtlasIMG,
41
+ }) => {
42
+ const rewardItems = (quest.rewards || []).flatMap((reward: any) => reward.items || []);
43
+ const visibleRewards = rewardItems.slice(0, 6);
44
+ const overflowCount = Math.max(rewardItems.length - visibleRewards.length, 0);
45
+ const progressText = getProgressText(quest);
46
+
47
+ return (
48
+ <QuestCard>
49
+ <Header>
50
+ <Title>{formatQuestText(quest.title)}</Title>
51
+ <Status style={{ color: getQuestStatusColor(quest.status) }}>
52
+ {formatQuestStatus(quest.status) || 'Unknown'}
53
+ </Status>
54
+ </Header>
55
+ {!compact && (
56
+ <>
57
+ <Description>{quest.description}</Description>
58
+ <MetaRow>
59
+ {quest.status === QuestStatus.Completed ? 'Completed' : progressText}
60
+ </MetaRow>
61
+ {!!visibleRewards.length && (
62
+ <RewardStrip>
63
+ {visibleRewards.map((item: any) => (
64
+ <RewardIcon key={`${item.key}-${item.qty}`}>
65
+ {itemsAtlasJSON && itemsAtlasIMG && item.texturePath ? (
66
+ <ItemInfoWrapper item={{ ...item, stackQty: item.qty } as any} atlasJSON={itemsAtlasJSON} atlasIMG={itemsAtlasIMG}>
67
+ <SpriteFromAtlas
68
+ atlasJSON={itemsAtlasJSON}
69
+ atlasIMG={itemsAtlasIMG}
70
+ spriteKey={item.texturePath}
71
+ width={24}
72
+ height={24}
73
+ imgScale={1}
74
+ centered
75
+ />
76
+ </ItemInfoWrapper>
77
+ ) : (
78
+ <FallbackIcon>{item.name?.[0] || '?'}</FallbackIcon>
79
+ )}
80
+ </RewardIcon>
81
+ ))}
82
+ {overflowCount > 0 && <MoreRewards>+{overflowCount} more</MoreRewards>}
83
+ </RewardStrip>
84
+ )}
85
+ </>
86
+ )}
87
+ </QuestCard>
88
+ );
89
+ };
90
+
91
+ export const formatQuestText = (text: string) => {
92
+ if (!text) return '';
93
+ return text
94
+ .split('-')
95
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
96
+ .join(' ');
97
+ };
98
+
99
+ export const getQuestStatusColor = (status?: QuestStatus) => {
100
+ switch (status) {
101
+ case QuestStatus.Pending:
102
+ return uiColors.orange;
103
+ case QuestStatus.InProgress:
104
+ return uiColors.blue;
105
+ case QuestStatus.Completed:
106
+ return uiColors.lightGreen;
107
+ default:
108
+ return uiColors.white;
109
+ }
110
+ };
111
+
112
+ export const formatQuestStatus = (status?: QuestStatus) => {
113
+ if (!status) return '';
114
+ return status
115
+ .split(/(?=[A-Z])/)
116
+ .join(' ')
117
+ .replace(/^\w/, c => c.toUpperCase());
118
+ };
119
+
120
+ const QuestCard = styled.div`
121
+ background-color: ${uiColors.darkGray};
122
+ padding: 12px;
123
+ margin-bottom: 10px;
124
+ border-radius: 8px;
125
+ border: 1px solid ${uiColors.gray};
126
+ `;
127
+
128
+ const Header = styled.div`
129
+ display: flex;
130
+ justify-content: space-between;
131
+ gap: 10px;
132
+ `;
133
+
134
+ const Title = styled.span`
135
+ color: ${uiColors.yellow};
136
+ font-weight: bold;
137
+ `;
138
+
139
+ const Status = styled.span`
140
+ white-space: nowrap;
141
+ `;
142
+
143
+ const Description = styled.p`
144
+ color: ${uiColors.white};
145
+ margin: 8px 0;
146
+ `;
147
+
148
+ const MetaRow = styled.div`
149
+ color: ${uiColors.lightGreen};
150
+ font-size: 0.7rem;
151
+ margin-bottom: 6px;
152
+ `;
153
+
154
+ const RewardStrip = styled.div`
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 6px;
158
+ `;
159
+
160
+ const RewardIcon = styled.div`
161
+ width: 24px;
162
+ height: 24px;
163
+ `;
164
+
165
+ const FallbackIcon = styled.div`
166
+ width: 24px;
167
+ height: 24px;
168
+ border: 1px solid ${uiColors.gray};
169
+ color: ${uiColors.yellow};
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ `;
174
+
175
+ const MoreRewards = styled.span`
176
+ color: ${uiColors.lightGray};
177
+ font-size: 0.65rem;
178
+ `;
@@ -53,7 +53,7 @@ const MetadataDisplay: React.FC<{
53
53
  <InfoBox />
54
54
  <span>Skin:</span>
55
55
  </MetadataLabel>
56
- <MetadataValue>{metadata.selectedSkinName || 'Custom skin'}</MetadataValue>
56
+ <MetadataValue>{metadata.selectedSkinName || metadata.selectedSkin || 'Custom skin'}</MetadataValue>
57
57
  </MetadataInfo>
58
58
  );
59
59
  default:
@@ -159,6 +159,7 @@ export const CartView: React.FC<ICartViewProps> = ({
159
159
  const getSpriteKey = (textureKey: string) => {
160
160
  return textureKey + '/down/standing/0.png';
161
161
  };
162
+ const selectedSkinTextureKey = cartItem.metadata?.selectedSkinTextureKey ?? cartItem.metadata?.selectedSkin;
162
163
 
163
164
  return (
164
165
  <CartItemRow key={cartItem.item.key}>
@@ -166,7 +167,7 @@ export const CartView: React.FC<ICartViewProps> = ({
166
167
  <SpriteFromAtlas
167
168
  atlasJSON={cartItem.item.metadataType === MetadataType.CharacterSkin ? characterAtlasJSON : atlasJSON}
168
169
  atlasIMG={cartItem.item.metadataType === MetadataType.CharacterSkin ? characterAtlasIMG : atlasIMG}
169
- spriteKey={cartItem.item.metadataType === MetadataType.CharacterSkin && cartItem.metadata?.selectedSkinTextureKey ? getSpriteKey(cartItem.metadata.selectedSkinTextureKey) : cartItem.item.texturePath}
170
+ spriteKey={cartItem.item.metadataType === MetadataType.CharacterSkin && selectedSkinTextureKey ? getSpriteKey(selectedSkinTextureKey) : cartItem.item.texturePath}
170
171
  width={24}
171
172
  height={24}
172
173
  imgScale={1.5}
@@ -2,6 +2,11 @@ import { MetadataType } from '@rpg-engine/shared';
2
2
  import React, { useCallback, useEffect, useRef } from 'react';
3
3
  import { CharacterSkinSelectionModal } from '../Character/CharacterSkinSelectionModal';
4
4
 
5
+ interface ICharacterSkinOption {
6
+ name: string;
7
+ textureKey: string;
8
+ }
9
+
5
10
  export interface IMetadataCollectorProps {
6
11
  metadataType: MetadataType;
7
12
  config: Record<string, any>;
@@ -9,6 +14,21 @@ export interface IMetadataCollectorProps {
9
14
  onCancel: () => void;
10
15
  }
11
16
 
17
+ const getCharacterSkinMetadata = (
18
+ selectedSkinTextureKey: string,
19
+ availableCharacters: ICharacterSkinOption[]
20
+ ) => {
21
+ const selectedSkin = availableCharacters.find(
22
+ (character) => character.textureKey === selectedSkinTextureKey
23
+ );
24
+
25
+ return {
26
+ selectedSkin: selectedSkinTextureKey,
27
+ selectedSkinName: selectedSkin?.name ?? selectedSkinTextureKey,
28
+ selectedSkinTextureKey,
29
+ };
30
+ };
31
+
12
32
  export const MetadataCollector: React.FC<IMetadataCollectorProps> = ({
13
33
  metadataType,
14
34
  config,
@@ -54,7 +74,14 @@ export const MetadataCollector: React.FC<IMetadataCollectorProps> = ({
54
74
  <CharacterSkinSelectionModal
55
75
  isOpen
56
76
  onClose={handleCancel}
57
- onConfirm={(selectedSkin: any) => handleCollect({ selectedSkin })}
77
+ onConfirm={(selectedSkin: string) =>
78
+ handleCollect(
79
+ getCharacterSkinMetadata(
80
+ selectedSkin,
81
+ config.availableCharacters || []
82
+ )
83
+ )
84
+ }
58
85
  availableCharacters={config.availableCharacters || []}
59
86
  atlasJSON={config.atlasJSON}
60
87
  atlasIMG={config.atlasIMG}
@@ -72,11 +72,16 @@ describe('MetadataCollector', () => {
72
72
  });
73
73
 
74
74
  it('collects once without cancelling again when the modal closes after confirm', () => {
75
+ const availableCharacters = [
76
+ { name: 'Superior Knight', textureKey: 'superior-knight' },
77
+ { name: 'Black Knight', textureKey: 'black-knight' },
78
+ ];
79
+
75
80
  act(() => {
76
81
  ReactDOM.render(
77
82
  <MetadataCollector
78
83
  metadataType={MetadataType.CharacterSkin}
79
- config={{ availableCharacters: ['char1', 'char2'] }}
84
+ config={{ availableCharacters }}
80
85
  onCollect={mockOnCollect}
81
86
  onCancel={mockOnCancel}
82
87
  />,
@@ -87,11 +92,15 @@ describe('MetadataCollector', () => {
87
92
  const callArgs = CharacterSkinSelectionModal.mock.calls[0][0];
88
93
 
89
94
  act(() => {
90
- callArgs.onConfirm('char1');
95
+ callArgs.onConfirm('superior-knight');
91
96
  callArgs.onClose();
92
97
  });
93
98
 
94
- expect(mockOnCollect).toHaveBeenCalledWith({ selectedSkin: 'char1' });
99
+ expect(mockOnCollect).toHaveBeenCalledWith({
100
+ selectedSkin: 'superior-knight',
101
+ selectedSkinName: 'Superior Knight',
102
+ selectedSkinTextureKey: 'superior-knight',
103
+ });
95
104
  expect(mockOnCollect).toHaveBeenCalledTimes(1);
96
105
  expect(mockOnCancel).not.toHaveBeenCalled();
97
106
 
@@ -9,6 +9,8 @@ export interface ITutorialLesson {
9
9
  title: string;
10
10
  body?: React.ReactNode | string;
11
11
  text?: string;
12
+ bodyHighlights?: string[];
13
+ textHighlights?: string[];
12
14
  image: string;
13
15
  imageUrl?: string;
14
16
  }
@@ -28,6 +28,7 @@ export const SimpleTooltip: React.FC<TooltipProps> = ({
28
28
  const tooltipRef = useRef<HTMLDivElement>(null);
29
29
  const triggerRef = useRef<HTMLDivElement>(null);
30
30
  const timeoutRef = useRef<NodeJS.Timeout>();
31
+ const rafId = useRef<number | null>(null);
31
32
 
32
33
  const calculatePosition = () => {
33
34
  if (!triggerRef.current || !tooltipRef.current) return;
@@ -87,26 +88,34 @@ export const SimpleTooltip: React.FC<TooltipProps> = ({
87
88
 
88
89
  useEffect(() => {
89
90
  const handleMouseMove = (event: MouseEvent) => {
90
- if (visible && tooltipRef.current && triggerRef.current) {
91
- const tooltipRect = tooltipRef.current.getBoundingClientRect();
92
- const triggerRect = triggerRef.current.getBoundingClientRect();
93
-
94
- const isOutsideTooltip =
95
- event.clientX < tooltipRect.left ||
96
- event.clientX > tooltipRect.right ||
97
- event.clientY < tooltipRect.top ||
98
- event.clientY > tooltipRect.bottom;
99
-
100
- const isOutsideTrigger =
101
- event.clientX < triggerRect.left ||
102
- event.clientX > triggerRect.right ||
103
- event.clientY < triggerRect.top ||
104
- event.clientY > triggerRect.bottom;
105
-
106
- if (isOutsideTooltip && isOutsideTrigger) {
107
- hideTooltip();
91
+ if (rafId.current !== null) return;
92
+
93
+ const { clientX, clientY } = event;
94
+
95
+ rafId.current = requestAnimationFrame(() => {
96
+ rafId.current = null;
97
+
98
+ if (visible && tooltipRef.current && triggerRef.current) {
99
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
100
+ const triggerRect = triggerRef.current.getBoundingClientRect();
101
+
102
+ const isOutsideTooltip =
103
+ clientX < tooltipRect.left ||
104
+ clientX > tooltipRect.right ||
105
+ clientY < tooltipRect.top ||
106
+ clientY > tooltipRect.bottom;
107
+
108
+ const isOutsideTrigger =
109
+ clientX < triggerRect.left ||
110
+ clientX > triggerRect.right ||
111
+ clientY < triggerRect.top ||
112
+ clientY > triggerRect.bottom;
113
+
114
+ if (isOutsideTooltip && isOutsideTrigger) {
115
+ hideTooltip();
116
+ }
108
117
  }
109
- }
118
+ });
110
119
  };
111
120
 
112
121
  const handleScroll = () => {
@@ -129,6 +138,10 @@ export const SimpleTooltip: React.FC<TooltipProps> = ({
129
138
  document.removeEventListener('mousemove', handleMouseMove);
130
139
  window.removeEventListener('scroll', handleScroll);
131
140
  window.removeEventListener('resize', handleResize);
141
+ if (rafId.current !== null) {
142
+ cancelAnimationFrame(rafId.current);
143
+ rafId.current = null;
144
+ }
132
145
  clearTimeout(timeoutRef.current);
133
146
  };
134
147
  }, [visible]);
package/src/index.tsx CHANGED
@@ -62,6 +62,7 @@ export * from './components/PropertySelect/PropertySelect';
62
62
  export * from './components/QuantitySelector/QuantitySelectorModal';
63
63
  export * from './components/Quests/QuestInfo/QuestInfo';
64
64
  export * from './components/Quests/QuestList';
65
+ export * from './components/Quests/QuestListRow';
65
66
  export * from './components/RadioButton';
66
67
  export * from './components/RadioSelectCard/RadioSelectCard';
67
68
  export * from './components/RangeSlider';
@@ -98,4 +99,3 @@ export * from './components/Truncate';
98
99
  export * from './components/Tutorial/TutorialStepper';
99
100
  export * from './components/typography/DynamicText';
100
101
  export { useEventListener } from './hooks/useEventListener';
101
-