From 04df9e8f629e51eb87549a2f79469d596d3a5de6 Mon Sep 17 00:00:00 2001 From: esefxdz Date: Sat, 11 Jul 2026 21:40:18 +0300 Subject: [PATCH] found a new portrait site --- blue_archive/constants.py | 57 ++++++++++++++++++++---------- blue_archive/data.py | 41 +++++----------------- blue_archive/gacha_renderer.py | 63 ++++++++++------------------------ cogs/sysinfo.py | 5 ++- 4 files changed, 69 insertions(+), 97 deletions(-) diff --git a/blue_archive/constants.py b/blue_archive/constants.py index df29f6c..d2b78b7 100644 --- a/blue_archive/constants.py +++ b/blue_archive/constants.py @@ -11,8 +11,7 @@ BG_PATH = ASSETS_DIR / "gacha_bg.png" # ── APIs ──────────────────────────────────────────────────────────────────── BANNER_API = "https://api.ennead.cc/buruaka/banner" -SCHALE_PORTRAIT = "https://raw.githubusercontent.com/SchaleDB/SchaleDB/main/images/student/portrait" -WIKI_CDN = "https://static.wikitide.net/bluearchivewiki" +JOEXYZ_CDN = "https://ba.joexyz.online/cdn/v2/images" # ── Gacha rates (matching real Blue Archive) ──────────────────────────────── # Regular/Pickup: 3★ = 3.0%, 2★ = 18.5%, 1★ = 78.5% @@ -69,25 +68,45 @@ PAGE_SIZE = 10 POOL_TOTALS = {3: 106, 2: 22, 1: 11} # standard-pool size for progress bars FILTER_LABELS: dict[str, int | None] = {"All": None, "3★": 3, "2★": 2, "1★": 1} -# ── Wiki name overrides (SchaleDB name → wiki Portrait_ filename stem) ────── -WIKI_NAME_MAP: dict[str, str] = { - "Aris": "Arisu", - "Shiroko (Cycling)": "Shiroko_(Riding)", - "Shun (Small)": "Shun_(Kid)", - "Neru (Bunny)": "Neru_(Bunny_Girl)", - "Karin (Bunny)": "Karin_(Bunny_Girl)", - "Asuna (Bunny)": "Asuna_(Bunny_Girl)", - "Utaha (Cheer Squad)": "Utaha_(Cheerleader)", - "Hibiki (Cheer Squad)": "Hibiki_(Cheerleader)", - "Akane (Bunny)": "Akane_(Bunny_Girl)", - "Aris (Maid)": "Arisu_(Maid)", - "Toki (Bunny)": "Toki_(Bunny_Girl)", - "Kotori (Cheer Squad)": "Kotori_(Cheerleader)", - "Kotama (Camp)": "Kotama_(Camping)", - "Hare (Camp)": "Hare_(Camping)", - "Shiroko Terror": "Shiroko_(Terror)", +# ── CDN icon slug mapping (SchaleDB variant suffix → joexyz CDN slug suffix) ─ +# Base names without a parenthesised variant use the lowercased name directly. +# "Aris" is a special case — the CDN uses "aris", not "arisu". +CDN_SUFFIX_MAP: dict[str, str] = { + "New Year": "newyear", + "Hot Spring": "onsen", + "Bunny": "bunnygirl", + "Cheer Squad": "cheerleader", + "Camp": "camp", + "Cycling": "cycling", + "Dress": "dress", + "Small": "small", + "Maid": "maid", + "Track": "track", + "Swimsuit": "swimsuit", + "Band": "band", + "Christmas": "christmas", + "Casual": "casual", + "Guide": "guide", } +def cdn_icon_slug(name: str) -> str: + """Convert a SchaleDB student Name to the joexyz CDN icon filename slug.""" + # Normalise "Name Terror" into the same parenthesised path + if name.endswith(" Terror"): + name = name[:-7] + " (Terror)" + # "Aris" is "aris" on the CDN, not "arisu" + if name == "Aris": + return "aris" + + if "(" in name: + base, rest = name.split("(", 1) + base = base.strip().lower().replace(" ", "_") + variant = rest.rstrip(")").strip() + suffix = CDN_SUFFIX_MAP.get(variant, variant.lower().replace(" ", "_")) + return f"{base}_{suffix}" + + return name.lower().replace(" ", "_") + # ── Font paths ────────────────────────────────────────────────────────────── FONT_PATHS = [ "C:/Windows/Fonts/segoeui.ttf", diff --git a/blue_archive/data.py b/blue_archive/data.py index f65fd78..8716cb0 100644 --- a/blue_archive/data.py +++ b/blue_archive/data.py @@ -1,11 +1,9 @@ #this module loads student data and fetches live banner info from the api## """Student and banner data layer for the Blue Archive gacha simulator.""" ###################################################################### -import hashlib import json import logging import random -import urllib.parse from datetime import datetime, timezone from pathlib import Path from typing import Optional @@ -15,9 +13,8 @@ import aiohttp from .constants import ( BUNDLED_DB, BANNER_API, - SCHALE_PORTRAIT, - WIKI_CDN, - WIKI_NAME_MAP, + JOEXYZ_CDN, + cdn_icon_slug, BANNER_RATES, DEFAULT_RATES, PULL10_RATES, @@ -26,27 +23,6 @@ from .constants import ( log = logging.getLogger(__name__) -def _wiki_filename(student: dict) -> str: - """Build the wiki Portrait_ filename (no extension) for a student.""" - name = student["Name"] - mapped = WIKI_NAME_MAP.get(name) - if mapped is not None: - return mapped - return name.replace(" ", "_") - - -def wiki_portrait_url(student: dict) -> str: - """Compute the CDN URL for a student's wiki portrait (252x204 face image).""" - filename = f"Portrait_{_wiki_filename(student)}.png" - h = hashlib.md5(filename.encode()).hexdigest() - return f"{WIKI_CDN}/{h[0]}/{h[:2]}/{urllib.parse.quote(filename)}" - - -def schale_portrait_url(student_id: int) -> str: - """Full-body sprite URL (fallback if wiki portrait is unavailable).""" - return f"{SCHALE_PORTRAIT}/{student_id}.webp" - - class StudentDB: """In-memory student database loaded from the bundled SchaleDB JSON.""" @@ -177,13 +153,14 @@ class StudentDB: pool = self.by_rarity.get(rarity, []) return random.choice(pool) if pool else random.choice(self.students) - def wiki_portrait(self, student: dict) -> str: - """Wiki face-portrait CDN URL for a student dict (252x204).""" - return wiki_portrait_url(student) + def cdn_icon(self, student: dict) -> str: + """joexyz CDN icon URL for a student (face/upper-body).""" + slug = cdn_icon_slug(student["Name"]) + return f"{JOEXYZ_CDN}/students/icons/{slug}.png" - def schale_portrait(self, student_id: int) -> str: - """SchaleDB full-body sprite URL (fallback).""" - return schale_portrait_url(student_id) + def cdn_skill_portrait(self, student: dict) -> str: + """joexyz CDN skill-portrait URL (full-body fallback).""" + return f"{JOEXYZ_CDN}/skill-portraits/Skill_Portrait_{student['DevName']}.png" # Global singleton diff --git a/blue_archive/gacha_renderer.py b/blue_archive/gacha_renderer.py index 41ffe3c..541c99d 100644 --- a/blue_archive/gacha_renderer.py +++ b/blue_archive/gacha_renderer.py @@ -45,56 +45,42 @@ FOOTER_FONT = _get_font(18, bold=False) # ── Image fetching ───────────────────────────────────────────────────────── async def _fetch_portrait(session: aiohttp.ClientSession, student: dict) -> Optional[Image.Image]: - """Download a student's face portrait. + """Download a student's face icon from the joexyz CDN. - Tries the wiki CDN first (252x204 face image). Falls back to SchaleDB - full-body sprite with an upper-body crop on failure. + Uses the student icon CDN (face/upper-body crop). Falls back to the + skill-portrait (full-body art) if the icon is unavailable. """ - student_id = student["Id"] - - # 1. Try wiki face portrait - wiki_url = db.wiki_portrait(student) + # 1. Try the face icon + icon_url = db.cdn_icon(student) try: - async with session.get(wiki_url, timeout=aiohttp.ClientTimeout(total=10)) as resp: + async with session.get(icon_url, timeout=aiohttp.ClientTimeout(total=10)) as resp: if resp.status == 200: data = await resp.read() return Image.open(io.BytesIO(data)).convert("RGBA") except Exception: - log.debug("Wiki portrait failed for %s, trying SchaleDB fallback", student["Name"]) + log.debug("CDN icon failed for %s, trying skill portrait fallback", student["Name"]) - # 2. Fallback: SchaleDB full-body sprite with upper-body crop - schale_url = db.schale_portrait(student_id) + # 2. Fallback: skill portrait (full-body character art) + sp_url = db.cdn_skill_portrait(student) try: - async with session.get(schale_url, timeout=aiohttp.ClientTimeout(total=10)) as resp: + async with session.get(sp_url, timeout=aiohttp.ClientTimeout(total=10)) as resp: if resp.status == 200: data = await resp.read() img = Image.open(io.BytesIO(data)).convert("RGBA") - # Crop top portion for face/upper-body (source is ~614x1024) + # Skill portraits are full-body; crop upper portion for card fit p_w, p_h = img.size crop_h = int(p_w * 1.20) if crop_h < p_h: img = img.crop((0, 0, p_w, crop_h)) return img except Exception: - log.exception("Failed to fetch portrait for student %d", student_id) + log.exception("Failed to fetch any portrait for %s", student["Name"]) return None # ── Drawing helpers ──────────────────────────────────────────────────────── -def _rounded_rect( - draw: ImageDraw.Draw, - xy: tuple[int, int, int, int], - radius: int, - fill: Optional[tuple] = None, - outline: Optional[tuple] = None, - width: int = 3, -) -> None: - """Draw a rounded rectangle.""" - draw.rounded_rectangle(xy, radius=radius, fill=fill, outline=outline, width=width) - - def _star_polygon(cx: int, cy: int, outer_r: int, inner_r: int) -> list[tuple[int, int]]: """Generate vertex list for a 5-pointed star centered at (cx, cy). @@ -132,17 +118,6 @@ def _draw_star_icons( draw.polygon(pts, fill=color) -def _draw_rounded_rect( - draw: ImageDraw.Draw, - xy: tuple[int, int, int, int], - radius: int, - fill: Optional[tuple] = None, - outline: Optional[tuple] = None, - width: int = 3, -) -> None: - _rounded_rect(draw, xy, radius, fill, outline, width) - - # ── Main render ──────────────────────────────────────────────────────────── async def render_pull( @@ -257,18 +232,17 @@ def _draw_card( bg = RARITY_BG.get(rarity, (20, 20, 20)) # Card background with rounded corners - _draw_rounded_rect(draw, (x, y, x + CARD_W, y + CARD_H), radius=CARD_RADIUS, fill=bg + (255,)) + draw.rounded_rectangle((x, y, x + CARD_W, y + CARD_H), radius=CARD_RADIUS, fill=bg + (255,)) # Card border - _draw_rounded_rect( - draw, (x, y, x + CARD_W, y + CARD_H), radius=CARD_RADIUS, outline=color, width=2 + draw.rounded_rectangle( + (x, y, x + CARD_W, y + CARD_H), radius=CARD_RADIUS, outline=color, width=2 ) # ── Star banner strip at top ──────────────────────────────────────── banner_top = y + 1 banner_bottom = y + STAR_BANNER_H # Semi-transparent dark strip - _draw_rounded_rect( - draw, + draw.rounded_rectangle( (x + 3, banner_top, x + CARD_W - 3, banner_bottom), radius=CARD_RADIUS - 2, fill=(0, 0, 0, 80), @@ -299,7 +273,7 @@ def _draw_card( poy = py + (ph - new_h) // 2 canvas.paste(portrait, (pox, poy), portrait if portrait.mode == "RGBA" else None) else: - _draw_rounded_rect(draw, (px, py, px + pw, py + ph), radius=6, fill=(30, 30, 40, 180)) + draw.rounded_rectangle((px, py, px + pw, py + ph), radius=6, fill=(30, 30, 40, 180)) ph_font = _get_font(16) msg = "No Image" mw = draw.textlength(msg, font=ph_font) @@ -307,8 +281,7 @@ def _draw_card( # ── Name strip at bottom ──────────────────────────────────────────── name_y = y + CARD_H - NAME_STRIP_H + 2 - _draw_rounded_rect( - draw, + draw.rounded_rectangle( (x + 3, name_y, x + CARD_W - 3, y + CARD_H - 3), radius=CARD_RADIUS - 3, fill=(0, 0, 0, 150), diff --git a/cogs/sysinfo.py b/cogs/sysinfo.py index 8e0cc18..86244fb 100644 --- a/cogs/sysinfo.py +++ b/cogs/sysinfo.py @@ -18,7 +18,10 @@ class SysInfo(commands.Cog): await ctx.reply('\n'.join(sys_commands)) ###################################################################### - @commands.command() + @commands.command() + + + async def fetch(self, ctx): result = subprocess.run(['fastfetch', '--pipe', '--logo', 'none'], capture_output=True, text=True) clean = self.strip_ansi(result.stdout)