350 lines
12 KiB
Python
350 lines
12 KiB
Python
|
|
"""
|
|||
|
|
卡牌YAML加载器
|
|||
|
|
从YAML文件加载卡牌定义并创建卡牌对象
|
|||
|
|
"""
|
|||
|
|
import yaml
|
|||
|
|
from typing import List, Dict, Optional, Set, Any
|
|||
|
|
from pathlib import Path
|
|||
|
|
from ..core.enums import Nation, Rarity
|
|||
|
|
from .card import UnitCard, CardStats
|
|||
|
|
from .card_params import ParameterDefinition
|
|||
|
|
|
|||
|
|
|
|||
|
|
class CardLoadError(Exception):
|
|||
|
|
"""卡牌加载错误"""
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
class CardLoader:
|
|||
|
|
"""卡牌YAML加载器"""
|
|||
|
|
|
|||
|
|
def __init__(self, cards_dir: Optional[str] = None):
|
|||
|
|
"""
|
|||
|
|
初始化卡牌加载器
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
cards_dir: 卡牌文件目录,默认为 assets/cards/
|
|||
|
|
"""
|
|||
|
|
if cards_dir is None:
|
|||
|
|
# 从当前文件位置推断assets目录
|
|||
|
|
current_dir = Path(__file__).parent
|
|||
|
|
project_root = current_dir.parent.parent # kards_battle -> kards
|
|||
|
|
self.cards_dir = project_root / "assets" / "cards"
|
|||
|
|
else:
|
|||
|
|
self.cards_dir = Path(cards_dir)
|
|||
|
|
|
|||
|
|
# 缓存模板定义
|
|||
|
|
self._templates = {}
|
|||
|
|
self._templates_loaded = False
|
|||
|
|
|
|||
|
|
def _parse_nation(self, nation_str: str) -> Nation:
|
|||
|
|
"""将字符串转换为Nation枚举"""
|
|||
|
|
nation_map = {
|
|||
|
|
'germany': Nation.GERMANY,
|
|||
|
|
'italy': Nation.ITALY,
|
|||
|
|
'finland': Nation.FINLAND,
|
|||
|
|
'japan': Nation.JAPAN,
|
|||
|
|
'usa': Nation.USA,
|
|||
|
|
'uk': Nation.UK,
|
|||
|
|
'soviet': Nation.SOVIET,
|
|||
|
|
'france': Nation.FRANCE,
|
|||
|
|
'poland': Nation.POLAND,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
nation_lower = nation_str.lower()
|
|||
|
|
if nation_lower not in nation_map:
|
|||
|
|
raise CardLoadError(f"未知的国家: {nation_str}")
|
|||
|
|
|
|||
|
|
return nation_map[nation_lower]
|
|||
|
|
|
|||
|
|
def _parse_rarity(self, rarity: int) -> Rarity:
|
|||
|
|
"""将整数转换为Rarity枚举"""
|
|||
|
|
rarity_map = {
|
|||
|
|
1: Rarity.ELITE,
|
|||
|
|
2: Rarity.RARE,
|
|||
|
|
3: Rarity.UNCOMMON,
|
|||
|
|
4: Rarity.COMMON
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if rarity not in rarity_map:
|
|||
|
|
raise CardLoadError(f"无效的稀有度: {rarity},必须是1-4")
|
|||
|
|
|
|||
|
|
return rarity_map[rarity]
|
|||
|
|
|
|||
|
|
def _parse_exile_nations(self, exile_nations: Optional[List[str]]) -> Optional[Set[Nation]]:
|
|||
|
|
"""解析流亡国家列表"""
|
|||
|
|
if not exile_nations:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
result = set()
|
|||
|
|
for nation_str in exile_nations:
|
|||
|
|
result.add(self._parse_nation(nation_str))
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
def _load_yaml_file(self, file_path: Path) -> Dict[str, Any]:
|
|||
|
|
"""加载YAML文件"""
|
|||
|
|
try:
|
|||
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|||
|
|
return yaml.safe_load(f)
|
|||
|
|
except FileNotFoundError:
|
|||
|
|
raise CardLoadError(f"卡牌文件不存在: {file_path}")
|
|||
|
|
except yaml.YAMLError as e:
|
|||
|
|
raise CardLoadError(f"YAML解析错误 ({file_path}): {e}")
|
|||
|
|
|
|||
|
|
def _parse_card_parameters(self, params_data: Optional[List[Dict[str, Any]]]) -> List[ParameterDefinition]:
|
|||
|
|
"""解析卡牌参数定义"""
|
|||
|
|
if not params_data:
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
parameters = []
|
|||
|
|
for param_data in params_data:
|
|||
|
|
try:
|
|||
|
|
param_def = ParameterDefinition.from_dict(param_data)
|
|||
|
|
parameters.append(param_def)
|
|||
|
|
except Exception as e:
|
|||
|
|
raise CardLoadError(f"解析参数定义失败: {e}")
|
|||
|
|
|
|||
|
|
return parameters
|
|||
|
|
|
|||
|
|
def _load_templates(self):
|
|||
|
|
"""加载卡牌模板定义"""
|
|||
|
|
if self._templates_loaded:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
spec_file = self.cards_dir / "cards_spec.yaml"
|
|||
|
|
if not spec_file.exists():
|
|||
|
|
self._templates_loaded = True
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
spec_data = self._load_yaml_file(spec_file)
|
|||
|
|
templates = spec_data.get('card_templates', [])
|
|||
|
|
|
|||
|
|
for template in templates:
|
|||
|
|
if 'id' not in template:
|
|||
|
|
continue
|
|||
|
|
self._templates[template['id']] = template
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
raise CardLoadError(f"Failed to load templates: {e}")
|
|||
|
|
|
|||
|
|
self._templates_loaded = True
|
|||
|
|
|
|||
|
|
def _apply_template(self, card_data: Dict[str, Any]) -> Dict[str, Any]:
|
|||
|
|
"""应用模板到卡牌数据"""
|
|||
|
|
self._load_templates()
|
|||
|
|
|
|||
|
|
template_id = card_data.get('template')
|
|||
|
|
if not template_id:
|
|||
|
|
return card_data
|
|||
|
|
|
|||
|
|
if template_id not in self._templates:
|
|||
|
|
raise CardLoadError(f"Template not found: {template_id}")
|
|||
|
|
|
|||
|
|
template = self._templates[template_id]
|
|||
|
|
|
|||
|
|
# 创建合并后的卡牌数据
|
|||
|
|
merged_data = dict(card_data) # 复制原始数据
|
|||
|
|
|
|||
|
|
# 合并模板字段(卡牌数据优先)
|
|||
|
|
for key, value in template.items():
|
|||
|
|
if key == 'id': # 跳过模板ID
|
|||
|
|
continue
|
|||
|
|
if key not in merged_data:
|
|||
|
|
merged_data[key] = value
|
|||
|
|
elif key == 'params':
|
|||
|
|
# 参数需要特殊合并逻辑
|
|||
|
|
template_params = template.get('params', [])
|
|||
|
|
card_params = card_data.get('params', [])
|
|||
|
|
merged_data['params'] = template_params + card_params
|
|||
|
|
|
|||
|
|
return merged_data
|
|||
|
|
|
|||
|
|
def _create_unit_card(self, card_data: Dict[str, Any]) -> UnitCard:
|
|||
|
|
"""从数据字典创建单位卡"""
|
|||
|
|
# 应用模板
|
|||
|
|
card_data = self._apply_template(card_data)
|
|||
|
|
|
|||
|
|
required_fields = ['id', 'name', 'type', 'cost', 'nation', 'rarity', 'unit_definition_id']
|
|||
|
|
for field in required_fields:
|
|||
|
|
if field not in card_data:
|
|||
|
|
raise CardLoadError(f"单位卡缺少必需字段: {field}")
|
|||
|
|
|
|||
|
|
# 验证卡牌类型
|
|||
|
|
if card_data['type'] != 'unit':
|
|||
|
|
raise CardLoadError(f"期望单位卡,但得到: {card_data['type']}")
|
|||
|
|
|
|||
|
|
# 解析基础属性
|
|||
|
|
nation = self._parse_nation(card_data['nation'])
|
|||
|
|
rarity = self._parse_rarity(card_data['rarity'])
|
|||
|
|
exile_nations = self._parse_exile_nations(card_data.get('exile_nations'))
|
|||
|
|
|
|||
|
|
# 解析参数定义
|
|||
|
|
parameters = self._parse_card_parameters(card_data.get('params'))
|
|||
|
|
|
|||
|
|
# 创建单位卡
|
|||
|
|
card = UnitCard(
|
|||
|
|
name=card_data['name'],
|
|||
|
|
cost=card_data['cost'],
|
|||
|
|
nation=nation,
|
|||
|
|
rarity=rarity,
|
|||
|
|
unit_definition_id=card_data['unit_definition_id'],
|
|||
|
|
exile_nations=exile_nations,
|
|||
|
|
description=card_data.get('description', ''),
|
|||
|
|
card_id=card_data['id'],
|
|||
|
|
parameters=parameters
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return card
|
|||
|
|
|
|||
|
|
def load_unit_cards_from_file(self, file_path: str) -> List[UnitCard]:
|
|||
|
|
"""
|
|||
|
|
从YAML文件加载单位卡
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
file_path: YAML文件路径(相对于cards_dir)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
单位卡列表
|
|||
|
|
"""
|
|||
|
|
full_path = self.cards_dir / file_path
|
|||
|
|
data = self._load_yaml_file(full_path)
|
|||
|
|
|
|||
|
|
if 'unit_cards' not in data:
|
|||
|
|
raise CardLoadError(f"YAML文件中找不到 'unit_cards' 节点: {file_path}")
|
|||
|
|
|
|||
|
|
cards = []
|
|||
|
|
for card_data in data['unit_cards']:
|
|||
|
|
try:
|
|||
|
|
card = self._create_unit_card(card_data)
|
|||
|
|
cards.append(card)
|
|||
|
|
except Exception as e:
|
|||
|
|
raise CardLoadError(f"创建卡牌失败 (文件: {file_path}, ID: {card_data.get('id', 'unknown')}): {e}")
|
|||
|
|
|
|||
|
|
return cards
|
|||
|
|
|
|||
|
|
def load_nation_cards(self, nation: Nation) -> List[UnitCard]:
|
|||
|
|
"""
|
|||
|
|
加载指定国家的所有卡牌
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
nation: 国家
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
该国家的所有卡牌
|
|||
|
|
"""
|
|||
|
|
nation_file_map = {
|
|||
|
|
Nation.GERMANY: 'germany.yaml',
|
|||
|
|
Nation.ITALY: 'italy.yaml',
|
|||
|
|
Nation.USA: 'usa.yaml',
|
|||
|
|
Nation.UK: 'uk.yaml',
|
|||
|
|
Nation.SOVIET: 'soviet.yaml',
|
|||
|
|
Nation.JAPAN: 'japan.yaml',
|
|||
|
|
Nation.FRANCE: 'france.yaml',
|
|||
|
|
Nation.POLAND: 'poland.yaml',
|
|||
|
|
Nation.FINLAND: 'finland.yaml',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if nation not in nation_file_map:
|
|||
|
|
raise CardLoadError(f"不支持的国家: {nation}")
|
|||
|
|
|
|||
|
|
filename = nation_file_map[nation]
|
|||
|
|
|
|||
|
|
# 检查文件是否存在
|
|||
|
|
file_path = self.cards_dir / filename
|
|||
|
|
if not file_path.exists():
|
|||
|
|
# 如果文件不存在,返回空列表(某些国家可能还没有卡牌)
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
return self.load_unit_cards_from_file(filename)
|
|||
|
|
|
|||
|
|
def load_all_available_cards(self) -> Dict[Nation, List[UnitCard]]:
|
|||
|
|
"""
|
|||
|
|
加载所有可用的卡牌
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
按国家分组的卡牌字典
|
|||
|
|
"""
|
|||
|
|
all_cards = {}
|
|||
|
|
|
|||
|
|
# 遍历所有支持的国家
|
|||
|
|
for nation in Nation:
|
|||
|
|
try:
|
|||
|
|
cards = self.load_nation_cards(nation)
|
|||
|
|
if cards: # 只添加有卡牌的国家
|
|||
|
|
all_cards[nation] = cards
|
|||
|
|
except CardLoadError:
|
|||
|
|
# 如果某个国家的卡牌加载失败,跳过但记录
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
return all_cards
|
|||
|
|
|
|||
|
|
def validate_card_data(self, card_data: Dict[str, Any]) -> List[str]:
|
|||
|
|
"""
|
|||
|
|
验证卡牌数据的完整性
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
card_data: 卡牌数据字典
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
错误信息列表,空列表表示验证通过
|
|||
|
|
"""
|
|||
|
|
errors = []
|
|||
|
|
|
|||
|
|
# 检查必需字段
|
|||
|
|
required_fields = ['id', 'name', 'type', 'cost', 'nation', 'rarity']
|
|||
|
|
for field in required_fields:
|
|||
|
|
if field not in card_data:
|
|||
|
|
errors.append(f"缺少必需字段: {field}")
|
|||
|
|
|
|||
|
|
# 检查卡牌类型
|
|||
|
|
if 'type' in card_data:
|
|||
|
|
valid_types = ['unit', 'order', 'countermeasure']
|
|||
|
|
if card_data['type'] not in valid_types:
|
|||
|
|
errors.append(f"无效的卡牌类型: {card_data['type']}")
|
|||
|
|
|
|||
|
|
# 检查费用
|
|||
|
|
if 'cost' in card_data:
|
|||
|
|
if not isinstance(card_data['cost'], int) or card_data['cost'] < 0:
|
|||
|
|
errors.append(f"费用必须是非负整数: {card_data['cost']}")
|
|||
|
|
|
|||
|
|
# 检查稀有度
|
|||
|
|
if 'rarity' in card_data:
|
|||
|
|
if not isinstance(card_data['rarity'], int) or card_data['rarity'] < 1 or card_data['rarity'] > 4:
|
|||
|
|
errors.append(f"稀有度必须是1-4的整数: {card_data['rarity']}")
|
|||
|
|
|
|||
|
|
# 检查国家
|
|||
|
|
if 'nation' in card_data:
|
|||
|
|
try:
|
|||
|
|
self._parse_nation(card_data['nation'])
|
|||
|
|
except CardLoadError as e:
|
|||
|
|
errors.append(str(e))
|
|||
|
|
|
|||
|
|
# 单位卡特殊验证
|
|||
|
|
if card_data.get('type') == 'unit':
|
|||
|
|
if 'unit_definition_id' not in card_data:
|
|||
|
|
errors.append("单位卡必须包含 unit_definition_id 字段")
|
|||
|
|
|
|||
|
|
return errors
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 全局加载器实例
|
|||
|
|
_default_loader = None
|
|||
|
|
|
|||
|
|
def get_card_loader() -> CardLoader:
|
|||
|
|
"""获取默认的卡牌加载器实例"""
|
|||
|
|
global _default_loader
|
|||
|
|
if _default_loader is None:
|
|||
|
|
_default_loader = CardLoader()
|
|||
|
|
return _default_loader
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_nation_cards(nation: Nation) -> List[UnitCard]:
|
|||
|
|
"""便捷函数:加载指定国家的卡牌"""
|
|||
|
|
return get_card_loader().load_nation_cards(nation)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_all_cards() -> Dict[Nation, List[UnitCard]]:
|
|||
|
|
"""便捷函数:加载所有可用卡牌"""
|
|||
|
|
return get_card_loader().load_all_available_cards()
|