Files
cultivation-world-simulator/src/sim/new_avatar.py
2026-01-19 21:14:20 +08:00

864 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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