Use bluearchive.wiki portraits for gacha cards
This commit is contained in:
1 parent
e4b5f0d849
commit
c510a1b63f
3 files changed
+47
-19
No files matched your search
@@ -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
@@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
try:
|
||||||
"""
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
|
||||||
url = db.cdn_portrait(student)
|
if resp.status == 200:
|
||||||
try:
|
data = await resp.read()
|
||||||
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
|
img = Image.open(io.BytesIO(data)).convert("RGBA")
|
||||||
if resp.status == 200:
|
# Full-body art is tall; crop the upper portion to fit the card.
|
||||||
data = await resp.read()
|
p_w, p_h = img.size
|
||||||
img = Image.open(io.BytesIO(data)).convert("RGBA")
|
crop_h = int(p_w * 1.20)
|
||||||
# Full-body art; crop the upper portion to fit the card.
|
if crop_h < p_h:
|
||||||
p_w, p_h = img.size
|
img = img.crop((0, 0, p_w, crop_h))
|
||||||
crop_h = int(p_w * 1.20)
|
return img
|
||||||
if crop_h < p_h:
|
except Exception:
|
||||||
img = img.crop((0, 0, p_w, crop_h))
|
log.debug("portrait fetch failed via %s", url)
|
||||||
return img
|
|
||||||
except Exception:
|
|
||||||
log.exception("Failed to fetch portrait for %s", student["Name"])
|
|
||||||
|
|
||||||
|
log.warning("no portrait for %s", student["Name"])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user