@rpg-engine/long-bow 0.8.235 → 0.8.236
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/Spellbook/Spell.d.ts +2 -1
- package/dist/components/Spellbook/Spellbook.d.ts +2 -1
- package/dist/components/Spellbook/cards/SpellTooltip.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +561 -200
- 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 +562 -201
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +13 -3
- package/src/components/Shortcuts/ShortcutsSetter.tsx +6 -3
- package/src/components/Spellbook/Spell.tsx +195 -234
- package/src/components/Spellbook/Spellbook.tsx +407 -111
- package/src/components/Spellbook/__tests__/SpellInfoWrapper.test.tsx +102 -0
- package/src/components/Spellbook/__tests__/Spellbook.test.tsx +113 -0
- package/src/components/Spellbook/__tests__/imageMock.js +1 -0
- package/src/components/Spellbook/cards/SpellInfo.tsx +84 -29
- package/src/components/Spellbook/cards/SpellInfoWrapper.tsx +48 -13
- package/src/components/Spellbook/cards/SpellTooltip.tsx +13 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpg-engine/long-bow",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.236",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
"storybook": "start-storybook -p 6006",
|
|
31
31
|
"build-storybook": "build-storybook",
|
|
32
32
|
"configure": "./environment/download-credentials.sh",
|
|
33
|
-
"update:shared-deps": "yarn add @rpg-engine/shared"
|
|
33
|
+
"update:shared-deps": "yarn add @rpg-engine/shared",
|
|
34
|
+
"preview:spellbook": "vite --config scripts/preview-spellbook/vite.config.ts",
|
|
35
|
+
"verify:spellbook": "jest --config jest.spellbook.config.js --runInBand && tsc -p tsconfig.spellbook.json",
|
|
36
|
+
"verify": "yarn verify:spellbook && tsc --noEmit"
|
|
34
37
|
},
|
|
35
38
|
"peerDependencies": {
|
|
36
39
|
"react": ">=16"
|
|
@@ -79,7 +82,14 @@
|
|
|
79
82
|
"tsdx": "^0.14.1",
|
|
80
83
|
"tslib": "^2.3.1",
|
|
81
84
|
"typescript": "^4.6.2",
|
|
82
|
-
"uuid": "^8.3.2"
|
|
85
|
+
"uuid": "^8.3.2",
|
|
86
|
+
"@swc/jest": "0.2.29",
|
|
87
|
+
"@testing-library/react": "12.1.5",
|
|
88
|
+
"jest": "29.7.0",
|
|
89
|
+
"jest-environment-jsdom": "29.7.0",
|
|
90
|
+
"vite": "2.9.16",
|
|
91
|
+
"@vitejs/plugin-react": "1.3.2",
|
|
92
|
+
"@swc/core": "1.3.94"
|
|
83
93
|
},
|
|
84
94
|
"dependencies": {
|
|
85
95
|
"@capacitor/core": "^6.1.0",
|
|
@@ -85,13 +85,16 @@ export const ShortcutsSetter: React.FC<ShortcutsSetterProps> = ({
|
|
|
85
85
|
{Array.from({ length: 12 }).map((_, i) => (
|
|
86
86
|
<Shortcut
|
|
87
87
|
key={i}
|
|
88
|
-
|
|
88
|
+
type="button"
|
|
89
|
+
aria-label={`${shortcuts[i]?.payload ? 'Clear' : 'Set'} shortcut ${i + 1}`}
|
|
90
|
+
onClick={() => {
|
|
89
91
|
if (settingShortcutIndex !== -1) setSettingShortcutIndex(-1);
|
|
90
92
|
|
|
93
|
+
const wasEmpty = !shortcuts[i] || shortcuts[i].type === ShortcutType.None;
|
|
91
94
|
removeShortcut(i);
|
|
92
95
|
if (
|
|
93
96
|
settingShortcutIndex === -1 &&
|
|
94
|
-
|
|
97
|
+
wasEmpty
|
|
95
98
|
)
|
|
96
99
|
setSettingShortcutIndex(i);
|
|
97
100
|
}}
|
|
@@ -99,7 +102,7 @@ export const ShortcutsSetter: React.FC<ShortcutsSetterProps> = ({
|
|
|
99
102
|
isBeingSet={settingShortcutIndex === i}
|
|
100
103
|
id={`shortcutSetter_${i}`}
|
|
101
104
|
>
|
|
102
|
-
{getContent(i)}
|
|
105
|
+
{getContent(i) || i + 1}
|
|
103
106
|
</Shortcut>
|
|
104
107
|
))}
|
|
105
108
|
</List>
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { ISpell } from '@rpg-engine/shared';
|
|
1
|
+
import { ISpell, ItemRarities } from '@rpg-engine/shared';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import styled from 'styled-components';
|
|
4
|
-
import { uiColors } from '../../constants/uiColors';
|
|
5
|
-
import { uiFonts } from '../../constants/uiFonts';
|
|
3
|
+
import styled, { css } from 'styled-components';
|
|
6
4
|
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
7
5
|
import { SpellInfoWrapper } from './cards/SpellInfoWrapper';
|
|
8
6
|
|
|
@@ -18,271 +16,234 @@ export interface ISpellProps {
|
|
|
18
16
|
spellKey: string;
|
|
19
17
|
spell: ISpell;
|
|
20
18
|
activeCooldown?: number;
|
|
19
|
+
// Rarity the character's copy of this spell was learned at. Absent means unknown, never Common.
|
|
20
|
+
rarity?: ItemRarities;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
swordFighting: 'sword fighting',
|
|
27
|
-
axeFighting: 'axe fighting',
|
|
28
|
-
clubFighting: 'club fighting',
|
|
29
|
-
shielding: 'shielding',
|
|
30
|
-
fishing: 'fishing',
|
|
31
|
-
cooking: 'cooking',
|
|
32
|
-
fistFighting: 'fist fighting',
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const getSkillName = (attribute: string): string => {
|
|
36
|
-
return SKILL_NAMES[attribute] || attribute;
|
|
37
|
-
};
|
|
23
|
+
// 'strength' -> 'Strength', 'magicResistance' -> 'Magic Resistance'
|
|
24
|
+
const skillLabel = (skill: string): string =>
|
|
25
|
+
skill.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\b[a-z]/g, letter => letter.toUpperCase());
|
|
38
26
|
|
|
39
27
|
export const Spell: React.FC<ISpellProps> = ({
|
|
40
|
-
atlasIMG,
|
|
41
|
-
|
|
42
|
-
spellKey,
|
|
43
|
-
charMana,
|
|
44
|
-
charMagicLevel,
|
|
45
|
-
charSkillLevels,
|
|
46
|
-
onPointerUp,
|
|
47
|
-
onClick,
|
|
48
|
-
isSettingShortcut,
|
|
49
|
-
spell,
|
|
50
|
-
activeCooldown,
|
|
28
|
+
atlasIMG, atlasJSON, spellKey, charMana, charMagicLevel, charSkillLevels,
|
|
29
|
+
onPointerUp, onClick, isSettingShortcut, spell, activeCooldown = 0, rarity,
|
|
51
30
|
}) => {
|
|
52
|
-
const {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const requiredLevel = getRequiredLevel();
|
|
74
|
-
const characterSkillLevel = getCharacterSkillLevel();
|
|
75
|
-
const disabled = isSettingShortcut
|
|
76
|
-
? characterSkillLevel < requiredLevel
|
|
77
|
-
: manaCost > charMana || characterSkillLevel < requiredLevel;
|
|
78
|
-
|
|
79
|
-
const CONTAINER_STYLE = { width: '32px', height: '32px' };
|
|
80
|
-
const IMAGE_SCALE = 2;
|
|
81
|
-
|
|
31
|
+
const { manaCost, minMagicLevelRequired, minSkillLevelRequired, attribute, magicWords, name, description } = spell;
|
|
32
|
+
const usesAttribute = !!attribute && minSkillLevelRequired !== undefined;
|
|
33
|
+
const requiredLevel = usesAttribute ? minSkillLevelRequired! : minMagicLevelRequired;
|
|
34
|
+
const skillLevel = usesAttribute && charSkillLevels ? charSkillLevels[attribute!] ?? 0 : charMagicLevel;
|
|
35
|
+
const skillName = usesAttribute ? attribute! : 'magic';
|
|
36
|
+
const lacksSkill = skillLevel < requiredLevel;
|
|
37
|
+
const lacksMana = !isSettingShortcut && manaCost > charMana;
|
|
38
|
+
const cooling = !isSettingShortcut && activeCooldown > 0;
|
|
39
|
+
const unavailable = lacksSkill || lacksMana || cooling;
|
|
40
|
+
// An unmet requirement is already spelled out (and flagged) by its own row, so only mana, cooldown
|
|
41
|
+
// and shortcut assignment need a note. A ready card has none: clicking it casts.
|
|
42
|
+
const note = lacksSkill ? ''
|
|
43
|
+
: lacksMana ? 'Not enough mana'
|
|
44
|
+
: cooling ? `Ready in ${activeCooldown.toFixed(activeCooldown < 10 ? 1 : 0)}s`
|
|
45
|
+
: isSettingShortcut ? 'Select to assign' : '';
|
|
46
|
+
const knownRarity = rarity && Object.values(ItemRarities).includes(rarity) ? rarity : undefined;
|
|
47
|
+
const descriptionId = `spell-description-${spellKey}`;
|
|
48
|
+
const factsId = `spell-facts-${spellKey}`;
|
|
49
|
+
const statusId = `spell-status-${spellKey}`;
|
|
50
|
+
const rarityId = `spell-rarity-${spellKey}`;
|
|
82
51
|
const handleClick = onClick ?? onPointerUp;
|
|
83
52
|
|
|
84
53
|
return (
|
|
85
54
|
<SpellInfoWrapper spell={spell}>
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
isSettingShortcut={isSettingShortcut && !disabled}
|
|
55
|
+
<Card
|
|
56
|
+
type="button"
|
|
89
57
|
className="spell"
|
|
58
|
+
aria-label={`${isSettingShortcut ? 'Assign' : 'Cast'} ${name}`}
|
|
59
|
+
aria-describedby={`${knownRarity ? `${rarityId} ` : ''}${descriptionId} ${factsId}${note ? ` ${statusId}` : ''} spell-details-${spellKey}`}
|
|
60
|
+
disabled={unavailable}
|
|
61
|
+
$assigning={!!isSettingShortcut}
|
|
62
|
+
onClick={() => handleClick?.(spellKey)}
|
|
90
63
|
>
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
</
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<Info>
|
|
114
|
-
<Title>
|
|
115
|
-
<span>{name}</span>
|
|
116
|
-
<span className="spell">({magicWords})</span>
|
|
117
|
-
</Title>
|
|
118
|
-
<Description>{description}</Description>
|
|
119
|
-
</Info>
|
|
120
|
-
|
|
121
|
-
<Divider />
|
|
122
|
-
<Cost>
|
|
123
|
-
<span>Mana cost:</span>
|
|
124
|
-
<span className="mana">{manaCost}</span>
|
|
125
|
-
</Cost>
|
|
126
|
-
</Container>
|
|
64
|
+
<Heading>
|
|
65
|
+
<Icon aria-hidden="true">
|
|
66
|
+
<SpriteFromAtlas atlasIMG={atlasIMG} atlasJSON={atlasJSON}
|
|
67
|
+
spriteKey={spell.texturePath ?? ''} imgScale={1.5}
|
|
68
|
+
width={40} height={40} centered />
|
|
69
|
+
</Icon>
|
|
70
|
+
<Name><strong>{name}</strong></Name>
|
|
71
|
+
</Heading>
|
|
72
|
+
<Meta>
|
|
73
|
+
<Words>{magicWords}</Words>
|
|
74
|
+
{knownRarity && <Rarity id={rarityId} data-rarity={knownRarity} $rarity={knownRarity}>{knownRarity}</Rarity>}
|
|
75
|
+
</Meta>
|
|
76
|
+
<Description id={descriptionId}>{description}</Description>
|
|
77
|
+
<Facts id={factsId}>
|
|
78
|
+
<Requirement $unmet={lacksSkill}>{`Requires ${skillLabel(skillName)} ${requiredLevel}`}</Requirement>
|
|
79
|
+
<Stats>
|
|
80
|
+
<Stat><Label>Mana cost</Label><Value $tone={lacksMana ? 'bad' : 'mana'}>{manaCost}</Value></Stat>
|
|
81
|
+
<Stat><Label>Cooldown</Label><Value>{`${spell.cooldown}s`}</Value></Stat>
|
|
82
|
+
</Stats>
|
|
83
|
+
</Facts>
|
|
84
|
+
{note && <Status id={statusId} $unavailable={unavailable}>{note}</Status>}
|
|
85
|
+
</Card>
|
|
127
86
|
</SpellInfoWrapper>
|
|
128
87
|
);
|
|
129
88
|
};
|
|
130
89
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
90
|
+
const Card = styled.button<{ $assigning: boolean }>`
|
|
91
|
+
box-sizing: border-box;
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
flex: 1 1 auto;
|
|
136
95
|
width: 100%;
|
|
96
|
+
min-width: 0;
|
|
97
|
+
padding: 7px 8px 8px;
|
|
98
|
+
text-align: left;
|
|
99
|
+
color: #392718;
|
|
100
|
+
background: rgba(255, 247, 215, 0.5);
|
|
101
|
+
border: 1px solid ${({ $assigning }) => $assigning ? '#8a5a14' : 'rgba(89, 56, 28, .4)'};
|
|
102
|
+
box-shadow: ${({ $assigning }) => $assigning ? 'inset 0 0 0 1px #8a5a14' : 'none'};
|
|
103
|
+
border-radius: 3px;
|
|
104
|
+
font: inherit;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
transition: background .12s, border-color .12s;
|
|
107
|
+
&:hover:not(:disabled) { background: rgba(255, 252, 235, .9); border-color: #8a5a14; box-shadow: inset 0 0 0 1px #8a5a14; }
|
|
108
|
+
&:focus-visible { outline: 3px solid #3a2110; outline-offset: 2px; }
|
|
109
|
+
/* RPGUI greys and recolors every :disabled (0,2,0): keep the dashed card and its red/blue stats */
|
|
110
|
+
&&:disabled { cursor: default; background: rgba(113, 81, 49, .1); border-style: dashed; color: #392718; filter: none; }
|
|
111
|
+
&:disabled img, &:disabled canvas { opacity: .55; }
|
|
112
|
+
`;
|
|
113
|
+
const Heading = styled.span`
|
|
137
114
|
display: flex;
|
|
138
|
-
|
|
139
|
-
gap: 1rem;
|
|
140
115
|
align-items: center;
|
|
141
|
-
|
|
142
|
-
text-align: left;
|
|
143
|
-
position: relative;
|
|
144
|
-
|
|
145
|
-
animation: ${({ isSettingShortcut }) =>
|
|
146
|
-
isSettingShortcut ? 'border-color-change 1s infinite' : 'none'};
|
|
147
|
-
|
|
148
|
-
@keyframes border-color-change {
|
|
149
|
-
0% {
|
|
150
|
-
border-color: ${uiColors.yellow};
|
|
151
|
-
}
|
|
152
|
-
50% {
|
|
153
|
-
border-color: transparent;
|
|
154
|
-
}
|
|
155
|
-
100% {
|
|
156
|
-
border-color: ${uiColors.yellow};
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
&:hover,
|
|
161
|
-
&:focus {
|
|
162
|
-
background-color: ${uiColors.darkGray};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
&:active {
|
|
166
|
-
background: none;
|
|
167
|
-
}
|
|
116
|
+
gap: 8px;
|
|
168
117
|
`;
|
|
169
|
-
|
|
170
|
-
const SpellImage = styled.div`
|
|
171
|
-
width: 4rem;
|
|
172
|
-
height: 4rem;
|
|
173
|
-
font-size: ${uiFonts.size.xLarge};
|
|
174
|
-
font-weight: bold;
|
|
175
|
-
background-color: ${uiColors.darkGray};
|
|
176
|
-
color: ${uiColors.lightGray};
|
|
118
|
+
const Icon = styled.span`
|
|
177
119
|
display: flex;
|
|
178
|
-
|
|
120
|
+
flex: 0 0 40px;
|
|
121
|
+
height: 40px;
|
|
179
122
|
align-items: center;
|
|
180
|
-
|
|
181
|
-
position: relative;
|
|
123
|
+
justify-content: center;
|
|
182
124
|
overflow: hidden;
|
|
183
|
-
|
|
184
|
-
.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
left: 0;
|
|
188
|
-
width: 100%;
|
|
189
|
-
height: 100%;
|
|
190
|
-
background-color: rgba(0 0 0 / 20%);
|
|
191
|
-
color: ${uiColors.darkYellow};
|
|
192
|
-
font-weight: bold;
|
|
193
|
-
display: flex;
|
|
194
|
-
justify-content: center;
|
|
195
|
-
align-items: center;
|
|
196
|
-
}
|
|
125
|
+
background: rgba(72, 43, 21, .18);
|
|
126
|
+
border: 1px solid rgba(72, 43, 21, .35);
|
|
127
|
+
border-radius: 3px;
|
|
128
|
+
image-rendering: pixelated;
|
|
197
129
|
`;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
130
|
+
// The game client's RPGUI `.rpgui-content span` rule (0,1,1) forces white outlined 22px text;
|
|
131
|
+
// the doubled `&&` class (0,2,0) keeps the parchment typography below.
|
|
132
|
+
const Name = styled.span`
|
|
201
133
|
flex: 1;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
134
|
+
min-width: 0;
|
|
135
|
+
strong { font-weight: normal; overflow-wrap: anywhere; }
|
|
136
|
+
/* the block's own font-size sets its line-box strut: match the name so two-line names stay 29px */
|
|
137
|
+
&& { font-size: 11px; line-height: 1.3; color: #392718; text-shadow: none; }
|
|
206
138
|
`;
|
|
207
|
-
|
|
139
|
+
// One strip under the heading, so every card has the same rows whether or not it carries a rarity: the magic
|
|
140
|
+
// words on the left and, when the character's copy has a known rarity, its badge on the right. The badge
|
|
141
|
+
// drops to its own line only when the pair does not fit the card width.
|
|
142
|
+
const Meta = styled.span`
|
|
208
143
|
display: flex;
|
|
209
144
|
flex-wrap: wrap;
|
|
210
145
|
align-items: center;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
span
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
146
|
+
justify-content: space-between;
|
|
147
|
+
gap: 3px 6px;
|
|
148
|
+
margin-top: 5px;
|
|
149
|
+
/* RPGUI's span rule would give this wrapper a 22px line */
|
|
150
|
+
&& { line-height: 1.4; }
|
|
151
|
+
`;
|
|
152
|
+
const Words = styled.span`
|
|
153
|
+
display: block;
|
|
154
|
+
min-width: 0;
|
|
155
|
+
overflow-wrap: anywhere;
|
|
156
|
+
&& { font-size: 9px; line-height: inherit; color: #6b4d32; text-shadow: none; }
|
|
157
|
+
`;
|
|
158
|
+
// Item palette hues (Item/Inventory/ItemSlotRarity) with the type inked darker for >= 4.5:1 on parchment.
|
|
159
|
+
// The label always carries the rarity; colour only reinforces it.
|
|
160
|
+
const RARITY_INK: Record<ItemRarities, string> = {
|
|
161
|
+
[ItemRarities.Common]: '#5a3f27',
|
|
162
|
+
[ItemRarities.Uncommon]: '#1e5e1e',
|
|
163
|
+
[ItemRarities.Rare]: '#1d4580',
|
|
164
|
+
[ItemRarities.Epic]: '#6a1b8f',
|
|
165
|
+
[ItemRarities.Legendary]: '#7a4f00',
|
|
166
|
+
};
|
|
167
|
+
const Rarity = styled.span<{ $rarity: ItemRarities }>`
|
|
168
|
+
display: inline-block;
|
|
169
|
+
flex: none;
|
|
170
|
+
padding: 1px 5px;
|
|
171
|
+
border: 1px solid ${({ $rarity }) => RARITY_INK[$rarity]};
|
|
172
|
+
border-radius: 2px;
|
|
173
|
+
background: rgba(255, 250, 229, .7);
|
|
174
|
+
&& { font-size: 9px; line-height: 1.3; color: ${({ $rarity }) => RARITY_INK[$rarity]}; text-shadow: none; }
|
|
175
|
+
`;
|
|
176
|
+
const Description = styled.span`
|
|
177
|
+
display: block;
|
|
178
|
+
margin: 6px 0 5px;
|
|
179
|
+
overflow-wrap: anywhere;
|
|
180
|
+
&& { font-size: 10px; line-height: 1.5; color: #392718; text-shadow: none; }
|
|
181
|
+
/* Art book (> 960px, see FLAT in Spellbook.tsx): four cards across, all as tall as the tallest, so one long
|
|
182
|
+
description would stretch every card. Clamp to three lines; the full text stays in the DOM (aria-describedby)
|
|
183
|
+
and in the hover / touch-hold tooltip. Flat and phone layouts show it all. */
|
|
184
|
+
@media (min-width: 961px) {
|
|
185
|
+
display: -webkit-box;
|
|
186
|
+
-webkit-line-clamp: 3;
|
|
187
|
+
-webkit-box-orient: vertical;
|
|
188
|
+
overflow: hidden;
|
|
225
189
|
}
|
|
226
190
|
`;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
191
|
+
// Sits straight under the description (a 5px margin, the rule and 5px of padding): the card is stretched to the
|
|
192
|
+
// tallest on the page, so a shorter card keeps its facts here and leaves the spare height below them.
|
|
193
|
+
const Facts = styled.span`
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
gap: 4px;
|
|
197
|
+
padding-top: 5px;
|
|
198
|
+
border-top: 1px solid rgba(89, 56, 28, .25);
|
|
231
199
|
`;
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
height:
|
|
236
|
-
|
|
237
|
-
|
|
200
|
+
// An unmet requirement gets a red bar and tint as well as red type, so it never relies on hue alone.
|
|
201
|
+
const Requirement = styled.span<{ $unmet: boolean }>`
|
|
202
|
+
display: block;
|
|
203
|
+
&& { font-size: 9px; line-height: 1.5; color: #392718; text-shadow: none; }
|
|
204
|
+
${({ $unmet }) => $unmet && css`
|
|
205
|
+
padding: 3px 6px;
|
|
206
|
+
background: rgba(138, 42, 34, .12);
|
|
207
|
+
border-left: 3px solid #8a2a22;
|
|
208
|
+
border-radius: 2px;
|
|
209
|
+
&& { color: #8a2a22; }
|
|
210
|
+
`}
|
|
238
211
|
`;
|
|
239
|
-
|
|
240
|
-
|
|
212
|
+
const Stats = styled.span`
|
|
213
|
+
display: grid;
|
|
214
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
215
|
+
gap: 4px 8px;
|
|
216
|
+
`;
|
|
217
|
+
const Stat = styled.span`
|
|
241
218
|
display: flex;
|
|
242
|
-
align-items: center;
|
|
243
219
|
flex-direction: column;
|
|
244
|
-
gap:
|
|
245
|
-
|
|
246
|
-
div {
|
|
247
|
-
z-index: 1;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
.mana {
|
|
251
|
-
position: relative;
|
|
252
|
-
font-size: ${uiFonts.size.medium};
|
|
253
|
-
font-weight: bold;
|
|
254
|
-
z-index: 1;
|
|
255
|
-
|
|
256
|
-
&::after {
|
|
257
|
-
position: absolute;
|
|
258
|
-
content: '';
|
|
259
|
-
top: 0;
|
|
260
|
-
left: 0;
|
|
261
|
-
background-color: ${uiColors.blue};
|
|
262
|
-
width: 100%;
|
|
263
|
-
height: 100%;
|
|
264
|
-
border-radius: 50%;
|
|
265
|
-
transform: scale(1.8);
|
|
266
|
-
filter: blur(10px);
|
|
267
|
-
z-index: -1;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
220
|
+
gap: 2px;
|
|
221
|
+
min-width: 0;
|
|
270
222
|
`;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
border-radius: 1rem;
|
|
223
|
+
const Label = styled.span`
|
|
224
|
+
&& { font-size: 9px; line-height: 1.4; color: #5a3f27; text-shadow: none; }
|
|
225
|
+
`;
|
|
226
|
+
const STAT_TONES = { mana: '#1d4580', bad: '#8a2a22' };
|
|
227
|
+
const Value = styled.span<{ $tone?: 'mana' | 'bad' }>`
|
|
228
|
+
&& { font-size: 10px; line-height: 1.4; color: ${({ $tone }) => $tone ? STAT_TONES[$tone] : '#2a1708'}; text-shadow: none; }
|
|
229
|
+
`;
|
|
230
|
+
const Status = styled.span<{ $unavailable: boolean }>`
|
|
280
231
|
display: flex;
|
|
281
|
-
justify-content: center;
|
|
282
232
|
align-items: center;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
233
|
+
gap: 6px;
|
|
234
|
+
margin-top: 4px;
|
|
235
|
+
&& {
|
|
236
|
+
font-size: 9px;
|
|
237
|
+
line-height: 1.5;
|
|
238
|
+
color: ${({ $unavailable }) => $unavailable ? '#8a2a22' : '#4b4420'};
|
|
239
|
+
text-shadow: none;
|
|
240
|
+
}
|
|
241
|
+
&::before {
|
|
242
|
+
content: '';
|
|
243
|
+
flex: none;
|
|
244
|
+
width: 6px;
|
|
245
|
+
height: 6px;
|
|
246
|
+
background: currentColor;
|
|
247
|
+
clip-path: ${({ $unavailable }) => $unavailable ? 'none' : 'polygon(0 0, 100% 50%, 0 100%)'};
|
|
248
|
+
}
|
|
288
249
|
`;
|