recard 0.1.2__py3-none-any.whl
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.
- recard/__init__.py +29 -0
- recard/assets/backgrounds/ANEMO.png +0 -0
- recard/assets/backgrounds/CRYO.png +0 -0
- recard/assets/backgrounds/DENDRO.png +0 -0
- recard/assets/backgrounds/ELECTRO.png +0 -0
- recard/assets/backgrounds/ERROR.png +0 -0
- recard/assets/backgrounds/GEO.png +0 -0
- recard/assets/backgrounds/PYRO.png +0 -0
- recard/assets/backgrounds/hydro.png +0 -0
- recard/assets/constant/closed/ADAPT.png +0 -0
- recard/assets/constant/closed/CLOSED.png +0 -0
- recard/assets/constant/closed/CLOSE_CONST_ANEMO.png +0 -0
- recard/assets/constant/closed/CLOSE_CONST_CRYO.png +0 -0
- recard/assets/constant/closed/CLOSE_CONST_DENDRO.png +0 -0
- recard/assets/constant/closed/CLOSE_CONST_ELECTRO.png +0 -0
- recard/assets/constant/closed/CLOSE_CONST_ERROR.png +0 -0
- recard/assets/constant/closed/CLOSE_CONST_GEO.png +0 -0
- recard/assets/constant/closed/CLOSE_CONST_GYDRO.png +0 -0
- recard/assets/constant/closed/CLOSE_CONST_PYRO.png +0 -0
- recard/assets/constant/const_adapt.png +0 -0
- recard/assets/constant/maska_constant.png +0 -0
- recard/assets/constant/open/ADAPT.png +0 -0
- recard/assets/constant/open/OPEN_CONST_ANEMO.png +0 -0
- recard/assets/constant/open/OPEN_CONST_CRYO.png +0 -0
- recard/assets/constant/open/OPEN_CONST_DENDRO.png +0 -0
- recard/assets/constant/open/OPEN_CONST_ELECTRO.png +0 -0
- recard/assets/constant/open/OPEN_CONST_ERROR.png +0 -0
- recard/assets/constant/open/OPEN_CONST_GEO.png +0 -0
- recard/assets/constant/open/OPEN_CONST_GYDRO.png +0 -0
- recard/assets/constant/open/OPEN_CONST_PYRO.png +0 -0
- recard/assets/fonts/Genshin_Impact.ttf +0 -0
- recard/assets/icons/FRIENDS.png +0 -0
- recard/assets/icons/anemo.png +0 -0
- recard/assets/icons/atk.png +0 -0
- recard/assets/icons/cd.png +0 -0
- recard/assets/icons/cr.png +0 -0
- recard/assets/icons/cryo.png +0 -0
- recard/assets/icons/def.png +0 -0
- recard/assets/icons/dendro.png +0 -0
- recard/assets/icons/electro.png +0 -0
- recard/assets/icons/em.png +0 -0
- recard/assets/icons/er.png +0 -0
- recard/assets/icons/geo.png +0 -0
- recard/assets/icons/hp.png +0 -0
- recard/assets/icons/hydro.png +0 -0
- recard/assets/icons/no_data.png +0 -0
- recard/assets/icons/phys.png +0 -0
- recard/assets/icons/pyro.png +0 -0
- recard/assets/icons/radar_bg.png +0 -0
- recard/assets/icons/stars/Star1.png +0 -0
- recard/assets/icons/stars/Star2.png +0 -0
- recard/assets/icons/stars/Star3.png +0 -0
- recard/assets/icons/stars/Star4.png +0 -0
- recard/assets/icons/stars/Star5.png +0 -0
- recard/assets/icons/stars/bg.png +0 -0
- recard/assets/icons/stars/c_stars_4.png +0 -0
- recard/assets/icons/stars/c_stars_5.png +0 -0
- recard/assets/images/AVATAR.png +0 -0
- recard/assets/images/AVATAR_MASK.png +0 -0
- recard/assets/images/CHARTER_4.png +0 -0
- recard/assets/images/CHARTER_5.png +0 -0
- recard/assets/images/CHARTER_MASK.png +0 -0
- recard/assets/images/test.jpg +0 -0
- recard/assets/images/test1.jpg +0 -0
- recard/assets/logo.png +0 -0
- recard/assets/talents/10.png +0 -0
- recard/assets/talents/ADAPT.png +0 -0
- recard/assets/talents/bg.png +0 -0
- recard/cards/__init__.py +0 -0
- recard/cards/artifacts.py +170 -0
- recard/cards/character_card.py +618 -0
- recard/cards/hoyolab_character_detail.py +469 -0
- recard/cards/watermark.py +64 -0
- recard/client.py +186 -0
- recard/data/__init__.py +0 -0
- recard/data/avatars.json +4036 -0
- recard/data/char.json +4036 -0
- recard/data/data.json +869 -0
- recard/data/new.json +6183 -0
- recard/data/update_data.py +199 -0
- recard/services/__init__.py +0 -0
- recard/services/net.py +99 -0
- recard-0.1.2.dist-info/METADATA +105 -0
- recard-0.1.2.dist-info/RECORD +87 -0
- recard-0.1.2.dist-info/WHEEL +5 -0
- recard-0.1.2.dist-info/licenses/LICENSE +21 -0
- recard-0.1.2.dist-info/top_level.txt +1 -0
recard/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
recard - Genshin Impact character card generator.
|
|
3
|
+
|
|
4
|
+
Same usage pattern as zenka (the ZZZ card library):
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import recard
|
|
8
|
+
|
|
9
|
+
async def main():
|
|
10
|
+
async with recard.Client() as client:
|
|
11
|
+
result = await client.card(700000000, character="Hu Tao")
|
|
12
|
+
for card in result.cards:
|
|
13
|
+
card.card.save(f"{card.name}.png")
|
|
14
|
+
|
|
15
|
+
asyncio.run(main())
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .client import Card, CardResult, CharacterNotFound, Client, ShowcaseCharacter
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.2"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Client",
|
|
24
|
+
"ShowcaseCharacter",
|
|
25
|
+
"Card",
|
|
26
|
+
"CardResult",
|
|
27
|
+
"CharacterNotFound",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
recard/assets/logo.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
recard/cards/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
5
|
+
|
|
6
|
+
from pathlib import Path as _Path
|
|
7
|
+
_PKG_ROOT = _Path(__file__).resolve().parent.parent
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("recard")
|
|
10
|
+
|
|
11
|
+
# Stat Mapping for icons (Local files)
|
|
12
|
+
STAT_MAP = {
|
|
13
|
+
"FIGHT_PROP_HP": "hp", "FIGHT_PROP_HP_PERCENT": "hp",
|
|
14
|
+
"FIGHT_PROP_ATTACK": "atk", "FIGHT_PROP_ATTACK_PERCENT": "atk",
|
|
15
|
+
"FIGHT_PROP_DEFENSE": "def", "FIGHT_PROP_DEFENSE_PERCENT": "def",
|
|
16
|
+
"FIGHT_PROP_ELEMENT_MASTERY": "em", "FIGHT_PROP_CHARGE_EFFICIENCY": "er",
|
|
17
|
+
"FIGHT_PROP_CRITICAL": "cr", "FIGHT_PROP_CRITICAL_HURT": "cd",
|
|
18
|
+
"FIGHT_PROP_FIRE_ADD_HURT": "pyro", "FIGHT_PROP_WATER_ADD_HURT": "hydro",
|
|
19
|
+
"FIGHT_PROP_GRASS_ADD_HURT": "dendro", "FIGHT_PROP_ELEC_ADD_HURT": "electro",
|
|
20
|
+
"FIGHT_PROP_ICE_ADD_HURT": "cryo", "FIGHT_PROP_WIND_ADD_HURT": "anemo",
|
|
21
|
+
"FIGHT_PROP_ROCK_ADD_HURT": "geo", "FIGHT_PROP_PHYSICAL_ADD_HURT": "phys"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Flower, Feather, Sands, Goblet, Circlet
|
|
25
|
+
EQUIP_ORDER = ["EQUIP_BRACER", "EQUIP_NECKLACE", "EQUIP_SHOES", "EQUIP_RING", "EQUIP_DRESS"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _compute_cv(main_data, sub_stats):
|
|
29
|
+
"""CV (crit value) = CR% * 2 + CD%, summed across the main stat (only
|
|
30
|
+
relevant for circlets, whose main stat can itself be CR or CD) and all
|
|
31
|
+
substats. Both crit stats stack additively across an artifact, so this
|
|
32
|
+
is just a running total, not an average."""
|
|
33
|
+
cr = 0.0
|
|
34
|
+
cd = 0.0
|
|
35
|
+
|
|
36
|
+
main_prop = main_data.get("mainPropId")
|
|
37
|
+
if main_prop == "FIGHT_PROP_CRITICAL":
|
|
38
|
+
cr += main_data.get("statValue", 0) or 0
|
|
39
|
+
elif main_prop == "FIGHT_PROP_CRITICAL_HURT":
|
|
40
|
+
cd += main_data.get("statValue", 0) or 0
|
|
41
|
+
|
|
42
|
+
for stat in sub_stats:
|
|
43
|
+
prop_id = stat.get("appendPropId")
|
|
44
|
+
if prop_id == "FIGHT_PROP_CRITICAL":
|
|
45
|
+
cr += stat.get("statValue", 0) or 0
|
|
46
|
+
elif prop_id == "FIGHT_PROP_CRITICAL_HURT":
|
|
47
|
+
cd += stat.get("statValue", 0) or 0
|
|
48
|
+
|
|
49
|
+
return cr * 2 + cd
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def draw_artifact_card(session, base_image, x, y, art_data, font):
|
|
53
|
+
draw = ImageDraw.Draw(base_image)
|
|
54
|
+
CARD_W, CARD_H = 330, 200
|
|
55
|
+
ICONS_PATH = f"{_PKG_ROOT}/assets/icons/"
|
|
56
|
+
STARS_PATH = f"{_PKG_ROOT}/assets/icons/stars/"
|
|
57
|
+
|
|
58
|
+
# 1. Background Card - Using a slightly more transparent fill for that glassmorphism look
|
|
59
|
+
draw.rounded_rectangle([x, y, x + CARD_W, y + CARD_H], radius=12, fill=(0, 0, 0, 80), outline=(255, 255, 255, 40))
|
|
60
|
+
|
|
61
|
+
flat = art_data.get("flat", {})
|
|
62
|
+
relic_core = art_data.get("reliquary", {})
|
|
63
|
+
main_data = flat.get("reliquaryMainstat", {})
|
|
64
|
+
sub_stats = flat.get("reliquarySubstats", [])
|
|
65
|
+
|
|
66
|
+
# 2. Artifact Piece Icon (The Gear piece)
|
|
67
|
+
icon_name = flat.get("icon")
|
|
68
|
+
# Live HoYoLAB data (character/detail fallback) gives a full,
|
|
69
|
+
# directly-fetchable CDN URL - it's hash-named, so it can't be
|
|
70
|
+
# reconstructed as an enka.network/ui/... path the way Enka's own
|
|
71
|
+
# semantic icon names can.
|
|
72
|
+
icon_url = flat.get("icon_url") or (f"https://enka.network/ui/{icon_name}.png" if icon_name else None)
|
|
73
|
+
if icon_url:
|
|
74
|
+
url = icon_url
|
|
75
|
+
async with session.get(url) as response:
|
|
76
|
+
if response.status == 200:
|
|
77
|
+
img_data = await response.read()
|
|
78
|
+
art_img = Image.open(BytesIO(img_data)).convert("RGBA").resize((140, 140), Image.Resampling.LANCZOS)
|
|
79
|
+
# Icon background
|
|
80
|
+
base_image.paste(art_img, (x + 15, y + 30), art_img)
|
|
81
|
+
|
|
82
|
+
# 4. Rarity Stars (NEW)
|
|
83
|
+
rarity = flat.get("rankLevel", 5)
|
|
84
|
+
try:
|
|
85
|
+
# 1. Open and convert
|
|
86
|
+
star_img = Image.open(f"{STARS_PATH}Star{rarity}.png").convert("RGBA")
|
|
87
|
+
|
|
88
|
+
# 2. Resize directly to the target size (Re-assigning the variable!)
|
|
89
|
+
star_img = star_img.resize((98, 28), Image.Resampling.LANCZOS)
|
|
90
|
+
|
|
91
|
+
# 3. Paste
|
|
92
|
+
base_image.paste(star_img, (x + 30, y + 140), star_img)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.warning("Error loading star image Star%s.png: %s", rarity, e)
|
|
95
|
+
# 3. Main Stat Icon - PASTE THIS LAST so it's on top
|
|
96
|
+
main_prop = main_data.get("mainPropId")
|
|
97
|
+
if main_prop:
|
|
98
|
+
icon_key = STAT_MAP.get(main_prop, "atk").lower()
|
|
99
|
+
try:
|
|
100
|
+
# We move the main stat icon to the top right of the gear box
|
|
101
|
+
m_icon = Image.open(f"{ICONS_PATH}{icon_key}.png").convert("RGBA").resize((28, 28))
|
|
102
|
+
base_image.paste(m_icon, (x + 10, y + 10), m_icon)
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
# 4. Sub-Stat Grid
|
|
107
|
+
grid_x = x + 150
|
|
108
|
+
for i, stat in enumerate(sub_stats[:4]):
|
|
109
|
+
col, row = i % 1, i // 1
|
|
110
|
+
bx = grid_x + (col * 135)
|
|
111
|
+
by = y + 15 + (row * 45)
|
|
112
|
+
|
|
113
|
+
# Substat box
|
|
114
|
+
draw.rounded_rectangle([bx, by, bx + 170, by + 38], radius=10, fill=(20, 20, 30, 150))
|
|
115
|
+
|
|
116
|
+
prop_id = stat.get("appendPropId")
|
|
117
|
+
icon_file = STAT_MAP.get(prop_id, "atk").lower()
|
|
118
|
+
try:
|
|
119
|
+
s_icon = Image.open(f"{ICONS_PATH}{icon_file}.png").convert("RGBA").resize((22, 22))
|
|
120
|
+
base_image.paste(s_icon, (bx + 8, by + 8), s_icon)
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
val = stat.get("statValue")
|
|
125
|
+
is_percent = any(k in prop_id for k in ["PERCENT", "CRITICAL", "EFFICIENCY", "HURT"])
|
|
126
|
+
val_str = f"+{val}%" if is_percent else f"+{val}"
|
|
127
|
+
draw.text((bx + 55, by + 19), val_str, font=font, fill=(255, 255, 255), anchor="lm")
|
|
128
|
+
|
|
129
|
+
# 5. Main Stat Value - bottom left of the card (where the level used to sit).
|
|
130
|
+
if main_prop:
|
|
131
|
+
main_val = main_data.get("statValue")
|
|
132
|
+
is_percent = any(k in main_prop for k in ["PERCENT", "CRITICAL", "EFFICIENCY", "HURT"])
|
|
133
|
+
main_val_str = f"{main_val}%" if is_percent else f"{int(main_val)}"
|
|
134
|
+
draw.text((x + 5, y + CARD_H - 15), main_val_str, font=font, fill=(255, 255, 255), anchor="lm", stroke_width=1, stroke_fill=(0, 0, 0))
|
|
135
|
+
|
|
136
|
+
# 6. Enhancement level - bottom right of the card.
|
|
137
|
+
raw_level = relic_core.get("level", 1)
|
|
138
|
+
display_level = f"+{raw_level - 1}"
|
|
139
|
+
draw.text((x + CARD_W - 185, y + CARD_H - 15), display_level, font=font, fill=(255, 204, 0), stroke_width=1, stroke_fill=(0, 0, 0), anchor="rm")
|
|
140
|
+
|
|
141
|
+
# 7. CV (crit value) score - top right of the card. Drawn last, on top
|
|
142
|
+
# of everything else (icon/stars/grid), so it's never painted over.
|
|
143
|
+
cv_score = _compute_cv(main_data, sub_stats)
|
|
144
|
+
cv_text = f"CV{cv_score:.1f}"
|
|
145
|
+
draw.text((x + CARD_W - 185, y + 25), cv_text, font=font, fill=(255, 255, 255), stroke_width=1, stroke_fill=(0, 0, 0), anchor="rm")
|
|
146
|
+
|
|
147
|
+
async def draw_horizontal_artifacts(session, background, char_data, start_x, start_y, font):
|
|
148
|
+
"""
|
|
149
|
+
Finds all artifacts for a single character and draws them in a
|
|
150
|
+
horizontal row (Flower -> Feather -> Sands -> Goblet -> Circlet).
|
|
151
|
+
"""
|
|
152
|
+
# Filter only relics (artifacts)
|
|
153
|
+
relics = [e for e in char_data.get("equipList", []) if "reliquary" in e]
|
|
154
|
+
|
|
155
|
+
# Sort by EQUIP_ORDER
|
|
156
|
+
sorted_relics = sorted(
|
|
157
|
+
relics,
|
|
158
|
+
key=lambda x: EQUIP_ORDER.index(x.get("flat", {}).get("equipType", ""))
|
|
159
|
+
if x.get("flat", {}).get("equipType") in EQUIP_ORDER else 99
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
tasks = []
|
|
163
|
+
spacing_x = 345
|
|
164
|
+
|
|
165
|
+
for i, art in enumerate(sorted_relics[:5]):
|
|
166
|
+
curr_x = start_x + (i * spacing_x)
|
|
167
|
+
tasks.append(draw_artifact_card(session, background, curr_x, start_y, art, font))
|
|
168
|
+
|
|
169
|
+
if tasks:
|
|
170
|
+
await asyncio.gather(*tasks)
|