@rpg-engine/long-bow 0.8.136 → 0.8.138

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.
@@ -0,0 +1,230 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { FaTimes } from 'react-icons/fa';
3
+ import styled from 'styled-components';
4
+ import ModalPortal from '../Abstractions/ModalPortal';
5
+ import { Button, ButtonTypes } from '../Button';
6
+
7
+ export type MarketplacePaymentMethod = 'gold' | 'dc';
8
+
9
+ export interface IMarketplaceBuyModalProps {
10
+ goldPrice: number;
11
+ dcEquivalentPrice: number;
12
+ dcBalance: number;
13
+ onConfirm: (paymentMethod: MarketplacePaymentMethod) => void;
14
+ onClose: () => void;
15
+ }
16
+
17
+ export const MarketplaceBuyModal: React.FC<IMarketplaceBuyModalProps> = ({
18
+ goldPrice,
19
+ dcEquivalentPrice,
20
+ dcBalance,
21
+ onConfirm,
22
+ onClose,
23
+ }) => {
24
+ const [selected, setSelected] = useState<MarketplacePaymentMethod>('gold');
25
+ const hasSufficientDC = dcBalance >= dcEquivalentPrice;
26
+
27
+ const stopPropagation = useCallback(
28
+ (e: React.MouseEvent | React.TouchEvent | React.PointerEvent) => {
29
+ e.stopPropagation();
30
+ },
31
+ []
32
+ );
33
+
34
+ const handleConfirm = useCallback(() => {
35
+ onConfirm(selected);
36
+ }, [selected, onConfirm]);
37
+
38
+ return (
39
+ <ModalPortal>
40
+ <Overlay onPointerDown={onClose} />
41
+ <ModalContainer>
42
+ <ModalContent
43
+ onClick={stopPropagation as React.MouseEventHandler}
44
+ onTouchStart={stopPropagation as React.TouchEventHandler}
45
+ onPointerDown={stopPropagation as React.PointerEventHandler}
46
+ >
47
+ <Header>
48
+ <Title>Confirm Purchase</Title>
49
+ <CloseButton onPointerDown={onClose} aria-label="Close">
50
+ <FaTimes />
51
+ </CloseButton>
52
+ </Header>
53
+
54
+ <Options>
55
+ <RadioOption
56
+ $selected={selected === 'gold'}
57
+ onPointerDown={() => setSelected('gold')}
58
+ >
59
+ <RadioCircle $selected={selected === 'gold'} />
60
+ <OptionText>
61
+ <OptionLabel>Gold</OptionLabel>
62
+ <OptionSub>{goldPrice.toLocaleString()} gold</OptionSub>
63
+ </OptionText>
64
+ </RadioOption>
65
+
66
+ <RadioOption
67
+ $selected={selected === 'dc'}
68
+ $disabled={!hasSufficientDC}
69
+ onPointerDown={() => hasSufficientDC && setSelected('dc')}
70
+ >
71
+ <RadioCircle $selected={selected === 'dc'} />
72
+ <OptionText>
73
+ <OptionLabel $disabled={!hasSufficientDC}>Definya Coin</OptionLabel>
74
+ <OptionSub>
75
+ {dcEquivalentPrice.toLocaleString()} DC
76
+ {' '}
77
+ <BalanceHint $insufficient={!hasSufficientDC}>
78
+ ({dcBalance.toLocaleString()} available)
79
+ </BalanceHint>
80
+ </OptionSub>
81
+ </OptionText>
82
+ </RadioOption>
83
+ </Options>
84
+
85
+ <ConfirmRow>
86
+ <Button buttonType={ButtonTypes.RPGUIButton} onPointerDown={handleConfirm}>
87
+ Confirm
88
+ </Button>
89
+ </ConfirmRow>
90
+ </ModalContent>
91
+ </ModalContainer>
92
+ </ModalPortal>
93
+ );
94
+ };
95
+
96
+ const Overlay = styled.div`
97
+ position: fixed;
98
+ inset: 0;
99
+ background: rgba(0, 0, 0, 0.65);
100
+ z-index: 1000;
101
+ `;
102
+
103
+ const ModalContainer = styled.div`
104
+ position: fixed;
105
+ inset: 0;
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ z-index: 1001;
110
+ pointer-events: none;
111
+ `;
112
+
113
+ const ModalContent = styled.div`
114
+ background: #1a1a2e;
115
+ border: 2px solid #f59e0b;
116
+ border-radius: 8px;
117
+ padding: 20px 24px 24px;
118
+ min-width: 300px;
119
+ max-width: 90%;
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: 16px;
123
+ pointer-events: auto;
124
+ animation: scaleIn 0.15s ease-out;
125
+
126
+ @keyframes scaleIn {
127
+ from { transform: scale(0.85); opacity: 0; }
128
+ to { transform: scale(1); opacity: 1; }
129
+ }
130
+ `;
131
+
132
+ const Header = styled.div`
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: space-between;
136
+ `;
137
+
138
+ const Title = styled.h3`
139
+ margin: 0;
140
+ font-family: 'Press Start 2P', cursive;
141
+ font-size: 0.7rem;
142
+ color: #fef08a;
143
+ `;
144
+
145
+ const CloseButton = styled.button`
146
+ background: none;
147
+ border: none;
148
+ color: rgba(255, 255, 255, 0.6);
149
+ cursor: pointer;
150
+ font-size: 1rem;
151
+ padding: 4px;
152
+ display: flex;
153
+ align-items: center;
154
+
155
+ &:hover {
156
+ color: #ffffff;
157
+ }
158
+ `;
159
+
160
+ const Options = styled.div`
161
+ display: flex;
162
+ flex-direction: column;
163
+ gap: 8px;
164
+ `;
165
+
166
+ const RadioOption = styled.div<{ $selected: boolean; $disabled?: boolean }>`
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 12px;
170
+ padding: 10px 12px;
171
+ border: 1px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.15)')};
172
+ border-radius: 6px;
173
+ background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'transparent')};
174
+ cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
175
+ opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)};
176
+ transition: border-color 0.15s, background 0.15s;
177
+
178
+ &:hover {
179
+ border-color: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.15)' : '#f59e0b')};
180
+ }
181
+ `;
182
+
183
+ const RadioCircle = styled.div<{ $selected: boolean }>`
184
+ width: 16px;
185
+ height: 16px;
186
+ border-radius: 50%;
187
+ border: 2px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.4)')};
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: center;
191
+ flex-shrink: 0;
192
+
193
+ &::after {
194
+ content: '';
195
+ width: 8px;
196
+ height: 8px;
197
+ border-radius: 50%;
198
+ background: #f59e0b;
199
+ opacity: ${({ $selected }) => ($selected ? 1 : 0)};
200
+ transition: opacity 0.15s;
201
+ }
202
+ `;
203
+
204
+ const OptionText = styled.div`
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: 3px;
208
+ `;
209
+
210
+ const OptionLabel = styled.span<{ $disabled?: boolean }>`
211
+ font-family: 'Press Start 2P', cursive;
212
+ font-size: 0.65rem;
213
+ color: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.4)' : '#ffffff')};
214
+ `;
215
+
216
+ const OptionSub = styled.span`
217
+ font-family: 'Press Start 2P', cursive;
218
+ font-size: 0.5rem;
219
+ color: rgba(255, 255, 255, 0.55);
220
+ `;
221
+
222
+ const BalanceHint = styled.span<{ $insufficient: boolean }>`
223
+ color: ${({ $insufficient }) => ($insufficient ? '#ef4444' : 'rgba(255, 255, 255, 0.4)')};
224
+ `;
225
+
226
+ const ConfirmRow = styled.div`
227
+ display: flex;
228
+ justify-content: center;
229
+ margin-top: 4px;
230
+ `;
@@ -19,6 +19,7 @@ export interface IMarketPlaceRowsPropos {
19
19
  atlasIMG: any;
20
20
  item: IItem;
21
21
  itemPrice: number;
22
+ dcEquivalentPrice?: number;
22
23
  equipmentSet?: IEquipmentSet | null;
23
24
  scale?: number;
24
25
  onMarketPlaceItemBuy?: () => void;
@@ -31,6 +32,7 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
31
32
  atlasIMG,
32
33
  item,
33
34
  itemPrice,
35
+ dcEquivalentPrice,
34
36
  equipmentSet,
35
37
  scale,
36
38
  onMarketPlaceItemBuy,
@@ -88,23 +90,32 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
88
90
  </ItemIconContainer>
89
91
 
90
92
  <Flex>
91
- <ItemIconContainer>
92
- <GoldContainer>
93
- <SpriteFromAtlas
94
- atlasIMG={atlasIMG}
95
- atlasJSON={atlasJSON}
96
- spriteKey="others/gold-coin-qty-5.png"
97
- imgScale={2}
98
- />
99
- </GoldContainer>
100
- <PriceValue>
101
- <p>
102
- <Ellipsis maxLines={1} maxWidth="200px" fontSize="10px">
103
- ${itemPrice}
93
+ <PriceContainer>
94
+ <ItemIconContainer>
95
+ <GoldContainer>
96
+ <SpriteFromAtlas
97
+ atlasIMG={atlasIMG}
98
+ atlasJSON={atlasJSON}
99
+ spriteKey="others/gold-coin-qty-5.png"
100
+ imgScale={2}
101
+ />
102
+ </GoldContainer>
103
+ <PriceValue>
104
+ <p>
105
+ <Ellipsis maxLines={1} maxWidth="120px" fontSize="10px">
106
+ ${itemPrice}
107
+ </Ellipsis>
108
+ </p>
109
+ </PriceValue>
110
+ </ItemIconContainer>
111
+ {dcEquivalentPrice !== undefined && (
112
+ <DCPriceLabel>
113
+ <Ellipsis maxLines={1} maxWidth="80px" fontSize="9px">
114
+ {dcEquivalentPrice} DC
104
115
  </Ellipsis>
105
- </p>
106
- </PriceValue>
107
- </ItemIconContainer>
116
+ </DCPriceLabel>
117
+ )}
118
+ </PriceContainer>
108
119
  <ButtonContainer>
109
120
  <Button
110
121
  buttonType={ButtonTypes.RPGUIButton}
@@ -176,6 +187,20 @@ const SpriteContainer = styled.div`
176
187
  left: 0.5rem;
177
188
  `;
178
189
 
190
+ const PriceContainer = styled.div`
191
+ display: flex;
192
+ flex-direction: column;
193
+ align-items: flex-start;
194
+ gap: 2px;
195
+ `;
196
+
197
+ const DCPriceLabel = styled.p`
198
+ margin: 0;
199
+ margin-left: 40px;
200
+ color: #fef08a;
201
+ font-size: 0.7rem;
202
+ `;
203
+
179
204
  const PriceValue = styled.div`
180
205
  margin-left: 40px;
181
206
  `;
@@ -168,6 +168,8 @@ export const Store: React.FC<IStoreProps> = ({
168
168
  packs={packs.filter(pack => pack.priceUSD >= 9.99)}
169
169
  onAddToCart={handleAddPackToCart}
170
170
  onSelectPack={setSelectedPack}
171
+ atlasJSON={atlasJSON}
172
+ atlasIMG={atlasIMG}
171
173
  />
172
174
  ),
173
175
  },
@@ -176,9 +178,11 @@ export const Store: React.FC<IStoreProps> = ({
176
178
  title: packsTabLabel,
177
179
  content: customPacksContent ?? (
178
180
  <StorePacksSection
179
- packs={packs.filter(pack => pack.priceUSD < 9.99)}
181
+ packs={hidePremiumTab ? packs : packs.filter(pack => pack.priceUSD < 9.99)}
180
182
  onAddToCart={handleAddPackToCart}
181
183
  onSelectPack={setSelectedPack}
184
+ atlasJSON={atlasJSON}
185
+ atlasIMG={atlasIMG}
182
186
  />
183
187
  ),
184
188
  },
@@ -3,6 +3,7 @@ import React, { useCallback } from 'react';
3
3
  import { FaCartPlus } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import { CTAButton } from '../../shared/CTAButton/CTAButton';
6
+ import { SpriteFromAtlas } from '../../shared/SpriteFromAtlas';
6
7
  import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
7
8
  import { usePackFiltering } from '../../../hooks/usePackFiltering';
8
9
 
@@ -10,12 +11,16 @@ interface IStorePacksSectionProps {
10
11
  packs: IItemPack[];
11
12
  onAddToCart: (pack: IItemPack) => void;
12
13
  onSelectPack?: (pack: IItemPack) => void;
14
+ atlasJSON?: any;
15
+ atlasIMG?: string;
13
16
  }
14
17
 
15
18
  export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
16
19
  packs,
17
20
  onAddToCart,
18
21
  onSelectPack,
22
+ atlasJSON,
23
+ atlasIMG,
19
24
  }) => {
20
25
  const { searchQuery, setSearchQuery, filteredPacks } = usePackFiltering(packs);
21
26
 
@@ -24,11 +29,22 @@ export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
24
29
  return imageUrl.default || imageUrl.src;
25
30
  };
