@rpg-engine/long-bow 0.8.137 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.137",
3
+ "version": "0.8.138",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -24,6 +24,17 @@ const TRANSACTION_TYPE_LABELS: Record<string, string> = {
24
24
  AdminAdjustment: 'Admin',
25
25
  };
26
26
 
27
+ const TRANSACTION_TYPE_COLORS: Record<string, string> = {
28
+ Purchase: '#22c55e', // green — DC coming in
29
+ Transfer: '#60a5fa', // blue — P2P transfer
30
+ MarketplaceSale: '#fbbf24', // gold — sold item
31
+ MarketplacePurchase: '#f97316', // orange — bought item
32
+ StorePurchase: '#a78bfa', // purple — store
33
+ Fee: '#ef4444', // red — cost
34
+ Refund: '#06b6d4', // cyan — money back
35
+ AdminAdjustment: '#9ca3af', // gray — admin
36
+ };
37
+
27
38
  const TRANSACTION_TYPE_OPTIONS = [
28
39
  { value: '', label: 'All Types' },
29
40
  { value: 'Purchase', label: 'Purchase' },
@@ -101,12 +112,13 @@ export const DCHistoryPanel: React.FC<IDCHistoryPanelProps> = ({
101
112
  {transactions.map((tx) => {
102
113
  const isCredit = tx.amount > 0;
103
114
  const label = TRANSACTION_TYPE_LABELS[tx.type] ?? tx.type;
115
+ const color = TRANSACTION_TYPE_COLORS[tx.type] ?? '#f59e0b';
104
116
  const subtitle = tx.note ?? (tx.relatedCharacterName ? tx.relatedCharacterName : '');
105
117
 
106
118
  return (
107
119
  <TransactionRow key={tx._id}>
108
120
  <TxLeft>
109
- <TxType>{label}</TxType>
121
+ <TxType $color={color}>{label}</TxType>
110
122
  {subtitle ? <TxNote>{subtitle}</TxNote> : null}
111
123
  </TxLeft>
112
124
  <TxRight>
@@ -225,9 +237,9 @@ const TxRight = styled.div`
225
237
  flex-shrink: 0;
226
238
  `;
227
239
 
228
- const TxType = styled.span`
240
+ const TxType = styled.span<{ $color: string }>`
229
241
  font-size: 7px;
230
- color: #f59e0b;
242
+ color: ${({ $color }) => $color};
231
243
  font-family: 'Press Start 2P', cursive;
232
244
  `;
233
245
 
@@ -5,6 +5,7 @@ import styled from 'styled-components';
5
5
  import { ConfirmModal } from '../ConfirmModal';
6
6
  import { Dropdown } from '../Dropdown';
7
7
  import { Input } from '../Input';
8
+ import { MarketplaceBuyModal, MarketplacePaymentMethod } from './MarketplaceBuyModal';
8
9
  import { MarketplaceRows } from './MarketplaceRows';
9
10
  import { itemRarityOptions, itemTypeOptions, orderByOptions } from './filters';
10
11
 
@@ -26,11 +27,13 @@ export interface IBuyPanelProps {
26
27
  onChangePriceInput: (value: [number | undefined, number | undefined]) => void;
27
28
  scale?: number;
28
29
  equipmentSet?: IEquipmentSet | null;
29
- onMarketPlaceItemBuy?: (marketPlaceItemId: string) => void;
30
+ onMarketPlaceItemBuy?: (marketPlaceItemId: string, paymentMethod?: MarketplacePaymentMethod) => void;
30
31
  characterId: string;
31
32
  enableHotkeys?: () => void;
32
33
  disableHotkeys?: () => void;
33
34
  currentPage: number;
35
+ dcBalance?: number;
36
+ dcToGoldSwapRate?: number;
34
37
  }
35
38
 
36
39
  export const BuyPanel: React.FC<IBuyPanelProps> = ({
@@ -50,6 +53,8 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
50
53
  enableHotkeys,
51
54
  disableHotkeys,
52
55
  currentPage,
56
+ dcBalance = 0,
57
+ dcToGoldSwapRate = 0,
53
58
  }) => {
54
59
  const [name, setName] = useState('');
55
60
  const [mainLevel, setMainLevel] = useState<
@@ -70,9 +75,30 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
70
75
  itemsContainer.current?.scrollTo(0, 0);
71
76
  }, [currentPage]);
72
77
 
78
+ const buyingItem = buyingItemId
79
+ ? items.find(i => i._id === buyingItemId)
80
+ : null;
81
+ const hasDCBalance = dcBalance > 0 && dcToGoldSwapRate > 0;
82
+
83
+ const getDCEquivalentPrice = (goldPrice: number): number =>
84
+ dcToGoldSwapRate > 0 ? Math.ceil(goldPrice / dcToGoldSwapRate) : 0;
85
+
73
86
  return (
74
87
  <>
75
- {buyingItemId && (
88
+ {buyingItemId && buyingItem && hasDCBalance && (
89
+ <MarketplaceBuyModal
90
+ goldPrice={buyingItem.price}
91
+ dcEquivalentPrice={getDCEquivalentPrice(buyingItem.price)}
92
+ dcBalance={dcBalance}
93
+ onClose={() => setBuyingItemId(null)}
94
+ onConfirm={(paymentMethod) => {
95
+ onMarketPlaceItemBuy?.(buyingItemId, paymentMethod);
96
+ setBuyingItemId(null);
97
+ enableHotkeys?.();
98
+ }}
99
+ />
100
+ )}
101
+ {buyingItemId && !hasDCBalance && (
76
102
  <ConfirmModal
77
103
  onClose={setBuyingItemId.bind(null, null)}
78
104
  onConfirm={() => {
@@ -216,6 +242,7 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
216
242
  atlasJSON={atlasJSON}
217
243
  item={item}
218
244
  itemPrice={price}
245
+ dcEquivalentPrice={dcToGoldSwapRate > 0 ? getDCEquivalentPrice(price) : undefined}
219
246
  equipmentSet={equipmentSet}
220
247
  onMarketPlaceItemBuy={setBuyingItemId.bind(null, _id)}
221
248
  disabled={owner === characterId}
@@ -5,6 +5,7 @@ import { Button, ButtonTypes } from '../Button';
5
5
  import { DraggableContainer } from '../DraggableContainer';
6
6
  import { Pager } from '../Pager';
7
7
  import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
8
+ import { MarketplacePaymentMethod } from './MarketplaceBuyModal';
8
9
  import { BuyPanel } from './BuyPanel';
9
10
  import { ManagmentPanel } from './ManagmentPanel';
10
11
 
@@ -26,7 +27,7 @@ export interface IMarketPlaceProps {
26
27
  onChangePriceInput: (value: [number | undefined, number | undefined]) => void;
27
28
  scale?: number;
28
29
  equipmentSet?: IEquipmentSet | null;
29
- onMarketPlaceItemBuy?: (marketPlaceItemId: string) => void;
30
+ onMarketPlaceItemBuy?: (marketPlaceItemId: string, paymentMethod?: MarketplacePaymentMethod) => void;
30
31
  onMarketPlaceItemRemove?: (marketPlaceItemId: string) => void;
31
32
  availableGold: number;
32
33
  selectedItemToSell: IItem | null;
@@ -41,6 +42,8 @@ export interface IMarketPlaceProps {
41
42
  currentPage: number;
42
43
  itemsPerPage: number;
43
44
  onPageChange: (page: number) => void;
45
+ dcBalance?: number;
46
+ dcToGoldSwapRate?: number;
44
47
  }
45
48
 
46
49
  export const Marketplace: React.FC<IMarketPlaceProps> = props => {
@@ -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';