@rpg-engine/long-bow 0.8.206 → 0.8.208

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.206",
3
+ "version": "0.8.208",
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.109",
87
+ "@rpg-engine/shared": "^0.10.115",
88
88
  "dayjs": "^1.11.2",
89
89
  "font-awesome": "^4.7.0",
90
90
  "fs-extra": "^10.1.0",
@@ -5,6 +5,7 @@ import { FaTimes } from 'react-icons/fa';
5
5
  import styled, { keyframes } from 'styled-components';
6
6
  import ModalPortal from '../Abstractions/ModalPortal';
7
7
  import { ConfirmModal } from '../ConfirmModal';
8
+ import { gemColors } from '../Item/Inventory/ItemGem';
8
9
  import { CTAButton } from '../shared/CTAButton/CTAButton';
9
10
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
10
11
 
@@ -34,6 +35,12 @@ const RARITY_COLORS: Record<string, string> = {
34
35
  const rarityColor = (rarity?: string) =>
35
36
  RARITY_COLORS[(rarity ?? '').toLowerCase()] ?? RARITY_COLORS.common;
36
37
 
38
+ const rarityGlowColor = (rarity?: string): string | null => {
39
+ const key = (rarity ?? '').toLowerCase();
40
+ if (!key || key === 'common') return null;
41
+ return RARITY_COLORS[key] ?? null;
42
+ };
43
+
37
44
  const formatEquipmentSlot = (slot?: string) => {
38
45
  if (!slot) return 'Unknown';
39
46
 
@@ -86,9 +93,9 @@ export const CharacterDetailModal: React.FC<ICharacterDetailModalProps> = ({
86
93
  if (!isOpen || !listing) return null;
87
94
 
88
95
  const snap = listing.characterSnapshot;
89
- const topSkills = Object.entries(snap.skills ?? {})
90
- .sort(([, a], [, b]) => b - a)
91
- .slice(0, 8);
96
+ const topSkills = Object.entries(snap.skills ?? {}).sort(
97
+ ([, a], [, b]) => b - a
98
+ );
92
99
 
93
100
  return (
94
101
  <ModalPortal>
@@ -121,83 +128,101 @@ export const CharacterDetailModal: React.FC<ICharacterDetailModalProps> = ({
121
128
  </CloseButton>
122
129
  </Header>
123
130
 
124
- <HeroSection>
125
- <SpriteContainer>
126
- <SpriteFromAtlas
127
- atlasIMG={characterAtlasIMG}
128
- atlasJSON={characterAtlasJSON}
129
- spriteKey={`${snap.textureKey}/down/standing/0.png`}
130
- imgScale={4}
131
- height={96}
132
- width={96}
133
- centered
134
- />
135
- </SpriteContainer>
136
- <HeroInfo>
137
- <CharacterName>{snap.name || 'Unknown'}</CharacterName>
138
- <CharacterClass>
139
- Lv.{snap.level} · {snap.class}
140
- </CharacterClass>
141
- <CharacterOrigin>
142
- {snap.race} · {snap.faction}
143
- </CharacterOrigin>
144
- <ModeBadge $hardcore={snap.mode?.toLowerCase() === 'hardcore'}>
145
- {snap.mode || 'Standard'}
146
- </ModeBadge>
147
- </HeroInfo>
148
- </HeroSection>
149
-
150
- <Divider />
151
-
152
- <MetaColumns>
153
- {topSkills.length > 0 && (
154
- <Section>
155
- <SectionTitle>Skills</SectionTitle>
156
- <SkillsList>
157
- {topSkills.map(([name, value]) => (
158
- <SkillRow key={name}>
159
- <SkillName>{name}</SkillName>
160
- <SkillValue>{value}</SkillValue>
161
- </SkillRow>
162
- ))}
163
- </SkillsList>
164
- </Section>
165
- )}
166
-
167
- {snap.equipment?.length > 0 && (
168
- <Section>
169
- <SectionTitle>Equipment</SectionTitle>
170
- <EquipmentList>
171
- {snap.equipment.map((eq, i) => (
172
- <EquipmentRow key={i}>
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>
193
- </EquipmentRow>
194
- ))}
195
- </EquipmentList>
196
- </Section>
197
- )}
198
- </MetaColumns>
199
-
200
- <Divider />
131
+ <ScrollableBody>
132
+ <HeroSection>
133
+ <SpriteContainer>
134
+ <SpriteFromAtlas
135
+ atlasIMG={characterAtlasIMG}
136
+ atlasJSON={characterAtlasJSON}
137
+ spriteKey={`${snap.textureKey}/down/standing/0.png`}
138
+ imgScale={4}
139
+ height={96}
140
+ width={96}
141
+ centered
142
+ />
143
+ </SpriteContainer>
144
+ <HeroInfo>
145
+ <CharacterName>{snap.name || 'Unknown'}</CharacterName>
146
+ <CharacterClass>
147
+ Lv.{snap.level} · {snap.class}
148
+ </CharacterClass>
149
+ <CharacterOrigin>
150
+ {snap.race} · {snap.faction}
151
+ </CharacterOrigin>
152
+ <ModeBadge $hardcore={snap.mode?.toLowerCase() === 'hardcore'}>
153
+ {snap.mode || 'Standard'}
154
+ </ModeBadge>
155
+ {snap.gold != null && snap.gold > 0 && (
156
+ <GoldRow>
157
+ <GoldLabel>GOLD</GoldLabel>
158
+ <GoldAmount>{snap.gold.toLocaleString()}</GoldAmount>
159
+ </GoldRow>
160
+ )}
161
+ </HeroInfo>
162
+ </HeroSection>
163
+
164
+ <Divider />
165
+
166
+ <MetaColumns>
167
+ {topSkills.length > 0 && (
168
+ <Section>
169
+ <SectionTitle>Skills</SectionTitle>
170
+ <SkillsList>
171
+ {topSkills.map(([name, value]) => (
172
+ <SkillRow key={name}>
173
+ <SkillName>{name}</SkillName>
174
+ <SkillValue>{value}</SkillValue>
175
+ </SkillRow>
176
+ ))}
177
+ </SkillsList>
178
+ </Section>
179
+ )}
180
+
181
+ {snap.equipment?.length > 0 && (
182
+ <Section>
183
+ <SectionTitle>Equipment</SectionTitle>
184
+ <EquipmentList>
185
+ {snap.equipment.map((eq, i) => (
186
+ <EquipmentRow key={i}>
187
+ <EquipmentSprite $rarity={eq.rarity}>
188
+ <SpriteFromAtlas
189
+ atlasIMG={atlasIMG}
190
+ atlasJSON={atlasJSON}
191
+ spriteKey={eq.itemKey}
192
+ imgScale={2}
193
+ width={32}
194
+ height={32}
195
+ centered
196
+ />
197
+ {eq.attachedGems && eq.attachedGems.length > 0 && (
198
+ <GemRow>
199
+ {eq.attachedGems.map((gem, gi) => (
200
+ <GemDot
201
+ key={gi}
202
+ $color={gemColors[gem.key] ?? '#fff'}
203
+ />
204
+ ))}
205
+ </GemRow>
206
+ )}
207
+ </EquipmentSprite>
208
+ <EquipMeta>
209
+ <EquipName>{eq.itemName}</EquipName>
210
+ <EquipDetails>
211
+ <EquipSlot>{formatEquipmentSlot(eq.slot)}</EquipSlot>
212
+ <RarityBadge $rarity={eq.rarity}>
213
+ {eq.rarity || 'Common'}
214
+ </RarityBadge>
215
+ </EquipDetails>
216
+ </EquipMeta>
217
+ </EquipmentRow>
218
+ ))}
219
+ </EquipmentList>
220
+ </Section>
221
+ )}
222
+ </MetaColumns>
223
+ </ScrollableBody>
224
+
225
+ <FooterDivider />
201
226
 
202
227
  <Footer>
203
228
  <SellerInfo>Listed by {listing.listedByCharacterName}</SellerInfo>
@@ -257,29 +282,16 @@ const ModalContent = styled.div`
257
282
  background: #1a1a2e;
258
283
  border: 2px solid #f59e0b;
259
284
  border-radius: 8px;
260
- padding: 20px 24px;
285
+ padding: 20px 24px 16px;
261
286
  width: 580px;
262
287
  max-width: 96%;
263
288
  max-height: 85dvh;
264
289
  display: flex;
265
290
  flex-direction: column;
266
291
  gap: 14px;
267
- overflow-y: auto;
268
- overflow-x: hidden;
292
+ overflow: hidden;
269
293
  pointer-events: auto;
270
294
  animation: ${scaleIn} 0.15s ease-out;
271
-
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
- }
283
295
  `;
284
296
 
285
297
  const Header = styled.div`
@@ -377,6 +389,30 @@ const Divider = styled.hr`
377
389
  flex-shrink: 0;
378
390
  `;
379
391
 
392
+ const FooterDivider = styled(Divider)``;
393
+
394
+ const ScrollableBody = styled.div`
395
+ flex: 1;
396
+ overflow-y: auto;
397
+ overflow-x: hidden;
398
+ display: flex;
399
+ flex-direction: column;
400
+ gap: 14px;
401
+ min-height: 0;
402
+
403
+ &::-webkit-scrollbar {
404
+ width: 6px;
405
+ }
406
+ &::-webkit-scrollbar-track {
407
+ background: rgba(0, 0, 0, 0.2);
408
+ border-radius: 4px;
409
+ }
410
+ &::-webkit-scrollbar-thumb {
411
+ background: rgba(245, 158, 11, 0.3);
412
+ border-radius: 4px;
413
+ }
414
+ `;
415
+
380
416
  const Section = styled.div`
381
417
  display: flex;
382
418
  flex-direction: column;
@@ -445,13 +481,66 @@ const EquipmentRow = styled.div`
445
481
  min-width: 0;
446
482
  `;
447
483
 
448
- const EquipmentSprite = styled.div`
484
+ const EquipmentSprite = styled.div<{ $rarity?: string }>`
485
+ position: relative;
449
486
  display: flex;
450
487
  align-items: center;
451
488
  justify-content: center;
452
489
  width: 32px;
453
490
  height: 32px;
454
491
  flex-shrink: 0;
492
+ border-radius: 3px;
493
+ ${({ $rarity }) => {
494
+ const color = rarityGlowColor($rarity);
495
+ return color
496
+ ? `box-shadow: 0 0 4px 3px ${color} inset, 0 0 6px 2px ${color};`
497
+ : '';
498
+ }}
499
+ `;
500
+
501
+ const GemRow = styled.div`
502
+ position: absolute;
503
+ bottom: -1px;
504
+ left: 0;
505
+ display: flex;
506
+ gap: 1px;
507
+ pointer-events: none;
508
+ `;
509
+
510
+ const GemDot = styled.div<{ $color: string }>`
511
+ width: 5px;
512
+ height: 5px;
513
+ border-radius: 1px;
514
+ transform: rotate(45deg);
515
+ background: radial-gradient(
516
+ circle at 30% 30%,
517
+ rgba(255, 255, 255, 0.8),
518
+ transparent 40%
519
+ ),
520
+ linear-gradient(45deg, ${({ $color }) => $color}, rgba(255, 255, 255, 0.2));
521
+ border: 1px solid rgba(0, 0, 0, 0.6);
522
+ box-shadow: 0 0 3px ${({ $color }) => $color};
523
+ `;
524
+
525
+ const GoldRow = styled.div`
526
+ display: flex;
527
+ align-items: center;
528
+ gap: 4px;
529
+ margin-top: 2px;
530
+ `;
531
+
532
+ const GoldLabel = styled.span`
533
+ font-family: 'Press Start 2P', cursive !important;
534
+ font-size: 0.35rem !important;
535
+ color: #6b7280 !important;
536
+ text-transform: uppercase;
537
+ letter-spacing: 0.5px;
538
+ `;
539
+
540
+ const GoldAmount = styled.span`
541
+ font-family: 'Press Start 2P', cursive !important;
542
+ font-size: 0.38rem !important;
543
+ color: #fde68a !important;
455
544
  `;
456
545
 
457
546
  const EquipMeta = styled.div`
@@ -15,6 +15,8 @@ export interface ICharacterListingModalProps {
15
15
  isOpen: boolean;
16
16
  onClose: () => void;
17
17
  accountCharacters: ICharacter[];
18
+ /** ID of the currently active/playing character — cannot be listed */
19
+ activeCharacterId?: string;
18
20
  /** Items atlas — for UI sprites like the DC coin */
19
21
  atlasJSON: any;
20
22
  atlasIMG: any;
@@ -30,6 +32,7 @@ export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
30
32
  isOpen,
31
33
  onClose,
32
34
  accountCharacters,
35
+ activeCharacterId,
33
36
  atlasJSON,
34
37
  atlasIMG,
35
38
  characterAtlasJSON,
@@ -55,7 +58,9 @@ export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
55
58
  c => !c.isListedForSale && !c.tradedAt
56
59
  );
57
60
 
58
- const canList = !!selectedId && Number(price) > 0;
61
+ const isActiveCharacter = (c: ICharacter) => !!activeCharacterId && c._id === activeCharacterId;
62
+
63
+ const canList = !!selectedId && Number(price) > 0 && selectedId !== activeCharacterId;
59
64
 
60
65
  const handleClose = () => {
61
66
  setSelectedId(null);
@@ -110,22 +115,25 @@ export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
110
115
  ) : (
111
116
  eligibleCharacters.map(character => {
112
117
  const isSelected = selectedId === character._id;
118
+ const isActive = isActiveCharacter(character);
113
119
  return (
114
120
  <CharacterRow
115
121
  key={character._id}
116
- $selected={isSelected}
117
- onPointerDown={() => setSelectedId(character._id!)}
122
+ $selected={isSelected && !isActive}
123
+ $disabled={isActive}
124
+ onPointerDown={() => !isActive && setSelectedId(character._id!)}
118
125
  role="radio"
119
- aria-checked={isSelected}
120
- tabIndex={0}
126
+ aria-checked={isSelected && !isActive}
127
+ aria-disabled={isActive}
128
+ tabIndex={isActive ? -1 : 0}
121
129
  onKeyDown={e => {
122
- if (e.key === 'Enter' || e.key === ' ') {
130
+ if (!isActive && (e.key === 'Enter' || e.key === ' ')) {
123
131
  e.preventDefault();
124
132
  setSelectedId(character._id!);
125
133
  }
126
134
  }}
127
135
  >
128
- <RadioCircle $selected={isSelected} />
136
+ <RadioCircle $selected={isSelected && !isActive} $disabled={isActive} />
129
137
  <SpriteWrapper>
130
138
  <SpriteFromAtlas
131
139
  atlasIMG={characterAtlasIMG}
@@ -140,6 +148,7 @@ export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
140
148
  <CharacterName>{character.name || 'Unknown'}</CharacterName>
141
149
  <CharacterMeta>Level {getLevel(character)}</CharacterMeta>
142
150
  </CharacterInfo>
151
+ {isActive && <ActiveTag>In-game</ActiveTag>}
143
152
  </CharacterRow>
144
153
  );
145
154
  })
@@ -266,20 +275,21 @@ const CharacterList = styled.div`
266
275
  &::-webkit-scrollbar-thumb { background: rgba(245,158,11,0.3); border-radius: 4px; }
267
276
  `;
268
277
 
269
- const CharacterRow = styled.div<{ $selected: boolean }>`
278
+ const CharacterRow = styled.div<{ $selected: boolean; $disabled?: boolean }>`
270
279
  display: flex;
271
280
  align-items: center;
272
281
  gap: 12px;
273
282
  padding: 10px 12px;
274
- border: 1px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.08)')};
283
+ border: 1px solid ${({ $selected, $disabled }) => ($disabled ? 'rgba(255,255,255,0.04)' : $selected ? '#f59e0b' : 'rgba(255,255,255,0.08)')};
275
284
  border-radius: 6px;
276
- background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'rgba(255,255,255,0.02)')};
277
- cursor: pointer;
285
+ background: ${({ $selected, $disabled }) => ($disabled ? 'rgba(0,0,0,0.15)' : $selected ? 'rgba(245,158,11,0.1)' : 'rgba(255,255,255,0.02)')};
286
+ cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
287
+ opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)};
278
288
  transition: border-color 0.15s, background 0.15s;
279
289
 
280
290
  &:hover {
281
- border-color: ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(245,158,11,0.4)')};
282
- background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'rgba(245,158,11,0.05)')};
291
+ border-color: ${({ $selected, $disabled }) => ($disabled ? 'rgba(255,255,255,0.04)' : $selected ? '#f59e0b' : 'rgba(245,158,11,0.4)')};
292
+ background: ${({ $selected, $disabled }) => ($disabled ? 'rgba(0,0,0,0.15)' : $selected ? 'rgba(245,158,11,0.1)' : 'rgba(245,158,11,0.05)')};
283
293
  }
284
294
 
285
295
  &:focus-visible {
@@ -288,11 +298,11 @@ const CharacterRow = styled.div<{ $selected: boolean }>`
288
298
  }
289
299
  `;
290
300
 
291
- const RadioCircle = styled.div<{ $selected: boolean }>`
301
+ const RadioCircle = styled.div<{ $selected: boolean; $disabled?: boolean }>`
292
302
  width: 14px;
293
303
  height: 14px;
294
304
  border-radius: 50%;
295
- border: 2px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.4)')};
305
+ border: 2px solid ${({ $selected, $disabled }) => ($disabled ? 'rgba(255,255,255,0.15)' : $selected ? '#f59e0b' : 'rgba(255,255,255,0.4)')};
296
306
  flex-shrink: 0;
297
307
  display: flex;
298
308
  align-items: center;
@@ -303,12 +313,23 @@ const RadioCircle = styled.div<{ $selected: boolean }>`
303
313
  width: 6px;
304
314
  height: 6px;
305
315
  border-radius: 50%;
306
- background: #f59e0b;
316
+ background: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.15)' : '#f59e0b')};
307
317
  opacity: ${({ $selected }) => ($selected ? 1 : 0)};
308
318
  transition: opacity 0.15s;
309
319
  }
310
320
  `;
311
321
 
322
+ const ActiveTag = styled.span`
323
+ font-family: 'Press Start 2P', cursive !important;
324
+ font-size: 0.35rem !important;
325
+ color: #666 !important;
326
+ background: rgba(255,255,255,0.06);
327
+ padding: 3px 6px;
328
+ border-radius: 3px;
329
+ white-space: nowrap;
330
+ margin-left: auto;
331
+ `;
332
+
312
333
  const SpriteWrapper = styled.div`
313
334
  display: flex;
314
335
  align-items: center;
@@ -336,6 +336,7 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
336
336
  onCharacterDelist={onCharacterDelist ?? (() => {})}
337
337
  accountCharacters={accountCharacters ?? []}
338
338
  onCharacterList={onCharacterList ?? (() => {})}
339
+ activeCharacterId={props.characterId}
339
340
  atlasJSON={props.atlasJSON}
340
341
  atlasIMG={props.atlasIMG}
341
342
  characterAtlasJSON={characterAtlasJSON ?? props.atlasJSON}
@@ -20,6 +20,8 @@ export interface IMyCharacterListingsPanelProps {
20
20
  /** Characters that can be listed (used for the List a Character modal) */
21
21
  accountCharacters: ICharacter[];
22
22
  onCharacterList: (characterId: string, price: number) => void;
23
+ /** ID of the currently active character */
24
+ activeCharacterId?: string;
23
25
  /** Items atlas — for UI sprites like the DC coin */
24
26
  atlasJSON: any;
25
27
  atlasIMG: any;
@@ -39,6 +41,7 @@ export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps>
39
41
  onCharacterDelist,
40
42
  accountCharacters,
41
43
  onCharacterList,
44
+ activeCharacterId,
42
45
  atlasJSON,
43
46
  atlasIMG,
44
47
  characterAtlasJSON,
@@ -80,6 +83,7 @@ export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps>
80
83
  isOpen={isListingModalOpen}
81
84
  onClose={() => setIsListingModalOpen(false)}
82
85
  accountCharacters={accountCharacters}
86
+ activeCharacterId={activeCharacterId}
83
87
  atlasJSON={atlasJSON}
84
88
  atlasIMG={atlasIMG}
85
89
  characterAtlasJSON={characterAtlasJSON}