merge persona and traits & add rarity

This commit is contained in:
bridge
2025-11-11 23:07:36 +08:00
parent 29092efa90
commit 9949265748
14 changed files with 247 additions and 200 deletions

View File

@@ -38,7 +38,11 @@ class Harvest(TimedAction):
target_plant = random.choice(available_plants)
# 随机选择该植物的一种物品
item = random.choice(target_plant.items)
self.avatar.add_item(item, 1)
# 基础获得1个额外物品来自effects
base_quantity = 1
extra_items = int(self.avatar.effects.get("extra_harvest_items", 0) or 0)
total_quantity = base_quantity + extra_items
self.avatar.add_item(item, total_quantity)
def can_start(self) -> tuple[bool, str]:
region = self.avatar.tile.region

View File

@@ -38,7 +38,11 @@ class Hunt(TimedAction):
target_animal = random.choice(available_animals)
# 随机选择该动物的一种物品
item = random.choice(target_animal.items)
self.avatar.add_item(item, 1)
# 基础获得1个额外物品来自effects
base_quantity = 1
extra_items = int(self.avatar.effects.get("extra_hunt_items", 0) or 0)
total_quantity = base_quantity + extra_items
self.avatar.add_item(item, total_quantity)
def can_start(self) -> tuple[bool, str]:
region = self.avatar.tile.region

View File

@@ -29,7 +29,6 @@ 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
@@ -102,8 +101,6 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
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
# 动作冷却:记录动作类名 -> 上次完成月戳
@@ -124,14 +121,10 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
# 最大寿元已在 Age 构造时基于境界初始化
# 如果personas列表为空则随机分配两个符合条件且不互斥的persona
# 如果personas列表为空则随机分配符合条件且不互斥的persona
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)
# 出生即按宗门分配功法:
# - 散修:仅从无宗门功法抽样
# - 有宗门:从“无宗门 + 本宗门”集合抽样
@@ -158,8 +151,9 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
merged = _merge_effects(merged, self.technique.effects)
# 来自灵根
merged = _merge_effects(merged, self.root.effects)
# 来自特质
merged = _merge_effects(merged, self.trait.effects)
# 来自特质persona
for persona in self.personas:
merged = _merge_effects(merged, persona.effects)
# 来自法宝
if self.treasure is not None:
merged = _merge_effects(merged, self.treasure.effects)
@@ -235,8 +229,7 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
"灵根": root_info,
"功法": technique_info,
"境界": cultivation_info,
"个性": personas_info,
"特质": self.trait.get_detailed_info() if detailed else self.trait.get_info(),
"特质": personas_info,
"物品": items_info,
"外貌": appearance_info,
"法宝": treasure_info,
@@ -570,9 +563,12 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
add_kv(lines, "功法", tech_str)
if self.personas:
add_kv(lines, "个性", ", ".join([p.name for p in self.personas]))
add_kv(lines, "特质", self.trait.get_info())
# 使用颜色标记格式:<color:R,G,B>text</color>
persona_parts = []
for p in self.personas:
r, g, b = p.rarity.color_rgb
persona_parts.append(f"<color:{r},{g},{b}>{p.name}</color>")
add_kv(lines, "特质", ", ".join(persona_parts))
add_kv(lines, "位置", f"({self.pos_x}, {self.pos_y})")
add_kv(lines, "灵石", str(self.magic_stone))

View File

@@ -4,6 +4,8 @@ from typing import List, Optional, TYPE_CHECKING
from src.utils.df import game_configs
from src.utils.config import CONFIG
from src.classes.effect import load_effect_from_str
from src.classes.rarity import Rarity, get_rarity_from_str
ids_separator = CONFIG.df.ids_separator
@@ -14,14 +16,21 @@ if TYPE_CHECKING:
@dataclass
class Persona:
"""
角色个性
角色特质
包含个性、天赋等角色特征
"""
id: int
name: str
desc: str
exclusion_ids: List[int]
weight: float
rarity: Rarity
condition: str
effects: dict[str, object]
@property
def weight(self) -> float:
"""根据稀有度获取采样权重"""
return self.rarity.weight
def get_info(self) -> str:
return self.name
@@ -41,21 +50,25 @@ def _load_personas() -> tuple[dict[int, Persona], dict[str, Persona]]:
exclusion_ids = []
if exclusion_ids_str:
exclusion_ids = [int(x.strip()) for x in exclusion_ids_str.split(ids_separator) if x.strip()]
# 解析权重(缺失或为 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
# 解析稀有度(缺失或为 NaN 时默认为 N
rarity_val = row.get("rarity", "N")
rarity_str = str(rarity_val).strip().upper()
rarity = get_rarity_from_str(rarity_str) if rarity_str and rarity_str != "NAN" else get_rarity_from_str("N")
# 条件:可为空
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)
persona = Persona(
id=int(row["id"]),
name=str(row["name"]),
desc=str(row["desc"]),
exclusion_ids=exclusion_ids,
weight=weight,
rarity=rarity,
condition=condition,
effects=effects,
)
personas_by_id[persona.id] = persona
personas_by_name[persona.name] = persona

96
src/classes/rarity.py Normal file
View File

@@ -0,0 +1,96 @@
"""
稀有度系统
定义角色特质、物品等的稀有度等级
"""
from enum import Enum
from dataclasses import dataclass
class RarityLevel(Enum):
"""稀有度等级"""
N = "N" # Normal - 普通
R = "R" # Rare - 稀有
SR = "SR" # Super Rare - 超稀有
SSR = "SSR" # Super Super Rare - 传说
@dataclass
class Rarity:
"""
稀有度配置
包含等级、权重、颜色等信息
"""
level: RarityLevel
weight: float
color_rgb: tuple[int, int, int] # RGB颜色值
color_hex: str # 十六进制颜色值
chinese_name: str
def __str__(self) -> str:
return self.chinese_name
# 稀有度配置表
RARITY_CONFIGS = {
RarityLevel.N: Rarity(
level=RarityLevel.N,
weight=10.0,
color_rgb=(255, 255, 255), # 白色
color_hex="#FFFFFF",
chinese_name="普通"
),
RarityLevel.R: Rarity(
level=RarityLevel.R,
weight=5.0,
color_rgb=(74, 144, 226), # 蓝色
color_hex="#4A90E2",
chinese_name="稀有"
),
RarityLevel.SR: Rarity(
level=RarityLevel.SR,
weight=3.0,
color_rgb=(147, 112, 219), # 紫色
color_hex="#9370DB",
chinese_name="超稀有"
),
RarityLevel.SSR: Rarity(
level=RarityLevel.SSR,
weight=1.0,
color_rgb=(255, 215, 0), # 金色
color_hex="#FFD700",
chinese_name="传说"
),
}
def get_rarity_from_str(rarity_str: str) -> Rarity:
"""
从字符串获取稀有度配置
Args:
rarity_str: 稀有度字符串,如 "N", "R", "SR", "SSR"
Returns:
Rarity: 稀有度配置对象若无法识别则返回N
"""
rarity_str = str(rarity_str).strip().upper()
try:
level = RarityLevel(rarity_str)
return RARITY_CONFIGS[level]
except (ValueError, KeyError):
# 默认返回普通稀有度
return RARITY_CONFIGS[RarityLevel.N]
def get_weight_from_rarity(rarity_str: str) -> float:
"""
根据稀有度字符串获取权重
Args:
rarity_str: 稀有度字符串
Returns:
float: 对应的权重值
"""
rarity = get_rarity_from_str(rarity_str)
return rarity.weight

View File

@@ -1,106 +0,0 @@
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]