@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.
@@ -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: 'Governs your effectiveness when fighting unarmed or with punch-type weapons. Your level directly scales unarmed attack damage and attack speed.',
19
- howToTrain: 'Deal damage to enemies while fighting with no weapon equipped or while wearing leather gloves.',
20
- notes: 'No equipment is required to train this skill — useful for new players. Berserker class receives a strong affinity bonus.',
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: 'Determines your proficiency with mace-type weapons such as clubs and hammers. Higher level increases damage dealt and reduces attack interval.',
26
- howToTrain: 'Deal damage to enemies while wielding any mace-type weapon.',
27
- notes: 'Mace weapons deal more consistent damage than swords but have slower attack speed. Useful against heavily armored enemies.',
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: 'Governs your mastery of one-handed and two-handed swords. Level scales both the damage bonus and effective swing speed of sword attacks.',
33
- howToTrain: 'Deal damage to enemies while wielding any sword-type weapon.',
34
- notes: 'Swords are the most balanced combat skill — moderate damage, moderate speed. Knight class has an affinity bonus to sword progression.',
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: 'Controls your skill with axe-type weapons. High axe level gives the biggest per-hit damage increase of any melee weapon type.',
40
- howToTrain: 'Deal damage to enemies while wielding any axe-type weapon.',
41
- notes: 'Axes have the highest damage ceiling of melee weapons but the slowest base attack speed. Best for burst damage on single targets.',
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: 'Measures your accuracy and damage with ranged weapons such as bows, crossbows, and spears. Level reduces miss chance and increases projectile damage.',
47
- howToTrain: 'Deal damage to enemies using any ranged weapon.',
48
- notes: 'Allows attacking from a safe range. Archer class receives a notable affinity bonus. Vigor level affects the delay between ranged shots.',
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: 'Represents your skill at blocking with shields. Higher level increases the percentage of incoming damage blocked per successful shield block and lowers block failure chance.',
54
- howToTrain: 'Receive attacks from enemies while a shield is equipped in your off-hand slot.',
55
- notes: 'Purely defensive — trains only when you are being hit, not when you attack. Any shield enables training. Paladin and Knight classes have affinity bonuses.',
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: 'Governs your technique with daggers and short blades. Daggers are fast but deal less per-hit damage; high level compensates with additional attack speed and critical hit probability.',
61
- howToTrain: 'Deal damage to enemies while wielding any dagger-type weapon.',
62
- notes: 'Daggers have the fastest attack speed of any melee weapon type. Rogue class has a strong affinity bonus to dagger progression.',
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: 'Determines your success rate and yield when fishing. Higher levels unlock catches of rarer fish species and reduce waiting time between bites.',
69
- howToTrain: 'Use a fishing rod near any water body with a fish resource. Each successful catch grants Skill Points.',
70
- notes: 'A fishing rod must be equipped. Caught fish can be cooked with the Cooking skill. Druid class has a 10% bonus.',
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: 'Controls your efficiency when cultivating crops. Level affects harvest quantity, double-harvest chance, and which rare crop varieties you can grow.',
76
- howToTrain: 'Plant seeds in farming spots using a watering can, then harvest mature crops. Each harvest grants Skill Points.',
77
- notes: 'Requires seeds (purchasable from farming NPCs) and a watering can. Crops must be watered at least once before harvest. Druid class has a 20% affinity bonus.',
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: 'Governs your ability to extract ore and stone from rock nodes. Level determines which ore types you can mine and how many resources you yield per swing.',
83
- howToTrain: 'Mine rock or ore nodes with a pickaxe. Each successful resource extraction grants Skill Points.',
84
- notes: 'A pickaxe must be equipped. Higher mining levels unlock rarer ores found in deeper zones. Dwarf race has a 20% bonus.',
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: 'Controls your skill at chopping trees for wood. Level determines which tree types you can cut and the number of logs obtained per swing.',
90
- howToTrain: 'Chop tree resource nodes with an axe equipped. Each log obtained grants Skill Points.',
91
- notes: 'Any axe-type weapon doubles as a lumberjacking tool — there is no separate lumberjacking tool. Rarer wood types require higher skill and appear in higher-level zones.',
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: 'Determines your ability to craft metal equipment such as weapons and armor at forge stations. Higher level unlocks stronger recipes and improves crafted item quality.',
97
- howToTrain: "Craft metal items at a blacksmith's forge using ingots and other materials. Each successful craft grants Skill Points.",
98
- notes: 'Requires materials obtained from mining. Blacksmithing produces tradeable equipment items. Higher level recipes require rare ingots. Rogue class has a 10% bonus.',
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: 'Governs your ability to prepare food from raw ingredients. Cooked food restores HP or provides temporary stat buffs. Higher level produces more potent food.',
104
- howToTrain: 'Cook raw food items at a cooking station or campfire. Each successful cook grants Skill Points.',
105
- notes: 'Raw ingredients come from fishing, farming, and monster drops. Cooked meals restore more HP than potions at equivalent cost and can provide additional buffs.',
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: 'Controls your skill at brewing potions and elixirs. Higher levels unlock stronger potion recipes, reduce failure chance, and increase the potency of brewed potions.',
111
- howToTrain: 'Brew potions at an alchemy station using herbs, reagents, and vials. Each successful brew grants Skill Points.',
112
- notes: 'Herb reagents are gathered from nature nodes across the world. High-level alchemy produces Greater and Superior potions that substantially outperform shop-bought equivalents. Druid has a 20% bonus.',
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: 'Governs the power and efficiency of spells and magic-type weapons. Higher Magic level increases spell damage, spell range, and reduces mana consumption.',
119
- howToTrain: 'Deal damage to enemies using spells from your spellbook, or fight while wielding a magic staff or wand.',
120
- notes: 'Magic starts at level 0 and is one of the slower attributes to train since it requires active spell use. Sorcerer and Mage classes receive strong affinity bonuses. Also increases maximum mana.',
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: 'Reduces the damage taken from incoming magical attacks and spells. Each level of magic resistance reduces a percentage of spell damage received.',
126
- howToTrain: 'Receive magic damage from enemies (spells, magical attacks, staff hits aimed at you).',
127
- notes: 'Purely defensive — trains only when you are the target of magical attacks. Cannot be trained offensively. Essential for characters that frequently face casters or magic creatures.',
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: 'Directly increases melee attack damage for all physical weapon types. Also increases how much weight your character can carry.',
133
- howToTrain: 'Deal melee damage to enemies using any physical weapon or unarmed combat.',
134
- notes: 'Applies to all physical weapon types regardless of which combat skill you use. Warrior and Berserker classes gain Strength faster due to class affinity.',
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: 'Reduces incoming physical damage from melee and ranged attacks. Higher resistance provides a percentage reduction to the physical damage you receive per hit.',
140
- howToTrain: 'Receive physical damage from enemies (melee hits, arrows, projectiles).',
141
- notes: 'Passive defense — trains only when you take physical damage. Tank-oriented classes (Paladin, Knight) receive faster resistance growth. Stacks with Shielding for substantial physical damage reduction.',
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: 'Increases your attack speed (reduces attack interval) and improves your chance to dodge incoming attacks. Also slightly improves accuracy with ranged weapons.',
147
- howToTrain: 'Deal physical damage to enemies or successfully dodge incoming attacks.',
148
- notes: 'Attack speed compounds with damage — more attacks per second means proportionally more total damage. One of the most impactful combat attributes. Archer and Rogue classes have strong dexterity affinity.',
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: 'Your overall character level, derived from total Experience Points (XP) accumulated. It summarizes your overall progress and may unlock access to certain areas or content.',
155
- howToTrain: 'XP is gained by killing monsters, completing quests, and reaching skill level milestones. Unlike skill SP, all XP accumulates into a single shared pool that determines your character level.',
156
- notes: 'Uses a cubic progression formula (level³ × 3 XP per level), so higher levels require exponentially more XP. Character level itself does not directly affect combat — individual skill levels handle that.',
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
  });
@@ -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
- return getAtlasBaseNameLookup(atlasJSON).get(baseName) ?? null;
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
  };