From bb8614407dc3de64a122f62b8b800893950a3477 Mon Sep 17 00:00:00 2001 From: bridge Date: Wed, 26 Nov 2025 15:22:48 +0800 Subject: [PATCH] refactor nickname --- src/classes/avatar.py | 10 ++- src/classes/nickname.py | 26 ++++-- src/classes/nickname_data.py | 30 +++++++ src/sim/load/avatar_load_mixin.py | 6 +- src/sim/save/avatar_save_mixin.py | 1 + static/game_configs/celestial_phenomenon.csv | 8 +- static/templates/nickname.txt | 3 +- .../game/panels/info/InfoPanelContainer.vue | 82 ++++++++++++++++++- 8 files changed, 147 insertions(+), 19 deletions(-) create mode 100644 src/classes/nickname_data.py diff --git a/src/classes/avatar.py b/src/classes/avatar.py index a77b404..e8c73c3 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -45,6 +45,7 @@ from src.classes.appearance import Appearance, get_random_appearance from src.classes.battle import get_base_strength from src.classes.spirit_animal import SpiritAnimal from src.classes.long_term_objective import LongTermObjective +from src.classes.nickname_data import Nickname persona_num = CONFIG.avatar.persona_num @@ -110,7 +111,7 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): # 灵兽:最多一个;若再次捕捉则覆盖 spirit_animal: Optional[SpiritAnimal] = None # 绰号:江湖中对该角色的称谓,满足条件后生成,永久不变 - nickname: Optional[str] = None + nickname: Optional[Nickname] = None # 自定义头像ID:如果设置,优先使用此ID显示头像 custom_pic_id: Optional[int] = None # 当月/当步新设动作标记:在 commit_next_plan 设为 True,首次 tick_action 后清为 False @@ -264,7 +265,7 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): } # 绰号:仅在存在时显示 if self.nickname is not None: - info_dict["绰号"] = self.nickname + info_dict["绰号"] = self.nickname.value # 灵兽:仅在存在时显示 if self.spirit_animal is not None: info_dict["灵兽"] = spirit_animal_info @@ -295,7 +296,8 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): "thinking": self.thinking, "short_term_objective": self.short_term_objective, "long_term_objective": self.long_term_objective.content if self.long_term_objective else "", - "nickname": self.nickname, + "nickname": self.nickname.value if self.nickname else None, + "nickname_reason": self.nickname.reason if self.nickname else None, } # 复杂对象结构化 @@ -687,7 +689,7 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): lines: list[str] = [] # 基础信息 if self.nickname: - add_kv(lines, "绰号", f"「{self.nickname}」") + add_kv(lines, "绰号", f"「{self.nickname.value}」") add_kv(lines, "性别", self.gender) add_kv(lines, "年龄", self.age) diff --git a/src/classes/nickname.py b/src/classes/nickname.py index ee05091..85b7209 100644 --- a/src/classes/nickname.py +++ b/src/classes/nickname.py @@ -13,6 +13,8 @@ from src.utils.config import CONFIG from src.utils.llm import call_llm_with_template, LLMMode from src.run.log import get_logger +from src.classes.nickname_data import Nickname + logger = get_logger().logger @@ -50,7 +52,7 @@ def can_get_nickname(avatar: "Avatar") -> bool: return major_count >= major_threshold and minor_count >= minor_threshold -async def generate_nickname(avatar: "Avatar") -> Optional[str]: +async def generate_nickname(avatar: "Avatar") -> Optional[dict]: """ 为角色生成绰号 @@ -60,7 +62,7 @@ async def generate_nickname(avatar: "Avatar") -> Optional[str]: avatar: 要生成绰号的角色 Returns: - 生成的绰号,失败则返回None + 包含 nickname 和 reason 的字典,失败则返回None """ try: # 获取 expanded_info(包含详细信息和事件历史) @@ -77,15 +79,19 @@ async def generate_nickname(avatar: "Avatar") -> Optional[str]: nickname = response_data.get("nickname", "").strip() thinking = response_data.get("thinking", "") + reason = response_data.get("reason", "").strip() if not nickname: logger.warning(f"为角色 {avatar.name} 生成绰号失败:返回空绰号") return None - logger.info(f"为角色 {avatar.name} 生成绰号:{nickname}") + logger.info(f"为角色 {avatar.name} 生成绰号:{nickname} (原因:{reason})") logger.debug(f"绰号生成思考过程:{thinking}") - return nickname + return { + "nickname": nickname, + "reason": reason + } except Exception as e: logger.error(f"生成绰号时出错:{e}") @@ -107,15 +113,19 @@ async def process_avatar_nickname(avatar: "Avatar") -> Optional[Event]: if not can_get_nickname(avatar): return None - nickname = await generate_nickname(avatar) - if not nickname: + result = await generate_nickname(avatar) + if not result: return None - avatar.nickname = nickname + nickname_str = result["nickname"] + reason = result["reason"] + + avatar.nickname = Nickname(value=nickname_str, reason=reason) + # 生成事件:角色获得绰号 event = Event( avatar.world.month_stamp, - f"{avatar.name}在修仙界中闯出名号,被人称为「{nickname}」。", + f"{avatar.name}在修仙界中闯出名号,被人称为「{nickname_str}」。", related_avatars=[avatar.id], is_major=True ) diff --git a/src/classes/nickname_data.py b/src/classes/nickname_data.py new file mode 100644 index 0000000..47f0c58 --- /dev/null +++ b/src/classes/nickname_data.py @@ -0,0 +1,30 @@ +""" +Nickname 数据类 +""" +from dataclasses import dataclass, asdict +from typing import Optional + +@dataclass +class Nickname: + """ + 绰号数据类 + 包含绰号本身及其来源原因 + """ + value: str + reason: str + + def to_dict(self) -> dict: + return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "Nickname": + if not data: + return None + return cls( + value=data["value"], + reason=data["reason"] + ) + + def __str__(self) -> str: + return self.value + diff --git a/src/sim/load/avatar_load_mixin.py b/src/sim/load/avatar_load_mixin.py index 3ae9e5e..4def9ed 100644 --- a/src/sim/load/avatar_load_mixin.py +++ b/src/sim/load/avatar_load_mixin.py @@ -144,10 +144,14 @@ class AvatarLoadMixin: # 设置外貌(通过level获取完整的Appearance对象) avatar.appearance = get_appearance_by_level(data.get("appearance", 5)) + + # 恢复绰号 + from src.classes.nickname_data import Nickname + avatar.nickname = Nickname.from_dict(data.get("nickname")) # 设置行动与AI avatar.thinking = data.get("thinking", "") - avatar.short_term_objective = data.get("short_term_objective", data.get("objective", "")) # 兼容旧存档 + avatar.short_term_objective = data.get("short_term_objective", "") avatar._action_cd_last_months = data.get("_action_cd_last_months", {}) # 加载长期目标 diff --git a/src/sim/save/avatar_save_mixin.py b/src/sim/save/avatar_save_mixin.py index d720af3..8db12ef 100644 --- a/src/sim/save/avatar_save_mixin.py +++ b/src/sim/save/avatar_save_mixin.py @@ -89,6 +89,7 @@ class AvatarSaveMixin: "alignment": self.alignment.name if self.alignment else None, "persona_ids": [p.id for p in self.personas] if self.personas else [], "appearance": self.appearance.level, + "nickname": self.nickname.to_dict() if self.nickname else None, # 行动与AI "current_action": current_action_dict, diff --git a/static/game_configs/celestial_phenomenon.csv b/static/game_configs/celestial_phenomenon.csv index c80120a..43fd875 100644 --- a/static/game_configs/celestial_phenomenon.csv +++ b/static/game_configs/celestial_phenomenon.csv @@ -7,18 +7,18 @@ id,name,rarity,effects,desc,duration_years 6,土厚之世,R,"{damage_reduction: 0.15, extra_max_hp: 150}",土德厚重载物无疆,根基稳固血气充盈,百邪难侵万法不破,5 7,五行逆乱,SR,"{extra_cultivate_exp: -10, extra_breakthrough_success_rate: 0.3}",五行失序天地大乱,修炼艰难却蕴含突破良机,5 8,天道均衡,SR,"[{when: 'avatar.cultivation_progress.realm.value >= 6', extra_cultivate_exp: -25}, {when: 'avatar.cultivation_progress.realm.value < 6', extra_cultivate_exp: 10}]",天道显化强者受抑,弱者得助万物归中,5 -9,劫数将至,SR,"{extra_battle_strength_points: 5, extra_fortune_probability: -0.05}",劫数降临戾气弥漫,修士战力暴涨却杀机四伏,5 +9,劫数将至,SR,"{extra_battle_strength_points: 5, extra_fortune_probability: -0.005}",劫数降临戾气弥漫,修士战力暴涨却杀机四伏,5 10,灵气复苏,SSR,"{extra_cultivate_exp: 25, extra_breakthrough_success_rate: 0.1}",天地灵气井喷复苏,修士修炼如沐春风,5 11,灵气枯竭,R,"{extra_cultivate_exp: -20}",灵气枯竭末法将至,修炼如逆水行舟举步维艰,5 12,魔道兴盛,R,"[{when: 'avatar.alignment == Alignment.EVIL', extra_battle_strength_points: 5, extra_cultivate_exp: 10}, {when: 'avatar.alignment == Alignment.GOOD', extra_battle_strength_points: -5}]",魔气滔天邪道横行,正道式微难以抗衡,5 13,正气浩然,R,"[{when: 'avatar.alignment == Alignment.GOOD', extra_battle_strength_points: 5, extra_cultivate_exp: 10}, {when: 'avatar.alignment == Alignment.EVIL', extra_battle_strength_points: -0}]",浩然正气镇压邪祟,正道昌盛,5 14,神兵出世,SR,"{extra_weapon_proficiency_gain: 1.0}",神兵有灵百兵齐鸣,温养进境一日千里,5 15,阴阳交泰,R,"{extra_dual_cultivation_exp: 20}",阴阳交泰天地和合,双修之道水到渠成,5 -16,杀劫降临,SR,"{extra_battle_strength_points: 5, extra_fortune_probability: -0.1}",血光冲天杀机四伏,战斗凶险倍增,5 +16,杀劫降临,SR,"{extra_battle_strength_points: 5, extra_fortune_probability: -0.005}",血光冲天杀机四伏,战斗凶险倍增,5 17,太平盛世,R,"{extra_cultivate_exp: -10, extra_fortune_probability: 0.1}",天下太平万物安宁,修炼迟缓却机缘频生,5 -18,气运加身,R,"[{when: 'any(p.name == ""气运之子"" for p in avatar.personas)', extra_cultivate_exp: 25, extra_fortune_probability: 0.15}]",天命眷顾气运之子,修炼奇遇皆蒙福泽,5 +18,气运加身,R,"[{when: 'any(p.name == ""气运之子"" for p in avatar.personas)', extra_cultivate_exp: 25, extra_fortune_probability: 0.005}]",天命眷顾气运之子,修炼奇遇皆蒙福泽,5 19,血月当空,SR,"{extra_battle_strength_points: 7, extra_cultivate_exp: -10}",血月高悬杀机暴涨,战斗狂热但修心不易,5 20,飞升之门,SSR,"{extra_cultivate_exp: 30, extra_breakthrough_success_rate: 0.2}",天门大开飞升有望,巅峰修士得窥天道,7 21,法则显化,SSR,"{extra_breakthrough_success_rate: 0.5}",天地法则显化于世,众修士感悟突破,3 -22,时空乱流,SSR,"{extra_fortune_probability: 0.1}",时空错乱奇遇频生,机缘无数,5 +22,时空乱流,SSR,"{extra_fortune_probability: 0.005}",时空错乱奇遇频生,机缘无数,5 diff --git a/static/templates/nickname.txt b/static/templates/nickname.txt index 9c50eba..c6ea599 100644 --- a/static/templates/nickname.txt +++ b/static/templates/nickname.txt @@ -14,7 +14,8 @@ 返回JSON格式: {{ "thinking": "分析角色特点、主要事迹、性格特质,思考什么绰号最能体现这个人物...但也不用过度思考", - "nickname": "绰号" + "nickname": "绰号", + "reason": "修仙界形成这个绰号的原因,30字内" }} 注意: diff --git a/web/src/components/game/panels/info/InfoPanelContainer.vue b/web/src/components/game/panels/info/InfoPanelContainer.vue index 7ff980b..faba0b4 100644 --- a/web/src/components/game/panels/info/InfoPanelContainer.vue +++ b/web/src/components/game/panels/info/InfoPanelContainer.vue @@ -32,10 +32,23 @@ const subTitle = computed(() => { return ''; }); +const hasNicknameReason = computed(() => { + return uiStore.detailData && 'nickname_reason' in uiStore.detailData && uiStore.detailData.nickname_reason; +}); + +const showNicknameReason = ref(false); + +function toggleNicknameReason() { + if (hasNicknameReason.value) { + showNicknameReason.value = !showNicknameReason.value; + } +} + // --- Interaction --- function close() { uiStore.clearSelection(); + showNicknameReason.value = false; } // Click outside to close @@ -57,6 +70,7 @@ function close() { // Record open time watch(() => uiStore.selectedTarget, (val) => { if (val) lastOpenAt = performance.now(); + showNicknameReason.value = false; }); onMounted(() => { @@ -74,7 +88,22 @@ onUnmounted(() => {
{{ title }}
-
{{ subTitle }}
+
+ {{ subTitle }} +
+ + +
+
+
+ {{ uiStore.detailData.nickname_reason }} +
+
@@ -144,6 +173,57 @@ onUnmounted(() => { display: flex; align-items: baseline; gap: 8px; + position: relative; +} + +.sub-title { + font-size: 12px; + color: #888; +} + +.sub-title.clickable { + cursor: pointer; + border-bottom: 1px dashed #666; +} + +.sub-title.clickable:hover { + color: #bbb; + border-bottom-color: #999; +} + +.nickname-reason-popover { + position: absolute; + top: 100%; + left: 0; + margin-top: 8px; + background: rgba(50, 50, 50, 0.98); + border: 1px solid #555; + border-radius: 4px; + padding: 8px 12px; + z-index: 100; + width: max-content; + max-width: 260px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); + pointer-events: none; /* Prevents blocking if it overlaps something vital, though typically we want to select text */ + pointer-events: auto; +} + +.popover-arrow { + position: absolute; + top: -5px; + left: 20px; /* Approximate alignment under the nickname */ + width: 8px; + height: 8px; + background: rgba(50, 50, 50, 0.98); + border-left: 1px solid #555; + border-top: 1px solid #555; + transform: rotate(45deg); +} + +.popover-content { + font-size: 12px; + color: #ccc; + line-height: 1.4; } .main-title {