@rpg-engine/long-bow 0.8.202 → 0.8.205

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.
Files changed (45) hide show
  1. package/dist/components/Item/Inventory/ErrorBoundary.d.ts +2 -0
  2. package/dist/components/Marketplace/BuyOrderPanel.d.ts +2 -2
  3. package/dist/components/Marketplace/BuyOrderRows.d.ts +4 -4
  4. package/dist/components/Marketplace/HistoryPanel.d.ts +2 -2
  5. package/dist/components/Marketplace/MarketplaceRows.d.ts +2 -0
  6. package/dist/components/Tutorial/TutorialStepper.d.ts +2 -1
  7. package/dist/components/shared/MMORPGNumber.d.ts +6 -0
  8. package/dist/long-bow.cjs.development.js +794 -34640
  9. package/dist/long-bow.cjs.development.js.map +1 -1
  10. package/dist/long-bow.cjs.production.min.js +1 -1
  11. package/dist/long-bow.cjs.production.min.js.map +1 -1
  12. package/dist/long-bow.esm.js +795 -34641
  13. package/dist/long-bow.esm.js.map +1 -1
  14. package/dist/utils/numberUtils.d.ts +7 -0
  15. package/package.json +1 -1
  16. package/src/components/DCWallet/DCWalletContent.tsx +5 -3
  17. package/src/components/Item/Inventory/ErrorBoundary.tsx +25 -8
  18. package/src/components/Item/Inventory/ItemPropertyColorSelector.tsx +3 -1
  19. package/src/components/Marketplace/BuyOrderPanel.tsx +2 -2
  20. package/src/components/Marketplace/BuyOrderRows.tsx +13 -7
  21. package/src/components/Marketplace/CharacterDetailModal.tsx +99 -17
  22. package/src/components/Marketplace/CharacterListingModal.tsx +2 -1
  23. package/src/components/Marketplace/GroupedRowContainer.tsx +10 -10
  24. package/src/components/Marketplace/HistoryPanel.tsx +5 -5
  25. package/src/components/Marketplace/MarketplaceBuyModal.tsx +4 -4
  26. package/src/components/Marketplace/MarketplaceRows.tsx +13 -7
  27. package/src/components/Marketplace/__test__/CharacterDetailModal.spec.tsx +137 -0
  28. package/src/components/Store/CartView.tsx +2 -1
  29. package/src/components/Store/FeaturedBanner.tsx +1 -0
  30. package/src/components/Store/PaymentMethodModal.tsx +11 -3
  31. package/src/components/Store/StoreCharacterSkinRow.tsx +9 -3
  32. package/src/components/Store/StoreItemDetails.tsx +9 -1
  33. package/src/components/Store/StoreItemRow.tsx +13 -3
  34. package/src/components/Store/sections/StorePacksSection.tsx +7 -0
  35. package/src/components/Store/sections/StoreRedeemSection.tsx +2 -1
  36. package/src/components/Tutorial/TutorialStepper.tsx +16 -3
  37. package/src/components/shared/DCRateStrip.tsx +3 -2
  38. package/src/components/shared/MMORPGNumber.tsx +22 -0
  39. package/src/stories/Features/marketplace/CharacterDetailModal.stories.tsx +69 -15
  40. package/src/stories/Features/marketplace/CharacterMarketplace.stories.tsx +105 -11
  41. package/src/stories/Features/store/Store.stories.tsx +125 -35
  42. package/src/stories/Features/trading/Marketplace.stories.tsx +8 -4
  43. package/src/utils/__test__/atlasUtils.spec.ts +15 -0
  44. package/src/utils/atlasUtils.ts +78 -0
  45. package/src/utils/numberUtils.ts +31 -0
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Formats a number using modern gaming standards (k for thousands, M for millions, B for billions).
3
+ * @param num The number to format
4
+ * @param useKNotation Whether to use the k/M/B notation
5
+ * @returns Formatted string
6
+ */
7
+ export declare const formatMMORPGNumber: (num: number, useKNotation?: boolean) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.202",
3
+ "version": "0.8.205",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -5,6 +5,7 @@ import { InternalTabs } from '../InternalTabs/InternalTabs';
5
5
  import { DCRateStrip } from '../shared/DCRateStrip';
