151 lines
5.5 KiB
Python
151 lines
5.5 KiB
Python
import random
|
||
from dataclasses import dataclass
|
||
from typing import List, Optional, TYPE_CHECKING
|
||
|
||
from src.utils.df import game_configs, get_str, get_list_str, get_int
|
||
from src.utils.config import CONFIG
|
||
from src.classes.effect import load_effect_from_str
|
||
from src.classes.rarity import Rarity, get_rarity_from_str
|
||
|
||
if TYPE_CHECKING:
|
||
# 仅用于类型检查,避免运行时循环导入
|
||
from src.classes.avatar import Avatar
|
||
|
||
@dataclass
|
||
class Persona:
|
||
"""
|
||
角色特质
|
||
包含个性、天赋等角色特征
|
||
"""
|
||
id: int
|
||
name: str
|
||
desc: str
|
||
exclusion_names: List[str]
|
||
rarity: Rarity
|
||
condition: str
|
||
effects: dict[str, object]
|
||
effect_desc: str = ""
|
||
|
||
@property
|
||
def weight(self) -> float:
|
||
"""根据稀有度获取采样权重"""
|
||
return self.rarity.weight
|
||
|
||
def get_info(self) -> str:
|
||
return self.name
|
||
|
||
def get_detailed_info(self) -> str:
|
||
desc_part = f"({self.desc})" if self.desc else ""
|
||
effect_part = f"\n效果:{self.effect_desc}" if self.effect_desc else ""
|
||
return f"{self.name}{desc_part}{effect_part}"
|
||
|
||
def get_colored_info(self) -> str:
|
||
"""获取带颜色标记的信息,供前端渲染使用"""
|
||
r, g, b = self.rarity.color_rgb
|
||
return f"<color:{r},{g},{b}>{self.name}</color>"
|
||
|
||
def get_structured_info(self) -> dict:
|
||
return {
|
||
"name": self.name,
|
||
"desc": self.desc,
|
||
"rarity": self.rarity.level.value,
|
||
"color": self.rarity.color_rgb,
|
||
"effect_desc": self.effect_desc,
|
||
}
|
||
|
||
def _load_personas() -> tuple[dict[int, Persona], dict[str, Persona]]:
|
||
"""从配表加载persona数据"""
|
||
personas_by_id: dict[int, Persona] = {}
|
||
personas_by_name: dict[str, Persona] = {}
|
||
|
||
persona_df = game_configs["persona"]
|
||
for row in persona_df:
|
||
# 解析exclusion_names字符串,转换为字符串列表
|
||
exclusion_names = get_list_str(row, "exclusion_names")
|
||
|
||
# 解析稀有度(缺失或为 NaN 时默认为 N)
|
||
rarity_str = get_str(row, "rarity", "N").upper()
|
||
rarity = get_rarity_from_str(rarity_str) if rarity_str and rarity_str != "NAN" else get_rarity_from_str("N")
|
||
|
||
# 条件
|
||
condition = get_str(row, "condition")
|
||
|
||
# 解析effects
|
||
effects = load_effect_from_str(get_str(row, "effects"))
|
||
from src.classes.effect import format_effects_to_text
|
||
effect_desc = format_effects_to_text(effects)
|
||
|
||
persona = Persona(
|
||
id=get_int(row, "id"),
|
||
name=get_str(row, "name"),
|
||
desc=get_str(row, "desc"),
|
||
exclusion_names=exclusion_names,
|
||
rarity=rarity,
|
||
condition=condition,
|
||
effects=effects,
|
||
effect_desc=effect_desc,
|
||
)
|
||
personas_by_id[persona.id] = persona
|
||
personas_by_name[persona.name] = persona
|
||
|
||
return personas_by_id, personas_by_name
|
||
|
||
# 从配表加载persona数据
|
||
personas_by_id, personas_by_name = _load_personas()
|
||
|
||
def _is_persona_allowed(persona_id: int, already_selected_ids: set[int], avatar: Optional["Avatar"]) -> bool:
|
||
"""
|
||
统一判断:persona 是否允许被选择(条件 + 互斥)。
|
||
- 条件:当存在 avatar 且配置了 condition 时,通过安全 eval 判断。
|
||
- 互斥:与已选 persona 双向互斥(通过名字比较)。
|
||
"""
|
||
persona = personas_by_id[persona_id]
|
||
# 条件判定
|
||
if avatar is not None and persona.condition:
|
||
allowed = bool(eval(persona.condition, {"__builtins__": {}}, {"avatar": avatar}))
|
||
if not allowed:
|
||
return False
|
||
# 与已选互斥检查(双向,通过名字)
|
||
for sid in already_selected_ids:
|
||
other = personas_by_id[sid]
|
||
# 检查当前persona是否在对方的互斥列表中,或对方是否在当前persona的互斥列表中
|
||
if (persona.name in other.exclusion_names) or (other.name in persona.exclusion_names):
|
||
return False
|
||
return True
|
||
|
||
def get_random_compatible_personas(num_personas: int = 2, avatar: Optional["Avatar"] = None) -> List[Persona]:
|
||
"""
|
||
随机选择指定数量的互相不冲突的persona
|
||
|
||
Args:
|
||
num_personas: 需要选择的persona数量,默认为2
|
||
avatar: 可选,若提供则按 persona.condition 过滤
|
||
|
||
Returns:
|
||
List[Persona]: 互相不冲突的persona列表
|
||
|
||
Raises:
|
||
ValueError: 如果无法找到足够数量的兼容persona
|
||
"""
|
||
# 初始候选:若提供 avatar,则先按条件过滤;否则全量
|
||
initial_ids = set(personas_by_id.keys())
|
||
if avatar is not None:
|
||
initial_ids = {pid for pid in initial_ids if _is_persona_allowed(pid, set(), avatar)}
|
||
|
||
selected_personas: List[Persona] = []
|
||
selected_ids: set[int] = set()
|
||
|
||
for i in range(num_personas):
|
||
# 按当前已选进行二次过滤(互斥 + 条件)
|
||
available_ids = [pid for pid in initial_ids if pid not in selected_ids and _is_persona_allowed(pid, selected_ids, avatar)]
|
||
if not available_ids:
|
||
raise ValueError(f"只能找到{i}个兼容的persona,无法满足需要的{num_personas}个")
|
||
|
||
candidates: List[Persona] = [personas_by_id[pid] for pid in available_ids]
|
||
weights: List[float] = [max(0.0, c.weight) for c in candidates]
|
||
selected_persona = random.choices(candidates, weights=weights, k=1)[0]
|
||
selected_personas.append(selected_persona)
|
||
selected_ids.add(selected_persona.id)
|
||
|
||
return selected_personas
|