Use bluearchive.wiki portraits for gacha cards

This commit is contained in:
esef committed 2026-09-15 16:35:04 +03:00
1 parent e4b5f0d849
commit c510a1b63f
3 files changed
+36 -8

No files matched your search

+11
View File
@@ -17,6 +17,17 @@ GACHA_ANIM_DURATION = 3.5 # 195 frames × 100 ms (used to be 19.5 will be chang
BANNER_API = "https://api.ennead.cc/buruaka/banner" BANNER_API = "https://api.ennead.cc/buruaka/banner"
CHARACTER_API = "https://api.ennead.cc/buruaka/character" CHARACTER_API = "https://api.ennead.cc/buruaka/character"
PORTRAIT_CDN = "https://schaledb.com/images" PORTRAIT_CDN = "https://schaledb.com/images"
WIKI_PORTRAIT_BASE = "https://bluearchive.wiki/wiki/Special:FilePath/"
WIKI_VARIANT_MAP: dict[str, str] = {
"Bunny": "Bunny Girl",
"School": "School Uniform",
"Armed": "Battle",
"Track": "Sportswear",
"Cheer Squad": "Cheerleader",
"Pajamas": "Pajama",
"Cycling": "Riding",
"Small": "Kid",
}
# ── Gacha rates (matching real Blue Archive) ──────────────────────────────── # ── Gacha rates (matching real Blue Archive) ────────────────────────────────
# Regular/Pickup: 3★ = 3.0%, 2★ = 18.5%, 1★ = 78.5% # Regular/Pickup: 3★ = 3.0%, 2★ = 18.5%, 1★ = 78.5%
+20 -1
View File
@@ -7,6 +7,7 @@ import random
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from urllib.parse import quote
import aiohttp import aiohttp
@@ -15,6 +16,8 @@ from .constants import (
BANNER_API, BANNER_API,
CHARACTER_API, CHARACTER_API,
PORTRAIT_CDN, PORTRAIT_CDN,
WIKI_PORTRAIT_BASE,
WIKI_VARIANT_MAP,
BANNER_RATES, BANNER_RATES,
DEFAULT_RATES, DEFAULT_RATES,
PULL10_RATES, PULL10_RATES,
@@ -304,7 +307,23 @@ class StudentDB:
return random.choice(pullable or self.students) return random.choice(pullable or self.students)
def cdn_portrait(self, student: dict) -> str: def cdn_portrait(self, student: dict) -> str:
"""SchaleDB CDN full-body portrait URL for a student (keyed by ID).""" """bluearchive.wiki portrait URL (bust art, keyed by student name)."""
name = student["Name"]
if name.endswith(" Terror"):
name = name[:-7] + " (Terror)"
if name == "Aris":
name = "Arisu"
elif name.startswith("Aris ("):
name = "Arisu " + name[5:]
if "(" in name:
base, variant = name.split("(", 1)
variant = variant.rstrip(")").strip()
variant = WIKI_VARIANT_MAP.get(variant, variant)
name = f"{base.strip()} ({variant})"
return WIKI_PORTRAIT_BASE + quote(f"Portrait {name}.png", safe="")
def cdn_sprite(self, student: dict) -> str:
"""SchaleDB CDN full-body sprite URL (fallback, keyed by ID)."""
return f"{PORTRAIT_CDN}/student/portrait/{student['Id']}.webp" return f"{PORTRAIT_CDN}/student/portrait/{student['Id']}.webp"
+5 -7
View File
@@ -48,25 +48,23 @@ FOOTER_FONT = _get_font(18, bold=False)
# ── Image fetching ───────────────────────────────────────────────────────── # ── Image fetching ─────────────────────────────────────────────────────────
async def _fetch_portrait(session: aiohttp.ClientSession, student: dict) -> Optional[Image.Image]: async def _fetch_portrait(session: aiohttp.ClientSession, student: dict) -> Optional[Image.Image]:
"""Download a student's full-body portrait and crop it to the card area. """Download a student's portrait (wiki bust art first, full sprite fallback)."""
for url in (db.cdn_portrait(student), db.cdn_sprite(student)):
Portraits come from the SchaleDB image CDN, keyed by student ID.
"""
url = db.cdn_portrait(student)
try: try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp: async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
if resp.status == 200: if resp.status == 200:
data = await resp.read() data = await resp.read()
img = Image.open(io.BytesIO(data)).convert("RGBA") img = Image.open(io.BytesIO(data)).convert("RGBA")
# Full-body art; crop the upper portion to fit the card. # Full-body art is tall; crop the upper portion to fit the card.
p_w, p_h = img.size p_w, p_h = img.size
crop_h = int(p_w * 1.20) crop_h = int(p_w * 1.20)
if crop_h < p_h: if crop_h < p_h:
img = img.crop((0, 0, p_w, crop_h)) img = img.crop((0, 0, p_w, crop_h))
return img return img
except Exception: except Exception:
log.exception("Failed to fetch portrait for %s", student["Name"]) log.debug("portrait fetch failed via %s", url)
log.warning("no portrait for %s", student["Name"])
return None return None