add trait

This commit is contained in:
bridge
2025-11-03 00:16:45 +08:00
parent 9a9b044afc
commit d1c440bb5f
5 changed files with 150 additions and 2 deletions

View File

@@ -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))

View File

@@ -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 []

106
src/classes/trait.py Normal file
View File

@@ -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]

View File

@@ -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)

View File

@@ -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}"
1 id name desc weight condition effects
2 名称 描述/用于LLM输入的文本 采样时不同权重被采样到的概率 选取条件(可用avatar字段/Alignment中文或英文等) JSON形式
3 1 无特殊天赋 19
4 2 气运之子 天生气运加身,更易遇到奇遇,战斗力也略有提升 1 {"extra_fortune_probability": 0.04, "extra_battle_strength_points": 2}