@rpg-engine/long-bow 0.8.146 → 0.8.148

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.146",
3
+ "version": "0.8.148",
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": "file:../rpg-shared",
87
+ "@rpg-engine/shared": "^0.10.90",
88
88
  "dayjs": "^1.11.2",
89
89
  "font-awesome": "^4.7.0",
90
90
  "fs-extra": "^10.1.0",
@@ -3,7 +3,8 @@ import {
3
3
  IMarketplaceBlueprintSummary,
4
4
  ItemSubType,
5
5
  } from '@rpg-engine/shared';
6
- import React, { useState } from 'react';
6
+ import { debounce } from 'lodash';
7
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
8
  import { FaTimes } from 'react-icons/fa';
8
9
  import styled, { keyframes } from 'styled-components';
9
10
  import { Dropdown } from '../Dropdown';
@@ -24,6 +25,8 @@ export interface IBlueprintSearchModalProps {
24
25
  isLoading: boolean;
25
26
  atlasJSON: any;
26
27
  atlasIMG: any;
28
+ enableHotkeys?: () => void;
29
+ disableHotkeys?: () => void;
27
30
  }
28
31
 
29
32
  const BLUEPRINTS_PER_PAGE = 10;
@@ -58,27 +61,49 @@ export const BlueprintSearchModal: React.FC<IBlueprintSearchModalProps> = ({
58
61
  isLoading,
59
62
  atlasJSON,
60
63
  atlasIMG,
64
+ enableHotkeys,
65
+ disableHotkeys,
61
66
  }) => {
62
67
  const [searchName, setSearchName] = useState('');
63
68
  const [selectedType, setSelectedType] = useState('');
64
69
  const [selectedSubType, setSelectedSubType] = useState('');
65
70
 
66
- if (!isOpen) return null;
71
+ const searchNameRef = useRef(searchName);
72
+ const selectedTypeRef = useRef(selectedType);
73
+ const selectedSubTypeRef = useRef(selectedSubType);
74
+
75
+ searchNameRef.current = searchName;
76
+ selectedTypeRef.current = selectedType;
77
+ selectedSubTypeRef.current = selectedSubType;
78
+
79
+ const triggerSearch = useCallback(
80
+ (overrides?: Partial<{ name: string; itemType: string; itemSubType: string; page: number }>): void => {
81
+ onSearch({
82
+ npcId: '',
83
+ name: overrides?.name ?? searchNameRef.current,
84
+ itemType: overrides?.itemType ?? selectedTypeRef.current,
85
+ itemSubType: overrides?.itemSubType ?? selectedSubTypeRef.current,
86
+ page: overrides?.page ?? 1,
87
+ limit: BLUEPRINTS_PER_PAGE,
88
+ });
89
+ },
90
+ [onSearch]
91
+ );
67
92
 
68
- const triggerSearch = (overrides?: Partial<{ name: string; itemType: string; itemSubType: string; page: number }>): void => {
69
- onSearch({
70
- npcId: '',
71
- name: overrides?.name ?? searchName,
72
- itemType: overrides?.itemType ?? selectedType,
73
- itemSubType: overrides?.itemSubType ?? selectedSubType,
74
- page: overrides?.page ?? 1,
75
- limit: BLUEPRINTS_PER_PAGE,
76
- });
77
- };
93
+ const debouncedNameSearch = useMemo(
94
+ () => debounce((name: string) => triggerSearch({ name, page: 1 }), 300),
95
+ [triggerSearch]
96
+ );
97
+
98
+ useEffect(() => {
99
+ return () => { debouncedNameSearch.cancel(); };
100
+ }, [debouncedNameSearch]);
101
+
102
+ if (!isOpen) return null;
78
103
 
79
104
  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
80
105
  setSearchName(e.target.value);
81
- triggerSearch({ name: e.target.value, page: 1 });
106
+ debouncedNameSearch(e.target.value);
82
107
  };
83
108
 
