refactor nickname
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
30
src/classes/nickname_data.py
Normal file
30
src/classes/nickname_data.py
Normal 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
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
# 加载长期目标
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
@@ -14,7 +14,8 @@
|
||||
返回JSON格式:
|
||||
{{
|
||||
"thinking": "分析角色特点、主要事迹、性格特质,思考什么绰号最能体现这个人物...但也不用过度思考",
|
||||
"nickname": "绰号"
|
||||
"nickname": "绰号",
|
||||
"reason": "修仙界形成这个绰号的原因,30字内"
|
||||
}}
|
||||
|
||||
注意:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user