from dataclasses import dataclass, field from pathlib import Path import json from src.classes.alignment import Alignment from src.utils.df import game_configs, get_str, get_float, get_int from src.classes.effect import load_effect_from_str from src.utils.config import CONFIG from typing import TYPE_CHECKING if TYPE_CHECKING: from src.classes.avatar import Avatar from src.classes.technique import Technique from src.classes.sect_ranks import SectRank """ 宗门、宗门总部基础数据。 驻地名称与描述已迁移到 sect_region.csv,供地图区域系统使用。 此处仅保留宗门本体信息与头像编辑所需的静态字段。 """ # 宗门驻地(基础展示数据,具体地图位置在 sect_region.csv 中定义) @dataclass class SectHeadQuarter: """ 宗门总部 """ name: str desc: str image: Path @dataclass class Sect: """ 宗门 """ id: int name: str desc: str member_act_style: str alignment: Alignment headquarter: SectHeadQuarter # 本宗关联的功法名称(来自 technique.csv 的 sect 列) technique_names: list[str] # 随机选择宗门时使用的权重(默认1) weight: float = 1.0 # 宗门倾向的兵器类型(字符串,如"剑"、"刀"等) preferred_weapon: str = "" # 影响角色或系统的效果 effects: dict[str, object] = field(default_factory=dict) effect_desc: str = "" # 宗门自定义职位名称(可选):SectRank -> 名称 rank_names: dict[str, str] = field(default_factory=dict) # 运行时成员列表:Avatar ID -> Avatar members: dict[str, "Avatar"] = field(default_factory=dict, init=False) # 功法对象列表:Technique techniques: list["Technique"] = field(default_factory=list, init=False) def __post_init__(self): self.members = {} self.techniques = [] def add_member(self, avatar: "Avatar") -> None: """添加成员到宗门""" if avatar.id not in self.members: self.members[avatar.id] = avatar def remove_member(self, avatar: "Avatar") -> None: """从宗门移除成员""" if avatar.id in self.members: del self.members[avatar.id] def get_info(self) -> str: hq = self.headquarter return f"{self.name}(阵营:{self.alignment},驻地:{hq.name})" def get_detailed_info(self) -> str: # 详细描述:风格、阵营、驻地 hq = self.headquarter effect_part = f" 效果:{self.effect_desc}" if self.effect_desc else "" return f"{self.name}(阵营:{self.alignment},风格:{self.member_act_style},驻地:{hq.name}){effect_part}" def get_rank_name(self, rank: "SectRank") -> str: """ 获取宗门的职位名称(支持自定义) Args: rank: 宗门职位枚举 Returns: 职位名称字符串 """ from src.classes.sect_ranks import SectRank, DEFAULT_RANK_NAMES # 优先使用自定义名称,否则使用默认名称 return self.rank_names.get(rank.value, DEFAULT_RANK_NAMES.get(rank, "弟子")) def get_structured_info(self) -> dict: hq = self.headquarter from src.classes.sect_ranks import RANK_ORDER from src.server.main import resolve_avatar_pic_id from src.classes.technique import techniques_by_name # 成员列表:直接从 self.members 获取 members_list = [] for a in self.members.values(): rank_enum = getattr(a, "sect_rank", None) sort_val = 999 if rank_enum and rank_enum in RANK_ORDER: sort_val = RANK_ORDER[rank_enum] members_list.append({ "id": str(a.id), "name": a.name, "pic_id": resolve_avatar_pic_id(a), "gender": a.gender.value if hasattr(a.gender, "value") else "male", "rank": a.get_sect_rank_name(), "realm": a.cultivation_progress.realm.value if hasattr(a, 'cultivation_progress') else "未知", "_sort_val": sort_val }) # 按职位排序 members_list.sort(key=lambda x: x["_sort_val"]) # 清理排序字段 for m in members_list: del m["_sort_val"] # 填充 techniques # 使用 technique_names 从全局字典中查找对应的 Technique 对象并序列化 techniques_data = [] for t_name in self.technique_names: t_obj = techniques_by_name.get(t_name) if t_obj: techniques_data.append(t_obj.get_structured_info()) else: # Fallback for missing techniques: create a minimal structure techniques_data.append({ "name": t_name, "desc": "(未知功法)", "grade": "", "color": (200, 200, 200), # Gray "attribute": "", "effect_desc": "" }) return { "id": self.id, "name": self.name, "desc": self.desc, "alignment": str(self.alignment), # 直接返回中文 "style": self.member_act_style, "hq_name": hq.name, "hq_desc": hq.desc, "effect_desc": self.effect_desc, "techniques": techniques_data, # 兼容旧字段,如果前端还要用的话(建议迁移后废弃) "technique_names": self.technique_names, "preferred_weapon": self.preferred_weapon, "members": members_list } def _split_names(value: object) -> list[str]: raw = "" if value is None or str(value) == "nan" else str(value) sep = CONFIG.df.ids_separator parts = [x.strip() for x in raw.split(sep) if x.strip()] if raw else [] return parts def _load_sects() -> tuple[dict[int, Sect], dict[str, Sect]]: """从配表加载 sect 数据""" sects_by_id: dict[int, Sect] = {} sects_by_name: dict[str, Sect] = {} df = game_configs["sect"] # 读取宗门驻地映射(优先从 sect_region.csv 获取驻地地名/描述) sect_region_df = game_configs.get("sect_region") hq_by_sect_id: dict[int, tuple[str, str]] = {} if sect_region_df is not None: for sr in sect_region_df: sid = get_int(sr, "sect_id", -1) if sid == -1: continue hq_name = get_str(sr, "headquarter_name") hq_desc = get_str(sr, "headquarter_desc") hq_by_sect_id[sid] = (hq_name, hq_desc) # 可能不存在 technique 配表或未添加 sect 列,做容错 tech_df = game_configs.get("technique") assets_base = Path("assets/sects") for row in df: name = get_str(row, "name") image_path = assets_base / f"{name}.png" # 收集该宗门下配置的功法名称 technique_names: list[str] = [] # 检查 tech_df 是否存在以及是否有数据 if tech_df: # 检查是否存在 sect 字段 (检查第一行或当前行) technique_names = [ get_str(t, "name") for t in tech_df if get_str(t, "sect") == name and get_str(t, "name") ] weight = get_float(row, "weight", 1.0) # 读取 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) # 读取倾向兵器类型 preferred_weapon = get_str(row, "preferred_weapon") # 解析自定义职位 raw_ranks = get_str(row, "rank_names") rank_names_map = {} if raw_ranks: # 格式:掌门;长老;内门;外门 parts = [x.strip() for x in raw_ranks.split(";") if x.strip()] if len(parts) == 4: from src.classes.sect_ranks import SectRank rank_names_map = { SectRank.Patriarch.value: parts[0], SectRank.Elder.value: parts[1], SectRank.InnerDisciple.value: parts[2], SectRank.OuterDisciple.value: parts[3], } # 从 sect_region.csv 中优先取驻地名称/描述;否则兼容旧列或退回宗门名 sid = get_int(row, "id") csv_hq = hq_by_sect_id.get(sid) hq_name_from_csv = (csv_hq[0] if csv_hq else "").strip() if csv_hq else "" hq_desc_from_csv = (csv_hq[1] if csv_hq else "").strip() if csv_hq else "" hq_name = hq_name_from_csv or get_str(row, "headquarter_name") or name hq_desc = hq_desc_from_csv or get_str(row, "headquarter_desc") sect = Sect( id=sid, name=name, desc=get_str(row, "desc"), member_act_style=get_str(row, "member_act_style"), alignment=Alignment.from_str(get_str(row, "alignment")), headquarter=SectHeadQuarter( name=hq_name, desc=hq_desc, image=image_path, ), technique_names=technique_names, weight=weight, preferred_weapon=preferred_weapon, effects=effects, effect_desc=effect_desc, rank_names=rank_names_map, ) sects_by_id[sect.id] = sect sects_by_name[sect.name] = sect return sects_by_id, sects_by_name # 导出:从配表加载 sect 数据 sects_by_id, sects_by_name = _load_sects() def get_sect_info_with_rank(avatar: "Avatar", detailed: bool = False) -> str: """ 获取包含职位的宗门信息字符串 Args: avatar: 角色对象 detailed: 是否包含宗门详细信息(阵营、风格、驻地等) Returns: - 散修:返回"散修" - detailed=False:返回"明心剑宗长老" - detailed=True:返回"明心剑宗长老(阵营:正,风格:...,驻地:...)" """ from typing import TYPE_CHECKING if TYPE_CHECKING: from src.classes.avatar import Avatar # 散修直接返回 if avatar.sect is None: return "散修" # 获取职位+宗门名(如"明心剑宗长老") sect_rank_str = avatar.get_sect_str() # 如果不需要详细信息,直接返回职位字符串 if not detailed: return sect_rank_str # 需要详细信息:拼接宗门的详细描述 sect_detail = avatar.sect.get_detailed_info() # "明心剑宗(阵营:正,...)" # 提取括号及其内容 if "(" in sect_detail: detail_part = sect_detail[sect_detail.index("("):] return f"{sect_rank_str}{detail_part}" # 如果没有括号(理论上不应该出现),直接返回职位字符串 return sect_rank_str