@rpg-engine/long-bow 0.6.69 → 0.6.71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.6.69",
3
+ "version": "0.6.71",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -10,6 +10,7 @@ import {
10
10
  TableCell,
11
11
  TableHeader,
12
12
  TableRow,
13
+ UserActionLink,
13
14
  } from '../Table/Table';
14
15
  import { SearchFriend } from './SearchFriend';
15
16
  export type IFriend = Pick<ICharacter, 'name' | '_id' | 'isOnline'>;
@@ -89,18 +90,18 @@ export const FriendList: React.FC<IFriendListProps> = ({
89
90
  <TableCell>{friend.name}</TableCell>
90
91
  <TableCell>
91
92
  <ActionButtons>
92
- <UserAction
93
- color={uiColors.yellow}
93
+ <UserActionLink
94
+ color={uiColors.white}
94
95
  onClick={() => onOpenPrivateMessage(friend)}
95
96
  >
96
97
  Chat
97
- </UserAction>
98
- <UserAction
98
+ </UserActionLink>
99
+ <UserActionLink
99
100
  color={uiColors.red}
100
101
  onClick={() => onRemoveFriend(friend)}
101
102
  >
102
103
  Remove
103
- </UserAction>
104
+ </UserActionLink>
104
105
  </ActionButtons>
105
106
  </TableCell>
106
107
  </TableRow>
@@ -152,17 +153,3 @@ const ButtonContainer = styled.div`
152
153
  width: 100%;
153
154
  margin-top: 1rem;
154
155
  `;
155
-
156
- interface IUserActionProps {
157
- color: string;
158
- }
159
-
160
- const UserAction = styled.span<IUserActionProps>`
161
- color: ${({ color }) => color} !important;
162
- cursor: pointer;
163
- margin-right: 0.5rem;
164
-
165
- &:hover {
166
- text-decoration: underline;
167
- }
168
- `;
@@ -1,11 +1,12 @@
1
- import React, { useState } from 'react';
1
+ import { debounce } from 'lodash';
2
+ import React, { useCallback, useState } from 'react';
2
3
  import styled from 'styled-components';
4
+ import { TableTab, UserActionLink } from '../..';
5
+ import { uiColors } from '../../constants/uiColors';
3
6
  import { uiFonts } from '../../constants/uiFonts';
4
- import { Button, ButtonTypes } from '../Button';
5
- import { Column } from '../shared/Column';
6
- import type { IFriend } from './FriendList';
7
+ import { IFriend } from './FriendList';
7
8
 
8
- interface ISearchFriendProp {
9
+ interface ISearchFriendProps {
9
10
  searchedCharacters: IFriend[];
10
11
  friendRequests: IFriend[];
11
12
  onFocus?: () => void;
@@ -16,142 +17,194 @@ interface ISearchFriendProp {
16
17
  onRejectRequest: (character: IFriend) => void;
17
18
  }
18
19
 
19
- export const SearchFriend = (props: ISearchFriendProp) => {
20
- const {
21
- searchedCharacters,
22
- friendRequests,
23
- onBlur,
24
- onFocus,
25
- onSearch,
26
- onSendFriendRequest,
27
- onAcceptRequest,
28
- onRejectRequest,
29
- } = props;
20
+ export const SearchFriend: React.FC<ISearchFriendProps> = ({
21
+ searchedCharacters,
22
+ friendRequests,
23
+ onBlur,
24
+ onFocus,
25
+ onSearch,
26
+ onSendFriendRequest,
27
+ onAcceptRequest,
28
+ onRejectRequest,
29
+ }) => {
30
30
  const [characterName, setCharacterName] = useState('');
31
31
 
32
+ const debouncedSearch = useCallback(
33
+ debounce((name: string) => {
34
+ onSearch(name);
35
+ }, 300),
36
+ []
37
+ );
38
+
39
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
40
+ const name = e.target.value;
41
+ setCharacterName(name);
42
+ debouncedSearch(name);
43
+ };
44
+
32
45
  const handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
33
46
  event.preventDefault();
34
- if (!characterName || characterName.trim() === '') return;
35
- onSearch(characterName);
47
+ if (characterName.trim()) {
48
+ onSearch(characterName);
49
+ }
36
50
  };
37
51
 
52
+ const searchTabContent = (
53
+ <ListContainer>
54
+ <SearchForm onSubmit={handleSubmit}>
55
+ <SearchInput
56
+ value={characterName}
57
+ id="characterName"
58
+ name="characterName"
59
+ onChange={handleInputChange}
60
+ type="text"
61
+ autoComplete="off"
62
+ onFocus={onFocus}
63
+ onBlur={onBlur}
64
+ onPointerDown={onFocus}
65
+ autoFocus
66
+ placeholder="Search for a character..."
67
+ />
68
+ </SearchForm>
69
+ <CharacterList
70
+ characters={searchedCharacters}
71
+ onAction={onSendFriendRequest}
72
+ actionLabel="Add Friend"
73
+ />
74
+ </ListContainer>
75
+ );
76
+
77
+ const requestsTabContent = (
78
+ <ListContainer>
79
+ <FriendRequestSection
80
+ friendRequests={friendRequests}
81
+ onAccept={onAcceptRequest}
82
+ onReject={onRejectRequest}
83
+ />
84
+ </ListContainer>
85
+ );
86
+
87
+ const tabs = [
88
+ { id: 'search', title: 'Search', content: searchTabContent },
89
+ {
90
+ id: 'requests',
91
+ title: `Requests (${friendRequests.length})`,
92
+ content: requestsTabContent,
93
+ },
94
+ ];
95
+
38
96
  return (
39
- <>
40
- <Form onSubmit={handleSubmit}>
41
- <Column flex={70}>
42
- <TextField
43
- value={characterName}
44
- id="characterName"
45
- name="characterName"
46
- onChange={e => {
47
- setCharacterName(e.target.value);
48
- }}
49
- height={20}
50
- type="text"
51
- autoComplete="off"
52
- onFocus={onFocus}
53
- onBlur={onBlur}
54
- onPointerDown={onFocus}
55
- autoFocus
56
- />
57
- </Column>
58
- <Column justifyContent="flex-end">
59
- <Button type="submit" buttonType={ButtonTypes.RPGUIButton}>
60
- Search
61
- </Button>
62
- </Column>
63
- </Form>
97
+ <Container>
98
+ <TableTab
99
+ tabs={tabs}
100
+ activeTextColor="#000"
101
+ inactiveColor="#777"
102
+ borderColor="#f59e0b"
103
+ />
104
+ </Container>
105
+ );
106
+ };
107
+
108
+ const CharacterList: React.FC<{
109
+ characters: IFriend[];
110
+ onAction: (character: IFriend) => void;
111
+ actionLabel: string;
112
+ }> = ({ characters, onAction, actionLabel }) => (
113
+ <ListContainer>
114
+ {characters.map(character => (
115
+ <ListItem key={character._id}>
116
+ <CharacterName>{character.name}</CharacterName>
117
+ <UserActionLink
118
+ color={uiColors.lightGreen}
119
+ onClick={() => onAction(character)}
120
+ >
121
+ {actionLabel}
122
+ </UserActionLink>
123
+ </ListItem>
124
+ ))}
125
+ </ListContainer>
126
+ );
127
+
128
+ const FriendRequestSection: React.FC<{
129
+ friendRequests: IFriend[];
130
+ onAccept: (character: IFriend) => void;
131
+ onReject: (character: IFriend) => void;
132
+ }> = ({ friendRequests, onAccept, onReject }) => (
133
+ <>
134
+ {friendRequests.length > 0 && (
64
135
  <ListContainer>
65
- {searchedCharacters.map(character => (
66
- <ListElement key={character._id}>
67
- <p>{character.name}</p>
68
-
69
- <Button
70
- buttonType={ButtonTypes.RPGUIButton}
71
- onPointerDown={() => onSendFriendRequest(character)}
72
- >
73
- Add Friend
74
- </Button>
75
- </ListElement>
136
+ {friendRequests.map(character => (
137
+ <ListItem key={character._id}>
138
+ <CharacterName>{character.name}</CharacterName>
139
+ <AcceptRejectActions>
140
+ <UserActionLink
141
+ color={uiColors.lightGreen}
142
+ onClick={() => onAccept(character)}
143
+ >
144
+ Accept
145
+ </UserActionLink>
146
+ <UserActionLink
147
+ color={uiColors.red}
148
+ onClick={() => onReject(character)}
149
+ >
150
+ Reject
151
+ </UserActionLink>
152
+ </AcceptRejectActions>
153
+ </ListItem>
76
154
  ))}
77
155
  </ListContainer>
78
- <FriendRequestTitle>
79
- Friend Requests <span>({friendRequests.length})</span>
80
- </FriendRequestTitle>
81
- {friendRequests.length > 0 && (
82
- <ListContainer>
83
- {friendRequests.map(character => (
84
- <ListElement key={character._id}>
85
- <p>{character.name}</p>
86
-
87
- <AcceptRejectButtonContainer>
88
- <Button
89
- buttonType={ButtonTypes.RPGUIButton}
90
- onPointerDown={() => onAcceptRequest(character)}
91
- >
92
- Accept
93
- </Button>
94
- <Button
95
- buttonType={ButtonTypes.RPGUIButton}
96
- onPointerDown={() => onRejectRequest(character)}
97
- >
98
- Reject
99
- </Button>
100
- </AcceptRejectButtonContainer>
101
- </ListElement>
102
- ))}
103
- </ListContainer>
104
- )}
105
- </>
106
- );
107
- };
156
+ )}
157
+ </>
158
+ );
108
159
 
109
- const Form = styled.form`
160
+ const Container = styled.div`
161
+ display: flex;
162
+ flex-direction: column;
163
+ gap: 1rem;
164
+ `;
165
+
166
+ const SearchForm = styled.form`
110
167
  display: flex;
111
- width: 100%;
112
- justify-content: center;
113
168
  align-items: center;
169
+ width: 100%;
114
170
  margin-top: 1rem;
115
171
  `;
116
172
 
117
- const TextField = styled.input`
173
+ const SearchInput = styled.input`
118
174
  width: 100%;
119
- background-color: rgba(0, 0, 0, 0.25) !important;
120
- border: none !important;
121
- max-height: 28px !important;
175
+ background-color: rgba(0, 0, 0, 0.25);
176
+ border: none;
177
+ padding: 0.5rem;
178
+ font-size: ${uiFonts.size.small};
122
179
  `;
123
180
 
124
181
  const ListContainer = styled.ul`
125
- width: 100%;
126
- c4height: 50vh;
127
- border: none;
128
- overflow-y: scroll;
129
182
  list-style: none;
130
183
  padding: 0;
184
+ margin: 0;
185
+ width: 100%;
186
+ max-height: 50vh;
187
+ overflow-y: auto;
131
188
 
132
189
  @media (max-width: 768px) {
133
- max-height: 90wh;
190
+ max-height: 90vh;
134
191
  }
135
192
  `;
136
193
 
137
- const ListElement = styled.li`
138
- margin: 0.5rem 0 !important;
139
- font-size: ${uiFonts.size.small};
140
- padding: 0.5rem 2px;
194
+ const ListItem = styled.li`
141
195
  display: flex;
142
196
  align-items: center;
143
197
  justify-content: space-between;
198
+ padding: 0.5rem;
199
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
144
200
  `;
145
201
 
146
- const FriendRequestTitle = styled.h3`
147
- font-size: ${uiFonts.size.small} !important;
148
- margin-top: 0.5rem !important;
149
- text-align: center;
150
- gap: 0.5rem;
202
+ const CharacterName = styled.p`
203
+ font-size: ${uiFonts.size.small};
204
+ margin: 0;
151
205
  `;
152
206
 
153
- const AcceptRejectButtonContainer = styled.div`
207
+ const AcceptRejectActions = styled.div`
154
208
  display: flex;
155
- justify-content: space-between;
156
209
  gap: 0.5rem;
157
210
  `;
@@ -1,135 +1,172 @@
1
- import { IQuest } from '@rpg-engine/shared';
2
1
  import React from 'react';
2
+ import 'react-tippy/dist/tippy.css';
3
3
  import styled from 'styled-components';
4
- import { uiFonts } from '../constants/uiFonts';
5
- import { DraggableContainer } from './DraggableContainer';
6
- import { RPGUIContainerTypes } from './RPGUI/RPGUIContainer';
4
+
5
+ import {
6
+ IQuest,
7
+ IQuestObjectiveInteraction,
8
+ IQuestObjectiveKill,
9
+ QuestStatus,
10
+ } from '@rpg-engine/shared';
11
+ import { uiColors } from '../constants/uiColors';
12
+ import { Table, TableCell, TableHeader, TableRow } from './Table/Table';
13
+ import { Ellipsis } from './shared/Ellipsis';
14
+ import { SimpleTooltip } from './shared/SimpleTooltip';
7
15
 
8
16
  export interface IQuestListProps {
9
17
  quests?: IQuest[];
10
- onClose: () => void;
11
- scale?: number;
12
18
  }
13
19
 
14
- export const QuestList: React.FC<IQuestListProps> = ({
15
- quests,
16
- onClose,
17
- scale,
18
- }) => {
20
+ export const QuestList: React.FC<IQuestListProps> = ({ quests }) => {
19
21
  return (
20
- <QuestDraggableContainer
21
- type={RPGUIContainerTypes.Framed}
22
- onCloseButton={() => {
23
- if (onClose) onClose();
24
- }}
25
- width="520px"
26
- scale={scale}
27
- >
28
- <div style={{ width: '100%' }}>
29
- <Title>Quests</Title>
30
- <hr className="golden" />
31
-
32
- <QuestListContainer>
33
- {quests ? (
34
- quests.map((quest, i) => (
35
- <div className="quest-item" key={i}>
36
- <span className="quest-number">{i + 1}</span>
37
- <div className="quest-detail">
38
- <p className="quest-detail__title">{quest.title}</p>
39
- <p className="quest-detail__description">
40
- {quest.description}
41
- </p>
42
- </div>
43
- </div>
44
- ))
45
- ) : (
46
- <NoQuestContainer>
47
- <p>There are no ongoing quests</p>
48
- </NoQuestContainer>
49
- )}
50
- </QuestListContainer>
51
- </div>
52
- </QuestDraggableContainer>
22
+ <QuestListContainer>
23
+ {quests && quests.length > 0 ? (
24
+ <>
25
+ <Table>
26
+ <thead>
27
+ <TableRow>
28
+ <TableHeader>Status</TableHeader>
29
+ <TableHeader>Title</TableHeader>
30
+ <TableHeader>Description</TableHeader>
31
+ <TableHeader>Objectives</TableHeader>
32
+ <TableHeader>Rewards</TableHeader>
33
+ </TableRow>
34
+ </thead>
35
+ <tbody>
36
+ {quests.map((quest, i) => (
37
+ <TableRow key={i}>
38
+ <TableCell style={{ color: getStatusColor(quest.status) }}>
39
+ {quest.status ?? 'Unknown'}
40
+ </TableCell>
41
+ <TableCell>
42
+ <SimpleTooltip
43
+ content={formatText(quest.title)}
44
+ direction="bottom"
45
+ >
46
+ <Ellipsis maxWidth="300px">
47
+ {formatText(quest.title)}
48
+ </Ellipsis>
49
+ </SimpleTooltip>
50
+ </TableCell>
51
+ <TableCell>
52
+ <SimpleTooltip
53
+ content={quest.description}
54
+ direction="bottom"
55
+ >
56
+ <Ellipsis maxWidth="300px">{quest.description}</Ellipsis>
57
+ </SimpleTooltip>
58
+ </TableCell>
59
+ <TableCell>
60
+ <SimpleTooltip
61
+ content={formatObjectives(quest.objectives)}
62
+ direction="bottom"
63
+ >
64
+ <Ellipsis maxWidth="300px">
65
+ {formatObjectives(quest.objectives)}
66
+ </Ellipsis>
67
+ </SimpleTooltip>
68
+ </TableCell>
69
+ <TableCell>
70
+ <SimpleTooltip
71
+ content={formatRewards(quest.rewards)}
72
+ direction="bottom"
73
+ >
74
+ <Ellipsis maxWidth="200px">
75
+ {formatRewards(quest.rewards)}
76
+ </Ellipsis>
77
+ </SimpleTooltip>
78
+ </TableCell>
79
+ </TableRow>
80
+ ))}
81
+ </tbody>
82
+ </Table>
83
+ </>
84
+ ) : (
85
+ <NoQuestContainer>
86
+ <p>There are no ongoing quests</p>
87
+ </NoQuestContainer>
88
+ )}
89
+ </QuestListContainer>
53
90
  );
54
91
  };
55
92
 
56
- const QuestDraggableContainer = styled(DraggableContainer)`
57
- .container-close {
58
- top: 10px;
59
- right: 10px;
60
- }
93
+ // Updated helper function to format objectives
94
+ const formatObjectives = (
95
+ objectives: (IQuestObjectiveKill | IQuestObjectiveInteraction)[]
96
+ ) => {
97
+ return objectives
98
+ .map(objective => {
99
+ if ('killCountTarget' in objective) {
100
+ // This is an IQuestObjectiveKill
101
+ const killObjective = objective as IQuestObjectiveKill;
102
+ return `Kill ${formatText(killObjective.creatureKeys.join(', '))}: ${
103
+ killObjective.killCount
104
+ }/${killObjective.killCountTarget}`;
105
+ } else if ('targetNPCkey' in objective) {
106
+ // This is an IQuestObjectiveInteraction
107
+ const interactionObjective = objective as IQuestObjectiveInteraction;
108
+ return `Interact with NPC: ${formatText(
109
+ interactionObjective.targetNPCkey ?? 'Unknown'
110
+ )}`;
111
+ } else {
112
+ return 'Unknown objective';
113
+ }
114
+ })
115
+ .join('; ');
116
+ };
61
117
 
62
- .quest-title {
63
- text-align: left;
64
- margin-left: 44px;
65
- margin-top: 20px;
66
- color: yellow;
67
- }
118
+ // Other helper functions remain the same
119
+ const formatRewards = (rewards: IQuest['rewards']) => {
120
+ return rewards
121
+ .map(reward => {
122
+ return `${formatText(
123
+ reward.itemKeys.map(itemKey => itemKey + ' x' + reward.qty).join(', ')
124
+ )}${
125
+ reward.spellKeys
126
+ ? `, Spells: ${formatText(reward.spellKeys.join(', '))}`
127
+ : ''
128
+ }`;
129
+ })
130
+ .join('; ');
131
+ };
68
132
 
69
- .quest-desc {
70
- margin-top: 12px;
71
- margin-left: 44px;
72
- }
133
+ const formatText = (text: string) => {
134
+ return text
135
+ .split('-')
136
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
137
+ .join(' ');
138
+ };
73
139
 
74
- .rpgui-progress {
75
- min-width: 80%;
76
- margin: 0 auto;
140
+ const getStatusColor = (status?: QuestStatus) => {
141
+ switch (status) {
142
+ case QuestStatus.Pending:
143
+ return uiColors.orange; // Orange
144
+ case QuestStatus.InProgress:
145
+ return uiColors.blue; // Deep Sky Blue
146
+ case QuestStatus.Completed:
147
+ return uiColors.lightGreen; // Lime Green
148
+ default:
149
+ return uiColors.white; // Default to white
77
150
  }
78
- `;
79
-
80
- const Title = styled.h1`
81
- z-index: 22;
82
- font-size: ${uiFonts.size.medium} !important;
83
- color: yellow !important;
84
- `;
151
+ };
85
152
 
153
+ // Styled components
86
154
  const QuestListContainer = styled.div`
87
155
  margin-top: 20px;
88
156
  margin-bottom: 40px;
89
157
  overflow-y: auto;
90
158
  max-height: 400px;
91
-
92
- .quest-item {
93
- display: flex;
94
- align-items: flex-start;
95
- margin-bottom: 12px;
96
- }
97
-
98
- .quest-number {
99
- border-radius: 50%;
100
- width: 28px;
101
- height: 28px;
102
- display: flex;
103
- align-items: center;
104
- justify-content: center;
105
- margin-right: 16px;
106
- background-color: brown;
107
- flex-shrink: 0;
108
- }
109
-
110
- .quest-number.completed {
111
- background-color: yellow;
112
- }
113
-
114
- p {
115
- margin: 0;
116
- }
117
-
118
- .quest-detail__title {
119
- color: yellow;
120
- }
121
-
122
- .quest-detail__description {
123
- margin-top: 5px;
124
- }
125
- .Noquest-detail__description {
126
- margin-top: 5px;
127
- margin: auto;
128
- }
159
+ background-color: ${uiColors.darkGray};
160
+ padding: 20px;
161
+ border-radius: 10px;
162
+ border: 1px solid ${uiColors.gray};
163
+ font-size: 0.7rem;
129
164
  `;
165
+
130
166
  const NoQuestContainer = styled.div`
131
167
  text-align: center;
132
168
  p {
133
169
  margin-top: 5px;
170
+ color: ${uiColors.lightGray};
134
171
  }
135
172
  `;