refactor nickname

This commit is contained in:
bridge
2025-11-26 15:22:48 +08:00
parent 37b51b7650
commit bb8614407d
8 changed files with 147 additions and 19 deletions

View File

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

View File

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

View File

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

View File

@@ -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", {})
# 加载长期目标

View File

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

View File

@@ -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
1 id name rarity effects desc duration_years
7 6 土厚之世 R {damage_reduction: 0.15, extra_max_hp: 150} 土德厚重载物无疆,根基稳固血气充盈,百邪难侵万法不破 5
8 7 五行逆乱 SR {extra_cultivate_exp: -10, extra_breakthrough_success_rate: 0.3} 五行失序天地大乱,修炼艰难却蕴含突破良机 5
9 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
10 9 劫数将至 SR {extra_battle_strength_points: 5, extra_fortune_probability: -0.05} {extra_battle_strength_points: 5, extra_fortune_probability: -0.005} 劫数降临戾气弥漫,修士战力暴涨却杀机四伏 5
11 10 灵气复苏 SSR {extra_cultivate_exp: 25, extra_breakthrough_success_rate: 0.1} 天地灵气井喷复苏,修士修炼如沐春风 5
12 11 灵气枯竭 R {extra_cultivate_exp: -20} 灵气枯竭末法将至,修炼如逆水行舟举步维艰 5
13 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
14 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
15 14 神兵出世 SR {extra_weapon_proficiency_gain: 1.0} 神兵有灵百兵齐鸣,温养进境一日千里 5
16 15 阴阳交泰 R {extra_dual_cultivation_exp: 20} 阴阳交泰天地和合,双修之道水到渠成 5
17 16 杀劫降临 SR {extra_battle_strength_points: 5, extra_fortune_probability: -0.1} {extra_battle_strength_points: 5, extra_fortune_probability: -0.005} 血光冲天杀机四伏,战斗凶险倍增 5
18 17 太平盛世 R {extra_cultivate_exp: -10, extra_fortune_probability: 0.1} 天下太平万物安宁,修炼迟缓却机缘频生 5
19 18 气运加身 R [{when: 'any(p.name == "气运之子" for p in avatar.personas)', extra_cultivate_exp: 25, extra_fortune_probability: 0.15}] [{when: 'any(p.name == "气运之子" for p in avatar.personas)', extra_cultivate_exp: 25, extra_fortune_probability: 0.005}] 天命眷顾气运之子,修炼奇遇皆蒙福泽 5
20 19 血月当空 SR {extra_battle_strength_points: 7, extra_cultivate_exp: -10} 血月高悬杀机暴涨,战斗狂热但修心不易 5
21 20 飞升之门 SSR {extra_cultivate_exp: 30, extra_breakthrough_success_rate: 0.2} 天门大开飞升有望,巅峰修士得窥天道 7
22 21 法则显化 SSR {extra_breakthrough_success_rate: 0.5} 天地法则显化于世,众修士感悟突破 3
23 22 时空乱流 SSR {extra_fortune_probability: 0.1} {extra_fortune_probability: 0.005} 时空错乱奇遇频生,机缘无数 5
24

View File

@@ -14,7 +14,8 @@
返回JSON格式
{{
"thinking": "分析角色特点、主要事迹、性格特质,思考什么绰号最能体现这个人物...但也不用过度思考",
"nickname": "绰号"
"nickname": "绰号",
"reason": "修仙界形成这个绰号的原因30字内"
}}
注意:

View File

@@ -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(() => {
<div class="panel-header">
<div class="title-group">
<div class="main-title">{{ title }}</div>
<div v-if="subTitle" class="sub-title">{{ subTitle }}</div>
<div
v-if="subTitle"
class="sub-title"
:class="{ 'clickable': hasNicknameReason }"
@click="toggleNicknameReason"
>
{{ subTitle }}
</div>
<!-- Nickname Reason Popover -->
<div v-if="showNicknameReason && hasNicknameReason" class="nickname-reason-popover">
<div class="popover-arrow"></div>
<div class="popover-content">
{{ uiStore.detailData.nickname_reason }}
</div>
</div>
</div>
<button class="close-btn" @click="close">×</button>
</div>
@@ -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 {