diff --git a/blue_archive/data.py b/blue_archive/data.py index 2287d93..46a82c9 100644 --- a/blue_archive/data.py +++ b/blue_archive/data.py @@ -128,13 +128,16 @@ class StudentDB: return self.by_name.get(name.lower()) def build_pool(self, banner: Optional[dict]) -> dict[int, list[dict]]: - """Build the effective character pool for a banner. + """Build the pullable character pool for a specific banner. - Returns {1: [...], 2: [...], 3: [...]} with characters pullable on this banner. + Pool rules match the real game: + - Regular: standard characters only (IsLimited == 0) + - Pickup: standard pool + rate-up students + - Limited: standard pool + limited rate-up (only that banner) + - Fes: standard pool + ALL limiteds (IsLimited == 1) + rate-ups - - Regular Recruitment (banner=None): standard pool only - - PickupGacha / LimitedGacha: standard pool + rate-up students - - FesGacha: standard pool + all limiteds + rate-ups + Returns {1: [...], 2: [...], 3: [...]}. + Pass banner=None for regular recruitment. """ pool: dict[int, list[dict]] = { r: list(self._std_pool[r]) for r in (1, 2, 3) @@ -165,10 +168,12 @@ class StudentDB: return pool def weighted_pick(self, pool: list[dict], rateup_names: list[str], rarity: int) -> dict: - """Pick a student from a pool with rate-up weighting. + """Pick a random student from a rarity pool with rate-up weighting. - For 3★ rarity, rate-up students each get ~23% weight (matching the - real game's 0.7% per rate-up out of 3% total). For 1-2★, uniform. + For 3★: rate-up characters get 3x higher weight than standard ones, + producing roughly the same rate-up frequency as the real game's 0.7% + per rate-up out of the 3% total. + For 1-2★: uniform random (rate-ups don't affect lower rarities). """ if not pool: return random.choice(self.students) @@ -205,16 +210,17 @@ db = StudentDB() # --- Banner types & rates --------------------------------------------------- -# In-game rates per banner type -# Format: { gachaType: (rate_3star, rate_2star, rate_1star) } -# Fes banners double the 3-star rate to 6% +# Rate tables matching real Blue Archive gacha: +# Regular: 3★ = 3.0%, 2★ = 18.5%, 1★ = 78.5% +# Fes: 3★ = 6.0%, 2★ = 18.5%, 1★ = 75.5% +# 10th pull: 3★ = 3.0%, 2★ = 97.0%, 1★ = 0% (2★+ guarantee) BANNER_RATES: dict[str, tuple[float, float, float]] = { "FesGacha": (0.06, 0.185, 0.755), } DEFAULT_RATES = (0.03, 0.185, 0.785) -# The 10th pull on a 10-pull: guarantee at least 2-star -# (97% 2-star, 3% 3-star — the 1-star rate is redirected into 2-star) +# 10th pull on a 10-pull guarantees 2★ minimum: +# 3★ = 3.0%, 2★ = 97.0%, 1★ = 0% PULL10_RATES = (0.03, 0.97, 0.0) @@ -239,7 +245,8 @@ def get_rates_for_banner(banner: dict) -> tuple[float, float, float]: async def fetch_banners(session: Optional[aiohttp.ClientSession] = None) -> dict: """Fetch live banner data from the BlueArchiveAPI. - Returns the parsed JSON: {"current": [...], "upcoming": [...], "ended": [...]} + Returns {"current": [...], "upcoming": [...], "ended": [...]}. + Each banner has: id, gachaType, rateups (list of names), startedAt, endedAt. """ close_session = session is None if close_session: diff --git a/blue_archive/gacha.py b/blue_archive/gacha.py index 4f91b21..12cd8cc 100644 --- a/blue_archive/gacha.py +++ b/blue_archive/gacha.py @@ -101,7 +101,7 @@ class BlueArchiveGacha(commands.Cog): @commands.group(name="gacha", aliases=["banners", "g"], invoke_without_command=True) async def gacha(self, ctx: commands.Context) -> None: - """Show live recruitment banners. Use subcommands: pick, info.""" + """Show currently active recruitment banners from the live API.""" try: await asyncio.wait_for(self._cache_ready.wait(), timeout=15.0) except asyncio.TimeoutError: @@ -193,10 +193,10 @@ class BlueArchiveGacha(commands.Cog): @commands.command(name="pull") async def pull_gacha(self, ctx: commands.Context, *, mode: str = "10") -> None: - """Perform a gacha pull. Default is 10-pull. + """Pull on your active recruitment banner. - Usage: !pull (10-pull) - !pull single (1-pull) + Usage: !pull (10-pull with guaranteed 2★+ on 10th) + !pull single (1-pull) """ mode = mode.strip().lower() if mode in ("single", "1", "one"): diff --git a/blue_archive/gacha_renderer.py b/blue_archive/gacha_renderer.py index 59fb145..727a7c6 100644 --- a/blue_archive/gacha_renderer.py +++ b/blue_archive/gacha_renderer.py @@ -14,7 +14,8 @@ from .data import db log = logging.getLogger(__name__) -# ── Layout constants (1920×1080 canvas) ──────────────────────────────────── +# Layout: 1920x1080 canvas, 5x2 card grid, 10px rounded corners +# Each card: star banner → portrait → name strip CANVAS_W, CANVAS_H = 1920, 1080 CARD_W, CARD_H = 280, 390 CARD_RADIUS = 10 # corner radius @@ -32,16 +33,16 @@ NAME_STRIP_H = 42 # name/school strip at bottom PORTRAIT_TOP = STAR_BANNER_H + 4 # portrait starts below star banner PORTRAIT_H = CARD_H - PORTRAIT_TOP - NAME_STRIP_H - 4 -# Rarity colours +# Rarity frame and star colours RARITY_COLORS = { - 3: (210, 150, 250), # purple (bright) - 2: (255, 225, 80), # gold (bright) - 1: (130, 200, 255), # blue (bright) + 3: (240, 190, 255), # bright purple + 2: (255, 250, 140), # bright gold + 1: (170, 225, 255), # bright sky blue } RARITY_BG = { - 3: (30, 18, 42), - 2: (42, 34, 18), - 1: (18, 26, 42), + 3: (38, 22, 50), + 2: (50, 42, 22), + 1: (22, 30, 50), } ASSETS_DIR = Path(__file__).parent / "assets" @@ -126,7 +127,10 @@ def _rounded_rect( def _star_polygon(cx: int, cy: int, outer_r: int, inner_r: int) -> list[tuple[int, int]]: - """Return vertices for a 5-pointed star centered at (cx, cy).""" + """Generate vertex list for a 5-pointed star centered at (cx, cy). + + Uses 10 alternating outer/inner radius points, rotated by pi/5 each step. + """ import math points = [] for i in range(10):