6
6
  import { DCHistoryPanel, IDCTransaction } from './DCHistoryPanel';
7
7
  import { DCTransferPanel, IDCTransferCharacterResult } from './DCTransferPanel';
8
+ import { MMORPGNumber } from '../../components/shared/MMORPGNumber';
8
9
 
9
10
  export interface IDCWalletContentProps {
10
11
  dcBalance: number;
@@ -44,7 +45,6 @@ export const DCWalletContent: React.FC<IDCWalletContentProps> = ({
44
45
  searchResults,
45
46
  }) => {
46
47
  const usdValue = (dcBalance / DC_TO_USD).toFixed(2);
47
- const goldValue = (dcBalance * DC_TO_GOLD_SWAP_RATE).toLocaleString();
48
48
 
49
49
  const tabs = [
50
50
  {
@@ -86,8 +86,10 @@ export const DCWalletContent: React.FC<IDCWalletContentProps> = ({
86
86
  <BalanceTop>
87
87
  <BalanceBlock>
88
88
  <BalanceLabel>DC BALANCE</BalanceLabel>
89
- <BalanceAmount>{dcBalance.toLocaleString()} <BalanceDC>DC</BalanceDC></BalanceAmount>
90
- <BalanceEquiv>≈ ${usdValue} USD &nbsp;·&nbsp; {goldValue} Gold</BalanceEquiv>
89
+ <BalanceAmount><MMORPGNumber value={dcBalance} /> <BalanceDC>DC</BalanceDC></BalanceAmount>
90
+ <BalanceEquiv>
91
+ ≈ ${usdValue} USD &nbsp;·&nbsp; <MMORPGNumber value={dcBalance * DC_TO_GOLD_SWAP_RATE} /> Gold
92
+ </BalanceEquiv>
91
93
  </BalanceBlock>
92
94
  {onBuyDC && (
93
95
  <BuyButton onPointerDown={onBuyDC} role="button" tabIndex={0} title="Buy Definya Coins">
@@ -1,10 +1,10 @@
1
1
  import React, { Component, ErrorInfo, ReactNode } from 'react';
2
- import atlasJSON from '../../../mocks/atlas/items/items.json';
3
- import atlasIMG from '../../../mocks/atlas/items/items.png';
4
2
  import { SpriteFromAtlas } from '../../shared/SpriteFromAtlas';
5
3
 
6
4
  interface Props {
7
5
  children?: ReactNode;
6
+ atlasJSON?: any;
7
+ atlasIMG?: any;
8
8
  }
9
9
 
10
10
  interface State {
@@ -27,13 +27,30 @@ export class ErrorBoundary extends Component<Props, State> {
27
27
 
28
28
  public render() {
29
29
  if (this.state.hasError) {
30
+ if (this.props.atlasJSON && this.props.atlasIMG) {
31
+ return (
32
+ <SpriteFromAtlas
33
+ atlasIMG={this.props.atlasIMG}
34
+ atlasJSON={this.props.atlasJSON}
35
+ spriteKey={'others/no-image.png'}
36
+ imgScale={3}
37
+ />
38
+ );
39
+ }
30
40
  return (
31
- <SpriteFromAtlas
32
- atlasIMG={atlasIMG}
33
- atlasJSON={atlasJSON}
34
- spriteKey={'others/no-image.png'}
35
- imgScale={3}
36
- />
41
+ <div style={{
42
+ width: '32px',
43
+ height: '32px',
44
+ border: '1px dashed #ef4444',
45
+ display: 'flex',
46
+ alignItems: 'center',
47
+ justifyContent: 'center',
48
+ color: '#ef4444',
49
+ fontSize: '8px',
50
+ textAlign: 'center'
51
+ }}>
52
+ ERR
53
+ </div>
37
54
  );
38
55
  }
39
56
 
@@ -7,6 +7,8 @@ import { ConfirmModal } from '../../ConfirmModal';
7
7
  import { DraggableContainer } from '../../DraggableContainer';
8
8
  import { RPGUIContainerTypes } from '../../RPGUI/RPGUIContainer';
9
9
 
10
+ import { MMORPGNumber } from '../../../components/shared/MMORPGNumber';
11
+
10
12
  interface IColorSelectorProps {
11
13
  selectedColor: string;
12
14
  isOpen: boolean;
@@ -59,7 +61,7 @@ export const ColorSelector: React.FC<IColorSelectorProps> = ({
59
61
  <p>
60
62
  Cost:
61
63
  <CostDisplay>
62
- {costWarning?.cost.toLocaleString()}
64
+ <MMORPGNumber value={costWarning?.cost || 0} />
63
65
  {costWarning?.currency || ' gold'}
64
66
  </CostDisplay>
65
67
  </p>
@@ -15,8 +15,8 @@ import { BuyOrderDetailsModal } from './BuyOrderDetailsModal';
15
15
  import { BuyOrderRow } from './BuyOrderRows';
16
16
 
17
17
  export interface IBuyOrderPanelProps {
18
- atlasJSON?: any;
19
- atlasIMG?: any;
18
+ atlasJSON: any;
19
+ atlasIMG: any;
20
20
  selectedBlueprint?: IMarketplaceBlueprintSummary;
21
21
  onOpenBlueprintSearch: () => void;
22
22
  onCloseDetails?: () => void;
@@ -12,11 +12,12 @@ import { CTAButton } from '../shared/CTAButton/CTAButton';
12
12
  import { Ellipsis } from '../shared/Ellipsis';
13
13
  import { SimpleTooltip } from '../shared/SimpleTooltip';
14
14
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
15
+ import { MMORPGNumber } from '../../components/shared/MMORPGNumber';
15
16
 
16
17
  export interface IBuyOrderRowProps {
17
18
  buyOrder: IMarketplaceBuyOrderItem;
18
- atlasJSON?: any;
19
- atlasIMG?: any;
19
+ atlasJSON: any;
20
+ atlasIMG: any;
20
21
  isOwn?: boolean;
21
22
  onCancel?: (buyOrderId: string) => void;
22
23
  onFulfill?: (buyOrderId: string) => void;
@@ -25,7 +26,10 @@ export interface IBuyOrderRowProps {
25
26
  }
26
27
 
27
28
  const BUY_ORDER_DURATION_WEEKS = 4;
28
- type BuyOrderWithQuantityFallback = IMarketplaceBuyOrderItem & { quantity?: number };
29
+ type BuyOrderWithQuantityFallback = IMarketplaceBuyOrderItem & {
30
+ quantity?: number;
31
+ itemTexturePath?: string | null;
32
+ };
29
33
 
30
34
  // Format "Active" → "ACTIVE", "Fulfilled" → "FULFILLED"
31
35
  const formatStatusLabel = (status: string): string => {
@@ -74,7 +78,9 @@ export const BuyOrderRow: React.FC<IBuyOrderRowProps> = ({
74
78
  const stackQty = (buyOrder as BuyOrderWithQuantityFallback).stackQty ?? (buyOrder as BuyOrderWithQuantityFallback).quantity;
75
79
  const timeRemaining =
76
80
  buyOrder.status === MarketplaceBuyOrderStatus.Active ? getTimeRemaining(buyOrder.createdAt) : null;
77
- const spriteKey = resolveAtlasSpriteKey(atlasJSON, buyOrder.itemBlueprintKey);
81
+ const textureLookupKey =
82
+ (buyOrder as BuyOrderWithQuantityFallback).itemTexturePath ?? buyOrder.itemBlueprintKey;
83
+ const spriteKey = resolveAtlasSpriteKey(atlasJSON, textureLookupKey);
78
84
  const rarityGlow = buyOrder.itemRarity ? RARITY_COLORS[buyOrder.itemRarity] || null : null;
79
85
 
80
86
  return (
@@ -119,7 +125,7 @@ export const BuyOrderRow: React.FC<IBuyOrderRowProps> = ({
119
125
  </SimpleTooltip>
120
126
  </GoldIcon>
121
127
  )}
122
- <GoldPrice>{buyOrder.maxPrice}</GoldPrice>
128
+ <GoldPrice><MMORPGNumber value={buyOrder.maxPrice} /></GoldPrice>
123
129
  </GoldPriceRow>
124
130
  {showRequestTag && (
125
131
  <LabelPill
@@ -182,8 +188,8 @@ export const BuyOrderRow: React.FC<IBuyOrderRowProps> = ({
182
188
  export interface IGroupedBuyOrderRowProps {
183
189
  bestOrder: IMarketplaceBuyOrderItem;
184
190
  otherOrders: IMarketplaceBuyOrderItem[];
185
- atlasJSON?: any;
186
- atlasIMG?: any;
191
+ atlasJSON: any;
192
+ atlasIMG: any;
187
193
  isOwn?: boolean;
188
194
  onCancel?: (buyOrderId: string) => void;
189
195
  onFulfill?: (buyOrderId: string) => void;
@@ -34,6 +34,15 @@ const RARITY_COLORS: Record<string, string> = {
34
34
  const rarityColor = (rarity?: string) =>
35
35
  RARITY_COLORS[(rarity ?? '').toLowerCase()] ?? RARITY_COLORS.common;
36
36
 
37
+ const formatEquipmentSlot = (slot?: string) => {
38
+ if (!slot) return 'Unknown';
39
+
40
+ return slot
41
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
42
+ .replace(/[_-]+/g, ' ')
43
+ .replace(/\b\w/g, (char) => char.toUpperCase());
44
+ };
45
+
37
46
  export const CharacterDetailModal: React.FC<ICharacterDetailModalProps> = ({
38
47
  listing,
39
48
  isOpen,
@@ -103,7 +112,11 @@ export const CharacterDetailModal: React.FC<ICharacterDetailModalProps> = ({
103
112
  >
104
113
  <Header>
105
114
  <Title>Character Details</Title>
106
- <CloseButton onPointerDown={handleClose} aria-label="Close" type="button">
115
+ <CloseButton
116
+ onPointerDown={handleClose}
117
+ aria-label="Close"
118
+ type="button"
119
+ >
107
120
  <FaTimes />
108
121
  </CloseButton>
109
122
  </Header>
@@ -157,9 +170,26 @@ export const CharacterDetailModal: React.FC<ICharacterDetailModalProps> = ({
157
170
  <EquipmentList>
158
171
  {snap.equipment.map((eq, i) => (
159
172
  <EquipmentRow key={i}>
160
- <EquipSlot>{eq.slot}</EquipSlot>
161
- <EquipName>{eq.itemName}</EquipName>
162
- <RarityBadge $rarity={eq.rarity}>{eq.rarity || 'Common'}</RarityBadge>
173
+ <EquipmentSprite>
174
+ <SpriteFromAtlas
175
+ atlasIMG={atlasIMG}
176
+ atlasJSON={atlasJSON}
177
+ spriteKey={eq.itemKey}
178
+ imgScale={2}
179
+ width={32}
180
+ height={32}
181
+ centered
182
+ />
183
+ </EquipmentSprite>
184
+ <EquipMeta>
185
+ <EquipName>{eq.itemName}</EquipName>
186
+ <EquipDetails>
187
+ <EquipSlot>{formatEquipmentSlot(eq.slot)}</EquipSlot>
188
+ <RarityBadge $rarity={eq.rarity}>
189
+ {eq.rarity || 'Common'}
190
+ </RarityBadge>
191
+ </EquipDetails>
192
+ </EquipMeta>
163
193
  </EquipmentRow>
164
194
  ))}
165
195
  </EquipmentList>
@@ -239,9 +269,17 @@ const ModalContent = styled.div`
239
269
  pointer-events: auto;
240
270
  animation: ${scaleIn} 0.15s ease-out;
241
271
 
242
- &::-webkit-scrollbar { width: 6px; }
243
- &::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 4px; }
244
- &::-webkit-scrollbar-thumb { background: rgba(245,158,11,0.3); border-radius: 4px; }
272
+ &::-webkit-scrollbar {
273
+ width: 6px;
274
+ }
275
+ &::-webkit-scrollbar-track {
276
+ background: rgba(0, 0, 0, 0.2);
277
+ border-radius: 4px;
278
+ }
279
+ &::-webkit-scrollbar-thumb {
280
+ background: rgba(245, 158, 11, 0.3);
281
+ border-radius: 4px;
282
+ }
245
283
  `;
246
284
 
247
285
  const Header = styled.div`
@@ -268,7 +306,9 @@ const CloseButton = styled.button`
268
306
  display: flex;
269
307
  align-items: center;
270
308
 
271
- &:hover { color: #fff; }
309
+ &:hover {
310
+ color: #fff;
311
+ }
272
312
  `;
273
313
 
274
314
  const HeroSection = styled.div`
@@ -285,8 +325,8 @@ const SpriteContainer = styled.div`
285
325
  width: 96px;
286
326
  height: 96px;
287
327
  flex-shrink: 0;
288
- background: rgba(255,255,255,0.03);
289
- border: 1px solid rgba(255,255,255,0.06);
328
+ background: rgba(255, 255, 255, 0.03);
329
+ border: 1px solid rgba(255, 255, 255, 0.06);
290
330
  border-radius: 6px;
291
331
  `;
292
332
 
@@ -320,7 +360,9 @@ const ModeBadge = styled.span<{ $hardcore?: boolean }>`
320
360
  font-family: 'Press Start 2P', cursive !important;
321
361
  font-size: 0.35rem !important;
322
362
  color: ${({ $hardcore }) => ($hardcore ? '#ef4444' : '#6b7280')} !important;
323
- border: 1px solid ${({ $hardcore }) => ($hardcore ? 'rgba(239,68,68,0.4)' : 'rgba(107,114,128,0.3)')};
363
+ border: 1px solid
364
+ ${({ $hardcore }) =>
365
+ $hardcore ? 'rgba(239,68,68,0.4)' : 'rgba(107,114,128,0.3)'};
324
366
  border-radius: 3px;
325
367
  padding: 2px 6px;
326
368
  text-transform: uppercase;
@@ -330,7 +372,7 @@ const ModeBadge = styled.span<{ $hardcore?: boolean }>`
330
372
 
331
373
  const Divider = styled.hr`
332
374
  border: none;
333
- border-top: 1px solid rgba(255,255,255,0.06);
375
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
334
376
  margin: 0;
335
377
  flex-shrink: 0;
336
378
  `;
@@ -354,6 +396,10 @@ const MetaColumns = styled.div`
354
396
  grid-template-columns: 1fr 1fr;
355
397
  gap: 16px;
356
398
  align-items: start;
399
+
400
+ @media (max-width: 640px) {
401
+ grid-template-columns: 1fr;
402
+ }
357
403
  `;
358
404
 
359
405
  const SkillsList = styled.div`
@@ -393,9 +439,35 @@ const EquipmentRow = styled.div`
393
439
  align-items: center;
394
440
  gap: 8px;
395
441
  padding: 5px 8px;
396
- background: rgba(255,255,255,0.02);
397
- border: 1px solid rgba(255,255,255,0.05);
442
+ background: rgba(255, 255, 255, 0.02);
443
+ border: 1px solid rgba(255, 255, 255, 0.05);
398
444
  border-radius: 4px;
445
+ min-width: 0;
446
+ `;
447
+
448
+ const EquipmentSprite = styled.div`
449
+ display: flex;
450
+ align-items: center;
451
+ justify-content: center;
452
+ width: 32px;
453
+ height: 32px;
454
+ flex-shrink: 0;
455
+ `;
456
+
457
+ const EquipMeta = styled.div`
458
+ display: flex;
459
+ flex-direction: column;
460
+ gap: 4px;
461
+ min-width: 0;
462
+ flex: 1;
463
+ `;
464
+
465
+ const EquipDetails = styled.div`
466
+ display: flex;
467
+ align-items: center;
468
+ justify-content: space-between;
469
+ gap: 8px;
470
+ min-width: 0;
399
471
  `;
400
472
 
401
473
  const EquipSlot = styled.span`
@@ -403,8 +475,7 @@ const EquipSlot = styled.span`
403
475
  font-size: 0.34rem !important;
404
476
  color: #6b7280 !important;
405
477
  text-transform: capitalize;
406
- min-width: 52px;
407
- flex-shrink: 0;
478
+ min-width: 0;
408
479
  `;
409
480
 
410
481
  const EquipName = styled.span`
@@ -412,6 +483,10 @@ const EquipName = styled.span`
412
483
  font-size: 0.38rem !important;
413
484
  color: #d1d5db !important;
414
485
  flex: 1;
486
+ min-width: 0;
487
+ overflow: hidden;
488
+ text-overflow: ellipsis;
489
+ white-space: nowrap;
415
490
  `;
416
491
 
417
492
  const RarityBadge = styled.span<{ $rarity?: string }>`
@@ -445,6 +520,11 @@ const FooterActions = styled.div`
445
520
  align-items: center;
446
521
  justify-content: space-between;
447
522
  gap: 12px;
523
+
524
+ @media (max-width: 640px) {
525
+ flex-direction: column;
526
+ align-items: stretch;
527
+ }
448
528
  `;
449
529
 
450
530
  const PriceDisplay = styled.div`
@@ -473,7 +553,9 @@ const BuyBtn = styled(CTAButton)`
473
553
  padding: 10px 18px;
474
554
  height: 34px;
475
555
 
476
- span { font-size: 0.6rem; }
556
+ span {
557
+ font-size: 0.6rem;
558
+ }
477
559
  `;
478
560
 
479
561
  const PendingNotice = styled.span`
@@ -9,6 +9,7 @@ import { Input } from '../Input';
9
9
  import { CTAButton } from '../shared/CTAButton/CTAButton';
10
10
  import { DCRateStrip } from '../shared/DCRateStrip';
11
11
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
12
+ import { MMORPGNumber } from '../../components/shared/MMORPGNumber';
12
13
 
13
14
  export interface ICharacterListingModalProps {
14
15
  isOpen: boolean;
@@ -176,7 +177,7 @@ export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
176
177
  centered
177
178
  />
178
179
  </DCCoinWrapper>
179
- <PricePreviewAmount>{Number(price).toLocaleString()} DC</PricePreviewAmount>
180
+ <PricePreviewAmount><MMORPGNumber value={Number(price)} /> DC</PricePreviewAmount>
180
181
  </PricePreview>
181
182
  )}
182
183
  <DCRateStrip />
@@ -57,29 +57,29 @@ const GroupMeta = styled.div<{ $rightInset: number }>`
57
57
  transform: translateY(-50%);
58
58
  display: flex;
59
59
  align-items: center;
60
- gap: 4px;
61
- padding-left: 10px;
62
- background: linear-gradient(to left, rgba(25, 23, 23, 0.85), rgba(25, 23, 23, 0));
60
+ gap: 16px;
61
+ background: transparent;
63
62
  pointer-events: none;
64
- z-index: 1;
63
+ z-index: 10;
65
64
  `;
66
65
 
67
66
  const OfferBadge = styled.span`
68
67
  font-family: 'Press Start 2P', cursive;
69
68
  font-size: 0.5rem;
70
- color: rgba(255, 255, 255, 0.5);
71
- background: rgba(255, 255, 255, 0.08);
72
- padding: 2px 6px;
73
- border-radius: 8px;
69
+ color: #fbbf24;
70
+ background: transparent;
74
71
  white-space: nowrap;
72
+ letter-spacing: 0.5px;
73
+ text-shadow: 0 0 8px rgba(251, 191, 36, 0.4);
75
74
  `;
76
75
 
77
76
  const Chevron = styled.span<{ $expanded: boolean }>`
78
77
  display: inline-block;
79
- font-size: 0.7rem;
80
- color: rgba(255, 255, 255, 0.4);
78
+ font-size: 0.8rem;
79
+ color: #fbbf24;
81
80
  transition: transform 0.2s ease;
82
81
  transform: rotate(${({ $expanded }) => ($expanded ? '90deg' : '0deg')});
82
+ text-shadow: 0 0 5px rgba(251, 191, 36, 0.3);
83
83
  `;
84
84
 
85
85
  const SubRows = styled.div`
@@ -1,5 +1,4 @@
1
1
  import {
2
- formatDCAmount,
3
2
  goldToDC,
4
3
  IMarketplaceTransaction,
5
4
  MarketplaceTransactionType,
@@ -7,6 +6,7 @@ import {
7
6
  import React from 'react';
8
7
  import styled from 'styled-components';
9
8
  import { uiColors } from '../../constants/uiColors';
9
+ import { MMORPGNumber } from '../../components/shared/MMORPGNumber';
10
10
  import { Dropdown } from '../Dropdown';
11
11
  import { IOptionsProps } from '../Dropdown';
12
12
  import { Pager } from '../Pager';
@@ -22,8 +22,8 @@ export interface IHistoryPanelProps {
22
22
  selectedType: string;
23
23
  onTypeChange: (type: string) => void;
24
24
  onPageChange: (page: number) => void;
25
- atlasJSON?: any;
26
- atlasIMG?: any;
25
+ atlasJSON: any;
26
+ atlasIMG: any;
27
27
  dcToGoldSwapRate?: number;
28
28
  }
29
29
 
@@ -177,7 +177,7 @@ export const HistoryPanel: React.FC<IHistoryPanelProps> = ({
177
177
  )}
178
178
  <DCPrice $type={tx.type}>
179
179
  {getGoldSign(tx.type)}
180
- {formatDCAmount(getDCEquivalentPrice(tx.goldAmount))}
180
+ <MMORPGNumber value={getDCEquivalentPrice(tx.goldAmount)} />
181
181
  </DCPrice>
182
182
  </DCPriceRow>
183
183
  ) : (
@@ -197,7 +197,7 @@ export const HistoryPanel: React.FC<IHistoryPanelProps> = ({
197
197
  )}
198
198
  <GoldAmount $type={tx.type}>
199
199
  {getGoldSign(tx.type)}
200
- {tx.goldAmount}g
200
+ <MMORPGNumber value={tx.goldAmount} />g
201
201
  </GoldAmount>
202
202
  </GoldPriceRow>
203
203
  )}
@@ -1,9 +1,9 @@
1
- import { formatDCAmount } from '@rpg-engine/shared';
2
1
  import React, { useCallback, useState } from 'react';
3
2
  import { FaTimes } from 'react-icons/fa';
4
3
  import styled from 'styled-components';
5
4
  import ModalPortal from '../Abstractions/ModalPortal';
6
5
  import { Button, ButtonTypes } from '../Button';
6
+ import { MMORPGNumber } from '../../components/shared/MMORPGNumber';
7
7
 
8
8
  export type MarketplacePaymentMethod = 'gold' | 'dc';
9
9
 
@@ -60,7 +60,7 @@ export const MarketplaceBuyModal: React.FC<IMarketplaceBuyModalProps> = ({
60
60
  <RadioCircle $selected={selected === 'gold'} />
61
61
  <OptionText>
62
62
  <OptionLabel>Gold</OptionLabel>
63
- <OptionSub>{goldPrice.toLocaleString()} gold</OptionSub>
63
+ <OptionSub><MMORPGNumber value={goldPrice} /> gold</OptionSub>
64
64
  </OptionText>
65
65
  </RadioOption>
66
66
 
@@ -73,10 +73,10 @@ export const MarketplaceBuyModal: React.FC<IMarketplaceBuyModalProps> = ({
73
73
  <OptionText>
74
74
  <OptionLabel $disabled={!hasSufficientDC}>Definya Coin</OptionLabel>
75
75
  <OptionSub>
76
- {formatDCAmount(dcEquivalentPrice)} DC
76
+ <MMORPGNumber value={dcEquivalentPrice} /> DC
77
77
  {' '}
78
78
  <BalanceHint $insufficient={!hasSufficientDC}>
79
- ({formatDCAmount(dcBalance)} available)
79
+ (<MMORPGNumber value={dcBalance} /> available)
80
80
  </BalanceHint>
81
81
  </OptionSub>
82
82
  </OptionText>
@@ -20,6 +20,7 @@ import { CTAButton } from '../shared/CTAButton/CTAButton';
20
20
  import { Ellipsis } from '../shared/Ellipsis';
21
21
  import { SimpleTooltip } from '../shared/SimpleTooltip';
22
22
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
23
+ import { MMORPGNumber } from '../../components/shared/MMORPGNumber';
23
24
 
24
25
  export interface IMarketPlaceRowsPropos {
25
26
  atlasJSON: any;
@@ -34,6 +35,8 @@ export interface IMarketPlaceRowsPropos {
34
35
  onMarketPlaceItemRemove?: () => void;
35
36
  onDCCoinClick?: () => void;
36
37
  disabled?: boolean;
38
+ /** If true, adds padding-right to details to avoid overlapping with grouped badges */
39
+ $isGrouped?: boolean;
37
40
  }
38
41
 
39
42
  export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
@@ -49,6 +52,7 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
49
52
  onMarketPlaceItemRemove,
50
53
  onDCCoinClick,
51
54
  disabled,
55
+ $isGrouped,
52
56
  }) => {
53
57
  const renderGems = (item: IItem) => {
54
58
  return item.attachedGems && onRenderGems(item);
@@ -91,7 +95,7 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
91
95
  </ItemInfoWrapper>
92
96
  </SpriteContainer>
93
97
 
94
- <ItemDetails>
98
+ <ItemDetails $isGrouped={$isGrouped}>
95
99
  <ItemName>
96
100
  <Ellipsis maxLines={1} maxWidth="200px" fontSize="10px">
97
101
  {item.name}
@@ -110,7 +114,7 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
110
114
  />
111
115
  </SimpleTooltip>
112
116
  </GoldIcon>
113
- <GoldPrice>{itemPrice}</GoldPrice>
117
+ <GoldPrice><MMORPGNumber value={itemPrice} /></GoldPrice>
114
118
  </GoldPriceRow>
115
119
  )}
116
120
  {dcEquivalentPrice !== undefined && (
@@ -174,7 +178,7 @@ export const GroupedMarketplaceRow: React.FC<IGroupedMarketplaceRowProps> = ({
174
178
  onBuy,
175
179
  onDCCoinClick,
176
180
  }) => {
177
- const makeRow = (listing: IMarketplaceItem) => {
181
+ const makeRow = (listing: IMarketplaceItem, isMain: boolean = false) => {
178
182
  const listingCurrency = listing.acceptedCurrency || MarketplaceAcceptedCurrency.GoldOrDc;
179
183
  const isDcOnly = listingCurrency === MarketplaceAcceptedCurrency.Dc;
180
184
  const showDcPrice = isDcOnly || (dcToGoldSwapRate > 0 && listingCurrency !== MarketplaceAcceptedCurrency.Gold);
@@ -192,16 +196,17 @@ export const GroupedMarketplaceRow: React.FC<IGroupedMarketplaceRowProps> = ({
192
196
  onMarketPlaceItemBuy={() => onBuy(listing._id)}
193
197
  onDCCoinClick={isDcOnly ? undefined : onDCCoinClick}
194
198
  disabled={listing.owner === characterId}
199
+ $isGrouped={isMain}
195
200
  />
196
201
  );
197
202
  };
198
203
 
199
204
  return (
200
205
  <GroupedRowContainer
201
- mainRow={makeRow(bestListing)}
202
- subRows={otherListings.map(makeRow)}
206
+ mainRow={makeRow(bestListing, true)}
207
+ subRows={otherListings.map(l => makeRow(l, false))}
203
208
  badgeLabel="offers"
204
- metaRightInset={132}
209
+ metaRightInset={180}
205
210
  />
206
211
  );
207
212
  };
@@ -221,12 +226,13 @@ const SpriteContainer = styled.div`
221
226
  min-width: 44px; /* Ensure wide stack quantities don't overlap ItemDetails */
222
227
  `;
223
228
 
224
- const ItemDetails = styled.div`
229
+ const ItemDetails = styled.div<{ $isGrouped?: boolean }>`
225
230
  display: flex;
226
231
  flex-direction: column;
227
232
  gap: 0.2rem;
228
233
  min-width: 0;
229
234
  margin-left: 1rem;
235
+ padding-right: ${({ $isGrouped }) => ($isGrouped ? '280px' : '0')};
230
236
  `;
231
237
 
232
238
  const ItemName = styled.div`