@rpg-engine/long-bow 0.8.197 → 0.8.199
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/components/Store/Store.d.ts +11 -1
- package/dist/components/Store/sections/StoreRedeemSection.d.ts +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +331 -95
- 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 +332 -97
- 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 +27 -4
- package/src/components/Store/__test__/StoreRedeemSection.spec.tsx +232 -0
- package/src/components/Store/sections/StoreRedeemSection.tsx +258 -0
- package/src/constants/skillInfoData.ts +124 -60
- package/src/index.tsx +1 -0
- package/src/utils/__test__/atlasUtils.spec.ts +15 -0
- package/src/utils/atlasUtils.ts +59 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { FaCheckCircle, FaExclamationCircle, FaTicketAlt } from 'react-icons/fa';
|
|
3
|
+
import styled, { keyframes } from 'styled-components';
|
|
4
|
+
import { uiColors } from '../../../constants/uiColors';
|
|
5
|
+
import { CTAButton } from '../../shared/CTAButton/CTAButton';
|
|
6
|
+
|
|
7
|
+
type RedeemStatus = 'idle' | 'loading' | 'success' | 'error';
|
|
8
|
+
|
|
9
|
+
export interface IStoreRedeemSectionProps {
|
|
10
|
+
onRedeem: (code: string) => Promise<{ success: boolean; dcAmount?: number; error?: string }>;
|
|
11
|
+
onInputFocus?: () => void;
|
|
12
|
+
onInputBlur?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const StoreRedeemSection: React.FC<IStoreRedeemSectionProps> = ({
|
|
16
|
+
onRedeem,
|
|
17
|
+
onInputFocus,
|
|
18
|
+
onInputBlur,
|
|
19
|
+
}) => {
|
|
20
|
+
const [code, setCode] = useState('');
|
|
21
|
+
const [status, setStatus] = useState<RedeemStatus>('idle');
|
|
22
|
+
const [dcAmount, setDcAmount] = useState<number | undefined>();
|
|
23
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
24
|
+
|
|
25
|
+
const canSubmit = code.trim().length > 0 && status !== 'loading';
|
|
26
|
+
|
|
27
|
+
const handleSubmit = async (): Promise<void> => {
|
|
28
|
+
if (!canSubmit) return;
|
|
29
|
+
|
|
30
|
+
const normalizedCode = code.trim().toUpperCase();
|
|
31
|
+
setStatus('loading');
|
|
32
|
+
setErrorMessage('');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await onRedeem(normalizedCode);
|
|
36
|
+
if (result.success) {
|
|
37
|
+
setStatus('success');
|
|
38
|
+
setDcAmount(result.dcAmount);
|
|
39
|
+
} else {
|
|
40
|
+
setStatus('error');
|
|
41
|
+
setErrorMessage(result.error ?? 'Redemption failed. Please try again.');
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
setStatus('error');
|
|
45
|
+
setErrorMessage('Something went wrong. Please try again.');
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleReset = (): void => {
|
|
50
|
+
setCode('');
|
|
51
|
+
setStatus('idle');
|
|
52
|
+
setDcAmount(undefined);
|
|
53
|
+
setErrorMessage('');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleKeyDown = (e: React.KeyboardEvent): void => {
|
|
57
|
+
if (e.key === 'Enter') {
|
|
58
|
+
void handleSubmit();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (status === 'success') {
|
|
63
|
+
return (
|
|
64
|
+
<Container>
|
|
65
|
+
<ResultContainer>
|
|
66
|
+
<SuccessIcon>
|
|
67
|
+
<FaCheckCircle size={32} />
|
|
68
|
+
</SuccessIcon>
|
|
69
|
+
<SuccessTitle>Code Redeemed!</SuccessTitle>
|
|
70
|
+
{dcAmount != null && (
|
|
71
|
+
<DCAmountDisplay>+{dcAmount.toLocaleString()} DC</DCAmountDisplay>
|
|
72
|
+
)}
|
|
73
|
+
<SuccessHint>Your wallet balance has been updated.</SuccessHint>
|
|
74
|
+
<ButtonWrapper>
|
|
75
|
+
<CTAButton
|
|
76
|
+
icon={<FaTicketAlt />}
|
|
77
|
+
label="Redeem Another"
|
|
78
|
+
onClick={handleReset}
|
|
79
|
+
/>
|
|
80
|
+
</ButtonWrapper>
|
|
81
|
+
</ResultContainer>
|
|
82
|
+
</Container>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (status === 'error') {
|
|
87
|
+
return (
|
|
88
|
+
<Container>
|
|
89
|
+
<ResultContainer>
|
|
90
|
+
<ErrorIcon>
|
|
91
|
+
<FaExclamationCircle size={32} />
|
|
92
|
+
</ErrorIcon>
|
|
93
|
+
<ErrorTitle>{errorMessage}</ErrorTitle>
|
|
94
|
+
<ButtonWrapper>
|
|
95
|
+
<CTAButton
|
|
96
|
+
icon={<FaTicketAlt />}
|
|
97
|
+
label="Try Again"
|
|
98
|
+
onClick={handleReset}
|
|
99
|
+
/>
|
|
100
|
+
</ButtonWrapper>
|
|
101
|
+
</ResultContainer>
|
|
102
|
+
</Container>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Container>
|
|
108
|
+
<Title>Redeem a Voucher Code</Title>
|
|
109
|
+
<Description>
|
|
110
|
+
Enter your voucher code below to receive Definya Coins.
|
|
111
|
+
</Description>
|
|
112
|
+
<InputRow>
|
|
113
|
+
<CodeInput
|
|
114
|
+
type="text"
|
|
115
|
+
value={code}
|
|
116
|
+
onChange={(e) => setCode(e.target.value)}
|
|
117
|
+
onFocus={onInputFocus}
|
|
118
|
+
onBlur={onInputBlur}
|
|
119
|
+
onKeyDown={handleKeyDown}
|
|
120
|
+
placeholder="Enter code..."
|
|
121
|
+
disabled={status === 'loading'}
|
|
122
|
+
autoComplete="off"
|
|
123
|
+
spellCheck={false}
|
|
124
|
+
/>
|
|
125
|
+
</InputRow>
|
|
126
|
+
<ButtonWrapper>
|
|
127
|
+
<CTAButton
|
|
128
|
+
icon={<FaTicketAlt />}
|
|
129
|
+
label={status === 'loading' ? 'Redeeming...' : 'Redeem Code'}
|
|
130
|
+
onClick={() => { void handleSubmit(); }}
|
|
131
|
+
disabled={!canSubmit}
|
|
132
|
+
fullWidth
|
|
133
|
+
/>
|
|
134
|
+
</ButtonWrapper>
|
|
135
|
+
</Container>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const Container = styled.div`
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
padding: 2rem 1.5rem;
|
|
145
|
+
max-width: 420px;
|
|
146
|
+
margin: 0 auto;
|
|
147
|
+
gap: 1rem;
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
const Title = styled.h3`
|
|
151
|
+
font-family: 'Press Start 2P', cursive;
|
|
152
|
+
font-size: 0.85rem;
|
|
153
|
+
color: ${uiColors.white};
|
|
154
|
+
margin: 0;
|
|
155
|
+
text-align: center;
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const Description = styled.p`
|
|
159
|
+
font-family: 'Press Start 2P', cursive;
|
|
160
|
+
font-size: 0.55rem;
|
|
161
|
+
color: ${uiColors.lightGray};
|
|
162
|
+
margin: 0;
|
|
163
|
+
text-align: center;
|
|
164
|
+
line-height: 1.6;
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const InputRow = styled.div`
|
|
168
|
+
width: 100%;
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
const CodeInput = styled.input`
|
|
172
|
+
width: 100%;
|
|
173
|
+
padding: 12px 14px;
|
|
174
|
+
font-family: 'Press Start 2P', cursive;
|
|
175
|
+
font-size: 0.75rem;
|
|
176
|
+
color: ${uiColors.white};
|
|
177
|
+
background: rgba(0, 0, 0, 0.4);
|
|
178
|
+
border: 2px solid #f59e0b;
|
|
179
|
+
border-radius: 4px;
|
|
180
|
+
text-transform: uppercase;
|
|
181
|
+
letter-spacing: 2px;
|
|
182
|
+
text-align: center;
|
|
183
|
+
box-sizing: border-box;
|
|
184
|
+
outline: none;
|
|
185
|
+
|
|
186
|
+
&::placeholder {
|
|
187
|
+
color: ${uiColors.lightGray};
|
|
188
|
+
text-transform: none;
|
|
189
|
+
letter-spacing: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
&:focus {
|
|
193
|
+
border-color: #fbbf24;
|
|
194
|
+
box-shadow: 0 0 8px rgba(251, 191, 36, 0.3);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
&:disabled {
|
|
198
|
+
opacity: 0.5;
|
|
199
|
+
cursor: not-allowed;
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
const ButtonWrapper = styled.div`
|
|
204
|
+
width: 100%;
|
|
205
|
+
margin-top: 0.5rem;
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
const ResultContainer = styled.div`
|
|
209
|
+
display: flex;
|
|
210
|
+
flex-direction: column;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 1rem;
|
|
213
|
+
padding: 1rem 0;
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
const glowPulse = keyframes`
|
|
217
|
+
0%, 100% { opacity: 1; }
|
|
218
|
+
50% { opacity: 0.7; }
|
|
219
|
+
`;
|
|
220
|
+
|
|
221
|
+
const SuccessIcon = styled.div`
|
|
222
|
+
color: ${uiColors.green};
|
|
223
|
+
animation: ${glowPulse} 1.5s ease-in-out infinite;
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
const SuccessTitle = styled.h3`
|
|
227
|
+
font-family: 'Press Start 2P', cursive;
|
|
228
|
+
font-size: 0.85rem;
|
|
229
|
+
color: ${uiColors.green};
|
|
230
|
+
margin: 0;
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
const DCAmountDisplay = styled.div`
|
|
234
|
+
font-family: 'Press Start 2P', cursive;
|
|
235
|
+
font-size: 1.2rem;
|
|
236
|
+
color: #fef08a;
|
|
237
|
+
text-shadow: 0 0 10px rgba(254, 240, 138, 0.5);
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
const SuccessHint = styled.p`
|
|
241
|
+
font-family: 'Press Start 2P', cursive;
|
|
242
|
+
font-size: 0.5rem;
|
|
243
|
+
color: ${uiColors.lightGray};
|
|
244
|
+
margin: 0;
|
|
245
|
+
`;
|
|
246
|
+
|
|
247
|
+
const ErrorIcon = styled.div`
|
|
248
|
+
color: ${uiColors.red};
|
|
249
|
+
`;
|
|
250
|
+
|
|
251
|
+
const ErrorTitle = styled.p`
|
|
252
|
+
font-family: 'Press Start 2P', cursive;
|
|
253
|
+
font-size: 0.65rem;
|
|
254
|
+
color: ${uiColors.red};
|
|
255
|
+
margin: 0;
|
|
256
|
+
text-align: center;
|
|
257
|
+
line-height: 1.6;
|
|
258
|
+
`;
|
|
@@ -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
|
};
|
package/src/index.tsx
CHANGED
|
@@ -85,6 +85,7 @@ export * from './components/Store/PaymentMethodModal';
|
|
|
85
85
|
export * from './components/Store/PurchaseSuccess';
|
|
86
86
|
export * from './components/Store/Store';
|
|
87
87
|
export * from './components/Store/StoreBadges';
|
|
88
|
+
export * from './components/Store/sections/StoreRedeemSection';
|
|
88
89
|
export * from './components/Store/TrustBar';
|
|
89
90
|
export * from './components/Table/Table';
|
|
90
91
|
export * from './components/TextArea';
|
|
@@ -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
|
};
|