diff --git a/blue_archive/constants.py b/blue_archive/constants.py new file mode 100644 index 0000000..df29f6c --- /dev/null +++ b/blue_archive/constants.py @@ -0,0 +1,99 @@ +#this holds all shared constants for the blue archive gacha system## +"""Centralised constants — colours, rates, layout, and paths.""" +###################################################################### +from pathlib import Path + +# ── Paths ─────────────────────────────────────────────────────────────────── +BUNDLED_DB = Path(__file__).parent / "ba_students.json" +BANNER_FILE = Path("data/ba_banner_state.json") +ASSETS_DIR = Path(__file__).parent / "assets" +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" + +# ── Gacha rates (matching real Blue Archive) ──────────────────────────────── +# Regular/Pickup: 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% (guaranteed 2★+) +DEFAULT_RATES = (0.03, 0.185, 0.785) +PULL10_RATES = (0.03, 0.97, 0.0) +BANNER_RATES: dict[str, tuple[float, float, float]] = { + "FesGacha": (0.06, 0.185, 0.755), +} + +# ── Spark & Eligma ────────────────────────────────────────────────────────── +SPARK_TARGET = 200 +ELIGMA_YIELD = {1: 1, 2: 10, 3: 50} + +# ── Rarity colours (frame borders, stars, card backgrounds) ───────────────── +RARITY_COLOR = { + 3: (250, 210, 255), # bright purple + 2: (255, 252, 160), # bright gold + 1: (190, 235, 255), # bright sky blue +} +RARITY_BG = { + 3: (42, 26, 54), + 2: (54, 48, 26), + 1: (26, 34, 54), +} +RARITY_EMBED_COLOR = { + 3: 0xF0D0FF, + 2: 0xFFF9A0, + 1: 0xC0E8FF, +} +RARITY_LABEL = {3: "3★", 2: "2★", 1: "1★"} +RARITY_SYMBOL = {3: "◆", 2: "⬟", 1: "●"} + +# ── Canvas & card layout (1920×1080) ──────────────────────────────────────── +CANVAS_W, CANVAS_H = 1920, 1080 +CARD_W, CARD_H = 280, 390 +CARD_RADIUS = 10 +GAP_X, GAP_Y = 25, 20 +COLS, ROWS = 5, 2 +GRID_W = COLS * CARD_W + (COLS - 1) * GAP_X # 1500 +GRID_H = ROWS * CARD_H + (ROWS - 1) * GAP_Y # 800 +GRID_X = (CANVAS_W - GRID_W) // 2 # 210 +GRID_Y = 260 +HEADER_Y = 120 +FOOTER_Y = GRID_Y + GRID_H + 50 +STAR_BANNER_H = 36 +NAME_STRIP_H = 42 +PORTRAIT_TOP = STAR_BANNER_H + 4 +PORTRAIT_H = CARD_H - PORTRAIT_TOP - NAME_STRIP_H - 4 + +# ── Inventory display ─────────────────────────────────────────────────────── +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)", +} + +# ── Font paths ────────────────────────────────────────────────────────────── +FONT_PATHS = [ + "C:/Windows/Fonts/segoeui.ttf", + "C:/Windows/Fonts/segoeuib.ttf", + "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/arialbd.ttf", + "/usr/share/fonts/TTF/DejaVuSans.ttf", + "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf", +] diff --git a/blue_archive/data.py b/blue_archive/data.py index 46a82c9..f65fd78 100644 --- a/blue_archive/data.py +++ b/blue_archive/data.py @@ -12,37 +12,19 @@ from typing import Optional import aiohttp +from .constants import ( + BUNDLED_DB, + BANNER_API, + SCHALE_PORTRAIT, + WIKI_CDN, + WIKI_NAME_MAP, + BANNER_RATES, + DEFAULT_RATES, + PULL10_RATES, +) + log = logging.getLogger(__name__) -BUNDLED_DB = Path(__file__).parent / "ba_students.json" -BANNER_API = "https://api.ennead.cc/buruaka/banner" -SCHALE_PORTRAIT = "https://raw.githubusercontent.com/SchaleDB/SchaleDB/main/images/student/portrait" -SCHALE_ICON = "https://raw.githubusercontent.com/SchaleDB/SchaleDB/main/images/student/icon" - -# Wiki portrait CDN: static.wikitide.net/bluearchivewiki/{md5[0]}/{md5[:2]}/{filename} -WIKI_CDN = "https://static.wikitide.net/bluearchivewiki" - -# schaleDB Name → wiki Portrait_ filename (without .png extension) -# Most work automatically by replacing spaces with underscores; these are the exceptions. -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)", - # Track variants — no wiki portrait; fall back to SchaleDB -} - def _wiki_filename(student: dict) -> str: """Build the wiki Portrait_ filename (no extension) for a student.""" @@ -208,24 +190,8 @@ class StudentDB: db = StudentDB() -# --- Banner types & rates --------------------------------------------------- - -# 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) - -# 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) - - def roll_rarity(rates: tuple[float, float, float]) -> int: - """Roll rarity tier given (p_3star, p_2star, p_1star). Returns 3, 2, or 1.""" + """Roll rarity tier — returns 3, 2, or 1.""" r = random.random() if r < rates[0]: return 3 diff --git a/blue_archive/db.py b/blue_archive/db.py index 463bd5c..e55c2e5 100644 --- a/blue_archive/db.py +++ b/blue_archive/db.py @@ -5,13 +5,12 @@ import sqlite3 import logging from pathlib import Path +from .constants import ELIGMA_YIELD + log = logging.getLogger(__name__) DB_PATH = Path("data/ba_gacha.db") -# Eligma earned per duplicate by rarity -ELIGMA_YIELD = {1: 1, 2: 10, 3: 50} - def _connect() -> sqlite3.Connection: """Open a connection with WAL mode for concurrent reads.""" diff --git a/blue_archive/gacha.py b/blue_archive/gacha.py index 3cba764..bdc1cf5 100644 --- a/blue_archive/gacha.py +++ b/blue_archive/gacha.py @@ -2,6 +2,7 @@ """Blue Archive Gacha Simulator — in-game-accurate recruitment with live banners.""" ###################################################################### import asyncio +import json import logging from pathlib import Path from typing import Optional @@ -16,16 +17,12 @@ from .data import ( format_banner_embed, roll_rarity, get_rates_for_banner, - PULL10_RATES, - DEFAULT_RATES, ) from .gacha_renderer import render_pull +from .constants import SPARK_TARGET, BANNER_FILE, DEFAULT_RATES, PULL10_RATES log = logging.getLogger(__name__) -SPARK_TARGET = 200 -BANNER_FILE = Path("data/ba_banner_state.json") - def _load_banner_state() -> dict: """Which banner each user has picked. Lightweight JSON, separate from SQLite.""" diff --git a/blue_archive/gacha_renderer.py b/blue_archive/gacha_renderer.py index 727a7c6..41ffe3c 100644 --- a/blue_archive/gacha_renderer.py +++ b/blue_archive/gacha_renderer.py @@ -11,56 +11,25 @@ import aiohttp from PIL import Image, ImageDraw, ImageFont from .data import db +from .constants import ( + CANVAS_W, CANVAS_H, + CARD_W, CARD_H, CARD_RADIUS, + GAP_X, GAP_Y, COLS, ROWS, + GRID_W, GRID_H, GRID_X, GRID_Y, + HEADER_Y, FOOTER_Y, + STAR_BANNER_H, NAME_STRIP_H, PORTRAIT_TOP, PORTRAIT_H, + RARITY_COLOR, RARITY_BG, + BG_PATH, + FONT_PATHS, +) log = logging.getLogger(__name__) -# 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 -GAP_X, GAP_Y = 25, 20 -COLS, ROWS = 5, 2 -GRID_W = COLS * CARD_W + (COLS - 1) * GAP_X # 1500 -GRID_H = ROWS * CARD_H + (ROWS - 1) * GAP_Y # 800 -GRID_X = (CANVAS_W - GRID_W) // 2 # 210 -GRID_Y = 260 # below the header -HEADER_Y = 120 -FOOTER_Y = GRID_Y + GRID_H + 50 - -STAR_BANNER_H = 36 # star rating strip at top of card -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 frame and star colours -RARITY_COLORS = { - 3: (240, 190, 255), # bright purple - 2: (255, 250, 140), # bright gold - 1: (170, 225, 255), # bright sky blue -} -RARITY_BG = { - 3: (38, 22, 50), - 2: (50, 42, 22), - 1: (22, 30, 50), -} - -ASSETS_DIR = Path(__file__).parent / "assets" -BG_PATH = ASSETS_DIR / "gacha_bg.png" - # ── Font helpers ─────────────────────────────────────────────────────────── def _get_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont: """Try to load a nice font; fall back to default.""" - font_paths = [ - "C:/Windows/Fonts/segoeui.ttf", - "C:/Windows/Fonts/segoeuib.ttf", - "C:/Windows/Fonts/arial.ttf", - "C:/Windows/Fonts/arialbd.ttf", - "/usr/share/fonts/TTF/DejaVuSans.ttf", - "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf", - ] - for fp in font_paths: + for fp in FONT_PATHS: if Path(fp).exists(): try: return ImageFont.truetype(fp, size) @@ -284,7 +253,7 @@ def _draw_card( ) -> None: """Draw a single recruitment result card at (x, y).""" rarity = student["StarGrade"] - color = RARITY_COLORS.get(rarity, (150, 150, 150)) + color = RARITY_COLOR.get(rarity, (150, 150, 150)) bg = RARITY_BG.get(rarity, (20, 20, 20)) # Card background with rounded corners diff --git a/blue_archive/inventory.py b/blue_archive/inventory.py index c5f6e11..e7c9430 100644 --- a/blue_archive/inventory.py +++ b/blue_archive/inventory.py @@ -8,23 +8,18 @@ from discord.ext import commands from . import db as gacha_db from .data import db +from .constants import ( + PAGE_SIZE, POOL_TOTALS, FILTER_LABELS, + RARITY_SYMBOL, RARITY_LABEL, RARITY_EMBED_COLOR, + ELIGMA_YIELD, +) log = logging.getLogger(__name__) -PAGE_SIZE = 10 -RARITY_ICON = {3: "<:purple:>", 2: "<:gold:>", 1: "<:blue:>"} -RARITY_SYMBOL = {3: "◆", 2: "⬟", 1: "●"} -RARITY_LABEL = {3: "3★", 2: "2★", 1: "1★"} -RARITY_COLOR = {3: 0xE6B8FF, 2: 0xFFF58C, 1: 0xAAD4FF} -ELIGMA_YIELD = {1: 1, 2: 10, 3: 50} - -# Pool totals for progress bars (standard pool only) -POOL_TOTALS = {3: 106, 2: 22, 1: 11} - +# Local UI labels PAGE_LEFT = "◀" PAGE_RIGHT = "▶" FILTER_ALL = "All" -FILTER_LABELS = {FILTER_ALL: None, "3★": 3, "2★": 2, "1★": 1} def _bar(current: int, total: int, width: int = 8) -> str: