refactor relationship changes

This commit is contained in:
bridge
2025-11-26 15:06:41 +08:00
parent e8bf436797
commit 37b51b7650
13 changed files with 333 additions and 101 deletions

View File

@@ -90,9 +90,8 @@ class Battle(InstantAction):
# 生成战斗小故事
target = self._get_target(avatar_name)
start_text = self._start_event_content if hasattr(self, '_start_event_content') else result_event.content
story = await StoryTeller.tell_story(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT)
# 战斗强制双人模式,允许改变关系
story = await StoryTeller.tell_story(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT, allow_relation_changes=True)
story_event = Event(self.world.month_stamp, story, related_avatars=rel_ids, is_story=True)
return [result_event, story_event]

View File

@@ -135,8 +135,7 @@ class Breakthrough(TimedAction):
# 故事参与者:本体 +(可选)相关角色
prompt = TribulationSelector.get_story_prompt(str(calamity))
story = await StoryTeller.tell_story(core_text, ("突破成功" if result_ok else "突破失败"), self.avatar, self._calamity_other, prompt=prompt)
# 突破强制单人模式,不改变关系(因为没有双修/战斗那样的互动)
story = await StoryTeller.tell_story(core_text, ("突破成功" if result_ok else "突破失败"), self.avatar, self._calamity_other, prompt=prompt, allow_relation_changes=False)
events.append(Event(self.world.month_stamp, story, related_avatars=rel_ids, is_story=True))
return events

View File

@@ -478,7 +478,8 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]:
base_event = Event(month_at_finish, event_text, related_avatars=related_avatars, is_major=True)
# 生成故事事件
story = await StoryTeller.tell_story(event_text, res_text, *actors_for_story, prompt=story_prompt)
# 奇遇强制单人模式,不改变关系(因为关系已经在硬逻辑中处理了)
story = await StoryTeller.tell_story(event_text, res_text, *actors_for_story, prompt=story_prompt, allow_relation_changes=False)
story_event = Event(month_at_finish, story, related_avatars=related_avatars, is_story=True)
# 返回基础事件和故事事件
@@ -488,5 +489,3 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]:
__all__ = [
"try_trigger_fortune",
]

View File

