fix rust
This commit is contained in:
1
python/api/app/services/__init__.py
Normal file
1
python/api/app/services/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Services Package
|
288
python/api/app/services/atproto.py
Normal file
288
python/api/app/services/atproto.py
Normal file
@ -0,0 +1,288 @@
|
||||
"""atproto integration service"""
|
||||
import json
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
import httpx
|
||||
from atproto import Client, SessionString
|
||||
from atproto.exceptions import AtProtocolError
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.card import Card, CardRarity
|
||||
|
||||
|
||||
class AtprotoService:
|
||||
"""atproto integration service"""
|
||||
|
||||
def __init__(self):
|
||||
self.client = None
|
||||
self.session_string = None
|
||||
|
||||
async def login(self, identifier: str, password: str) -> SessionString:
|
||||
"""
|
||||
Login to atproto PDS
|
||||
|
||||
Args:
|
||||
identifier: Handle or DID
|
||||
password: App password
|
||||
|
||||
Returns:
|
||||
Session string for future requests
|
||||
"""
|
||||
self.client = Client()
|
||||
try:
|
||||
self.client.login(identifier, password)
|
||||
self.session_string = self.client.export_session_string()
|
||||
return self.session_string
|
||||
except AtProtocolError as e:
|
||||
raise Exception(f"Failed to login to atproto: {str(e)}")
|
||||
|
||||
def restore_session(self, session_string: str):
|
||||
"""Restore session from string"""
|
||||
self.client = Client()
|
||||
self.client.login_with_session_string(session_string)
|
||||
self.session_string = session_string
|
||||
|
||||
async def verify_did(self, did: str, handle: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Verify DID is valid
|
||||
|
||||
Args:
|
||||
did: DID to verify
|
||||
handle: Optional handle to cross-check
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
"""
|
||||
try:
|
||||
# Use public API to resolve DID
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"https://plc.directory/{did}",
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return False
|
||||
|
||||
data = response.json()
|
||||
|
||||
# Verify handle if provided
|
||||
if handle and data.get("alsoKnownAs"):
|
||||
expected_handle = f"at://{handle}"
|
||||
return expected_handle in data["alsoKnownAs"]
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def get_profile(self, did: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get user profile from atproto"""
|
||||
if not self.client:
|
||||
raise Exception("Not logged in")
|
||||
|
||||
try:
|
||||
profile = self.client.get_profile(did)
|
||||
return {
|
||||
"did": profile.did,
|
||||
"handle": profile.handle,
|
||||
"display_name": profile.display_name,
|
||||
"avatar": profile.avatar,
|
||||
"description": profile.description
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def create_card_record(
|
||||
self,
|
||||
did: str,
|
||||
card: Card,
|
||||
collection: str = "ai.card.collection"
|
||||
) -> str:
|
||||
"""
|
||||
Create card record in user's PDS
|
||||
|
||||
Args:
|
||||
did: User's DID
|
||||
card: Card data
|
||||
collection: Collection name (lexicon)
|
||||
|
||||
Returns:
|
||||
Record URI
|
||||
"""
|
||||
if not self.client:
|
||||
raise Exception("Not logged in")
|
||||
|
||||
# Prepare card data for atproto
|
||||
record_data = {
|
||||
"$type": collection,
|
||||
"cardId": card.id,
|
||||
"cp": card.cp,
|
||||
"status": card.status.value,
|
||||
"skill": card.skill,
|
||||
"obtainedAt": card.obtained_at.isoformat(),
|
||||
"isUnique": card.is_unique,
|
||||
"uniqueId": card.unique_id,
|
||||
"createdAt": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
try:
|
||||
# Create record
|
||||
response = self.client.com.atproto.repo.create_record(
|
||||
repo=did,
|
||||
collection=collection,
|
||||
record=record_data
|
||||
)
|
||||
|
||||
return response.uri
|
||||
|
||||
except AtProtocolError as e:
|
||||
raise Exception(f"Failed to create card record: {str(e)}")
|
||||
|
||||
async def get_user_cards(
|
||||
self,
|
||||
did: str,
|
||||
collection: str = "ai.card.collection",
|
||||
limit: int = 100
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get user's cards from PDS
|
||||
|
||||
Args:
|
||||
did: User's DID
|
||||
collection: Collection name
|
||||
limit: Maximum records to fetch
|
||||
|
||||
Returns:
|
||||
List of card records
|
||||
"""
|
||||
if not self.client:
|
||||
raise Exception("Not logged in")
|
||||
|
||||
try:
|
||||
# List records
|
||||
response = self.client.com.atproto.repo.list_records(
|
||||
repo=did,
|
||||
collection=collection,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
cards = []
|
||||
for record in response.records:
|
||||
card_data = record.value
|
||||
card_data["uri"] = record.uri
|
||||
card_data["cid"] = record.cid
|
||||
cards.append(card_data)
|
||||
|
||||
return cards
|
||||
|
||||
except AtProtocolError:
|
||||
# Collection might not exist yet
|
||||
return []
|
||||
|
||||
async def delete_card_record(self, did: str, record_uri: str):
|
||||
"""Delete a card record from PDS"""
|
||||
if not self.client:
|
||||
raise Exception("Not logged in")
|
||||
|
||||
try:
|
||||
# Parse collection and rkey from URI
|
||||
# Format: at://did/collection/rkey
|
||||
parts = record_uri.split("/")
|
||||
if len(parts) < 5:
|
||||
raise ValueError("Invalid record URI")
|
||||
|
||||
collection = parts[3]
|
||||
rkey = parts[4]
|
||||
|
||||
self.client.com.atproto.repo.delete_record(
|
||||
repo=did,
|
||||
collection=collection,
|
||||
rkey=rkey
|
||||
)
|
||||
|
||||
except AtProtocolError as e:
|
||||
raise Exception(f"Failed to delete record: {str(e)}")
|
||||
|
||||
async def create_oauth_session(self, code: str, redirect_uri: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Handle OAuth callback and create session
|
||||
|
||||
Args:
|
||||
code: Authorization code
|
||||
redirect_uri: Redirect URI used in authorization
|
||||
|
||||
Returns:
|
||||
Session data including DID and access token
|
||||
"""
|
||||
# TODO: Implement when atproto OAuth is available
|
||||
raise NotImplementedError("OAuth support is not yet available in atproto")
|
||||
|
||||
|
||||
class CardLexicon:
|
||||
"""Card collection lexicon definition"""
|
||||
|
||||
LEXICON_ID = "ai.card.collection"
|
||||
|
||||
@staticmethod
|
||||
def get_lexicon() -> Dict[str, Any]:
|
||||
"""Get lexicon definition for card collection"""
|
||||
return {
|
||||
"lexicon": 1,
|
||||
"id": CardLexicon.LEXICON_ID,
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "record",
|
||||
"description": "A collectible card",
|
||||
"key": "tid",
|
||||
"record": {
|
||||
"type": "object",
|
||||
"required": ["cardId", "cp", "status", "obtainedAt", "createdAt"],
|
||||
"properties": {
|
||||
"cardId": {
|
||||
"type": "integer",
|
||||
"description": "Card type ID (0-15)",
|
||||
"minimum": 0,
|
||||
"maximum": 15
|
||||
},
|
||||
"cp": {
|
||||
"type": "integer",
|
||||
"description": "Card power",
|
||||
"minimum": 1,
|
||||
"maximum": 999
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Card rarity",
|
||||
"enum": ["normal", "rare", "super_rare", "kira", "unique"]
|
||||
},
|
||||
"skill": {
|
||||
"type": "string",
|
||||
"description": "Card skill",
|
||||
"maxLength": 1000
|
||||
},
|
||||
"obtainedAt": {
|
||||
"type": "string",
|
||||
"format": "datetime",
|
||||
"description": "When the card was obtained"
|
||||
},
|
||||
"isUnique": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this is a unique card",
|
||||
"default": False
|
||||
},
|
||||
"uniqueId": {
|
||||
"type": "string",
|
||||
"description": "Global unique identifier",
|
||||
"format": "uuid"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "datetime",
|
||||
"description": "Record creation time"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
142
python/api/app/services/card_master.py
Normal file
142
python/api/app/services/card_master.py
Normal file
@ -0,0 +1,142 @@
|
||||
"""
|
||||
Card master data fetcher from external ai.json
|
||||
"""
|
||||
import httpx
|
||||
import json
|
||||
from typing import Dict, List, Optional
|
||||
from functools import lru_cache
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CARD_MASTER_URL = "https://git.syui.ai/ai/ai/raw/branch/main/ai.json"
|
||||
|
||||
# Default CP ranges for cards (matching existing gacha.py values)
|
||||
DEFAULT_CP_RANGES = {
|
||||
0: (10, 100),
|
||||
1: (20, 120),
|
||||
2: (30, 130),
|
||||
3: (40, 140),
|
||||
4: (50, 150),
|
||||
5: (25, 125),
|
||||
6: (15, 115),
|
||||
7: (60, 160),
|
||||
8: (80, 180),
|
||||
9: (70, 170),
|
||||
10: (90, 190),
|
||||
11: (35, 135),
|
||||
12: (65, 165),
|
||||
13: (75, 175),
|
||||
14: (100, 200),
|
||||
15: (85, 185),
|
||||
135: (95, 195), # world card
|
||||
}
|
||||
|
||||
|
||||
class CardMasterService:
|
||||
def __init__(self):
|
||||
self._cache = None
|
||||
self._cache_time = 0
|
||||
self._cache_duration = 3600 # 1 hour cache
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def fetch_card_master_data(self) -> Optional[Dict]:
|
||||
"""Fetch card master data from external source"""
|
||||
try:
|
||||
response = httpx.get(CARD_MASTER_URL, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch card master data: {e}")
|
||||
return None
|
||||
|
||||
def get_card_info(self) -> Dict[int, Dict]:
|
||||
"""Get card information in the format expected by gacha service"""
|
||||
master_data = self.fetch_card_master_data()
|
||||
|
||||
if not master_data:
|
||||
# Fallback to hardcoded data
|
||||
return self._get_fallback_card_info()
|
||||
|
||||
try:
|
||||
cards = master_data.get("ai", {}).get("card", {}).get("cards", [])
|
||||
card_info = {}
|
||||
|
||||
for card in cards:
|
||||
card_id = card.get("id")
|
||||
if card_id is not None:
|
||||
# Use name from JSON, fallback to English name
|
||||
name = card.get("name", f"card_{card_id}")
|
||||
|
||||
# Get CP range from defaults
|
||||
cp_range = DEFAULT_CP_RANGES.get(card_id, (50, 150))
|
||||
|
||||
card_info[card_id] = {
|
||||
"name": name,
|
||||
"base_cp_range": cp_range,
|
||||
"ja_name": card.get("lang", {}).get("ja", {}).get("name", name),
|
||||
"description": card.get("lang", {}).get("ja", {}).get("text", "")
|
||||
}
|
||||
|
||||
return card_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse card master data: {e}")
|
||||
return self._get_fallback_card_info()
|
||||
|
||||
def _get_fallback_card_info(self) -> Dict[int, Dict]:
|
||||
"""Fallback card info if external source fails"""
|
||||
return {
|
||||
0: {"name": "ai", "base_cp_range": (10, 100)},
|
||||
1: {"name": "dream", "base_cp_range": (20, 120)},
|
||||
2: {"name": "radiance", "base_cp_range": (30, 130)},
|
||||
3: {"name": "neutron", "base_cp_range": (40, 140)},
|
||||
4: {"name": "sun", "base_cp_range": (50, 150)},
|
||||
5: {"name": "night", "base_cp_range": (25, 125)},
|
||||
6: {"name": "snow", "base_cp_range": (15, 115)},
|
||||
7: {"name": "thunder", "base_cp_range": (60, 160)},
|
||||
8: {"name": "ultimate", "base_cp_range": (80, 180)},
|
||||
9: {"name": "sword", "base_cp_range": (70, 170)},
|
||||
10: {"name": "destruction", "base_cp_range": (90, 190)},
|
||||
11: {"name": "earth", "base_cp_range": (35, 135)},
|
||||
12: {"name": "galaxy", "base_cp_range": (65, 165)},
|
||||
13: {"name": "create", "base_cp_range": (75, 175)},
|
||||
14: {"name": "supernova", "base_cp_range": (100, 200)},
|
||||
15: {"name": "world", "base_cp_range": (85, 185)},
|
||||
}
|
||||
|
||||
def get_all_cards(self) -> List[Dict]:
|
||||
"""Get all cards with full information"""
|
||||
master_data = self.fetch_card_master_data()
|
||||
|
||||
if not master_data:
|
||||
return []
|
||||
|
||||
try:
|
||||
cards = master_data.get("ai", {}).get("card", {}).get("cards", [])
|
||||
result = []
|
||||
|
||||
for card in cards:
|
||||
card_id = card.get("id")
|
||||
if card_id is not None:
|
||||
cp_range = DEFAULT_CP_RANGES.get(card_id, (50, 150))
|
||||
|
||||
result.append({
|
||||
"id": card_id,
|
||||
"name": card.get("name", f"card_{card_id}"),
|
||||
"ja_name": card.get("lang", {}).get("ja", {}).get("name", ""),
|
||||
"description": card.get("lang", {}).get("ja", {}).get("text", ""),
|
||||
"base_cp_min": cp_range[0],
|
||||
"base_cp_max": cp_range[1]
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get all cards: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# Singleton instance
|
||||
card_master_service = CardMasterService()
|
184
python/api/app/services/card_sync.py
Normal file
184
python/api/app/services/card_sync.py
Normal file
@ -0,0 +1,184 @@
|
||||
"""Card synchronization service for atproto"""
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
# from app.services.atproto import AtprotoService, CardLexicon
|
||||
from app.repositories.card import CardRepository
|
||||
from app.repositories.user import UserRepository
|
||||
from app.models.card import Card as CardModel
|
||||
from app.db.models import UserCard
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class CardSyncService:
|
||||
"""Service for syncing cards between database and atproto PDS"""
|
||||
|
||||
def __init__(self, session: AsyncSession, atproto_session: Optional[str] = None):
|
||||
self.db_session = session
|
||||
self.card_repo = CardRepository(session)
|
||||
self.user_repo = UserRepository(session)
|
||||
self.atproto_service = AtprotoService()
|
||||
|
||||
# Restore atproto session if provided
|
||||
if atproto_session:
|
||||
self.atproto_service.restore_session(atproto_session)
|
||||
|
||||
async def sync_card_to_pds(self, user_card: UserCard, user_did: str) -> Optional[str]:
|
||||
"""
|
||||
Sync a single card to user's PDS
|
||||
|
||||
Args:
|
||||
user_card: Card from database
|
||||
user_did: User's DID
|
||||
|
||||
Returns:
|
||||
Record URI if successful
|
||||
"""
|
||||
if not settings.atproto_handle or not settings.atproto_password:
|
||||
# Skip if atproto credentials not configured
|
||||
return None
|
||||
|
||||
try:
|
||||
# Login if not already
|
||||
if not self.atproto_service.client:
|
||||
await self.atproto_service.login(
|
||||
settings.atproto_handle,
|
||||
settings.atproto_password
|
||||
)
|
||||
|
||||
# Convert to API model
|
||||
card_model = CardModel(
|
||||
id=user_card.card_id,
|
||||
cp=user_card.cp,
|
||||
status=user_card.status,
|
||||
skill=user_card.skill,
|
||||
owner_did=user_did,
|
||||
obtained_at=user_card.obtained_at,
|
||||
is_unique=user_card.is_unique,
|
||||
unique_id=str(user_card.unique_id) if user_card.unique_id else None
|
||||
)
|
||||
|
||||
# Create record in PDS
|
||||
uri = await self.atproto_service.create_card_record(
|
||||
did=user_did,
|
||||
card=card_model,
|
||||
collection=CardLexicon.LEXICON_ID
|
||||
)
|
||||
|
||||
# Store URI in database for future reference
|
||||
# (You might want to add a field to UserCard model for this)
|
||||
|
||||
return uri
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to sync card to PDS: {e}")
|
||||
return None
|
||||
|
||||
async def sync_all_user_cards(self, user_id: int, user_did: str) -> int:
|
||||
"""
|
||||
Sync all user's cards to PDS
|
||||
|
||||
Args:
|
||||
user_id: Database user ID
|
||||
user_did: User's DID
|
||||
|
||||
Returns:
|
||||
Number of cards synced
|
||||
"""
|
||||
# Get all user cards from database
|
||||
user_cards = await self.card_repo.get_user_cards(user_id)
|
||||
|
||||
synced_count = 0
|
||||
for card in user_cards:
|
||||
uri = await self.sync_card_to_pds(card, user_did)
|
||||
if uri:
|
||||
synced_count += 1
|
||||
|
||||
return synced_count
|
||||
|
||||
async def import_cards_from_pds(self, user_did: str) -> int:
|
||||
"""
|
||||
Import cards from user's PDS to database
|
||||
|
||||
Args:
|
||||
user_did: User's DID
|
||||
|
||||
Returns:
|
||||
Number of cards imported
|
||||
"""
|
||||
if not self.atproto_service.client:
|
||||
return 0
|
||||
|
||||
# Get user from database
|
||||
user = await self.user_repo.get_by_did(user_did)
|
||||
if not user:
|
||||
return 0
|
||||
|
||||
# Get cards from PDS
|
||||
pds_cards = await self.atproto_service.get_user_cards(
|
||||
did=user_did,
|
||||
collection=CardLexicon.LEXICON_ID
|
||||
)
|
||||
|
||||
imported_count = 0
|
||||
for pds_card in pds_cards:
|
||||
# Check if card already exists
|
||||
existing_count = await self.card_repo.count_user_cards(
|
||||
user.id,
|
||||
pds_card.get("cardId")
|
||||
)
|
||||
|
||||
if existing_count == 0:
|
||||
# Import card
|
||||
await self.card_repo.create_user_card(
|
||||
user_id=user.id,
|
||||
card_id=pds_card.get("cardId"),
|
||||
cp=pds_card.get("cp"),
|
||||
status=pds_card.get("status"),
|
||||
skill=pds_card.get("skill"),
|
||||
is_unique=pds_card.get("isUnique", False)
|
||||
)
|
||||
imported_count += 1
|
||||
|
||||
await self.db_session.commit()
|
||||
return imported_count
|
||||
|
||||
async def verify_card_ownership(
|
||||
self,
|
||||
user_did: str,
|
||||
card_id: int,
|
||||
unique_id: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Verify user owns a card by checking both database and PDS
|
||||
|
||||
Args:
|
||||
user_did: User's DID
|
||||
card_id: Card type ID
|
||||
unique_id: Unique ID for unique cards
|
||||
|
||||
Returns:
|
||||
True if user owns the card
|
||||
"""
|
||||
# Check database first
|
||||
user = await self.user_repo.get_by_did(user_did)
|
||||
if user:
|
||||
user_cards = await self.card_repo.get_user_cards(user.id)
|
||||
for card in user_cards:
|
||||
if card.card_id == card_id:
|
||||
if not unique_id or str(card.unique_id) == unique_id:
|
||||
return True
|
||||
|
||||
# Check PDS if configured
|
||||
if self.atproto_service.client:
|
||||
try:
|
||||
pds_cards = await self.atproto_service.get_user_cards(user_did)
|
||||
for card in pds_cards:
|
||||
if card.get("cardId") == card_id:
|
||||
if not unique_id or card.get("uniqueId") == unique_id:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
164
python/api/app/services/gacha.py
Normal file
164
python/api/app/services/gacha.py
Normal file
@ -0,0 +1,164 @@
|
||||
"""ガチャシステムのロジック"""
|
||||
import random
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.card import Card, CardRarity
|
||||
from app.repositories.user import UserRepository
|
||||
from app.repositories.card import CardRepository, UniqueCardRepository
|
||||
from app.db.models import DrawHistory
|
||||
from app.services.card_master import card_master_service
|
||||
|
||||
|
||||
class GachaService:
|
||||
"""ガチャシステムのサービスクラス"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
self.user_repo = UserRepository(session)
|
||||
self.card_repo = CardRepository(session)
|
||||
self.unique_repo = UniqueCardRepository(session)
|
||||
# Load card info from external source
|
||||
self.CARD_INFO = card_master_service.get_card_info()
|
||||
|
||||
async def draw_card(self, user_did: str, is_paid: bool = False) -> Tuple[Card, bool]:
|
||||
"""
|
||||
カードを抽選する
|
||||
|
||||
Args:
|
||||
user_did: ユーザーのDID
|
||||
is_paid: 課金ガチャかどうか
|
||||
|
||||
Returns:
|
||||
(Card, is_unique): 抽選されたカードとuniqueかどうか
|
||||
"""
|
||||
# Get or create user
|
||||
user = await self.user_repo.get_or_create(user_did)
|
||||
# レアリティ抽選
|
||||
rarity = self._determine_rarity(is_paid)
|
||||
|
||||
# カード種類を選択
|
||||
card_id = self._select_card_id(rarity)
|
||||
|
||||
# CPを決定
|
||||
cp = self._calculate_cp(card_id, rarity)
|
||||
|
||||
# uniqueカードチェック
|
||||
is_unique = False
|
||||
|
||||
if rarity == CardRarity.UNIQUE:
|
||||
# uniqueカードの場合、利用可能かチェック
|
||||
is_available = await self.unique_repo.is_card_available(card_id)
|
||||
if not is_available:
|
||||
# 利用不可の場合はキラカードに変更
|
||||
rarity = CardRarity.KIRA
|
||||
else:
|
||||
is_unique = True
|
||||
|
||||
# データベースにカードを保存
|
||||
user_card = await self.card_repo.create_user_card(
|
||||
user_id=user.id,
|
||||
card_id=card_id,
|
||||
cp=cp,
|
||||
status=rarity,
|
||||
skill=self._get_skill_for_card(card_id, rarity),
|
||||
is_unique=is_unique
|
||||
)
|
||||
|
||||
# 抽選履歴を保存
|
||||
draw_history = DrawHistory(
|
||||
user_id=user.id,
|
||||
card_id=card_id,
|
||||
status=rarity,
|
||||
cp=cp,
|
||||
is_paid=is_paid
|
||||
)
|
||||
self.session.add(draw_history)
|
||||
|
||||
# API用のCardモデルに変換
|
||||
card = Card(
|
||||
id=card_id,
|
||||
cp=cp,
|
||||
status=rarity,
|
||||
skill=user_card.skill,
|
||||
owner_did=user_did,
|
||||
obtained_at=user_card.obtained_at,
|
||||
is_unique=is_unique,
|
||||
unique_id=str(user_card.unique_id) if user_card.unique_id else None
|
||||
)
|
||||
|
||||
# atproto PDSに同期(非同期で実行)
|
||||
try:
|
||||
from app.services.card_sync import CardSyncService
|
||||
sync_service = CardSyncService(self.session)
|
||||
await sync_service.sync_card_to_pds(user_card, user_did)
|
||||
except Exception:
|
||||
# 同期失敗してもガチャは成功とする
|
||||
pass
|
||||
|
||||
return card, is_unique
|
||||
|
||||
def _determine_rarity(self, is_paid: bool) -> CardRarity:
|
||||
"""レアリティを抽選する"""
|
||||
rand = random.random() * 100
|
||||
|
||||
if is_paid:
|
||||
# 課金ガチャは確率アップ
|
||||
if rand < settings.prob_unique * 2: # 0.0002%
|
||||
return CardRarity.UNIQUE
|
||||
elif rand < settings.prob_kira * 2: # 0.2%
|
||||
return CardRarity.KIRA
|
||||
elif rand < 0.5: # 0.5%
|
||||
return CardRarity.SUPER_RARE
|
||||
elif rand < 5: # 5%
|
||||
return CardRarity.RARE
|
||||
else:
|
||||
# 通常ガチャ
|
||||
if rand < settings.prob_unique:
|
||||
return CardRarity.UNIQUE
|
||||
elif rand < settings.prob_kira:
|
||||
return CardRarity.KIRA
|
||||
elif rand < settings.prob_super_rare:
|
||||
return CardRarity.SUPER_RARE
|
||||
elif rand < settings.prob_rare:
|
||||
return CardRarity.RARE
|
||||
|
||||
return CardRarity.NORMAL
|
||||
|
||||
def _select_card_id(self, rarity: CardRarity) -> int:
|
||||
"""レアリティに応じてカードIDを選択"""
|
||||
if rarity in [CardRarity.UNIQUE, CardRarity.KIRA]:
|
||||
# レアカードは特定のIDに偏らせる
|
||||
weights = [1, 1, 2, 2, 3, 1, 1, 3, 5, 4, 5, 2, 3, 4, 6, 5]
|
||||
else:
|
||||
# 通常は均等
|
||||
weights = [1] * 16
|
||||
|
||||
return random.choices(range(16), weights=weights)[0]
|
||||
|
||||
def _calculate_cp(self, card_id: int, rarity: CardRarity) -> int:
|
||||
"""カードのCPを計算"""
|
||||
base_range = self.CARD_INFO[card_id]["base_cp_range"]
|
||||
base_cp = random.randint(*base_range)
|
||||
|
||||
# レアリティボーナス
|
||||
multiplier = {
|
||||
CardRarity.NORMAL: 1.0,
|
||||
CardRarity.RARE: 1.5,
|
||||
CardRarity.SUPER_RARE: 2.0,
|
||||
CardRarity.KIRA: 3.0,
|
||||
CardRarity.UNIQUE: 5.0,
|
||||
}[rarity]
|
||||
|
||||
return int(base_cp * multiplier)
|
||||
|
||||
def _get_skill_for_card(self, card_id: int, rarity: CardRarity) -> Optional[str]:
|
||||
"""カードのスキルを取得"""
|
||||
if rarity in [CardRarity.KIRA, CardRarity.UNIQUE]:
|
||||
# TODO: スキル情報を返す
|
||||
return f"skill_{card_id}_{rarity.value}"
|
||||
return None
|
||||
|
Reference in New Issue
Block a user