extract constants to central module, brighter rarity colors
This commit is contained in:
1 parent
9500a2902f
commit
de63e3b098
6 files changed
+134
-109
No files matched your search
@@ -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",
|
||||||
|
]
|
||||||
+12
-46
@@ -12,37 +12,19 @@ from typing import Optional
|
|||||||
|
|
||||||
import aiohttp
|
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__)
|
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:
|
def _wiki_filename(student: dict) -> str:
|
||||||
"""Build the wiki Portrait_ filename (no extension) for a student."""
|
"""Build the wiki Portrait_ filename (no extension) for a student."""
|
||||||
@@ -208,24 +190,8 @@ class StudentDB:
|
|||||||
db = 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:
|
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()
|
r = random.random()
|
||||||
if r < rates[0]:
|
if r < rates[0]:
|
||||||
return 3
|
return 3
|
||||||
|
|||||||
+2
-3
@@ -5,13 +5,12 @@ import sqlite3
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .constants import ELIGMA_YIELD
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
DB_PATH = Path("data/ba_gacha.db")
|
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:
|
def _connect() -> sqlite3.Connection:
|
||||||
"""Open a connection with WAL mode for concurrent reads."""
|
"""Open a connection with WAL mode for concurrent reads."""
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"""Blue Archive Gacha Simulator — in-game-accurate recruitment with live banners."""
|
"""Blue Archive Gacha Simulator — in-game-accurate recruitment with live banners."""
|
||||||
######################################################################
|
######################################################################
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -16,16 +17,12 @@ from .data import (
|
|||||||
format_banner_embed,
|
format_banner_embed,
|
||||||
roll_rarity,
|
roll_rarity,
|
||||||
get_rates_for_banner,
|
get_rates_for_banner,
|
||||||
PULL10_RATES,
|
|
||||||
DEFAULT_RATES,
|
|
||||||
)
|
)
|
||||||
from .gacha_renderer import render_pull
|
from .gacha_renderer import render_pull
|
||||||
|
from .constants import SPARK_TARGET, BANNER_FILE, DEFAULT_RATES, PULL10_RATES
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
SPARK_TARGET = 200
|
|
||||||
BANNER_FILE = Path("data/ba_banner_state.json")
|
|
||||||
|
|
||||||
|
|
||||||
def _load_banner_state() -> dict:
|
def _load_banner_state() -> dict:
|
||||||
"""Which banner each user has picked. Lightweight JSON, separate from SQLite."""
|
"""Which banner each user has picked. Lightweight JSON, separate from SQLite."""
|
||||||
|
|||||||
@@ -11,56 +11,25 @@ import aiohttp
|
|||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
from .data import db
|
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__)
|
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 ───────────────────────────────────────────────────────────
|
# ── Font helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _get_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
|
def _get_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
|
||||||
"""Try to load a nice font; fall back to default."""
|
"""Try to load a nice font; fall back to default."""
|
||||||
font_paths = [
|
for fp in 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:
|
|
||||||
if Path(fp).exists():
|
if Path(fp).exists():
|
||||||
try:
|
try:
|
||||||
return ImageFont.truetype(fp, size)
|
return ImageFont.truetype(fp, size)
|
||||||
@@ -284,7 +253,7 @@ def _draw_card(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Draw a single recruitment result card at (x, y)."""
|
"""Draw a single recruitment result card at (x, y)."""
|
||||||
rarity = student["StarGrade"]
|
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))
|
bg = RARITY_BG.get(rarity, (20, 20, 20))
|
||||||
|
|
||||||
# Card background with rounded corners
|
# Card background with rounded corners
|
||||||
|
|||||||
@@ -8,23 +8,18 @@ from discord.ext import commands
|
|||||||
|
|
||||||
from . import db as gacha_db
|
from . import db as gacha_db
|
||||||
from .data import 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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
PAGE_SIZE = 10
|
# Local UI labels
|
||||||
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}
|
|
||||||
|
|
||||||
PAGE_LEFT = "◀"
|
PAGE_LEFT = "◀"
|
||||||
PAGE_RIGHT = "▶"
|
PAGE_RIGHT = "▶"
|
||||||
FILTER_ALL = "All"
|
FILTER_ALL = "All"
|
||||||
FILTER_LABELS = {FILTER_ALL: None, "3★": 3, "2★": 2, "1★": 1}
|
|
||||||
|
|
||||||
|
|
||||||
def _bar(current: int, total: int, width: int = 8) -> str:
|
def _bar(current: int, total: int, width: int = 8) -> str:
|
||||||
|
|||||||
Reference in new issue
Block a user