@@ -4,12 +4,9 @@ from pathlib import Path
from typing import TYPE_CHECKING
from .mutual_action import MutualAction
from src.classes.relation import relation_display_names, Relation
from src.classes.relations import (
get_possible_new_relations,
get_possible_cancel_relations,
set_relation,
cancel_relation,
process_relation_changes,
get_relation_change_context,
)
from src.classes.event import Event, NULL_EVENT
from src.utils.config import CONFIG
@@ -52,18 +49,15 @@ class Conversation(MutualAction):
avatar_name_2: target_avatar.get_info(detailed=True),
}
# 可能的后天关系(转中文名,给模板阅读)
# 注意:这里计算的是 target 相对于 avatar 的可能关系
possible_new_relations = [relation_display_names[r] for r in get_possible_new_relations(self.avatar, target_avatar)]
# 可能取消的关系
possible_cancel_relations = [relation_display_names[r] for r in get_possible_cancel_relations(target_avatar, self.avatar)]
# 获取关系上下文
possible_new_relations, possible_cancel_relations = get_relation_change_context(self.avatar, target_avatar)
return {
"avatar_infos": avatar_infos,
"avatar_name_1": avatar_name_1,
"avatar_name_2": avatar_name_2,
"possible_new_relations": possible_new_relations,
"possible_cancal_relations": possible_cancel_relations, # 保持模板中的拼写
"possible_cancel_relations": possible_cancel_relations,
}
def _can_start(self, target: "Avatar") -> tuple[bool, str]:
@@ -86,8 +80,6 @@ class Conversation(MutualAction):
Conversation 不需要反馈FEEDBACK_ACTIONS 为空),直接生成内容。
"""
conversation_content = str(result.get("conversation_content", "")).strip()
new_relation_str = str(result.get("new_relation", "")).strip()
cancel_relation_str = str(result.get("cancal_relation", "")).strip() # 保持模板中的拼写
# 使用开始时间戳
month_stamp = self._start_month_stamp if self._start_month_stamp is not None else self.world.month_stamp
@@ -101,32 +93,8 @@ class Conversation(MutualAction):
)
EventHelper.push_pair(content_event, initiator=self.avatar, target=target, to_sidebar_once=True)
# 处理进入新关系
if new_relation_str:
rel = Relation.from_chinese(new_relation_str)
if rel is not None:
set_relation(target, self.avatar, rel)
set_event = Event(
month_stamp,
f"{target.name}{self.avatar.name} 的关系变为:{relation_display_names.get(rel, str(rel))}",
related_avatars=[self.avatar.id, target.id],
is_major=True
)
EventHelper.push_pair(set_event, initiator=self.avatar, target=target, to_sidebar_once=True)
# 处理取消关系
if cancel_relation_str:
rel = Relation.from_chinese(cancel_relation_str)
if rel is not None:
success = cancel_relation(target, self.avatar, rel)
if success:
cancel_event = Event(
month_stamp,
f"{target.name}{self.avatar.name} 取消了关系:{relation_display_names.get(rel, str(rel))}",
related_avatars=[self.avatar.id, target.id],
is_major=True
)
EventHelper.push_pair(cancel_event, initiator=self.avatar, target=target, to_sidebar_once=True)
# 处理关系变化 (调用通用逻辑)
process_relation_changes(self.avatar, target, result, month_stamp)
return ActionResult(status=ActionStatus.COMPLETED, events=[])

View File

@@ -105,20 +105,15 @@ class DualCultivation(MutualAction):
if success:
gain = int(self._dual_exp_gain)
result_text = f"{self.avatar.name}{target.name} 成功双修,{self.avatar.name} 获得修为经验 +{gain}"
result_text = f"{self.avatar.name} 获得修为经验 +{gain}"
result_event = Event(self.world.month_stamp, result_text, related_avatars=[self.avatar.id, target.id], is_major=True)
events.append(result_event)
# 生成恋爱/双修小故事
start_text = self._start_event_content or result_event.content
story = await StoryTeller.tell_story(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT)
# 双修强制双人模式,允许改变关系
story = await StoryTeller.tell_story(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT, allow_relation_changes=True)
story_event = Event(self.world.month_stamp, story, related_avatars=[self.avatar.id, target.id], is_story=True)
events.append(story_event)
else:
result_text = f"{target.name} 拒绝了与 {self.avatar.name} 的双修"
result_event = Event(self.world.month_stamp, result_text, related_avatars=[self.avatar.id, target.id], is_major=True)
events.append(result_event)
return events

View File

@@ -88,21 +88,14 @@ class GiftSpiritStone(MutualAction):
return events
if success:
result_text = f"{self.avatar.name} 赠送了 {self.GIFT_AMOUNT} 灵石给 {target.name}{self.avatar.name} 灵石:{self.avatar.magic_stone + self.GIFT_AMOUNT}{self.avatar.magic_stone}{target.name} 灵石:{target.magic_stone - self.GIFT_AMOUNT}{target.magic_stone}"
result_text = f"{self.avatar.name} 赠送了 {self.GIFT_AMOUNT} 灵石给 {target.name}{self.avatar.
name} 灵石:{self.avatar.magic_stone + self.GIFT_AMOUNT}{self.avatar.magic_stone}{target.
name} 灵石:{target.magic_stone - self.GIFT_AMOUNT}{target.magic_stone}"
result_event = Event(
self.world.month_stamp,
result_text,
related_avatars=[self.avatar.id, target.id]
)
events.append(result_event)
else:
result_text = f"{target.name} 婉拒了 {self.avatar.name} 的灵石赠送"
result_event = Event(
self.world.month_stamp,
result_text,
related_avatars=[self.avatar.id, target.id]
)
events.append(result_event)
return events

View File

@@ -100,15 +100,7 @@ class Impart(MutualAction):
if success:
gain = int(self._impart_exp_gain)
result_text = f"{self.avatar.name}{target.name} 传道,{target.name} 获得修为经验 +{gain}"
result_event = Event(
self.world.month_stamp,
result_text,
related_avatars=[self.avatar.id, target.id]
)
events.append(result_event)
else:
result_text = f"{target.name} 婉拒了 {self.avatar.name} 的传道"
result_text = f"{target.name} 获得修为经验 +{gain}"
result_event = Event(
self.world.month_stamp,
result_text,

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING, List
from src.classes.relation import Relation, INNATE_RELATIONS, get_reciprocal, is_innate
from src.classes.relation import Relation, INNATE_RELATIONS, get_reciprocal, is_innate, relation_display_names
from src.classes.event import Event
from src.classes.action.event_helper import EventHelper
if TYPE_CHECKING:
from src.classes.avatar import Avatar
@@ -124,3 +126,58 @@ def get_possible_cancel_relations(from_avatar: "Avatar", to_avatar: "Avatar") ->
return [existing]
def get_relation_change_context(avatar1: "Avatar", avatar2: "Avatar") -> tuple[list[str], list[str]]:
"""
获取两角色间可能的新增关系和取消关系的中文显示列表。
用于构建 Prompt 上下文。
返回:(possible_new_relations, possible_cancel_relations)
"""
# 计算 avatar2 相对于 avatar1 的可能关系
new_rels = get_possible_new_relations(avatar1, avatar2)
cancel_rels = get_possible_cancel_relations(avatar1, avatar2)
new_strs = [relation_display_names[r] for r in new_rels]
cancel_strs = [relation_display_names[r] for r in cancel_rels]
return new_strs, cancel_strs
def process_relation_changes(initiator: "Avatar", target: "Avatar", result_dict: dict, month_stamp: int) -> None:
"""
处理 LLM 返回的关系变更请求。
兼容 Conversation 和 StoryTeller 的通用逻辑。
"""
new_relation_str = str(result_dict.get("new_relation", "")).strip()
# 兼容模板中的拼写错误 (cancal -> cancel)
cancel_relation_str = str(result_dict.get("cancel_relation", "")).strip()
if not cancel_relation_str:
cancel_relation_str = str(result_dict.get("cancal_relation", "")).strip()
# 处理进入新关系
if new_relation_str:
rel = Relation.from_chinese(new_relation_str)
if rel is not None:
set_relation(target, initiator, rel)
set_event = Event(
month_stamp,
f"{target.name}{initiator.name} 的关系变为:{relation_display_names.get(rel, str(rel))}",
related_avatars=[initiator.id, target.id],
is_major=True
)
EventHelper.push_pair(set_event, initiator=initiator, target=target, to_sidebar_once=True)
# 处理取消关系
if cancel_relation_str:
rel = Relation.from_chinese(cancel_relation_str)
if rel is not None:
success = cancel_relation(target, initiator, rel)
if success:
cancel_event = Event(
month_stamp,
f"{target.name}{initiator.name} 取消了关系:{relation_display_names.get(rel, str(rel))}",
related_avatars=[initiator.id, target.id],
is_major=True
)
EventHelper.push_pair(cancel_event, initiator=initiator, target=target, to_sidebar_once=True)

View File

@@ -8,6 +8,10 @@ if TYPE_CHECKING:
from src.utils.config import CONFIG
from src.utils.llm import call_llm_with_template, LLMMode
from src.classes.relations import (
process_relation_changes,
get_relation_change_context
)
story_styles = [
"平淡叙述:语句克制、少修饰、像旁观者记录。",
@@ -31,9 +35,11 @@ story_styles = [
class StoryTeller:
"""
故事生成器:基于模板与 LLM将给定事件扩展为简短的小故事。
同时负责处理可能的后天关系变化。
"""
TEMPLATE_PATH = CONFIG.paths.templates / "story.txt"
TEMPLATE_SINGLE_PATH = CONFIG.paths.templates / "story_single.txt"
TEMPLATE_DUAL_PATH = CONFIG.paths.templates / "story_dual.txt"
@staticmethod
def _build_avatar_infos(*actors: "Avatar") -> Dict[str, dict]:
@@ -54,45 +60,82 @@ class StoryTeller:
return avatar_infos
@staticmethod
def _build_template_data(event: str, res: str, avatar_infos: Dict[str, dict], prompt: str) -> dict:
def _build_template_data(event: str, res: str, avatar_infos: Dict[str, dict], prompt: str, *actors: "Avatar") -> dict:
"""构建模板渲染所需的数据字典"""
# 默认空关系列表
possible_new_relations = []
possible_cancel_relations = []
avatar_name_1 = ""
avatar_name_2 = ""
# 如果有两个有效角色,计算可能的关系
non_null = [a for a in actors if a is not None]
if len(non_null) >= 2:
# 计算 actors[1] 相对于 actors[0] 的可能关系
possible_new_relations, possible_cancel_relations = get_relation_change_context(non_null[0], non_null[1])
avatar_name_1 = non_null[0].name
avatar_name_2 = non_null[1].name
return {
"avatar_infos": avatar_infos,
"avatar_name_1": avatar_name_1,
"avatar_name_2": avatar_name_2,
"event": event,
"res": res,
"style": random.choice(story_styles),
"story_prompt": prompt,
"possible_new_relations": possible_new_relations,
"possible_cancel_relations": possible_cancel_relations,
}
@staticmethod
def _make_fallback_story(event: str, res: str, style: str) -> str:
"""生成降级文案"""
return f"{event}{res}{style}"
# 不再显示 style避免出戏
return f"{event}{res}"
@staticmethod
async def tell_story(event: str, res: str, *actors: "Avatar", prompt: str = "") -> str:
async def tell_story(event: str, res: str, *actors: "Avatar", prompt: str = "", allow_relation_changes: bool = False) -> str:
"""
生成小故事(异步版本)。
基于 `static/templates/story.txt` 模板,失败时返回降级文案。
根据 allow_relation_changes 参数选择模板:
- True: 使用 story_dual.txt支持关系变化需要至少2个角色
- False: 使用 story_single.txt仅生成故事无论角色数量
Args:
event: 事件描述
res: 结果描述
*actors: 参与的角色1-2个
prompt: 可选的故事提示词
allow_relation_changes: 是否允许故事导致关系变化默认为False单人模式
"""
avatar_infos = StoryTeller._build_avatar_infos(*actors)
infos = StoryTeller._build_template_data(event, res, avatar_infos, prompt)
non_null = [a for a in actors if a is not None]
try:
data = await call_llm_with_template(StoryTeller.TEMPLATE_PATH, infos, LLMMode.FAST)
story = data.get("story", "").strip()
if story:
return story
except Exception:
pass
# 只有当允许关系变化且有至少2个角色时才使用双人模板
is_dual = allow_relation_changes and len(non_null) >= 2
template_path = StoryTeller.TEMPLATE_DUAL_PATH if is_dual else StoryTeller.TEMPLATE_SINGLE_PATH
avatar_infos = StoryTeller._build_avatar_infos(*actors)
infos = StoryTeller._build_template_data(event, res, avatar_infos, prompt, *actors)
# 移除了 try-except 块,允许异常向上冒泡,以便 Fail Fast
data = await call_llm_with_template(template_path, infos, LLMMode.FAST)
story = data.get("story", "").strip()
# 仅在双人模式下处理关系变化
if is_dual:
avatar_1 = non_null[0]
avatar_2 = non_null[1]
# 尝试获取 month_stamp
month_stamp = getattr(avatar_1.world, "month_stamp", 0)
process_relation_changes(avatar_1, avatar_2, data, month_stamp)
if story:
return story
return StoryTeller._make_fallback_story(event, res, infos["style"])
__all__ = ["StoryTeller"]
__all__ = ["StoryTeller"]