@rpg-engine/long-bow 0.8.73 → 0.8.75

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.8.73",
3
+ "version": "0.8.75",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -1,4 +1,4 @@
1
- import { ITaskReward, RewardType } from '@rpg-engine/shared';
1
+ import { ITaskReward, RewardType, isMobileOrTablet } from '@rpg-engine/shared';
2
2
  import React from 'react';
3
3
  import styled from 'styled-components';
4
4
  import { uiColors } from '../../constants/uiColors';
@@ -20,6 +20,7 @@ export const DailyRewardsTooltip: React.FC<IDailyRewardsTooltipProps> = ({
20
20
  itemsAtlasIMG,
21
21
  }) => {
22
22
  const [isExpanded, setIsExpanded] = React.useState(true);
23
+ const isMobile = isMobileOrTablet();
23
24
 
24
25
  const sortedRewards = React.useMemo(() => {
25
26
  if (!rewards) return [];
@@ -51,25 +52,27 @@ export const DailyRewardsTooltip: React.FC<IDailyRewardsTooltipProps> = ({
51
52
 
52
53
  {isExpanded && (
53
54
  <CollapsibleContent>
54
- <RewardsList>
55
+ <RewardsList isMobile={isMobile} rewardCount={sortedRewards.length}>
55
56
  {sortedRewards.map((reward: ITaskReward, index: number) => (
56
57
  <RewardItem key={index}>
57
- <SpriteFromAtlas
58
- atlasJSON={itemsAtlasJSON}
59
- atlasIMG={itemsAtlasIMG}
60
- spriteKey={reward.texturePath ?? 'check.png'}
61
- width={24}
62
- height={24}
63
- imgScale={1.75}
64
- />
65
- <ItemContent>
66
- <RewardLabel>
67
- <Ellipsis maxWidth="100%" maxLines={2}>
68
- {formatTaskKey(reward.key ?? reward.type)}:
58
+ <RewardIcon>
59
+ <SpriteFromAtlas
60
+ atlasJSON={itemsAtlasJSON}
61
+ atlasIMG={itemsAtlasIMG}
62
+ spriteKey={reward.texturePath ?? 'check.png'}
63
+ width={isMobile ? 16 : 18}
64
+ height={isMobile ? 16 : 18}
65
+ imgScale={isMobile ? 1.5 : 1.5}
66
+ />
67
+ </RewardIcon>
68
+ <RewardContent>
69
+ <RewardLabel isMobile={isMobile}>
70
+ <Ellipsis maxWidth="100%" maxLines={1}>
71
+ {formatTaskKey(reward.key ?? reward.type)}
69
72
  </Ellipsis>
70
73
  </RewardLabel>
71
- </ItemContent>
72
- <RewardValue>x{reward.quantity}</RewardValue>
74
+ <RewardValue isMobile={isMobile}>×{reward.quantity}</RewardValue>
75
+ </RewardContent>
73
76
  </RewardItem>
74
77
  ))}
75
78
  </RewardsList>
@@ -86,10 +89,13 @@ const TooltipContainer = styled.div`
86
89
  width: 100%;
87
90
  `;
88
91
 
89
- const RewardsList = styled.div`
90
- display: flex;
91
- flex-direction: column;
92
- gap: 8px;
92
+ const RewardsList = styled.div<{ isMobile: boolean; rewardCount: number }>`
93
+ display: grid;
94
+ grid-template-columns: ${props =>
95
+ props.rewardCount > 2 && !props.isMobile ? 'repeat(2, 1fr)' : '1fr'
96
+ };
97
+ gap: ${props => props.isMobile ? '6px' : '8px'};
98
+ column-gap: ${props => props.rewardCount > 2 && !props.isMobile ? '16px' : '8px'};
93
99
  width: 100%;
94
100
  `;
95
101
 
@@ -104,50 +110,75 @@ const CollapsibleHeader = styled.div`
104
110
 
105
111
  const HeaderText = styled.span`
106
112
  color: ${uiColors.yellow} !important;
113
+ font-size: 0.75rem;
114
+ font-weight: 500;
107
115
  `;
108
116
 
109
117
  const ExpandIcon = styled.span`
110
118
  color: ${uiColors.yellow};
111
- font-size: 0.8rem;
119
+ font-size: 0.7rem;
112
120
  margin-left: 8px;
113
121
  `;
114
122
 
115
123
  const CollapsibleContent = styled.div`
116
124
  display: block;
117
- padding-top: 8px;
125
+ padding-top: 6px;
118
126
  width: 100%;
119
127
  `;
120
128
 
121
129
  const RewardItem = styled.div`
122
130
  display: flex;
123
131
  align-items: center;
124
- gap: 12px;
132
+ gap: 4px;
125
133
  width: 100%;
126
- min-width: 200px;
134
+ padding: 2px;
135
+ min-height: 22px;
127
136
  `;
128
137
 
129
- const ItemContent = styled.div`
138
+ const RewardIcon = styled.div`
139
+ flex-shrink: 0;
140
+ width: 20px;
141
+ height: 20px;
130
142
  display: flex;
131
- flex-direction: column;
132
- justify-content: center;
143
+ align-items: flex-start;
144
+ justify-content: flex-start;
145
+ font-style: normal;
146
+ margin-right: 4px;
147
+
148
+ * {
149
+ font-style: normal !important;
150
+ }
151
+
152
+ position: relative;
153
+ left: -0.5rem;
154
+ top: -0.4rem;
133
155
  `;
134
156
 
135
- const RewardLabel = styled.span`
157
+ const RewardContent = styled.div`
158
+ display: flex;
159
+ justify-content: space-between;
160
+ align-items: center;
161
+ flex: 1;
162
+ min-width: 0;
163
+ `;
164
+
165
+ const RewardLabel = styled.span<{ isMobile: boolean }>`
136
166
  color: ${uiColors.yellow};
137
- font-size: 0.9rem;
167
+ font-size: ${props => props.isMobile ? '0.65rem' : '0.7rem'};
138
168
  line-height: 1.2;
139
- padding-top: 15px;
140
- display: inline-flex;
169
+ display: flex;
141
170
  align-items: center;
171
+ flex: 1;
172
+ min-width: 0;
142
173
  `;
143
174
 
144
- const RewardValue = styled.span`
145
- color: ${uiColors.yellow};
146
- font-size: 0.9rem;
147
- margin-left: auto;
175
+ const RewardValue = styled.span<{ isMobile: boolean }>`
176
+ color: ${uiColors.green};
177
+ font-size: ${props => props.isMobile ? '0.6rem' : '0.65rem'};
148
178
  line-height: 1.2;
149
- display: inline-flex;
179
+ display: flex;
150
180
  align-items: center;
151
- min-width: 50px;
152
- text-align: right;
181
+ font-weight: 600;
182
+ flex-shrink: 0;
183
+ margin-left: 8px;
153
184
  `;
@@ -5,13 +5,15 @@ import {
5
5
  TaskType,
6
6
  } from '@rpg-engine/shared';
7
7
  import React from 'react';
8
- import styled from 'styled-components';
8
+ import styled, { css, keyframes } from 'styled-components';
9
9
  import { uiColors } from '../../constants/uiColors';
10
10
  import { Button, ButtonTypes } from '../Button';
11
11
  import { Ellipsis } from '../shared/Ellipsis';
12
12
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
13
- import { TaskProgress } from './TaskProgress';
14
- import { formatTaskKey } from './utils/dailyTasks.utils';
13
+ import { DailyRewardsTooltip } from './DailyRewardsTooltip';
14
+ import { TaskProgressDetails } from './TaskProgressDetails';
15
+ import { formatDifficulty, formatTaskKey, getStatusInfo } from './utils/dailyTasks.utils';
16
+
15
17
 
16
18
  interface IDailyTaskItemProps {
17
19
  task: ICharacterDailyTask;
@@ -22,6 +24,7 @@ interface IDailyTaskItemProps {
22
24
  iconAtlasJSON?: Record<string, any>;
23
25
  iconAtlasIMG?: string;
24
26
  isRewardClaimed: boolean;
27
+ isPinned?: boolean;
25
28
  }
26
29
 
27
30
  export const DailyTaskItem: React.FC<IDailyTaskItemProps> = ({
@@ -33,105 +36,306 @@ export const DailyTaskItem: React.FC<IDailyTaskItemProps> = ({
33
36
  iconAtlasJSON,
34
37
  iconAtlasIMG,
35
38
  isRewardClaimed,
39
+ isPinned,
36
40
  }) => {
37
41
  const isMobile = isMobileOrTablet();
38
42
 
39
43
  const isCompleted = task.status === TaskStatus.Completed;
44
+ const isInProgress = task.status === TaskStatus.InProgress;
45
+ const isNotStarted = task.status === TaskStatus.NotStarted;
40
46
  const isClaimed = task.claimed || isRewardClaimed;
47
+ const isChallenge = task.difficulty?.toLowerCase() === 'challenge';
41
48
 
42
49
  const handleClaimReward = () => {
43
50
  onClaimReward(task.key, task.type);
44
51
  };
45
52
 
46
53
  return (
47
- <TaskContainer>
48
- <TaskHeader>
49
- {iconAtlasJSON && iconAtlasIMG && (
50
- <NonInteractiveWrapper>
51
- <SpriteFromAtlas
52
- atlasJSON={iconAtlasJSON}
53
- atlasIMG={iconAtlasIMG}
54
- spriteKey={spriteKey}
55
- width={12}
56
- height={12}
57
- imgScale={2.75}
58
- />
59
- </NonInteractiveWrapper>
60
- )}
61
- <TaskContent>
62
- <TaskTitle>
63
- <Ellipsis maxWidth="100%" maxLines={isMobile ? 3 : 2}>
64
- {task.name ?? formatTaskKey(task.key)}
65
- </Ellipsis>
66
- </TaskTitle>
67
- <TaskDescription>
68
- <Ellipsis maxWidth="100%" maxLines={isMobile ? 5 : 3}>
69
- {task.description}
70
- </Ellipsis>
71
- </TaskDescription>
72
- </TaskContent>
54
+ <TaskContainer
55
+ isMobile={isMobile}
56
+ isCompleted={isCompleted}
57
+ isInProgress={isInProgress}
58
+ isNotStarted={isNotStarted}
59
+ isPinned={isPinned}
60
+ isChallenge={isChallenge}
61
+ >
62
+ <TaskHeader isMobile={isMobile}>
63
+ <TaskHeaderLeft>
64
+ {iconAtlasJSON && iconAtlasIMG && (
65
+ <IconWrapper>
66
+ <SpriteFromAtlas
67
+ atlasJSON={iconAtlasJSON}
68
+ atlasIMG={iconAtlasIMG}
69
+ spriteKey={spriteKey}
70
+ width={isMobile ? 10 : 12}
71
+ height={isMobile ? 10 : 12}
72
+ imgScale={2}
73
+
74
+ />
75
+ </IconWrapper>
76
+ )}
77
+ <TaskTitleSection>
78
+ <TaskTitle isMobile={isMobile}>
79
+ <Ellipsis maxWidth="100%" maxLines={1}>
80
+ {task.name ?? formatTaskKey(task.key)}
81
+ </Ellipsis>
82
+ </TaskTitle>
83
+ <TaskMeta isMobile={isMobile}>
84
+ <MetaItem>
85
+ <MetaLabel>Difficulty:</MetaLabel>
86
+ <TaskDifficulty difficulty={task.difficulty}>
87
+ {formatDifficulty(task.difficulty)}
88
+ </TaskDifficulty>
89
+ </MetaItem>
90
+ <MetaDivider>•</MetaDivider>
91
+ <MetaItem>
92
+ <MetaLabel>Status:</MetaLabel>
93
+ <StatusText color={getStatusInfo(task).color}>
94
+ {getStatusInfo(task).text}
95
+ </StatusText>
96
+ </MetaItem>
97
+ </TaskMeta>
98
+ </TaskTitleSection>
99
+ </TaskHeaderLeft>
100
+
101
+ <TaskHeaderRight>
102
+ {isClaimed && <ClaimedBadge isMobile={isMobile}>✓</ClaimedBadge>}
103
+ </TaskHeaderRight>
73
104
  </TaskHeader>
74
105
 
75
- <TaskProgress
76
- task={task}
77
- itemsAtlasJSON={itemsAtlasJSON}
78
- itemsAtlasIMG={itemsAtlasIMG}
79
- />
106
+ <TaskBody isMobile={isMobile}>
107
+ <TaskDescription isMobile={isMobile}>
108
+ <Ellipsis maxWidth="100%" maxLines={isMobile ? 2 : 1}>
109
+ {task.description}
110
+ </Ellipsis>
111
+ </TaskDescription>
112
+
113
+ <TaskProgressDetails task={task} />
114
+
115
+ {task.rewards && task.rewards.length > 0 && (
116
+ <RewardsSection>
117
+ <DailyRewardsTooltip
118
+ rewards={task.rewards}
119
+ itemsAtlasJSON={itemsAtlasJSON}
120
+ itemsAtlasIMG={itemsAtlasIMG}
121
+ iconAtlasJSON={iconAtlasJSON}
122
+ iconAtlasIMG={iconAtlasIMG}
123
+ />
124
+ </RewardsSection>
125
+ )}
126
+ </TaskBody>
80
127
 
81
128
  {isCompleted && !isClaimed && (
82
- <CollectWrapper>
129
+ <TaskFooter isMobile={isMobile}>
83
130
  <Button
84
131
  buttonType={ButtonTypes.RPGUIButton}
85
132
  onPointerDown={handleClaimReward}
86
133
  >
87
- Collect Reward
134
+ {isMobile ? 'Collect Reward' : 'Collect Reward'}
88
135
  </Button>
89
- </CollectWrapper>
136
+ </TaskFooter>
90
137
  )}
91
-
92
- {isClaimed && <ClaimedText>Reward Claimed</ClaimedText>}
93
138
  </TaskContainer>
94
139
  );
95
140
  };
96
141
 
97
- const TaskContainer = styled.div`
98
- background: rgba(0, 0, 0, 0.5) !important;
99
- border: 2px solid ${uiColors.darkGray} !important;
100
- border-radius: 8px;
101
- padding: 12px;
142
+ const pulseAnimation = keyframes`
143
+ 0% {
144
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
145
+ }
146
+ 50% {
147
+ box-shadow: 0 2px 8px rgba(255, 215, 0, 0.3);
148
+ }
149
+ 100% {
150
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
151
+ }
152
+ `;
153
+
154
+ const TaskContainer = styled.div<{
155
+ isMobile: boolean;
156
+ isCompleted: boolean;
157
+ isInProgress: boolean;
158
+ isNotStarted: boolean;
159
+ isPinned?: boolean;
160
+ isChallenge?: boolean;
161
+ }>`
162
+ background: ${props =>
163
+ props.isChallenge ? 'rgb(209, 39, 42)' :
164
+ props.isPinned ? 'rgba(255, 215, 0, 0.1)' :
165
+ 'rgba(0, 0, 0, 0.6)'
166
+ } !important;
167
+ border: 1px solid ${uiColors.darkGray} !important;
168
+ border-radius: ${props => props.isMobile ? '4px' : '6px'};
169
+ padding: ${props => props.isMobile ? '10px' : '14px'};
102
170
  display: flex;
103
171
  flex-direction: column;
104
- gap: 8px;
105
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
172
+ gap: ${props => props.isMobile ? '8px' : '12px'};
173
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
174
+ transition: all 0.2s ease;
175
+ font-style: normal;
176
+ width: 100%;
177
+ max-width: 100%;
178
+ box-sizing: border-box;
179
+
180
+ /* Task status visual states */
181
+ opacity: 1;
182
+
183
+ ${props => props.isNotStarted && `
184
+ filter: grayscale(0.7);
185
+ `}
186
+
187
+ ${props => props.isInProgress && css`
188
+ animation: ${pulseAnimation} 2s ease-in-out infinite;
189
+ `}
190
+
191
+ * {
192
+ font-style: normal !important;
193
+ }
194
+
195
+ &:hover {
196
+ background: ${props =>
197
+ props.isPinned ? 'rgba(255, 215, 0, 0.25)' : 'rgba(0, 0, 0, 0.7)'
198
+ } !important;
199
+ border-color: ${uiColors.yellow};
200
+ opacity: 1;
201
+ }
106
202
  `;
107
203
 
108
- const TaskHeader = styled.div`
204
+ const TaskHeader = styled.div<{ isMobile?: boolean }>`
109
205
  display: flex;
110
206
  width: 100%;
111
- justify-content: center;
112
- border-bottom: 1.3px solid ${uiColors.darkGray};
207
+ justify-content: space-between;
208
+ align-items: flex-start;
209
+ border-bottom: 1px solid ${uiColors.darkGray};
210
+ padding-bottom: ${props => props.isMobile ? '8px' : '10px'};
211
+ min-height: 36px;
113
212
  `;
114
213
 
115
- const NonInteractiveWrapper = styled.div`
116
- pointer-events: none;
117
- user-select: none;
118
- margin-top: 5px;
214
+ const TaskHeaderLeft = styled.div`
215
+ display: flex;
216
+ align-items: flex-start;
217
+ gap: 8px;
218
+ flex: 1;
219
+ `;
220
+
221
+ const TaskHeaderRight = styled.div`
222
+ display: flex;
223
+ align-items: center;
224
+ flex-shrink: 0;
119
225
  `;
120
226
 
121
- const TaskContent = styled.div`
227
+ const TaskTitleSection = styled.div`
122
228
  display: flex;
123
229
  flex-direction: column;
230
+ gap: 4px;
124
231
  flex: 1;
232
+ min-width: 0;
233
+ `;
234
+
235
+ const TaskMeta = styled.div<{ isMobile: boolean }>`
236
+ display: flex;
237
+ align-items: center;
238
+ gap: ${props => props.isMobile ? '6px' : '8px'};
239
+ flex-wrap: wrap;
240
+ `;
241
+
242
+ const MetaItem = styled.div`
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 4px;
246
+ `;
247
+
248
+ const MetaLabel = styled.span`
249
+ color: ${uiColors.lightGray};
250
+ font-size: 0.65rem;
251
+ opacity: 0.8;
125
252
  `;
126
253
 
127
- const TaskTitle = styled.h3`
254
+ const MetaDivider = styled.span`
255
+ color: ${uiColors.darkGray};
256
+ font-size: 0.7rem;
257
+ `;
258
+
259
+ const TaskDifficulty = styled.span<{ difficulty: string }>`
260
+ color: ${props =>
261
+ props.difficulty.toLowerCase() === 'challenge'
262
+ ? uiColors.red
263
+ : uiColors.lightGray};
264
+ font-size: 0.65rem;
265
+ font-weight: 500;
266
+ `;
267
+
268
+ const StatusText = styled.span<{ color: string }>`
269
+ color: ${props => props.color} !important;
270
+ font-size: 0.65rem;
271
+ font-weight: 500;
272
+ `;
273
+
274
+ const ClaimedBadge = styled.div<{ isMobile: boolean }>`
275
+ background: ${uiColors.green};
276
+ color: white;
277
+ border-radius: 50%;
278
+ width: ${props => props.isMobile ? '18px' : '20px'};
279
+ height: ${props => props.isMobile ? '18px' : '20px'};
280
+ display: flex;
281
+ align-items: center;
282
+ justify-content: center;
283
+ font-size: ${props => props.isMobile ? '10px' : '12px'};
284
+ font-weight: bold;
285
+ margin-left: 8px;
286
+ `;
287
+
288
+ const TaskBody = styled.div<{ isMobile?: boolean }>`
289
+ display: flex;
290
+ flex-direction: column;
291
+ gap: 8px;
292
+ padding: ${props => props.isMobile ? '4px 0' : '6px 0'};
293
+ `;
294
+
295
+ const RewardsSection = styled.div`
296
+ margin-top: 4px;
297
+ `;
298
+
299
+ const TaskFooter = styled.div<{ isMobile?: boolean }>`
300
+ display: flex;
301
+ justify-content: center;
302
+ padding-top: ${props => props.isMobile ? '8px' : '10px'};
303
+ border-top: 1px solid ${uiColors.darkGray};
304
+ `;
305
+
306
+ const IconWrapper = styled.div`
307
+ pointer-events: none;
308
+ user-select: none;
309
+ flex-shrink: 0;
310
+ width: 28px;
311
+ height: 28px;
312
+ display: flex;
313
+ align-items: flex-start;
314
+ justify-content: flex-start;
315
+ margin-top: 2px;
316
+ margin-left: 2px;
317
+ font-style: normal;
318
+
319
+
320
+
321
+ * {
322
+ font-style: normal !important;
323
+ }
324
+ `;
325
+
326
+
327
+ const TaskTitle = styled.h3<{ isMobile: boolean }>`
128
328
  && {
129
329
  color: ${uiColors.yellow};
130
- padding-left: 10px;
131
- text-align: center;
330
+ margin: 0;
331
+ padding: 0;
332
+ text-align: left;
333
+ font-size: ${props => props.isMobile ? '0.85rem' : '0.9rem'};
334
+ line-height: 1.2;
132
335
  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
133
- flex: 1;
134
- display: block;
336
+ font-weight: 600;
337
+ word-wrap: break-word;
338
+ overflow-wrap: break-word;
135
339
 
136
340
  span {
137
341
  color: inherit;
@@ -139,25 +343,14 @@ const TaskTitle = styled.h3`
139
343
  }
140
344
  `;
141
345
 
142
- const TaskDescription = styled.h3`
143
- font-size: 0.6rem !important;
144
- text-decoration: none !important;
145
- a
346
+ const TaskDescription = styled.div<{ isMobile: boolean }>`
347
+ color: ${uiColors.lightGray};
348
+ font-size: ${props => props.isMobile ? '0.7rem' : '0.75rem'};
349
+ line-height: 1.4;
350
+ margin: 0;
351
+ opacity: 0.9;
352
+ word-wrap: break-word;
353
+ overflow-wrap: break-word;
146
354
  `;
147
355
 
148
- const ClaimedText = styled.span`
149
- color: ${uiColors.green};
150
- font-size: 0.9rem;
151
- text-align: center;
152
- font-weight: bold;
153
- `;
154
356
 
155
- const CollectWrapper = styled.div`
156
- && {
157
- width: 100% !important;
158
- display: flex !important;
159
- justify-content: center !important;
160
- align-items: center !important;
161
- margin: 5px 0 !important;
162
- }
163
- `;