@rpg-engine/long-bow 0.8.127 → 0.8.129

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.127",
3
+ "version": "0.8.129",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -84,7 +84,7 @@
84
84
  "dependencies": {
85
85
  "@capacitor/core": "^6.1.0",
86
86
  "@rollup/plugin-image": "^2.1.1",
87
- "@rpg-engine/shared": "^0.10.69",
87
+ "@rpg-engine/shared": "0.10.70",
88
88
  "dayjs": "^1.11.2",
89
89
  "font-awesome": "^4.7.0",
90
90
  "fs-extra": "^10.1.0",
@@ -0,0 +1,241 @@
1
+ import { ILoginStreakMilestone, isMobileOrTablet } from '@rpg-engine/shared';
2
+ import React from 'react';
3
+ import styled from 'styled-components';
4
+ import { uiColors } from '../../constants/uiColors';
5
+ import { uiFonts } from '../../constants/uiFonts';
6
+ import { SimpleProgressBar } from '../SimpleProgressBar';
7
+ import { DraggableContainer } from '../DraggableContainer';
8
+ import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
9
+
10
+ export interface ILoginStreakPanelProps {
11
+ streak: number;
12
+ longestStreak: number;
13
+ xpBonusPercent: number;
14
+ maxXpBonusPercent: number;
15
+ maxBonusDay: number;
16
+ isConsecutive: boolean;
17
+ milestones: ILoginStreakMilestone[];
18
+ milestoneRewardGranted?: { itemName: string; quantity: number } | null;
19
+ onClose: () => void;
20
+ }
21
+
22
+ const getMotivationalText = (streak: number, isConsecutive: boolean, milestones: ILoginStreakMilestone[]): string => {
23
+ if (!isConsecutive && streak === 1) {
24
+ return 'Your streak has reset. Log in daily to build it back up!';
25
+ }
26
+
27
+ const nextMilestone = milestones.find(m => !m.reached);
28
+ if (nextMilestone) {
29
+ const daysUntil = nextMilestone.day - streak;
30
+ return `${daysUntil} day${daysUntil !== 1 ? 's' : ''} until Day ${nextMilestone.day} reward!`;
31
+ }
32
+
33
+ return 'Amazing streak! Keep it going for maximum XP bonus!';
34
+ };
35
+
36
+ export const LoginStreakPanel: React.FC<ILoginStreakPanelProps> = ({
37
+ streak,
38
+ longestStreak,
39
+ xpBonusPercent,
40
+ maxXpBonusPercent,
41
+ isConsecutive,
42
+ milestones,
43
+ milestoneRewardGranted,
44
+ onClose,
45
+ }): JSX.Element => {
46
+ const isMobile = isMobileOrTablet();
47
+ const isMaxBonus = xpBonusPercent >= maxXpBonusPercent;
48
+
49
+ return (
50
+ <DraggableContainer
51
+ title="Login Streak"
52
+ onCloseButton={onClose}
53
+ type={RPGUIContainerTypes.Framed}
54
+ width={isMobile ? '90vw' : '380px'}
55
+ >
56
+ <Container>
57
+ <StreakHeader>
58
+ <StreakDay>
59
+ {streak === 1 && !isConsecutive ? 'Day 1 — Fresh Start!' : `Day ${streak} Streak!`}
60
+ </StreakDay>
61
+ <LongestStreak>Longest: {longestStreak} days</LongestStreak>
62
+ </StreakHeader>
63
+
64
+ <XPBonusSection>
65
+ <XPBonusLabel>
66
+ XP Bonus: <XPBonusValue isMax={isMaxBonus}>+{xpBonusPercent}%</XPBonusValue>
67
+ {isMaxBonus && <MaxTag> MAX</MaxTag>}
68
+ </XPBonusLabel>
69
+ <SimpleProgressBar
70
+ value={maxXpBonusPercent > 0 ? (xpBonusPercent / maxXpBonusPercent) * 100 : 0}
71
+ bgColor={isMaxBonus ? uiColors.darkYellow : uiColors.lightGreen}
72
+ margin={0}
73
+ />
74
+ <ProgressLabel>{xpBonusPercent}% / {maxXpBonusPercent}% max</ProgressLabel>
75
+ </XPBonusSection>
76
+
77
+ {milestoneRewardGranted && (
78
+ <RewardToast>
79
+ <RewardText>
80
+ Milestone reward: {milestoneRewardGranted.quantity}x {milestoneRewardGranted.itemName}!
81
+ </RewardText>
82
+ </RewardToast>
83
+ )}
84
+
85
+ <hr className="golden" />
86
+
87
+ <SectionLabel>Milestone Rewards</SectionLabel>
88
+
89
+ <MilestoneList>
90
+ {milestones.map((milestone) => (
91
+ <MilestoneCard key={milestone.day} $reached={milestone.reached}>
92
+ <MilestoneDayLabel>Day {milestone.day}</MilestoneDayLabel>
93
+ <MilestoneRewardName>{milestone.quantity}x {milestone.itemName}</MilestoneRewardName>
94
+ <MilestoneBadge $reached={milestone.reached}>{milestone.reached ? '✓' : '○'}</MilestoneBadge>
95
+ </MilestoneCard>
96
+ ))}
97
+ </MilestoneList>
98
+
99
+ <MotivationalText>{getMotivationalText(streak, isConsecutive, milestones)}</MotivationalText>
100
+ </Container>
101
+ </DraggableContainer>
102
+ );
103
+ };
104
+
105
+ const Container = styled.div`
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 8px;
109
+ padding: 4px 14px 14px;
110
+ color: ${uiColors.white};
111
+ width: 100%;
112
+ box-sizing: border-box;
113
+
114
+ hr.golden {
115
+ margin: 4px 0;
116
+ }
117
+ `;
118
+
119
+ const StreakHeader = styled.div`
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: 2px;
123
+ `;
124
+
125
+ const StreakDay = styled.h2`
126
+ && {
127
+ margin: 0;
128
+ padding: 0;
129
+ font-size: ${uiFonts.size.xLarge};
130
+ font-weight: bold;
131
+ color: ${uiColors.yellow};
132
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
133
+ text-align: left;
134
+ }
135
+ `;
136
+
137
+ const LongestStreak = styled.div`
138
+ font-size: ${uiFonts.size.small};
139
+ color: ${uiColors.lightGray};
140
+ `;
141
+
142
+ const XPBonusSection = styled.div`
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: 4px;
146
+ padding: 10px 12px;
147
+ background: rgba(0, 0, 0, 0.4);
148
+ border: 1px solid ${uiColors.darkGray};
149
+ border-radius: 4px;
150
+ `;
151
+
152
+ const XPBonusLabel = styled.div`
153
+ font-size: ${uiFonts.size.medium};
154
+ `;
155
+
156
+ const XPBonusValue = styled.span<{ isMax: boolean }>`
157
+ color: ${(p) => (p.isMax ? uiColors.darkYellow : uiColors.lightGreen)};
158
+ font-weight: bold;
159
+ `;
160
+
161
+ const MaxTag = styled.span`
162
+ color: ${uiColors.darkYellow};
163
+ font-size: ${uiFonts.size.xsmall};
164
+ font-weight: bold;
165
+ letter-spacing: 1px;
166
+ margin-left: 2px;
167
+ `;
168
+
169
+ const ProgressLabel = styled.div`
170
+ font-size: ${uiFonts.size.xsmall};
171
+ color: ${uiColors.lightGray};
172
+ text-align: right;
173
+ `;
174
+
175
+ const RewardToast = styled.div`
176
+ padding: 8px 12px;
177
+ background: rgba(255, 200, 87, 0.15);
178
+ border: 1px solid ${uiColors.darkYellow};
179
+ border-radius: 4px;
180
+ `;
181
+
182
+ const RewardText = styled.div`
183
+ font-size: ${uiFonts.size.small};
184
+ color: ${uiColors.darkYellow};
185
+ font-weight: bold;
186
+ `;
187
+
188
+ const SectionLabel = styled.div`
189
+ font-size: ${uiFonts.size.xsmall};
190
+ color: ${uiColors.lightGray};
191
+ text-transform: uppercase;
192
+ letter-spacing: 1px;
193
+ `;
194
+
195
+ const MilestoneList = styled.div`
196
+ display: flex;
197
+ flex-direction: column;
198
+ gap: 6px;
199
+ `;
200
+
201
+ const MilestoneCard = styled.div<{ $reached: boolean }>`
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 10px;
205
+ padding: 8px 12px;
206
+ background: rgba(0, 0, 0, 0.6);
207
+ border: 1px solid ${uiColors.darkGray};
208
+ border-radius: 4px;
209
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
210
+ opacity: ${(p) => (p.$reached ? 1 : 0.5)};
211
+ transition: opacity 0.2s ease;
212
+ `;
213
+
214
+ const MilestoneDayLabel = styled.div`
215
+ font-size: ${uiFonts.size.small};
216
+ font-weight: bold;
217
+ color: ${uiColors.darkYellow};
218
+ width: 44px;
219
+ flex-shrink: 0;
220
+ `;
221
+
222
+ const MilestoneRewardName = styled.div`
223
+ font-size: ${uiFonts.size.small};
224
+ color: ${uiColors.white};
225
+ flex: 1;
226
+ `;
227
+
228
+ const MilestoneBadge = styled.div<{ $reached: boolean }>`
229
+ font-size: ${uiFonts.size.medium};
230
+ color: ${(p) => (p.$reached ? uiColors.lightGreen : uiColors.lightGray)};
231
+ font-weight: bold;
232
+ flex-shrink: 0;
233
+ `;
234
+
235
+ const MotivationalText = styled.div`
236
+ font-size: ${uiFonts.size.small};
237
+ color: ${uiColors.lightGray};
238
+ text-align: center;
239
+ font-style: italic;
240
+ padding-top: 2px;
241
+ `;
@@ -33,6 +33,7 @@ export interface IStoreProps {
33
33
  tabOrder?: TabId[];
34
34
  defaultActiveTab?: TabId;
35
35
  textInputItemKeys?: string[];
36
+ customPacksContent?: React.ReactNode;
36
37
  }
37
38
 
38
39
  export const Store: React.FC<IStoreProps> = ({
@@ -50,6 +51,7 @@ export const Store: React.FC<IStoreProps> = ({
50
51
  tabOrder,
51
52
  defaultActiveTab,
52
53
  textInputItemKeys = [],
54
+ customPacksContent,
53
55
  }) => {
54
56
  const [selectedPack, setSelectedPack] = useState<IItemPack | null>(null);
55
57
  const [activeTab, setActiveTab] = useState<TabId>(() => {
@@ -164,7 +166,7 @@ export const Store: React.FC<IStoreProps> = ({
164
166
  packs: {
165
167
  id: 'packs',
166
168
  title: 'Packs',
167
- content: (
169
+ content: customPacksContent ?? (
168
170
  <StorePacksSection
169
171
  packs={packs.filter(pack => pack.priceUSD < 9.99)}
170
172
  onAddToCart={handleAddPackToCart}
package/src/index.tsx CHANGED
@@ -11,6 +11,7 @@ export * from './components/CheckItem';
11
11
  export * from './components/CircularController/CircularController';
12
12
  export * from './components/CraftBook/CraftBook';
13
13
  export * from './components/DailyTasks/DailyTasks';
14
+ export * from './components/LoginStreak/LoginStreakPanel';
14
15
  export * from './components/DPad/JoystickDPad';
15
16
  export * from './components/DraggableContainer';
16
17
  export * from './components/Dropdown';
@@ -0,0 +1,134 @@
1
+ import { ILoginStreakMilestone } from '@rpg-engine/shared';
2
+ import { Meta, StoryObj } from '@storybook/react';
3
+ import React from 'react';
4
+ import { LoginStreakPanel, ILoginStreakPanelProps } from '../../../components/LoginStreak/LoginStreakPanel';
5
+ import { RPGUIRoot } from '../../../components/RPGUI/RPGUIRoot';
6
+
7
+ const meta = {
8
+ title: 'Features/Login Streak/LoginStreakPanel',
9
+ component: LoginStreakPanel,
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ } satisfies Meta<typeof LoginStreakPanel>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<ILoginStreakPanelProps>;
17
+
18
+ const mockMilestones: ILoginStreakMilestone[] = [
19
+ { day: 7, itemName: 'Light Healing Potion', quantity: 5, reached: true },
20
+ { day: 14, itemName: 'Greater Healing Potion', quantity: 3, reached: true },
21
+ { day: 21, itemName: 'Light Mana Potion', quantity: 5, reached: false },
22
+ { day: 30, itemName: 'Greater Mana Potion', quantity: 3, reached: false },
23
+ ];
24
+
25
+ const Template = (args: ILoginStreakPanelProps) => (
26
+ <RPGUIRoot>
27
+ <LoginStreakPanel {...args} />
28
+ </RPGUIRoot>
29
+ );
30
+
31
+ export const FreshStart: Story = {
32
+ render: Template,
33
+ args: {
34
+ streak: 1,
35
+ longestStreak: 1,
36
+ xpBonusPercent: 0,
37
+ maxXpBonusPercent: 50,
38
+ maxBonusDay: 30,
39
+ isConsecutive: false,
40
+ milestones: mockMilestones,
41
+ milestoneRewardGranted: null,
42
+ onClose: () => console.log('close'),
43
+ },
44
+ };
45
+
46
+ export const InProgress: Story = {
47
+ render: Template,
48
+ args: {
49
+ streak: 10,
50
+ longestStreak: 22,
51
+ xpBonusPercent: 20,
52
+ maxXpBonusPercent: 50,
53
+ maxBonusDay: 30,
54
+ isConsecutive: true,
55
+ milestones: [
56
+ { day: 7, itemName: 'Light Healing Potion', quantity: 5, reached: true },
57
+ { day: 14, itemName: 'Greater Healing Potion', quantity: 3, reached: false },
58
+ { day: 21, itemName: 'Light Mana Potion', quantity: 5, reached: false },
59
+ { day: 30, itemName: 'Greater Mana Potion', quantity: 3, reached: false },
60
+ ],
61
+ milestoneRewardGranted: null,
62
+ onClose: () => console.log('close'),
63
+ },
64
+ };
65
+
66
+ export const Day5Streak: Story = {
67
+ render: Template,
68
+ args: {
69
+ streak: 5,
70
+ longestStreak: 12,
71
+ xpBonusPercent: 10,
72
+ maxXpBonusPercent: 50,
73
+ maxBonusDay: 30,
74
+ isConsecutive: true,
75
+ milestones: mockMilestones,
76
+ milestoneRewardGranted: null,
77
+ onClose: () => console.log('close'),
78
+ },
79
+ };
80
+
81
+ export const MilestoneReached: Story = {
82
+ render: Template,
83
+ args: {
84
+ streak: 7,
85
+ longestStreak: 7,
86
+ xpBonusPercent: 15,
87
+ maxXpBonusPercent: 50,
88
+ maxBonusDay: 30,
89
+ isConsecutive: true,
90
+ milestones: [
91
+ { day: 7, itemName: 'Light Healing Potion', quantity: 5, reached: true },
92
+ { day: 14, itemName: 'Greater Healing Potion', quantity: 3, reached: false },
93
+ { day: 21, itemName: 'Light Mana Potion', quantity: 5, reached: false },
94
+ { day: 30, itemName: 'Greater Mana Potion', quantity: 3, reached: false },
95
+ ],
96
+ milestoneRewardGranted: { itemName: 'Light Healing Potion', quantity: 5 },
97
+ onClose: () => console.log('close'),
98
+ },
99
+ };
100
+
101
+ export const MaxBonus: Story = {
102
+ render: Template,
103
+ args: {
104
+ streak: 30,
105
+ longestStreak: 45,
106
+ xpBonusPercent: 50,
107
+ maxXpBonusPercent: 50,
108
+ maxBonusDay: 30,
109
+ isConsecutive: true,
110
+ milestones: [
111
+ { day: 7, itemName: 'Light Healing Potion', quantity: 5, reached: true },
112
+ { day: 14, itemName: 'Greater Healing Potion', quantity: 3, reached: true },
113
+ { day: 21, itemName: 'Light Mana Potion', quantity: 5, reached: true },
114
+ { day: 30, itemName: 'Greater Mana Potion', quantity: 3, reached: true },
115
+ ],
116
+ milestoneRewardGranted: null,
117
+ onClose: () => console.log('close'),
118
+ },
119
+ };
120
+
121
+ export const StreakReset: Story = {
122
+ render: Template,
123
+ args: {
124
+ streak: 1,
125
+ longestStreak: 22,
126
+ xpBonusPercent: 0,
127
+ maxXpBonusPercent: 50,
128
+ maxBonusDay: 30,
129
+ isConsecutive: false,
130
+ milestones: mockMilestones,
131
+ milestoneRewardGranted: null,
132
+ onClose: () => console.log('close'),
133
+ },
134
+ };