26
31
 
32
+ const renderPackIcon = useCallback(
33
+ (pack: IItemPack) => {
34
+ const imgSrc = getImageSrc(pack.image);
35
+ if (atlasJSON && atlasIMG && imgSrc && atlasJSON?.frames?.[imgSrc]) {
36
+ return <SpriteFromAtlas atlasJSON={atlasJSON} atlasIMG={atlasIMG} spriteKey={imgSrc} width={40} height={40} imgScale={1.2} centered />;
37
+ }
38
+ return <img src={imgSrc} alt={pack.title} />;
39
+ },
40
+ [atlasJSON, atlasIMG]
41
+ );
42
+
27
43
  const renderPack = useCallback(
28
44
  (pack: IItemPack) => (
29
45
  <PackRow key={pack.key} onClick={() => onSelectPack?.(pack)}>
30
46
  <PackIconContainer>
31
- <img src={getImageSrc(pack.image)} alt={pack.title} />
47
+ {renderPackIcon(pack)}
32
48
  </PackIconContainer>
33
49
 
34
50
  <PackDetails>
package/src/index.tsx CHANGED
@@ -35,6 +35,7 @@ export * from './components/itemSelector/ItemSelector';
35
35
  export * from './components/Leaderboard/Leaderboard';
36
36
  export * from './components/ListMenu';
37
37
  export * from './components/Marketplace/Marketplace';
38
+ export * from './components/Marketplace/MarketplaceBuyModal';
38
39
  export * from './components/Marketplace/MarketplaceRows';
39
40
  export * from './components/Multitab/TabBody';
40
41
  export * from './components/Multitab/TabsContainer';