add trait
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -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
106
src/classes/trait.py
Normal 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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
5
static/game_configs/trait.csv
Normal file
5
static/game_configs/trait.csv
Normal 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}"
|
||||
|
||||
|
Reference in New Issue
Block a user