84
109
  const handleTypeChange = (value: string): void => {
@@ -126,6 +151,8 @@ export const BlueprintSearchModal: React.FC<IBlueprintSearchModalProps> = ({
126
151
  value={searchName}
127
152
  onChange={handleNameChange}
128
153
  placeholder="Search by name..."
154
+ onFocus={disableHotkeys}
155
+ onBlur={enableHotkeys}
129
156
  />
130
157
  </InputWrapper>
131
158
 
@@ -145,17 +172,22 @@ export const BlueprintSearchModal: React.FC<IBlueprintSearchModalProps> = ({
145
172
  </FiltersRow>
146
173
 
147
174
  <ResultsWrapper>
148
- {isLoading ? (
149
- <EmptyState>Loading...</EmptyState>
150
- ) : blueprints.length === 0 ? (
151
- <EmptyState>No blueprints found</EmptyState>
175
+ {blueprints.length === 0 && !isLoading ? (
176
+ <EmptyState>No items found</EmptyState>
152
177
  ) : (
153
- <BlueprintTable
154
- blueprints={blueprints}
155
- atlasJSON={atlasJSON}
156
- atlasIMG={atlasIMG}
157
- onSelect={onSelect}
158
- />
178
+ <ResultsContent $dimmed={isLoading}>
179
+ <BlueprintTable
180
+ blueprints={blueprints}
181
+ atlasJSON={atlasJSON}
182
+ atlasIMG={atlasIMG}
183
+ onSelect={onSelect}
184
+ />
185
+ </ResultsContent>
186
+ )}
187
+ {isLoading && (
188
+ <LoadingOverlay>
189
+ <LoadingText>Loading...</LoadingText>
190
+ </LoadingOverlay>
159
191
  )}
160
192
  </ResultsWrapper>
161
193
 
@@ -268,18 +300,40 @@ const StyledDropdown = styled(Dropdown)`
268
300
  `;
269
301
 
270
302
  const ResultsWrapper = styled.div`
303
+ position: relative;
271
304
  overflow-y: auto;
272
- max-height: 320px;
305
+ height: 320px;
273
306
  background: rgba(0, 0, 0, 0.2);
274
307
  border: 1px solid rgba(255, 255, 255, 0.05);
275
308
  border-radius: 4px;
276
309
  `;
277
310
 
311
+ const ResultsContent = styled.div<{ $dimmed?: boolean }>`
312
+ opacity: ${(p) => (p.$dimmed ? 0.4 : 1)};
313
+ transition: opacity 0.15s ease;
314
+ `;
315
+
316
+ const LoadingOverlay = styled.div`
317
+ position: absolute;
318
+ inset: 0;
319
+ display: flex;
320
+ align-items: center;
321
+ justify-content: center;
322
+ pointer-events: none;
323
+ `;
324
+
325
+ const LoadingText = styled.span`
326
+ font-size: 0.55rem;
327
+ color: #f59e0b;
328
+ text-transform: uppercase;
329
+ letter-spacing: 1px;
330
+ `;
331
+
278
332
  const EmptyState = styled.div`
279
333
  display: flex;
280
334
  align-items: center;
281
335
  justify-content: center;
282
- height: 100px;
336
+ height: 100%;
283
337
  font-size: 0.55rem;
284
338
  color: #666;
285
339
  text-transform: uppercase;
@@ -25,6 +25,8 @@ export interface IBuyOrderDetailsModalProps {
25
25
  onConfirm: () => void;
26
26
  atlasJSON: any;
27
27
  atlasIMG: any;
28
+ enableHotkeys?: () => void;
29
+ disableHotkeys?: () => void;
28
30
  }
29
31
 
30
32
  const scaleIn = keyframes`
@@ -53,6 +55,8 @@ export const BuyOrderDetailsModal: React.FC<IBuyOrderDetailsModalProps> = ({
53
55
  onConfirm,
54
56
  atlasJSON,
55
57
  atlasIMG,
58
+ enableHotkeys,
59
+ disableHotkeys,
56
60
  }) => {
57
61
  if (!isOpen || !blueprint) return null;
58
62
 
@@ -107,6 +111,8 @@ export const BuyOrderDetailsModal: React.FC<IBuyOrderDetailsModalProps> = ({
107
111
  placeholder="Qty"
108
112
  type="number"
109
113
  min={1}
114
+ onFocus={disableHotkeys}
115
+ onBlur={enableHotkeys}
110
116
  />
111
117
  </FieldRow>
112
118
 
@@ -118,6 +124,8 @@ export const BuyOrderDetailsModal: React.FC<IBuyOrderDetailsModalProps> = ({
118
124
  placeholder="Max gold"
119
125
  type="number"
120
126
  min={1}
127
+ onFocus={disableHotkeys}
128
+ onBlur={enableHotkeys}
121
129
  />
122
130
  </FieldRow>
123
131
 
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  IMarketplaceBlueprintSummary,
3
3
  IMarketplaceBuyOrderItem,
4
+ MarketplaceBuyOrderStatus,
4
5
  } from '@rpg-engine/shared';
5
6
  import { Search } from 'pixelarticons/react/Search';
6
7
  import React, { useEffect, useState } from 'react';
7
8
  import styled from 'styled-components';
8
9
  import { Pager } from '../Pager';
9
10
  import { CTAButton } from '../shared/CTAButton/CTAButton';
11
+ import { SegmentedToggle } from '../shared/SegmentedToggle';
10
12
  import { rarityColor } from '../Item/Inventory/ItemSlotRarity';
11
13
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
12
14
  import { BuyOrderDetailsModal } from './BuyOrderDetailsModal';
@@ -31,6 +33,8 @@ export interface IBuyOrderPanelProps {
31
33
  yourBuyOrdersPage: number;
32
34
  onYourBuyOrdersPageChange: (page: number) => void;
33
35
  onCancelBuyOrder: (buyOrderId: string) => void;
36
+ enableHotkeys?: () => void;
37
+ disableHotkeys?: () => void;
34
38
  }
35
39
 
36
40
  const BUY_ORDERS_PER_PAGE = 5;
@@ -54,6 +58,8 @@ export const BuyOrderPanel: React.FC<IBuyOrderPanelProps> = (props) => {
54
58
  yourBuyOrdersPage,
55
59
  onYourBuyOrdersPageChange,
56
60
  onCancelBuyOrder,
61
+ enableHotkeys,
62
+ disableHotkeys,
57
63
  } = props;
58
64
 
59
65
  // Local blueprint display: cleared immediately on Place Request so the
@@ -61,6 +67,7 @@ export const BuyOrderPanel: React.FC<IBuyOrderPanelProps> = (props) => {
61
67
  // the prop. Cancel keeps it shown so the user can reopen the modal.
62
68
  const [displayedBlueprint, setDisplayedBlueprint] = useState(selectedBlueprint);
63
69
  const [isDetailsOpen, setIsDetailsOpen] = useState(!!selectedBlueprint);
70
+ const [statusFilter, setStatusFilter] = useState<'Active' | 'Fulfilled' | 'Expired' | 'All'>('Active');
64
71
 
65
72
  // Sync when consumer provides a new blueprint (e.g. after search selection)
66
73
  useEffect(() => {
@@ -76,11 +83,13 @@ export const BuyOrderPanel: React.FC<IBuyOrderPanelProps> = (props) => {
76
83
  setDisplayedBlueprint(undefined);
77
84
  setIsDetailsOpen(false);
78
85
  onCloseDetails?.();
86
+ enableHotkeys?.();
79
87
  };
80
88
 
81
89
  // Cancel: just close the modal, keep blueprint displayed for reopening
82
90
  const handleCloseDetails = () => {
83
91
  setIsDetailsOpen(false);
92
+ enableHotkeys?.();
84
93
  };
85
94
 
86
95
  return (
@@ -100,6 +109,8 @@ export const BuyOrderPanel: React.FC<IBuyOrderPanelProps> = (props) => {
100
109
  onConfirm={handleConfirm}
101
110
  atlasJSON={atlasJSON}
102
111
  atlasIMG={atlasIMG}
112
+ enableHotkeys={enableHotkeys}
113
+ disableHotkeys={disableHotkeys}
103
114
  />
104
115
  )}
105
116
 
@@ -134,23 +145,40 @@ export const BuyOrderPanel: React.FC<IBuyOrderPanelProps> = (props) => {
134
145
  </FormRow>
135
146
 
136
147
  {/* ── Scrollable lists ──────────────────────────────── */}
148
+ <FilterRow>
149
+ <SectionTitle>Your Buy Requests</SectionTitle>
150
+ <SegmentedToggle
151
+ options={[
152
+ { id: 'Active', label: 'Active' },
153
+ { id: 'Fulfilled', label: 'Fulfilled' },
154
+ { id: 'Expired', label: 'Expired' },
155
+ { id: 'All', label: 'All' },
156
+ ]}
157
+ activeId={statusFilter}
158
+ onChange={(id) => setStatusFilter(id as typeof statusFilter)}
159
+ />
160
+ </FilterRow>
137
161
  <ScrollArea id="MarketContainer">
138
162
  <Section>
139
- <SectionTitle>Your Buy Requests</SectionTitle>
140
- {yourBuyOrders.length === 0 ? (
141
- <EmptyState>No requests yet</EmptyState>
142
- ) : (
143
- yourBuyOrders.map((order) => (
144
- <BuyOrderRow
145
- key={order._id}
146
- buyOrder={order}
147
- atlasJSON={atlasJSON}
148
- atlasIMG={atlasIMG}
149
- isOwn
150
- onCancel={onCancelBuyOrder}
151
- />
152
- ))
153
- )}
163
+ {(() => {
164
+ const filtered = statusFilter === 'All'
165
+ ? yourBuyOrders
166
+ : yourBuyOrders.filter(o => o.status === MarketplaceBuyOrderStatus[statusFilter as keyof typeof MarketplaceBuyOrderStatus]);
167
+ return filtered.length === 0 ? (
168
+ <EmptyState>No requests yet</EmptyState>
169
+ ) : (
170
+ filtered.map((order) => (
171
+ <BuyOrderRow
172
+ key={order._id}
173
+ buyOrder={order}
174
+ atlasJSON={atlasJSON}
175
+ atlasIMG={atlasIMG}
176
+ isOwn
177
+ onCancel={onCancelBuyOrder}
178
+ />
179
+ ))
180
+ );
181
+ })()}
154
182
  {yourBuyOrdersTotal > BUY_ORDERS_PER_PAGE && (
155
183
  <PagerRow>
156
184
  <Pager
@@ -188,6 +216,13 @@ const FormRow = styled.div`
188
216
  overflow: visible;
189
217
  `;
190
218
 
219
+ const FilterRow = styled.div`
220
+ display: flex;
221
+ align-items: center;
222
+ justify-content: space-between;
223
+ padding: 0 4px;
224
+ `;
225
+
191
226
  const ScrollArea = styled.div`
192
227
  display: flex;
193
228
  flex-direction: column;
@@ -160,7 +160,7 @@ export const HistoryPanel: React.FC<IHistoryPanelProps> = ({
160
160
 
161
161
  {tx.goldAmount > 0 && (
162
162
  <PriceSection>
163
- {tx.currency === 'dc' ? (
163
+ {tx.metadata?.['currency'] === 'dc' ? (
164
164
  // Show DC only
165
165
  <DCPriceRow>
166
166
  {atlasIMG && atlasJSON && (
@@ -164,6 +164,7 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
164
164
 
165
165
  const handleBlueprintSelect = (blueprint: IMarketplaceBlueprintSummary) => {
166
166
  setIsBlueprintSearchOpen(false);
167
+ props.enableHotkeys?.();
167
168
  onBlueprintSelect?.(blueprint);
168
169
  };
169
170
 
@@ -239,10 +240,12 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
239
240
  yourBuyOrdersPage={yourBuyOrdersPage}
240
241
  onYourBuyOrdersPageChange={onYourBuyOrdersPageChange ?? (() => {})}
241
242
  onCancelBuyOrder={onCancelBuyOrder ?? (() => {})}
243
+ enableHotkeys={props.enableHotkeys}
244
+ disableHotkeys={props.disableHotkeys}
242
245
  />
243
246
  <BlueprintSearchModal
244
247
  isOpen={isBlueprintSearchOpen}
245
- onClose={() => setIsBlueprintSearchOpen(false)}
248
+ onClose={() => { setIsBlueprintSearchOpen(false); props.enableHotkeys?.(); }}
246
249
  onSelect={handleBlueprintSelect}
247
250
  onSearch={onBlueprintSearch ?? (() => {})}
248
251
  blueprints={blueprintSearchResults}
@@ -251,6 +254,8 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
251
254
  isLoading={blueprintSearchIsLoading}
252
255
  atlasJSON={props.atlasJSON}
253
256
  atlasIMG={props.atlasIMG}
257
+ enableHotkeys={props.enableHotkeys}
258
+ disableHotkeys={props.disableHotkeys}
254
259
  />
255
260
  </>
256
261
  )}
@@ -41,7 +41,7 @@ const mockTransactions: IMarketplaceTransaction[] = [
41
41
  itemKey: 'items/iron-sword.png',
42
42
  itemName: 'Iron Sword',
43
43
  counterpartName: 'SomePlayer',
44
- currency: 'gold',
44
+ metadata: { currency: 'gold' },
45
45
  createdAt: daysAgo(0),
46
46
  updatedAt: daysAgo(0),
47
47
  },
@@ -52,7 +52,7 @@ const mockTransactions: IMarketplaceTransaction[] = [
52
52
  itemKey: 'items/leather-armor.png',
53
53
  itemName: 'Leather Armor',
54
54
  counterpartName: 'AnotherPlayer',
55
- currency: 'dc', // DC sale
55
+ metadata: { currency: 'dc' },
56
56
  createdAt: daysAgo(1),
57
57
  updatedAt: daysAgo(1),
58
58
  },
@@ -63,7 +63,7 @@ const mockTransactions: IMarketplaceTransaction[] = [
63
63
  itemKey: 'items/angelic-sword.png',
64
64
  itemName: 'Angelic Sword',
65
65
  counterpartName: 'DCBuyer',
66
- currency: 'dc', // DC purchase
66
+ metadata: { currency: 'dc' },
67
67
  createdAt: daysAgo(2),
68
68
  updatedAt: daysAgo(2),
69
69
  },
@@ -49,9 +49,9 @@ const mockOpenBuyOrders: IMarketplaceBuyOrderItem[] = [
49
49
  ];
50
50
 
51
51
  const mockTransactions: IMarketplaceTransaction[] = [
52
- { owner: 'player-1', type: MarketplaceTransactionType.Purchase, goldAmount: 450, itemKey: 'items/broad-sword', itemName: 'Broad Sword', counterpartName: 'MerchantKing', currency: 'gold', createdAt: daysAgo(1), updatedAt: daysAgo(1) },
53
- { owner: 'player-1', type: MarketplaceTransactionType.Sale, goldAmount: 1200, itemKey: 'items/angelic-sword', itemName: 'Angelic Sword', counterpartName: 'ShadowBlade', currency: 'dc', createdAt: daysAgo(2), updatedAt: daysAgo(2) },
54
- { owner: 'player-1', type: MarketplaceTransactionType.Purchase, goldAmount: 275000, itemKey: 'items/angelic-sword', itemName: 'Angelic Sword', counterpartName: 'DCBuyer', currency: 'dc', createdAt: daysAgo(3), updatedAt: daysAgo(3) },
52
+ { owner: 'player-1', type: MarketplaceTransactionType.Purchase, goldAmount: 450, itemKey: 'items/broad-sword', itemName: 'Broad Sword', counterpartName: 'MerchantKing', metadata: { currency: 'gold' }, createdAt: daysAgo(1), updatedAt: daysAgo(1) },
53
+ { owner: 'player-1', type: MarketplaceTransactionType.Sale, goldAmount: 1200, itemKey: 'items/angelic-sword', itemName: 'Angelic Sword', counterpartName: 'ShadowBlade', metadata: { currency: 'dc' }, createdAt: daysAgo(2), updatedAt: daysAgo(2) },
54
+ { owner: 'player-1', type: MarketplaceTransactionType.Purchase, goldAmount: 275000, itemKey: 'items/angelic-sword', itemName: 'Angelic Sword', counterpartName: 'DCBuyer', metadata: { currency: 'dc' }, createdAt: daysAgo(3), updatedAt: daysAgo(3) },
55
55
  { owner: 'player-1', type: MarketplaceTransactionType.ListingFee, goldAmount: 60, itemKey: 'items/angelic-sword', itemName: 'Angelic Sword', createdAt: daysAgo(2), updatedAt: daysAgo(2) },
56
56
  { owner: 'player-1', type: MarketplaceTransactionType.Withdrawal, goldAmount: 3200, createdAt: daysAgo(4), updatedAt: daysAgo(4) },
57
57
  { owner: 'player-1', type: MarketplaceTransactionType.Expired, goldAmount: 0, itemKey: 'items/leather-armor', itemName: 'Leather Armor', createdAt: daysAgo(7), updatedAt: daysAgo(7) },