@rpg-engine/long-bow 0.8.197 → 0.8.198
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/dist/components/Marketplace/GroupedRowContainer.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +131 -78
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +131 -78
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Marketplace/BuyOrderRows.tsx +1 -0
- package/src/components/Marketplace/BuyPanel.tsx +6 -3
- package/src/components/Marketplace/CharacterMarketplaceRows.tsx +2 -2
- package/src/components/Marketplace/GroupedRowContainer.tsx +9 -4
- package/src/components/Marketplace/ManagmentPanel.tsx +2 -2
- package/src/components/Marketplace/Marketplace.tsx +2 -2
- package/src/components/Marketplace/MarketplaceRows.tsx +3 -2
- package/src/components/Store/Store.tsx +2 -2
- package/src/constants/skillInfoData.ts +124 -60
- package/src/utils/__test__/atlasUtils.spec.ts +15 -0
- package/src/utils/atlasUtils.ts +59 -1
package/package.json
CHANGED
|
@@ -642,9 +642,12 @@ const WrapperContainer = styled.div<{ $sell: boolean }>`
|
|
|
642
642
|
`;
|
|
643
643
|
|
|
644
644
|
const ItemComponentScrollWrapper = styled.div`
|
|
645
|
-
|
|
645
|
+
display: flex;
|
|
646
|
+
flex-direction: column;
|
|
647
|
+
overflow-y: auto;
|
|
646
648
|
overflow-x: hidden;
|
|
647
|
-
height: 390px;
|
|
649
|
+
max-height: 390px;
|
|
650
|
+
min-height: 120px;
|
|
648
651
|
width: 95%;
|
|
649
652
|
margin: 1rem auto 0 auto;
|
|
650
653
|
background: rgba(0, 0, 0, 0.2);
|
|
@@ -652,7 +655,7 @@ const ItemComponentScrollWrapper = styled.div`
|
|
|
652
655
|
border-radius: 4px;
|
|
653
656
|
|
|
654
657
|
@media (max-width: 950px) {
|
|
655
|
-
height: 250px;
|
|
658
|
+
max-height: 250px;
|
|
656
659
|
}
|
|
657
660
|
`;
|
|
658
661
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatDCAmount, ICharacterListing } from '@rpg-engine/shared';
|
|
2
|
-
import {
|
|
2
|
+
import { Wallet } from 'pixelarticons/react/Wallet';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import { ItemRowWrapper } from '../shared/ItemRowWrapper';
|
|
@@ -77,7 +77,7 @@ export const CharacterMarketplaceRows: React.FC<ICharacterMarketplaceRowsProps>
|
|
|
77
77
|
|
|
78
78
|
<ActionSection>
|
|
79
79
|
<CTAButton
|
|
80
|
-
icon={onCharacterBuy ? <
|
|
80
|
+
icon={onCharacterBuy ? <Wallet width={18} height={18} /> : undefined}
|
|
81
81
|
label={onCharacterBuy ? 'Buy' : 'Delist'}
|
|
82
82
|
disabled={disabled || isBeingBought}
|
|
83
83
|
onClick={() => {
|
|
@@ -5,12 +5,14 @@ export interface IGroupedRowContainerProps {
|
|
|
5
5
|
mainRow: React.ReactNode;
|
|
6
6
|
subRows: React.ReactNode[];
|
|
7
7
|
badgeLabel?: string;
|
|
8
|
+
metaRightInset?: number;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const GroupedRowContainer: React.FC<IGroupedRowContainerProps> = ({
|
|
11
12
|
mainRow,
|
|
12
13
|
subRows,
|
|
13
14
|
badgeLabel = 'offers',
|
|
15
|
+
metaRightInset = 120,
|
|
14
16
|
}) => {
|
|
15
17
|
const [expanded, setExpanded] = useState(false);
|
|
16
18
|
const hasMultiple = subRows.length > 0;
|
|
@@ -21,7 +23,7 @@ export const GroupedRowContainer: React.FC<IGroupedRowContainerProps> = ({
|
|
|
21
23
|
<GroupHeader $clickable={hasMultiple} onClick={hasMultiple ? () => setExpanded(e => !e) : undefined}>
|
|
22
24
|
{mainRow}
|
|
23
25
|
{hasMultiple && (
|
|
24
|
-
<GroupMeta>
|
|
26
|
+
<GroupMeta $rightInset={metaRightInset}>
|
|
25
27
|
<OfferBadge>{totalCount} {badgeLabel}</OfferBadge>
|
|
26
28
|
<Chevron $expanded={expanded}>▸</Chevron>
|
|
27
29
|
</GroupMeta>
|
|
@@ -48,15 +50,18 @@ const GroupHeader = styled.div<{ $clickable: boolean }>`
|
|
|
48
50
|
overflow-x: hidden;
|
|
49
51
|
`;
|
|
50
52
|
|
|
51
|
-
const GroupMeta = styled.div
|
|
53
|
+
const GroupMeta = styled.div<{ $rightInset: number }>`
|
|
52
54
|
position: absolute;
|
|
53
|
-
right:
|
|
55
|
+
right: ${({ $rightInset }) => `${$rightInset}px`};
|
|
54
56
|
top: 50%;
|
|
55
57
|
transform: translateY(-50%);
|
|
56
58
|
display: flex;
|
|
57
59
|
align-items: center;
|
|
58
|
-
gap:
|
|
60
|
+
gap: 4px;
|
|
61
|
+
padding-left: 10px;
|
|
62
|
+
background: linear-gradient(to left, rgba(25, 23, 23, 0.85), rgba(25, 23, 23, 0));
|
|
59
63
|
pointer-events: none;
|
|
64
|
+
z-index: 1;
|
|
60
65
|
`;
|
|
61
66
|
|
|
62
67
|
const OfferBadge = styled.span`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatDCAmount, goldToDC, IEquipmentSet, IItem, IMarketplaceItem, MarketplaceAcceptedCurrency } from '@rpg-engine/shared';
|
|
2
|
-
import {
|
|
2
|
+
import { Wallet } from 'pixelarticons/react/Wallet';
|
|
3
3
|
import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
|
|
4
4
|
import React, { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import styled from 'styled-components';
|
|
@@ -185,7 +185,7 @@ export const ManagmentPanel: React.FC<IManagmentPanelProps> = ({
|
|
|
185
185
|
</BalanceRow>
|
|
186
186
|
)}
|
|
187
187
|
<SmallCTAButton
|
|
188
|
-
icon={<
|
|
188
|
+
icon={<Wallet width={18} height={18} />}
|
|
189
189
|
label="Withdraw Gold"
|
|
190
190
|
disabled={availableGold === 0}
|
|
191
191
|
onClick={() => availableGold > 0 && onMoneyWithdraw()}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
IMarketplaceItem,
|
|
10
10
|
IMarketplaceTransaction,
|
|
11
11
|
} from '@rpg-engine/shared';
|
|
12
|
-
import {
|
|
12
|
+
import { DateTime } from 'pixelarticons/react/DateTime';
|
|
13
13
|
import { Settings2 } from 'pixelarticons/react/Settings2';
|
|
14
14
|
import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
|
|
15
15
|
import { ShoppingCart } from 'pixelarticons/react/ShoppingCart';
|
|
@@ -262,7 +262,7 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
|
|
|
262
262
|
{
|
|
263
263
|
id: 'history',
|
|
264
264
|
label: 'History',
|
|
265
|
-
icon: <
|
|
265
|
+
icon: <DateTime width={18} height={18} />,
|
|
266
266
|
},
|
|
267
267
|
...(showWalletTab && walletProps
|
|
268
268
|
? [
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
IMarketplaceItem,
|
|
7
7
|
MarketplaceAcceptedCurrency,
|
|
8
8
|
} from '@rpg-engine/shared';
|
|
9
|
-
import {
|
|
9
|
+
import { Wallet } from 'pixelarticons/react/Wallet';
|
|
10
10
|
import { Delete } from 'pixelarticons/react/Delete';
|
|
11
11
|
import React from 'react';
|
|
12
12
|
import styled from 'styled-components';
|
|
@@ -134,7 +134,7 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
|
|
|
134
134
|
|
|
135
135
|
<ActionSection>
|
|
136
136
|
<CTAButton
|
|
137
|
-
icon={onMarketPlaceItemBuy ? <
|
|
137
|
+
icon={onMarketPlaceItemBuy ? <Wallet width={18} height={18} /> : <Delete width={18} height={18} />}
|
|
138
138
|
label={onMarketPlaceItemBuy ? 'Buy' : 'Remove'}
|
|
139
139
|
disabled={disabled}
|
|
140
140
|
onClick={() => {
|
|
@@ -201,6 +201,7 @@ export const GroupedMarketplaceRow: React.FC<IGroupedMarketplaceRowProps> = ({
|
|
|
201
201
|
mainRow={makeRow(bestListing)}
|
|
202
202
|
subRows={otherListings.map(makeRow)}
|
|
203
203
|
badgeLabel="offers"
|
|
204
|
+
metaRightInset={132}
|
|
204
205
|
/>
|
|
205
206
|
);
|
|
206
207
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IItemPack, IPurchase, IProductBlueprint, ItemRarities, ItemSubType, ItemType, UserAccountTypes, PaymentCurrency, PurchaseType } from '@rpg-engine/shared';
|
|
2
|
-
import { Box } from 'pixelarticons/react/Box';
|
|
3
2
|
import { Crown } from 'pixelarticons/react/Crown';
|
|
4
3
|
import { Gift } from 'pixelarticons/react/Gift';
|
|
4
|
+
import { Package } from 'pixelarticons/react/Package';
|
|
5
5
|
import { Wallet } from 'pixelarticons/react/Wallet';
|
|
6
6
|
import React, { ReactNode, useMemo, useState } from 'react';
|
|
7
7
|
import { FaHistory, FaShoppingCart, FaWallet } from 'react-icons/fa';
|
|
@@ -318,7 +318,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
318
318
|
items: {
|
|
319
319
|
id: 'items',
|
|
320
320
|
title: 'Items',
|
|
321
|
-
icon: <
|
|
321
|
+
icon: <Package width={18} height={18} />,
|
|
322
322
|
content: (
|
|
323
323
|
<StoreItemsSection
|
|
324
324
|
items={filteredItems.items}
|
|
@@ -11,148 +11,212 @@ const CRAFTING_COLOR = '#597DCE';
|
|
|
11
11
|
const ATTRIBUTE_COLOR = '#6833A3';
|
|
12
12
|
const LEVEL_COLOR = '#0E79B2';
|
|
13
13
|
|
|
14
|
+
// Keep this copy aligned with ../rpg-api:
|
|
15
|
+
// SkillIncrease.ts, BattleDamageCalculator.ts, BattleEvent.ts,
|
|
16
|
+
// ResourceRequirementConstants.ts, PlantHarvest.ts,
|
|
17
|
+
// ItemCraftableQueue.ts, ItemMinLevelCalculator.ts, NPCExperience.ts.
|
|
14
18
|
export const SKILL_INFO_DATA: Record<string, ISkillInfoEntry> = {
|
|
15
19
|
first: {
|
|
16
20
|
name: 'Fist',
|
|
17
21
|
color: COMBAT_COLOR,
|
|
18
|
-
description:
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
description:
|
|
23
|
+
'Used for unarmed attacks. Higher Fist improves your chance to hit and the damage you deal while fighting empty-handed.',
|
|
24
|
+
howToTrain:
|
|
25
|
+
'Land non-spell hits with no weapon equipped.',
|
|
26
|
+
notes:
|
|
27
|
+
'Some gloves require Fist, but you train it by fighting unarmed. Berserkers learn it especially well.',
|
|
21
28
|
},
|
|
22
29
|
club: {
|
|
23
30
|
name: 'Club',
|
|
24
31
|
color: COMBAT_COLOR,
|
|
25
|
-
description:
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
description:
|
|
33
|
+
'Used for maces and clubs. Higher Club improves your chance to hit and the damage you deal with those weapons.',
|
|
34
|
+
howToTrain:
|
|
35
|
+
'Land non-spell hits with a mace or club weapon equipped.',
|
|
36
|
+
notes:
|
|
37
|
+
'Some mace-type weapons require Club to equip. Warriors and Berserkers learn it faster than most classes.',
|
|
28
38
|
},
|
|
29
39
|
sword: {
|
|
30
40
|
name: 'Sword',
|
|
31
41
|
color: COMBAT_COLOR,
|
|
32
|
-
description:
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
description:
|
|
43
|
+
'Used for swords. Higher Sword improves your chance to hit and the damage you deal with sword attacks.',
|
|
44
|
+
howToTrain:
|
|
45
|
+
'Land non-spell hits with a sword equipped.',
|
|
46
|
+
notes:
|
|
47
|
+
'Some swords require Sword to equip. Warriors and Berserkers are the natural Sword classes.',
|
|
35
48
|
},
|
|
36
49
|
axe: {
|
|
37
50
|
name: 'Axe',
|
|
38
51
|
color: COMBAT_COLOR,
|
|
39
|
-
description:
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
description:
|
|
53
|
+
'Used for axes. Higher Axe improves your chance to hit and the damage you deal with axe attacks.',
|
|
54
|
+
howToTrain:
|
|
55
|
+
'Land non-spell hits with an axe equipped.',
|
|
56
|
+
notes:
|
|
57
|
+
'Some axes require Axe to equip. Berserkers are the natural Axe class, with Warriors close behind.',
|
|
42
58
|
},
|
|
43
59
|
distance: {
|
|
44
60
|
name: 'Distance',
|
|
45
61
|
color: COMBAT_COLOR,
|
|
46
|
-
description:
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
description:
|
|
63
|
+
'Used for ranged weapons and spears. Higher Distance improves your accuracy and damage, especially for Hunters.',
|
|
64
|
+
howToTrain:
|
|
65
|
+
'Land non-spell hits with a ranged weapon or spear.',
|
|
66
|
+
notes:
|
|
67
|
+
'Some ranged weapons and abilities also require Distance. Hunters rely on it the most for bows and spears.',
|
|
49
68
|
},
|
|
50
69
|
shielding: {
|
|
51
70
|
name: 'Shielding',
|
|
52
71
|
color: COMBAT_COLOR,
|
|
53
|
-
description:
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
description:
|
|
73
|
+
'Used when reducing incoming damage with a shield. With a shield equipped, Shielding boosts your defense and lets the shield absorb part of each hit.',
|
|
74
|
+
howToTrain:
|
|
75
|
+
'Take hits or blocks while a shield is equipped.',
|
|
76
|
+
notes:
|
|
77
|
+
'Shielding gains use a hard cooldown, so extra hits do not spam progress. Warriors benefit from it the most, with Berserkers also training it well.',
|
|
56
78
|
},
|
|
57
79
|
dagger: {
|
|
58
80
|
name: 'Dagger',
|
|
59
81
|
color: COMBAT_COLOR,
|
|
60
|
-
description:
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
description:
|
|
83
|
+
'Used for daggers. Higher Dagger improves your chance to hit and the damage you deal with dagger attacks.',
|
|
84
|
+
howToTrain:
|
|
85
|
+
'Land non-spell hits with a dagger equipped.',
|
|
86
|
+
notes:
|
|
87
|
+
'Some daggers require Dagger to equip. Rogues are the main Dagger class, and Hunters also train it well.',
|
|
63
88
|
},
|
|
64
89
|
|
|
65
90
|
fishing: {
|
|
66
91
|
name: 'Fishing',
|
|
67
92
|
color: CRAFTING_COLOR,
|
|
68
|
-
description:
|
|
69
|
-
|
|
70
|
-
|
|
93
|
+
description:
|
|
94
|
+
'Governs fishing rewards and access to higher-tier catches. Higher Fishing lets you catch better fish and shell rewards.',
|
|
95
|
+
howToTrain:
|
|
96
|
+
'Successfully fish reward items such as fish or shell resources.',
|
|
97
|
+
notes:
|
|
98
|
+
'Higher-tier catches also depend on the rod you are using.',
|
|
71
99
|
},
|
|
72
100
|
farming: {
|
|
73
101
|
name: 'Farming',
|
|
74
102
|
color: CRAFTING_COLOR,
|
|
75
|
-
description:
|
|
76
|
-
|
|
77
|
-
|
|
103
|
+
description:
|
|
104
|
+
'Governs plant harvesting. Higher Farming increases crop yield and improves harvest rarity when plants are collected.',
|
|
105
|
+
howToTrain:
|
|
106
|
+
'Plant and harvest crops. Harvesting plants grants Farming SP.',
|
|
107
|
+
notes:
|
|
108
|
+
'Some seeds require Farming before you can plant them.',
|
|
78
109
|
},
|
|
79
110
|
mining: {
|
|
80
111
|
name: 'Mining',
|
|
81
112
|
color: CRAFTING_COLOR,
|
|
82
|
-
description:
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
description:
|
|
114
|
+
'Governs mining rewards and unlocks higher-tier ore, stone, and gem drops.',
|
|
115
|
+
howToTrain:
|
|
116
|
+
'Mine resource nodes and successfully receive mining rewards.',
|
|
117
|
+
notes:
|
|
118
|
+
'Better mining rewards depend on both your Mining level and your pickaxe.',
|
|
85
119
|
},
|
|
86
120
|
lumberjacking: {
|
|
87
121
|
name: 'Lumberjacking',
|
|
88
122
|
color: CRAFTING_COLOR,
|
|
89
|
-
description:
|
|
90
|
-
|
|
91
|
-
|
|
123
|
+
description:
|
|
124
|
+
'Governs woodcutting rewards and unlocks higher-tier wood drops.',
|
|
125
|
+
howToTrain:
|
|
126
|
+
'Chop resource nodes and successfully receive wood rewards.',
|
|
127
|
+
notes:
|
|
128
|
+
'Better wood tiers depend on both your Lumberjacking level and the tool you are using.',
|
|
92
129
|
},
|
|
93
130
|
blacksmithing: {
|
|
94
131
|
name: 'Blacksmithing',
|
|
95
132
|
color: CRAFTING_COLOR,
|
|
96
|
-
description:
|
|
97
|
-
|
|
98
|
-
|
|
133
|
+
description:
|
|
134
|
+
'Governs blacksmithing recipes and ingot processing. Higher Blacksmithing improves craft success and unlocks stronger smithing recipes.',
|
|
135
|
+
howToTrain:
|
|
136
|
+
'Successfully smelt bars or craft gear with Blacksmithing recipes.',
|
|
137
|
+
notes:
|
|
138
|
+
'You only gain SP from successful crafts.',
|
|
99
139
|
},
|
|
100
140
|
cooking: {
|
|
101
141
|
name: 'Cooking',
|
|
102
142
|
color: CRAFTING_COLOR,
|
|
103
|
-
description:
|
|
104
|
-
|
|
105
|
-
|
|
143
|
+
description:
|
|
144
|
+
'Governs cooking recipes. Higher Cooking improves craft success and unlocks recipes with higher cooking requirements.',
|
|
145
|
+
howToTrain:
|
|
146
|
+
'Successfully cook food with Cooking recipes.',
|
|
147
|
+
notes:
|
|
148
|
+
'You only gain SP from successful crafts.',
|
|
106
149
|
},
|
|
107
150
|
alchemy: {
|
|
108
151
|
name: 'Alchemy',
|
|
109
152
|
color: CRAFTING_COLOR,
|
|
110
|
-
description:
|
|
111
|
-
|
|
112
|
-
|
|
153
|
+
description:
|
|
154
|
+
'Governs alchemy recipes. Higher Alchemy improves craft success and unlocks recipes with higher alchemy requirements.',
|
|
155
|
+
howToTrain:
|
|
156
|
+
'Successfully brew potions or craft reagents with Alchemy recipes.',
|
|
157
|
+
notes:
|
|
158
|
+
'You only gain SP from successful crafts.',
|
|
113
159
|
},
|
|
114
160
|
|
|
115
161
|
magic: {
|
|
116
162
|
name: 'Magic',
|
|
117
163
|
color: ATTRIBUTE_COLOR,
|
|
118
|
-
description:
|
|
119
|
-
|
|
120
|
-
|
|
164
|
+
description:
|
|
165
|
+
'Used for spell power, rune scaling, staff or magic-weapon attacks, gear used by mage classes, and some carry-weight calculations.',
|
|
166
|
+
howToTrain:
|
|
167
|
+
'Cast spells or land non-spell hits with magic or staff weapons.',
|
|
168
|
+
notes:
|
|
169
|
+
'Sorcerers and Druids are the mage classes, and both lean on Magic and staves. They also use Magic instead of Strength for max carry weight.',
|
|
121
170
|
},
|
|
122
171
|
magicResistance: {
|
|
123
172
|
name: 'Magic Resistance',
|
|
124
173
|
color: ATTRIBUTE_COLOR,
|
|
125
|
-
description:
|
|
126
|
-
|
|
127
|
-
|
|
174
|
+
description:
|
|
175
|
+
'Used when reducing incoming magic damage. Higher Magic Resistance improves your defense against spells and other magical attacks.',
|
|
176
|
+
howToTrain:
|
|
177
|
+
'Take damage from spells, magic weapons, runes, or other magical attacks.',
|
|
178
|
+
notes:
|
|
179
|
+
'This only trains on incoming magic damage, not on casting. Sorcerers and Druids improve it faster than other classes.',
|
|
128
180
|
},
|
|
129
181
|
strength: {
|
|
130
182
|
name: 'Strength',
|
|
131
183
|
color: ATTRIBUTE_COLOR,
|
|
132
|
-
description:
|
|
133
|
-
|
|
134
|
-
|
|
184
|
+
description:
|
|
185
|
+
'Used for physical damage, carry weight for non-mages, item requirements, and many strength-scaling abilities.',
|
|
186
|
+
howToTrain:
|
|
187
|
+
'Land non-spell hits with physical weapons or while fighting unarmed.',
|
|
188
|
+
notes:
|
|
189
|
+
'When you attack with magic or staff weapons, Strength is not trained. Warriors and Berserkers get the most out of it.',
|
|
135
190
|
},
|
|
136
191
|
resistance: {
|
|
137
192
|
name: 'Resistance',
|
|
138
193
|
color: ATTRIBUTE_COLOR,
|
|
139
|
-
description:
|
|
140
|
-
|
|
141
|
-
|
|
194
|
+
description:
|
|
195
|
+
'Used in physical defense and damage reduction. Higher Resistance lowers the damage you take from non-magic attacks.',
|
|
196
|
+
howToTrain:
|
|
197
|
+
'Take non-magic damage from melee, ranged, or other physical attacks.',
|
|
198
|
+
notes:
|
|
199
|
+
'Shield users stack Resistance with Shielding for better mitigation. Warriors are strongest here, with Berserkers and Druids also holding up well.',
|
|
142
200
|
},
|
|
143
201
|
dexterity: {
|
|
144
202
|
name: 'Dexterity',
|
|
145
203
|
color: ATTRIBUTE_COLOR,
|
|
146
|
-
description:
|
|
147
|
-
|
|
148
|
-
|
|
204
|
+
description:
|
|
205
|
+
'Dexterity affects accuracy and blocking, and Hunters also rely on it heavily for ranged damage. Some Rogue and Hunter abilities scale from it.',
|
|
206
|
+
howToTrain:
|
|
207
|
+
'Make incoming attacks miss you. Each miss grants Dexterity SP.',
|
|
208
|
+
notes:
|
|
209
|
+
'Some accessories require Dexterity. Hunters and Rogues lean on it the most.',
|
|
149
210
|
},
|
|
150
211
|
|
|
151
212
|
level: {
|
|
152
213
|
name: 'Character Level',
|
|
153
214
|
color: LEVEL_COLOR,
|
|
154
|
-
description:
|
|
155
|
-
|
|
156
|
-
|
|
215
|
+
description:
|
|
216
|
+
'Your overall character level. It affects attack, defense, damage, movement speed, health, mana, spell unlocks, and other progression systems.',
|
|
217
|
+
howToTrain:
|
|
218
|
+
'Gain XP from defeated enemies and other experience rewards.',
|
|
219
|
+
notes:
|
|
220
|
+
'Level-ups refresh derived stats such as max health and max mana and can unlock new spells.',
|
|
157
221
|
},
|
|
158
222
|
};
|
|
@@ -20,6 +20,21 @@ describe('resolveAtlasSpriteKey', () => {
|
|
|
20
20
|
);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
it('matches camel-cased blueprint keys by normalized base name', () => {
|
|
24
|
+
expect(resolveAtlasSpriteKey(atlasJSON, 'items/FrostBow')).toBe(
|
|
25
|
+
'ranged-weapons/frost-bow.png'
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('maps known marketplace ids to atlas aliases', () => {
|
|
30
|
+
expect(resolveAtlasSpriteKey(atlasJSON, 'mystic-staff')).toBe(
|
|
31
|
+
'staffs/mystic-lightning-staff.png'
|
|
32
|
+
);
|
|
33
|
+
expect(resolveAtlasSpriteKey(atlasJSON, 'silver-arrow')).toBe(
|
|
34
|
+
'ranged-weapons/silvermoon-arrow.png'
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
23
38
|
it('returns null when no atlas sprite can be resolved', () => {
|
|
24
39
|
expect(resolveAtlasSpriteKey(atlasJSON, 'items/iron-sword')).toBeNull();
|
|
25
40
|
});
|
package/src/utils/atlasUtils.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export const NO_IMAGE_SPRITE_KEY = 'others/no-image.png';
|
|
2
2
|
|
|
3
|
+
const atlasSpriteAliases = new Map<string, string>([
|
|
4
|
+
['mysticstaff', 'staffs/mystic-lightning-staff.png'],
|
|
5
|
+
['silverarrow', 'ranged-weapons/silvermoon-arrow.png'],
|
|
6
|
+
]);
|
|
7
|
+
|
|
3
8
|
const atlasBaseNameLookupCache = new WeakMap<object, Map<string, string>>();
|
|
9
|
+
const atlasNormalizedNameLookupCache = new WeakMap<object, Map<string, string>>();
|
|
4
10
|
|
|
5
11
|
const getBaseName = (spriteKey: string): string | null => {
|
|
6
12
|
const normalizedKey = spriteKey.trim();
|
|
@@ -16,6 +22,18 @@ const getBaseName = (spriteKey: string): string | null => {
|
|
|
16
22
|
return fileName.replace(/\.png$/, '');
|
|
17
23
|
};
|
|
18
24
|
|
|
25
|
+
const normalizeBaseName = (spriteKey: string): string | null => {
|
|
26
|
+
const baseName = getBaseName(spriteKey);
|
|
27
|
+
if (!baseName) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return baseName
|
|
32
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
33
|
+
.replace(/[^a-zA-Z0-9]+/g, '')
|
|
34
|
+
.toLowerCase();
|
|
35
|
+
};
|
|
36
|
+
|
|
19
37
|
const getAtlasBaseNameLookup = (atlasJSON: any): Map<string, string> => {
|
|
20
38
|
if (!atlasJSON || typeof atlasJSON !== 'object') {
|
|
21
39
|
return new Map();
|
|
@@ -41,6 +59,31 @@ const getAtlasBaseNameLookup = (atlasJSON: any): Map<string, string> => {
|
|
|
41
59
|
return lookup;
|
|
42
60
|
};
|
|
43
61
|
|
|
62
|
+
const getAtlasNormalizedNameLookup = (atlasJSON: any): Map<string, string> => {
|
|
63
|
+
if (!atlasJSON || typeof atlasJSON !== 'object') {
|
|
64
|
+
return new Map();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cachedLookup = atlasNormalizedNameLookupCache.get(atlasJSON);
|
|
68
|
+
if (cachedLookup) {
|
|
69
|
+
return cachedLookup;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const frames = atlasJSON?.frames ?? {};
|
|
73
|
+
const lookup = new Map<string, string>();
|
|
74
|
+
|
|
75
|
+
Object.keys(frames).forEach((frameKey) => {
|
|
76
|
+
const normalizedBaseName = normalizeBaseName(frameKey);
|
|
77
|
+
if (normalizedBaseName && !lookup.has(normalizedBaseName)) {
|
|
78
|
+
lookup.set(normalizedBaseName, frameKey);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
atlasNormalizedNameLookupCache.set(atlasJSON, lookup);
|
|
83
|
+
|
|
84
|
+
return lookup;
|
|
85
|
+
};
|
|
86
|
+
|
|
44
87
|
export const resolveAtlasSpriteKey = (
|
|
45
88
|
atlasJSON: any,
|
|
46
89
|
spriteKey?: string | null
|
|
@@ -76,5 +119,20 @@ export const resolveAtlasSpriteKey = (
|
|
|
76
119
|
return null;
|
|
77
120
|
}
|
|
78
121
|
|
|
79
|
-
|
|
122
|
+
const directBaseMatch = getAtlasBaseNameLookup(atlasJSON).get(baseName);
|
|
123
|
+
if (directBaseMatch) {
|
|
124
|
+
return directBaseMatch;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const normalizedBaseName = normalizeBaseName(normalizedKey);
|
|
128
|
+
if (!normalizedBaseName) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const aliasedSpriteKey = atlasSpriteAliases.get(normalizedBaseName);
|
|
133
|
+
if (aliasedSpriteKey && frames[aliasedSpriteKey]) {
|
|
134
|
+
return aliasedSpriteKey;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return getAtlasNormalizedNameLookup(atlasJSON).get(normalizedBaseName) ?? null;
|
|
80
138
|
};
|