From d1c440bb5fb86319aa429f949354e6738f359458 Mon Sep 17 00:00:00 2001 From: bridge Date: Mon, 3 Nov 2025 00:16:45 +0800 Subject: [PATCH] add trait --- src/classes/avatar.py | 14 ++++- src/classes/fortune.py | 4 +- src/classes/trait.py | 106 ++++++++++++++++++++++++++++++++++ src/sim/new_avatar.py | 23 ++++++++ static/game_configs/trait.csv | 5 ++ 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/classes/trait.py create mode 100644 static/game_configs/trait.csv diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 9bdc595..dc00eaa 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -24,6 +24,7 @@ from src.classes.alignment import Alignment from src.classes.persona import Persona, personas_by_id, get_random_compatible_personas from src.classes.item import Item from src.classes.treasure import Treasure +from src.classes.trait import Trait, get_random_trait from src.classes.magic_stone import MagicStone from src.classes.hp_and_mp import HP, MP, HP_MAX_BY_REALM, MP_MAX_BY_REALM from src.utils.id_generator import get_avatar_id @@ -94,6 +95,8 @@ class Avatar: treasure: Optional[Treasure] = None # 灵兽:最多一个;若再次捕捉则覆盖 spirit_animal: Optional[SpiritAnimal] = None + # 特质:每个角色有且仅有一个 + trait: Trait = None # 将在__post_init__中初始化 # 当月/当步新设动作标记:在 commit_next_plan 设为 True,首次 tick_action 后清为 False _new_action_set_this_step: bool = False # 动作冷却:记录动作类名 -> 上次完成月戳 @@ -118,6 +121,10 @@ class Avatar: if not self.personas: self.personas = get_random_compatible_personas(persona_num, avatar=self) + # 如果trait为空,则随机分配一个 + if self.trait is None: + self.trait = get_random_trait(self) + # 出生即按宗门分配功法: # - 散修:仅从无宗门功法抽样 # - 有宗门:从“无宗门 + 本宗门”集合抽样 @@ -144,6 +151,8 @@ class Avatar: merged = _merge_effects(merged, self.technique.effects) # 来自灵根 merged = _merge_effects(merged, self.root.effects) + # 来自特质 + merged = _merge_effects(merged, self.trait.effects) # 来自法宝 if self.treasure is not None: merged = _merge_effects(merged, self.treasure.effects) @@ -218,6 +227,7 @@ class Avatar: "功法": technique_info, "境界": cultivation_info, "个性": personas_info, + "特质": self.trait.get_detailed_info() if detailed else self.trait.get_info(), "物品": items_info, "外貌": appearance_info, "法宝": treasure_info, @@ -274,7 +284,7 @@ class Avatar: plan = self.planned_actions.pop(0) try: action = self.create_action(plan.action_name) - except: + except Exception as e: logger = get_logger().logger logger.warning("非法动作: Avatar(name=%s,id=%s) 的动作 %s 参数=%s 无法启动,原因=%s", self.name, self.id, plan.action_name, plan.params, e) continue @@ -543,6 +553,8 @@ class Avatar: if self.personas: add_kv(lines, "个性", ", ".join([p.name for p in self.personas])) + add_kv(lines, "特质", self.trait.get_info()) + add_kv(lines, "位置", f"({self.pos_x}, {self.pos_y})") add_kv(lines, "灵石", str(self.magic_stone)) diff --git a/src/classes/fortune.py b/src/classes/fortune.py index 56ef462..05d4ec6 100644 --- a/src/classes/fortune.py +++ b/src/classes/fortune.py @@ -252,7 +252,9 @@ def try_trigger_fortune(avatar: Avatar) -> list[Event]: * 拜师:建立师徒关系 - 故事:仅给出主旨主题,由 LLM 自由发挥生成短故事。 """ - prob = float(getattr(CONFIG.game, "fortune_probability", 0.0)) + base_prob = float(getattr(CONFIG.game, "fortune_probability", 0.0)) + extra_prob = float(avatar.effects.get("extra_fortune_probability", 0.0)) + prob = base_prob + extra_prob if prob <= 0.0: return [] diff --git a/src/classes/trait.py b/src/classes/trait.py new file mode 100644 index 0000000..9aefd87 --- /dev/null +++ b/src/classes/trait.py @@ -0,0 +1,106 @@ +import random +from dataclasses import dataclass +from typing import Optional, TYPE_CHECKING + +from src.utils.df import game_configs +from src.classes.effect import load_effect_from_str + +if TYPE_CHECKING: + from src.classes.avatar import Avatar + + +@dataclass +class Trait: + """ + 角色特质 + 每个角色有且仅有一个特质(可能是占位特质"无") + """ + id: int + name: str + desc: str + weight: float + condition: str + effects: dict[str, object] + + def get_info(self) -> str: + return self.name + + def get_detailed_info(self) -> str: + return f"{self.name}({self.desc})" + + +def _load_traits() -> tuple[dict[int, Trait], dict[str, Trait]]: + """从配表加载trait数据""" + traits_by_id: dict[int, Trait] = {} + traits_by_name: dict[str, Trait] = {} + + trait_df = game_configs["trait"] + for _, row in trait_df.iterrows(): + # 解析权重(缺失或为 NaN 时默认为 1.0) + weight_val = row.get("weight", 1) + weight_str = str(weight_val).strip() + weight = float(weight_str) if weight_str and weight_str.lower() != "nan" else 1.0 + + # 条件:可为空 + condition_val = row.get("condition", "") + condition = "" if str(condition_val) == "nan" else str(condition_val).strip() + + # 解析effects + raw_effects_val = row.get("effects", "") + effects = load_effect_from_str(raw_effects_val) + + trait = Trait( + id=int(row["id"]), + name=str(row["name"]), + desc=str(row["desc"]), + weight=weight, + condition=condition, + effects=effects, + ) + traits_by_id[trait.id] = trait + traits_by_name[trait.name] = trait + + return traits_by_id, traits_by_name + + +# 从配表加载trait数据 +traits_by_id, traits_by_name = _load_traits() + + +def _is_trait_allowed(trait_id: int, avatar: Optional["Avatar"]) -> bool: + """ + 判断特质是否允许被选择(条件判定) + """ + trait = traits_by_id[trait_id] + # 条件判定 + if avatar is not None and trait.condition: + allowed = bool(eval(trait.condition, {"__builtins__": {}}, {"avatar": avatar})) + if not allowed: + return False + return True + + +def get_random_trait(avatar: Optional["Avatar"] = None) -> Trait: + """ + 根据权重随机选择一个特质 + + Args: + avatar: 可选,若提供则按 trait.condition 过滤 + + Returns: + Trait: 选中的特质 + """ + # 初始候选:若提供 avatar,则先按条件过滤;否则全量 + available_ids = list(traits_by_id.keys()) + if avatar is not None: + available_ids = [tid for tid in available_ids if _is_trait_allowed(tid, avatar)] + + if not available_ids: + # 如果没有可用的特质,返回第一个(通常是"无") + return list(traits_by_id.values())[0] + + candidates = [traits_by_id[tid] for tid in available_ids] + weights = [max(0.0, c.weight) for c in candidates] + + return random.choices(candidates, weights=weights, k=1)[0] + diff --git a/src/sim/new_avatar.py b/src/sim/new_avatar.py index 82c5bc1..fd82b9c 100644 --- a/src/sim/new_avatar.py +++ b/src/sim/new_avatar.py @@ -18,6 +18,7 @@ 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.treasure import treasures_by_sect_id, Treasure, treasures_by_id, treasures_by_name from src.classes.persona import Persona, personas_by_id, personas_by_name +from src.classes.trait import Trait, traits_by_id, traits_by_name # —— 参数常量(便于调参)—— @@ -565,6 +566,22 @@ def _parse_treasure(value: Union[str, int, Treasure, None]) -> Optional[Treasure return treasures_by_name.get(s) +def _parse_trait(value: Union[str, int, Trait, None]) -> Optional[Trait]: + """解析trait参数""" + if value is None: + return None + if isinstance(value, Trait): + return value + if isinstance(value, int): + return traits_by_id.get(value) + s = str(value).strip() + if not s: + return None + if s.isdigit(): + return traits_by_id.get(int(s)) + return traits_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 @@ -639,6 +656,7 @@ def get_new_avatar_with_config( technique: Union[str, int, Technique, None] = None, treasure: Union[str, int, Treasure, None] = None, personas: Union[str, int, Persona, List[Union[str, int, Persona]], None] = None, + trait: Union[str, int, Trait, None] = None, appearance: Optional[int] = None, ) -> Avatar: """ @@ -714,6 +732,11 @@ def get_new_avatar_with_config( if pers_list is not None and len(pers_list) > 0: avatar.personas = pers_list + # 覆盖:特质 + trait_obj = _parse_trait(trait) + if trait_obj is not None: + avatar.trait = trait_obj + # 覆盖:外貌/颜值 if isinstance(appearance, int): avatar.appearance = get_appearance_by_level(appearance) diff --git a/static/game_configs/trait.csv b/static/game_configs/trait.csv new file mode 100644 index 0000000..539ca35 --- /dev/null +++ b/static/game_configs/trait.csv @@ -0,0 +1,5 @@ +id,name,desc,weight,condition,effects +,名称,描述/用于LLM输入的文本,采样时不同权重被采样到的概率,选取条件(可用avatar字段/Alignment中文或英文等),"JSON形式" +1,无,无特殊天赋,19,, +2,气运之子,天生气运加身,更易遇到奇遇,战斗力也略有提升,1,,"{""extra_fortune_probability"": 0.04, ""extra_battle_strength_points"": 2}" +