import random from dataclasses import dataclass from typing import List, Optional, Dict, Tuple, Union from src.classes.world import World from src.classes.avatar import Avatar, Gender from src.classes.appearance import get_appearance_by_level from src.classes.calendar import MonthStamp from src.classes.region import Region from src.utils.resolution import resolve_query from src.classes.cultivation import CultivationProgress from src.classes.root import Root from src.classes.age import Age from src.classes.name import get_random_name_for_sect, pick_surname_for_sect, get_random_name_with_surname from src.utils.id_generator import get_avatar_id from src.classes.sect import Sect, sects_by_id, sects_by_name from src.classes.relation import Relation from src.classes.technique import get_technique_by_sect, attribute_to_root, Technique, techniques_by_id, techniques_by_name from src.classes.weapon import Weapon, weapons_by_id, weapons_by_name from src.classes.auxiliary import Auxiliary, auxiliaries_by_id, auxiliaries_by_name from src.classes.persona import Persona, personas_by_id, personas_by_name from src.classes.magic_stone import MagicStone # —— 参数常量(便于调参)—— SECT_MEMBER_RATIO: float = 2 / 3 AGE_MIN: int = 16 AGE_MAX: int = 150 LEVEL_MIN: int = 0 LEVEL_MAX: int = 120 FAMILY_PAIR_CAP_DIV: int = 6 # 家庭上限:n // 6 FAMILY_TRIGGER_PROB: float = 0.35 # 生成家庭对概率 FATHER_CHILD_PROB: float = 0.60 # 家庭为父子(同姓、父为男)的概率;否则母子(异姓、母为女) LOVERS_PAIR_CAP_DIV: int = 5 # 道侣两两预算:n // 5 LOVERS_TRIGGER_PROB: float = 0.25 # 生成一对道侣的概率(强制异性) MASTER_PAIR_PROB: float = 0.30 # 同宗门内生成一对师徒的概率 FRIEND_PROB: float = 0.18 # 朋友概率 ENEMY_PROB: float = 0.10 # 仇人概率(与朋友互斥) PARENT_MIN_DIFF: int = 16 # 父母与子女最小年龄差 PARENT_MAX_DIFF: int = 80 # 父母与子女最大年龄差(用于生成目标差值) PARENT_AGE_CAP: int = 120 # 父母年龄上限(修仙世界放宽) MASTER_LEVEL_MIN_DIFF: int = 20 # 师傅与徒弟最小等级差 MASTER_LEVEL_EXTRA_MAX: int = 10 # 在最小等级差基础上的额外浮动 # 父母-子女等级差(修仙世界中通常父母更强) PARENT_LEVEL_MIN_DIFF: int = 10 # 父母与子女最小等级差 PARENT_LEVEL_EXTRA_MAX: int = 10 # 在最小等级差基础上的额外浮动 # —— 新凡人(单个)生成相关概率与范围 —— NEW_MORTAL_PARENT_PROB: float = 0.30 # 有概率是某个既有角色的子女 NEW_MORTAL_SECT_PROB: float = 0.50 # 有概率成为某个“已有宗门”的弟子 NEW_MORTAL_MASTER_PROB: float = 0.40 # 若成为宗门弟子,有概率拜该宗门现有人物为师 NEW_MORTAL_LEVEL_MAX: int = 40 # 新凡人默认偏低等级上限 def random_gender() -> Gender: return Gender.MALE if random.random() < 0.5 else Gender.FEMALE class EquipmentAllocator: """ 负责所有初始装备分配逻辑,提供兵器与辅助装备的统一接口。 """ @staticmethod def assign_weapon(avatar: Avatar) -> None: """ 初始兵器逻辑: - 80% 继承宗门偏好兵器类型,否则完全随机 - 根据境界随机生成一把兵器 """ from src.classes.weapon import get_random_weapon_by_realm from src.classes.weapon_type import WeaponType weapon_type = None if avatar.sect is not None and avatar.sect.preferred_weapon: if random.random() < 0.8: for wt in WeaponType: if wt.value == avatar.sect.preferred_weapon: weapon_type = wt break avatar.weapon = get_random_weapon_by_realm(avatar.cultivation_progress.realm, weapon_type) @staticmethod def assign_auxiliary(avatar: Avatar) -> None: """ 初始辅助装备逻辑: - 根据境界随机生成一件辅助装备 """ from src.classes.auxiliary import get_random_auxiliary_by_realm avatar.auxiliary = get_random_auxiliary_by_realm(avatar.cultivation_progress.realm) @dataclass class MortalPlan: gender: Optional[Gender] = None sect: Optional[Sect] = None surname: Optional[str] = None parent_avatar: Optional[Avatar] = None master_avatar: Optional[Avatar] = None level: int = 1 pos_x: int = 0 pos_y: int = 0 @dataclass class PopulationPlan: sects: List[Optional[Sect]] genders: List[Optional[Gender]] surnames: List[Optional[str]] relations: Dict[Tuple[int, int], Relation] class MortalPlanner: """ 负责单个角色的前期规划(宗门、性别、关系、出生点等)。 """ @staticmethod def plan( world: World, name: str, age: Age, *, existed_sects: Optional[List[Sect]] = None, existing_avatars: Optional[List[Avatar]] = None, level: int = 1, allow_relations: bool = True, ) -> MortalPlan: plan = MortalPlan(level=level) plan.gender = random_gender() plan.pos_x = random.randint(0, world.map.width - 1) plan.pos_y = random.randint(0, world.map.height - 1) if existing_avatars is None: existing_avatars = world.avatar_manager.get_living_avatars() else: existing_avatars = [av for av in existing_avatars if not av.is_dead] if existed_sects is None: try: from src.classes.sect import sects_by_id as _sects_by_id existed_sects = list(_sects_by_id.values()) except Exception: existed_sects = [] if random.random() < NEW_MORTAL_SECT_PROB: picked = PopulationPlanner._pick_sects_balanced(existed_sects or [], 1) plan.sect = picked[0] if picked else None if allow_relations and existing_avatars: if random.random() < NEW_MORTAL_PARENT_PROB: candidates: list[Avatar] = [ av for av in existing_avatars if av.age.age >= age.age + PARENT_MIN_DIFF ] if candidates: parent = random.choice(candidates) plan.parent_avatar = parent if not name: if parent.gender is Gender.MALE: plan.surname = pick_surname_for_sect(plan.sect or parent.sect) else: mom_surname = pick_surname_for_sect(plan.sect or parent.sect) for _ in range(5): s = pick_surname_for_sect(plan.sect) if s != mom_surname: plan.surname = s break if plan.sect is not None and random.random() < NEW_MORTAL_MASTER_PROB: same_sect = [av for av in existing_avatars if av.sect is plan.sect] if same_sect: stronger = [ av for av in same_sect if av.cultivation_progress.level >= plan.level + MASTER_LEVEL_MIN_DIFF ] if stronger: plan.master_avatar = random.choice(stronger) return plan class PopulationPlanner: """ 负责批量角色的宗门/关系规划。 """ @staticmethod def plan_group(n: int, existed_sects: Optional[List[Sect]]) -> PopulationPlan: n = int(max(0, n)) use_sects = bool(existed_sects) planned_sect: list[Optional[Sect]] = [None] * n if n == 0: return PopulationPlan(planned_sect, [None] * 0, [None] * 0, {}) if use_sects and existed_sects: sect_member_target = int(n * SECT_MEMBER_RATIO) planned_sect[:sect_member_target] = PopulationPlanner._pick_sects_balanced(existed_sects, sect_member_target) paired = list(zip(planned_sect, list(range(n)))) random.shuffle(paired) planned_sect = [p[0] for p in paired] planned_gender: list[Optional[Gender]] = [None] * n planned_surname: list[Optional[str]] = [None] * n planned_relations: dict[tuple[int, int], Relation] = {} # — 家庭 — unused_indices = list(range(n)) random.shuffle(unused_indices) def _reserve_pair() -> tuple[int, int] | None: if len(unused_indices) < 2: return None a = unused_indices.pop() b = unused_indices.pop() return (a, b) family_pairs_budget = max(0, n // FAMILY_PAIR_CAP_DIV) for _ in range(family_pairs_budget): if random.random() < FAMILY_TRIGGER_PROB: pair = _reserve_pair() if pair is None: break a, b = pair if random.random() < FATHER_CHILD_PROB: surname = pick_surname_for_sect(planned_sect[a] or planned_sect[b]) planned_surname[a] = surname planned_surname[b] = surname planned_gender[a] = Gender.MALE # 设定 a 为父,b 为子 # (a, b) = PARENT -> a.relations[b] = PARENT (a 视 b 为子) planned_relations[(a, b)] = Relation.PARENT else: mother = a if random.random() < 0.5 else b child = b if mother == a else a planned_gender[mother] = Gender.FEMALE mom_surname = pick_surname_for_sect(planned_sect[mother]) planned_surname[mother] = mom_surname for _ in range(5): s = pick_surname_for_sect(planned_sect[child]) if s != mom_surname: planned_surname[child] = s break # (mother, child) = PARENT -> mother.relations[child] = PARENT planned_relations[(mother, child)] = Relation.PARENT leftover = unused_indices[:] # — 道侣 — random.shuffle(leftover) lovers_budget = max(0, n // LOVERS_PAIR_CAP_DIV) i = 0 while i + 1 < len(leftover) and lovers_budget > 0: if random.random() < LOVERS_TRIGGER_PROB: a = leftover[i] b = leftover[i + 1] if (a, b) not in planned_relations and (b, a) not in planned_relations: if planned_gender[a] is None and planned_gender[b] is None: planned_gender[a] = Gender.MALE if random.random() < 0.5 else Gender.FEMALE planned_gender[b] = Gender.FEMALE if planned_gender[a] is Gender.MALE else Gender.MALE elif planned_gender[a] is None: planned_gender[a] = Gender.MALE if planned_gender[b] is Gender.FEMALE else Gender.FEMALE elif planned_gender[b] is None: planned_gender[b] = Gender.MALE if planned_gender[a] is Gender.FEMALE else Gender.FEMALE if planned_gender[a] != planned_gender[b]: planned_relations[(a, b)] = Relation.LOVERS lovers_budget -= 1 i += 2 # — 师徒(同宗门)— if use_sects and existed_sects: members_by_sect: dict[int, list[int]] = {s.id: [] for s in existed_sects} for idx, sect in enumerate(planned_sect): if sect is not None: members_by_sect.setdefault(sect.id, []).append(idx) for members in members_by_sect.values(): random.shuffle(members) j = 0 while j + 1 < len(members): if random.random() < MASTER_PAIR_PROB: master, apprentice = members[j], members[j + 1] if (master, apprentice) not in planned_relations and (apprentice, master) not in planned_relations: # MASTER -> APPRENTICE (Master.relations[Apprentice] = APPRENTICE) planned_relations[(master, apprentice)] = Relation.APPRENTICE j += 2 # — 朋友/仇人 — all_indices = list(range(n)) random.shuffle(all_indices) k = 0 while k + 1 < len(all_indices): a, b = all_indices[k], all_indices[k + 1] if (a, b) in planned_relations or (b, a) in planned_relations: k += 2 continue r = random.random() if r < FRIEND_PROB: planned_relations[(a, b)] = Relation.FRIEND elif r < FRIEND_PROB + ENEMY_PROB: planned_relations[(a, b)] = Relation.ENEMY k += 2 for idx in range(n): if planned_gender[idx] is None: planned_gender[idx] = random_gender() return PopulationPlan(planned_sect, planned_gender, planned_surname, planned_relations) @staticmethod def _pick_sects_balanced(existed_sects: List[Sect], k: int) -> list[Optional[Sect]]: if not existed_sects or k <= 0: return [] counts: dict[int, int] = {s.id: 0 for s in existed_sects} chosen: list[Optional[Sect]] = [] for _ in range(k): min_count = min(counts.values()) if counts else 0 candidates = [s for s in existed_sects if counts.get(s.id, 0) == min_count] s = random.choice(candidates) counts[s.id] = counts.get(s.id, 0) + 1 chosen.append(s) return chosen class RelationApplier: """ 负责将规划关系写入 Avatar 实例。 """ @staticmethod def apply(avatars_by_index: List[Optional[Avatar]], relations: dict[tuple[int, int], Relation]) -> None: for (a, b), relation in relations.items(): if a >= len(avatars_by_index) or b >= len(avatars_by_index): continue av_a = avatars_by_index[a] av_b = avatars_by_index[b] if av_a is None or av_b is None or av_a is av_b: continue av_a.set_relation(av_b, relation) class SectRankAssigner: """ 负责宗门职位的分配,保证掌门唯一。 """ @staticmethod def assign_one(avatar: Avatar, world: World) -> None: if avatar.sect is None: avatar.sect_rank = None return from src.classes.sect_ranks import get_rank_from_realm, sect_has_patriarch, SectRank rank = get_rank_from_realm(avatar.cultivation_progress.realm) if rank == SectRank.Patriarch and sect_has_patriarch(avatar): rank = SectRank.Elder avatar.sect_rank = rank @staticmethod def assign_batch(avatars: List[Avatar], world: World) -> None: from src.classes.sect_ranks import get_rank_from_realm, SectRank for avatar in avatars: if avatar is None: continue if avatar.sect is None: avatar.sect_rank = None else: avatar.sect_rank = get_rank_from_realm(avatar.cultivation_progress.realm) sect_nascent_souls: Dict[int, List[Avatar]] = {} for avatar in avatars: if avatar is None or avatar.sect is None: continue if avatar.sect_rank == SectRank.Patriarch: sect_id = avatar.sect.id if sect_id not in sect_nascent_souls: sect_nascent_souls[sect_id] = [] sect_nascent_souls[sect_id].append(avatar) existing_patriarchs: Dict[int, bool] = {} for other in world.avatar_manager.avatars.values(): if other.sect is not None and other.sect_rank == SectRank.Patriarch: existing_patriarchs[other.sect.id] = True for sect_id, candidates in sect_nascent_souls.items(): if existing_patriarchs.get(sect_id, False): for avatar in candidates: avatar.sect_rank = SectRank.Elder else: candidates.sort(key=lambda av: av.cultivation_progress.level, reverse=True) for avatar in candidates[1:]: avatar.sect_rank = SectRank.Elder class AvatarFactory: """ 根据规划产出 Avatar,对装备、宗门职位和关系进行统一处理。 """ @staticmethod def build_from_plan( world: World, current_month_stamp: MonthStamp, *, name: str, age: Age, plan: MortalPlan, attach_relations: bool = True, overrides: Optional[Dict[str, object]] = None, ) -> Avatar: if name: final_name = name else: if plan.surname: final_name = get_random_name_with_surname(plan.gender, plan.surname, plan.sect) else: final_name = get_random_name_for_sect(plan.gender, plan.sect) birth_month_stamp = current_month_stamp - age.age * 12 + random.randint(0, 11) avatar = Avatar( world=world, name=final_name, id=get_avatar_id(), birth_month_stamp=MonthStamp(birth_month_stamp), age=age, gender=plan.gender, cultivation_progress=CultivationProgress(plan.level), pos_x=plan.pos_x, pos_y=plan.pos_y, sect=plan.sect, ) avatar.magic_stone = MagicStone(50) avatar.tile = world.map.get_tile(avatar.pos_x, avatar.pos_y) SectRankAssigner.assign_one(avatar, world) EquipmentAllocator.assign_weapon(avatar) EquipmentAllocator.assign_auxiliary(avatar) if attach_relations: if plan.parent_avatar is not None: # plan.parent_avatar 是长辈 # 设置关系:长辈.set_relation(自己, PARENT) # 底层逻辑:长辈.relations[自己] = PARENT (长辈认为自己是父母 -> 错误,是长辈认为自己是子女?) # 修正:Relation.PARENT 映射显示为“儿子/女儿”,即 relations[X]=PARENT 意味着 X 是儿子/女儿 # 所以 plan.parent_avatar.relations[avatar] = PARENT 是正确的,表示 parent 视 avatar 为子女 plan.parent_avatar.set_relation(avatar, Relation.PARENT) if plan.master_avatar is not None: # plan.master_avatar 是师傅 # 设置关系:师傅.set_relation(自己, APPRENTICE) # 底层逻辑:师傅.relations[自己] = APPRENTICE (师傅认为自己是徒弟) # 自己.relations[师傅] = MASTER (自己认为师傅是师傅) plan.master_avatar.set_relation(avatar, Relation.APPRENTICE) if avatar.technique is not None: mapped = attribute_to_root(avatar.technique.attribute) if mapped is not None: avatar.root = mapped if overrides: AvatarFactory._apply_overrides(avatar, overrides) return avatar @staticmethod def build_group( world: World, current_month_stamp: MonthStamp, population_plan: PopulationPlan, ) -> dict[str, Avatar]: planned_sect = population_plan.sects planned_gender = population_plan.genders planned_surname = population_plan.surnames planned_relations = population_plan.relations n = len(planned_sect) width, height = world.map.width, world.map.height ages: list[int] = [random.randint(AGE_MIN, AGE_MAX) for _ in range(n)] levels: list[int] = [random.randint(LEVEL_MIN, LEVEL_MAX) for _ in range(n)] for (a, b), rel in list(planned_relations.items()): if rel is Relation.CHILD: if ages[a] <= ages[b] + (PARENT_MIN_DIFF - 1): ages[a] = min(PARENT_AGE_CAP, ages[b] + random.randint(PARENT_MIN_DIFF, PARENT_MAX_DIFF)) for (a, b), rel in list(planned_relations.items()): if rel is Relation.CHILD: if levels[a] <= levels[b]: levels[a] = min(LEVEL_MAX, levels[b] + 1) if levels[a] < levels[b] + PARENT_LEVEL_MIN_DIFF: levels[a] = min(LEVEL_MAX, levels[b] + PARENT_LEVEL_MIN_DIFF + random.randint(0, PARENT_LEVEL_EXTRA_MAX)) for (a, b), rel in list(planned_relations.items()): if rel is Relation.APPRENTICE: if levels[a] < levels[b] + MASTER_LEVEL_MIN_DIFF: levels[a] = min(LEVEL_MAX, levels[b] + MASTER_LEVEL_MIN_DIFF + random.randint(0, MASTER_LEVEL_EXTRA_MAX)) # 确保年龄不超过境界寿命上限,避免角色一出生就老死 # 放在所有关系调整之后,因为关系调整可能会修改年龄和等级 for i in range(n): realm = CultivationProgress(levels[i]).realm max_lifespan = Age.REALM_LIFESPAN.get(realm, 100) if ages[i] >= max_lifespan: # 将年龄限制为寿命上限的 80%-95%,保留一定的生存空间 ages[i] = int(max_lifespan * random.uniform(0.8, 0.95)) avatars_by_index: list[Avatar] = [None] * n # type: ignore avatars_by_id: dict[str, Avatar] = {} for i in range(n): gender = planned_gender[i] or random_gender() sect = planned_sect[i] if planned_surname[i]: name = get_random_name_with_surname(gender, planned_surname[i] or "", sect) else: name = get_random_name_for_sect(gender, sect) level = levels[i] cultivation_progress = CultivationProgress(level) age_years = ages[i] age = Age(age_years, cultivation_progress.realm) x, y = random.randint(0, width - 1), random.randint(0, height - 1) birth_month_stamp = current_month_stamp - age_years * 12 + random.randint(0, 11) avatar = Avatar( world=world, name=name, id=get_avatar_id(), birth_month_stamp=MonthStamp(birth_month_stamp), age=age, gender=gender, cultivation_progress=cultivation_progress, pos_x=x, pos_y=y, root=random.choice(list(Root)), sect=sect, ) avatar.magic_stone = MagicStone(50) avatar.tile = world.map.get_tile(x, y) if sect is not None: avatar.alignment = sect.alignment avatar.technique = get_technique_by_sect(sect) EquipmentAllocator.assign_weapon(avatar) EquipmentAllocator.assign_auxiliary(avatar) if avatar.technique is not None: mapped = attribute_to_root(avatar.technique.attribute) if mapped is not None: avatar.root = mapped avatars_by_index[i] = avatar avatars_by_id[avatar.id] = avatar SectRankAssigner.assign_batch(avatars_by_index, world) RelationApplier.apply(avatars_by_index, planned_relations) return avatars_by_id @staticmethod def _apply_overrides(avatar: Avatar, overrides: Dict[str, object]) -> None: technique = overrides.get("technique") if isinstance(technique, Technique): avatar.technique = technique mapped = attribute_to_root(technique.attribute) if mapped is not None: avatar.root = mapped weapon = overrides.get("weapon") if isinstance(weapon, Weapon): avatar.weapon = weapon auxiliary = overrides.get("auxiliary") if isinstance(auxiliary, Auxiliary): avatar.auxiliary = auxiliary personas = overrides.get("personas") if isinstance(personas, list) and personas: avatar.personas = personas # type: ignore[assignment] appearance = overrides.get("appearance") if isinstance(appearance, int): avatar.appearance = get_appearance_by_level(appearance) def create_random_mortal(world: World, current_month_stamp: MonthStamp, name: str, age: Age, level: int = 1) -> Avatar: """ 创建一个完全随机的新修士,包含可能的亲属/师徒关系。 """ plan = MortalPlanner.plan(world, name=name, age=age, level=level, allow_relations=True) return AvatarFactory.build_from_plan(world, current_month_stamp, name=name, age=age, plan=plan) def make_avatars( world: World, count: int = 12, current_month_stamp: MonthStamp = MonthStamp(100 * 12), existed_sects: Optional[List[Sect]] = None, ) -> dict[str, Avatar]: population_plan = PopulationPlanner.plan_group(count, existed_sects) random_avatars = AvatarFactory.build_group(world, current_month_stamp, population_plan) return random_avatars # —— 指定参数创建:支持传入字符串并解析为对象 —— def _parse_gender(value: Union[str, Gender, None]) -> Optional[Gender]: if value is None: return None if isinstance(value, Gender): return value s = str(value).strip() if s == "男": return Gender.MALE if s == "女": return Gender.FEMALE return None def _parse_sect(value: Union[str, int, Sect, None]) -> Optional[Sect]: if value is None: return None if isinstance(value, Sect): return value # 纯数字视为 id if isinstance(value, int): return sects_by_id.get(value) s = str(value).strip() if not s: return None if s.isdigit(): return sects_by_id.get(int(s)) return sects_by_name.get(s) def _parse_technique(value: Union[str, int, Technique, None]) -> Optional[Technique]: if value is None: return None if isinstance(value, Technique): return value if isinstance(value, int): return techniques_by_id.get(value) s = str(value).strip() if not s: return None if s.isdigit(): return techniques_by_id.get(int(s)) return techniques_by_name.get(s) def _parse_weapon(value: Union[str, int, Weapon, None]) -> Optional[Weapon]: if value is None: return None if isinstance(value, Weapon): return value if isinstance(value, int): return weapons_by_id.get(value) s = str(value).strip() if not s: return None if s.isdigit(): return weapons_by_id.get(int(s)) return weapons_by_name.get(s) def _parse_auxiliary(value: Union[str, int, Auxiliary, None]) -> Optional[Auxiliary]: if value is None: return None if isinstance(value, Auxiliary): return value if isinstance(value, int): return auxiliaries_by_id.get(value) s = str(value).strip() if not s: return None if s.isdigit(): return auxiliaries_by_id.get(int(s)) return auxiliaries_by_name.get(s) def _parse_personas(value: Union[str, int, Persona, List[Union[str, int, Persona]], None]) -> Optional[List[Persona]]: if value is None: return None # 统一展开为列表,兼容 OmegaConf 的 ListConfig def _as_list(v: object) -> List[object]: # Persona 自身视为标量 if isinstance(v, Persona): return [v] # 原生序列 if isinstance(v, (list, tuple, set)): return list(v) # 兼容 OmegaConf.ListConfig(若存在) try: from omegaconf import ListConfig # type: ignore if isinstance(v, ListConfig): return list(v) except Exception: pass # 其它可迭代但非字符串:尽量展开 if hasattr(v, "__iter__") and not isinstance(v, (str, bytes)): try: return list(v) # type: ignore except Exception: return [v] return [v] raw_values = _as_list(value) values: List[Union[str, int, Persona]] = raw_values # type: ignore result: List[Persona] = [] for v in values: if isinstance(v, Persona): result.append(v) continue if isinstance(v, int): p = personas_by_id.get(v) if p is not None: result.append(p) continue s = str(v).strip() if not s: continue if s.isdigit(): p = personas_by_id.get(int(s)) if p is not None: result.append(p) else: p = personas_by_name.get(s) if p is not None: result.append(p) # 去重,保持顺序 seen: set[int] = set() unique: List[Persona] = [] for p in result: if p.id in seen: continue seen.add(p.id) unique.append(p) return unique if unique else None def create_avatar_from_request( world: World, current_month_stamp: MonthStamp, *, name: Optional[str] = None, age: Union[int, Age, None] = None, gender: Union[str, Gender, None] = None, sect: Union[str, int, Sect, None] = None, level: Optional[int] = None, pos: Optional[Tuple[int, int]] = None, technique: Union[str, int, Technique, None] = None, weapon: Union[str, int, Weapon, None] = None, auxiliary: Union[str, int, Auxiliary, None] = None, personas: Union[str, int, Persona, List[Union[str, int, Persona]], None] = None, appearance: Optional[int] = None, relations: Optional[List[Dict[str, str]]] = None, ) -> Avatar: """ 供前端使用的角色创建入口:支持字符串/ID 参数,且默认不生成亲友关系。 """ # 年龄(先取整数年龄,规划阶段只用到 age.age,不依赖 realm) if isinstance(age, Age): age_years = age.age elif isinstance(age, int): age_years = max(AGE_MIN, age) else: age_years = random.randint(AGE_MIN, AGE_MAX) tmp_age_for_plan = Age(age_years, CultivationProgress(LEVEL_MIN).realm) plan = MortalPlanner.plan(world, name=name or "", age=tmp_age_for_plan, allow_relations=False) # 覆盖:性别 g = _parse_gender(gender) if g is not None: plan.gender = g # 覆盖:宗门 s = _parse_sect(sect) if s is not None: plan.sect = s # 覆盖:等级 if isinstance(level, int): plan.level = max(LEVEL_MIN, min(LEVEL_MAX, level)) # 覆盖:坐标 if isinstance(pos, tuple) and len(pos) == 2: x, y = int(pos[0]), int(pos[1]) # 夹在地图范围内 x = max(0, min(world.map.width - 1, x)) y = max(0, min(world.map.height - 1, y)) plan.pos_x, plan.pos_y = x, y # 根据最终等级推导境界,再构造 Age final_realm = CultivationProgress(plan.level).realm final_age = Age(age_years, final_realm) # 生成 overrides: Dict[str, object] = {} tech_obj = _parse_technique(technique) if tech_obj is not None: overrides["technique"] = tech_obj weapon_obj = _parse_weapon(weapon) if weapon_obj is not None: overrides["weapon"] = weapon_obj auxiliary_obj = _parse_auxiliary(auxiliary) if auxiliary_obj is not None: overrides["auxiliary"] = auxiliary_obj pers_list = _parse_personas(personas) if pers_list: overrides["personas"] = pers_list if isinstance(appearance, int): overrides["appearance"] = appearance avatar = AvatarFactory.build_from_plan( world, current_month_stamp, name=name or "", age=final_age, plan=plan, attach_relations=False, overrides=overrides if overrides else None, ) if relations: for rel_item in relations: target_id = rel_item.get('target_id') rel_type = rel_item.get('relation') if not target_id or not rel_type: continue # 尝试转为字符串ID t_id_str = str(target_id) target = world.avatar_manager.avatars.get(t_id_str) if not target: continue # 解析关系 rel_enum = None for r in Relation: if r.value == rel_type: rel_enum = r break if rel_enum: avatar.set_relation(target, rel_enum) return avatar