merge persona and traits & add rarity
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
96
src/classes/rarity.py
Normal 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
|
||||
@@ -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]
|
||||
|
||||
@@ -334,9 +334,23 @@ def draw_avatars_and_pick_hover(
|
||||
|
||||
|
||||
def draw_tooltip(pygame_mod, screen, colors, lines: List[str], mouse_x: int, mouse_y: int, font, min_width: int = 260, top_limit: int = 0):
|
||||
"""
|
||||
绘制tooltip,支持颜色标记格式:<color:R,G,B>text</color>
|
||||
"""
|
||||
import re
|
||||
padding = 6
|
||||
spacing = 2
|
||||
surf_lines = [font.render(t, True, colors["text"]) for t in lines]
|
||||
|
||||
# 解析每行文本,生成渲染表面
|
||||
surf_lines = []
|
||||
for line in lines:
|
||||
# 检查是否包含颜色标记
|
||||
if "<color:" in line:
|
||||
# 使用正则表达式解析颜色标记
|
||||
surf_lines.append(_render_colored_text(pygame_mod, font, line, colors["text"]))
|
||||
else:
|
||||
surf_lines.append(font.render(line, True, colors["text"]))
|
||||
|
||||
width = max(s.get_width() for s in surf_lines) + padding * 2
|
||||
width = max(width, min_width)
|
||||
height = sum(s.get_height() for s in surf_lines) + padding * 2 + spacing * (len(surf_lines) - 1)
|
||||
@@ -359,6 +373,66 @@ def draw_tooltip(pygame_mod, screen, colors, lines: List[str], mouse_x: int, mou
|
||||
cursor_y += s.get_height() + spacing
|
||||
|
||||
|
||||
def _render_colored_text(pygame_mod, font, text: str, default_color) -> object:
|
||||
"""
|
||||
渲染带颜色标记的文本,格式:<color:R,G,B>text</color>
|
||||
返回一个合成的Surface
|
||||
"""
|
||||
import re
|
||||
|
||||
# 解析颜色标记
|
||||
pattern = r'<color:(\d+),(\d+),(\d+)>(.*?)</color>'
|
||||
parts = []
|
||||
last_end = 0
|
||||
|
||||
for match in re.finditer(pattern, text):
|
||||
# 添加标记前的普通文本
|
||||
if match.start() > last_end:
|
||||
plain_text = text[last_end:match.start()]
|
||||
parts.append((plain_text, default_color))
|
||||
|
||||
# 添加带颜色的文本
|
||||
r, g, b = int(match.group(1)), int(match.group(2)), int(match.group(3))
|
||||
colored_text = match.group(4)
|
||||
parts.append((colored_text, (r, g, b)))
|
||||
|
||||
last_end = match.end()
|
||||
|
||||
# 添加剩余的普通文本
|
||||
if last_end < len(text):
|
||||
parts.append((text[last_end:], default_color))
|
||||
|
||||
# 如果没有颜色标记,直接返回普通渲染
|
||||
if len(parts) == 1 and parts[0][1] == default_color:
|
||||
return font.render(text, True, default_color)
|
||||
|
||||
# 渲染每个部分并合成
|
||||
rendered_parts = []
|
||||
total_width = 0
|
||||
max_height = 0
|
||||
|
||||
for txt, color in parts:
|
||||
if txt:
|
||||
surf = font.render(txt, True, color)
|
||||
rendered_parts.append(surf)
|
||||
total_width += surf.get_width()
|
||||
max_height = max(max_height, surf.get_height())
|
||||
|
||||
# 创建合成Surface
|
||||
if not rendered_parts:
|
||||
return font.render("", True, default_color)
|
||||
|
||||
combined = pygame_mod.Surface((total_width, max_height), pygame_mod.SRCALPHA)
|
||||
combined.fill((0, 0, 0, 0)) # 透明背景
|
||||
|
||||
x_offset = 0
|
||||
for surf in rendered_parts:
|
||||
combined.blit(surf, (x_offset, 0))
|
||||
x_offset += surf.get_width()
|
||||
|
||||
return combined
|
||||
|
||||
|
||||
def draw_tooltip_for_avatar(pygame_mod, screen, colors, font, avatar: Avatar, tooltip_min_width: int = 260, status_bar_height: int = 32):
|
||||
# 改为从 Avatar.get_hover_info 获取信息行,避免前端重复拼接
|
||||
lines = avatar.get_hover_info()
|
||||
|
||||
@@ -47,7 +47,6 @@ class AvatarLoadMixin:
|
||||
from src.classes.root import Root
|
||||
from src.classes.alignment import Alignment
|
||||
from src.classes.persona import personas_by_id
|
||||
from src.classes.trait import traits_by_id
|
||||
from src.classes.appearance import get_appearance_by_level
|
||||
from src.classes.magic_stone import MagicStone
|
||||
from src.classes.action_runtime import ActionPlan
|
||||
@@ -133,11 +132,6 @@ class AvatarLoadMixin:
|
||||
persona_ids = data.get("persona_ids", [])
|
||||
avatar.personas = [personas_by_id[pid] for pid in persona_ids if pid in personas_by_id]
|
||||
|
||||
# 重建trait
|
||||
trait_id = data.get("trait_id")
|
||||
if trait_id is not None and trait_id in traits_by_id:
|
||||
avatar.trait = traits_by_id[trait_id]
|
||||
|
||||
# 设置外貌(通过level获取完整的Appearance对象)
|
||||
avatar.appearance = get_appearance_by_level(data.get("appearance", 5))
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ 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
|
||||
|
||||
|
||||
# —— 参数常量(便于调参)——
|
||||
@@ -572,22 +571,6 @@ 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
|
||||
@@ -662,7 +645,6 @@ 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:
|
||||
"""
|
||||
@@ -738,11 +720,6 @@ 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)
|
||||
|
||||
@@ -86,7 +86,6 @@ class AvatarSaveMixin:
|
||||
"sect_rank": self.sect_rank.value if self.sect_rank else None,
|
||||
"alignment": self.alignment.name if self.alignment else None,
|
||||
"persona_ids": [p.id for p in self.personas] if self.personas else [],
|
||||
"trait_id": self.trait.id if self.trait else None,
|
||||
"appearance": self.appearance.level,
|
||||
|
||||
# 行动与AI
|
||||
|
||||
Reference in New Issue
Block a user