fix double story bugs

This commit is contained in:
bridge
2025-12-13 22:18:39 +08:00
parent 9ed511aafb
commit fc668b3711
12 changed files with 154 additions and 94 deletions

View File

@@ -5,8 +5,7 @@ Avatar 效果计算 Mixin
"""
from __future__ import annotations
from collections import defaultdict
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from src.classes.avatar.core import Avatar
@@ -18,53 +17,97 @@ from src.classes.hp_and_mp import HP_MAX_BY_REALM
class EffectsMixin:
"""效果计算相关方法"""
def _evaluate_values(self, effects: dict[str, Any]) -> dict[str, Any]:
"""
评估效果字典中的动态值(字符串表达式)。
支持明确的 'eval(...)' 格式,以及包含 'avatar.' 的隐式表达式。
"""
result = {}
# 安全的 eval 上下文
context = {
"__builtins__": {},
"avatar": self,
"max": max,
"min": min,
"int": int,
"float": float,
"round": round,
}
for k, v in effects.items():
if isinstance(v, str):
s = v.strip()
expr = None
# 检查是否为表达式
if s.startswith("eval(") and s.endswith(")"):
expr = s[5:-1]
elif "avatar." in s: # 启发式:包含 avatar. 则视为表达式
expr = s
if expr:
try:
result[k] = eval(expr, context)
except Exception:
# 评估失败,保留原值(可能是普通字符串,或者表达式有误)
result[k] = v
else:
result[k] = v
else:
result[k] = v
return result
@property
def effects(self: "Avatar") -> dict[str, object]:
"""
合并所有来源的效果:宗门、功法、灵根、特质、兵器、辅助装备、灵兽、天地灵机
"""
merged: dict[str, object] = defaultdict(str)
merged: dict[str, object] = {}
def _process_source(source_obj):
if source_obj is None:
return
# 1. 评估条件 (when)
evaluated = _evaluate_conditional_effect(source_obj.effects, self)
# 2. 评估动态值 (expressions)
evaluated = self._evaluate_values(evaluated)
# 3. 合并到总效果
nonlocal merged
merged = _merge_effects(merged, evaluated)
# 来自宗门
if self.sect is not None:
evaluated = _evaluate_conditional_effect(self.sect.effects, self)
merged = _merge_effects(merged, evaluated)
_process_source(self.sect)
# 来自功法
evaluated = _evaluate_conditional_effect(self.technique.effects, self)
merged = _merge_effects(merged, evaluated)
if self.technique is not None:
_process_source(self.technique)
# 来自灵根
evaluated = _evaluate_conditional_effect(self.root.effects, self)
merged = _merge_effects(merged, evaluated)
if self.root is not None:
_process_source(self.root)
# 来自特质persona
for persona in self.personas:
evaluated = _evaluate_conditional_effect(persona.effects, self)
merged = _merge_effects(merged, evaluated)
_process_source(persona)
# 来自兵器
if self.weapon is not None:
evaluated = _evaluate_conditional_effect(self.weapon.effects, self)
merged = _merge_effects(merged, evaluated)
_process_source(self.weapon)
# 来自辅助装备
if self.auxiliary is not None:
evaluated = _evaluate_conditional_effect(self.auxiliary.effects, self)
merged = _merge_effects(merged, evaluated)
_process_source(self.auxiliary)
# 来自灵兽
if self.spirit_animal is not None:
evaluated = _evaluate_conditional_effect(self.spirit_animal.effects, self)
merged = _merge_effects(merged, evaluated)
_process_source(self.spirit_animal)
# 来自天地灵机世界级buff/debuff
if self.world.current_phenomenon is not None:
evaluated = _evaluate_conditional_effect(self.world.current_phenomenon.effects, self)
merged = _merge_effects(merged, evaluated)
# 评估动态效果表达式:值以 "eval(...)" 形式给出
final: dict[str, object] = {}
for k, v in merged.items():
if isinstance(v, str):
s = v.strip()
if s.startswith("eval(") and s.endswith(")"):
expr = s[5:-1]
final[k] = eval(expr, {"__builtins__": {}}, {"avatar": self})
continue
final[k] = v
return final
_process_source(self.world.current_phenomenon)
return merged
def recalc_effects(self: "Avatar") -> None:
"""
@@ -116,4 +159,3 @@ class EffectsMixin:
def move_step_length(self: "Avatar") -> int:
"""获取角色的移动步长"""
return self.cultivation_progress.get_move_step()

View File

@@ -307,10 +307,4 @@ async def handle_battle_finish(
# 处理死亡
if is_fatal:
handle_death(world, loser, DeathReason.BATTLE)
# 将事件分发给目标(如果目标不是发起者),发起者由 ActionMixin 处理
if target and target.id != attacker.id:
target.add_event(result_event)
target.add_event(story_event)
return [result_event, story_event]

View File

@@ -32,11 +32,15 @@ class MutualAttack(MutualAction):
def _settle_feedback(self, target_avatar: "Avatar", feedback_name: str) -> None:
fb = str(feedback_name).strip()
# 此处不产生新事件,仅改变目标行为
# 目标的行为改变会通过 _set_target_immediate_action -> commit_next_plan 产生新事件
# 且 commit_next_plan 内部会处理事件分发(理论上)
# 但我们看看基类的 _set_target_immediate_action 实现
if fb == "Escape":
params = {"avatar_name": self.avatar.name}
self._set_target_immediate_action(target_avatar, fb, params)
elif fb == "Attack":
params = {"avatar_name": self.avatar.name}
self._set_target_immediate_action(target_avatar, fb, params)

View File

@@ -11,7 +11,6 @@ from src.classes.relations import (
from src.classes.event import Event, NULL_EVENT
from src.utils.config import CONFIG
from src.classes.action_runtime import ActionResult, ActionStatus
from src.classes.action.event_helper import EventHelper
if TYPE_CHECKING:
from src.classes.avatar import Avatar
@@ -84,6 +83,8 @@ class Conversation(MutualAction):
# 使用开始时间戳
month_stamp = self._start_month_stamp if self._start_month_stamp is not None else self.world.month_stamp
events_to_return = []
# 记录对话内容
if conversation_content:
content_event = Event(
@@ -91,12 +92,17 @@ class Conversation(MutualAction):
f"{self.avatar.name}{target.name} 的交谈:{conversation_content}",
related_avatars=[self.avatar.id, target.id]
)
EventHelper.push_pair(content_event, initiator=self.avatar, target=target, to_sidebar_once=True)
events_to_return.append(content_event)
# 处理关系变化 (调用通用逻辑)
# 注意process_relation_changes 可能会生成关系变化的事件
# 这部分逻辑需要确认是否也遵循新模式。
# 假设 process_relation_changes 内部使用了 add_event则需要留意是否存在双重添加风险。
# 目前看来 process_relation_changes 是通过 EventHelper 或直接 add_event 操作的。
# 如果它内部逻辑完备(如使用了 EventHelper 去重),则无需改动。
process_relation_changes(self.avatar, target, result, month_stamp)
return ActionResult(status=ActionStatus.COMPLETED, events=[])
return ActionResult(status=ActionStatus.COMPLETED, events=events_to_return)
def step(self, target_avatar: "Avatar|str", **kwargs) -> ActionResult:
"""调用通用异步 step 逻辑"""

View File

@@ -51,11 +51,13 @@ class DualCultivation(MutualAction):
rel_ids = [self.avatar.id]
if target is not None:
rel_ids.append(target.id)
event = Event(self.world.month_stamp, f"{self.avatar.name} 邀请 {target_name} 进行双修", related_avatars=rel_ids, is_major=True)
# 仅写入历史
self.avatar.add_event(event, to_sidebar=False)
# 仅手动添加给 TargetSelf的部分由ActionMixin通过返回值处理
if target is not None:
target.add_event(event, to_sidebar=False)
# 记录开始文本用于故事生成
self._start_event_content = event.content
# 初始化内部标记,避免后续 getattr
@@ -103,6 +105,7 @@ class DualCultivation(MutualAction):
gain = int(self._dual_exp_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)
# 生成恋爱/双修小故事
@@ -110,6 +113,7 @@ class DualCultivation(MutualAction):
# 双修强制双人模式,允许改变关系
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)
return events

View File

@@ -126,25 +126,11 @@ class MutualAction(DefineAction, LLMAction, ActualActionMixin, TargetingMixin):
if isinstance(target_avatar, str):
return self.find_avatar_by_name(target_avatar)
return target_avatar
async def _execute(self, target_avatar: "Avatar|str") -> None:
"""异步执行互动动作"""
target_avatar = self._get_target_avatar(target_avatar)
if target_avatar is None:
return
infos = self._build_prompt_infos(target_avatar)
res = await self._call_llm_feedback(infos)
r = res.get(infos["avatar_name_2"], {})
thinking = r.get("thinking", "")
feedback = r.get("feedback", "")
target_avatar.thinking = thinking
self._settle_feedback(target_avatar, feedback)
fb_label = self.FEEDBACK_LABELS.get(str(feedback).strip(), str(feedback))
feedback_event = Event(self.world.month_stamp, f"{target_avatar.name}{self.avatar.name} 的反馈:{fb_label}", related_avatars=[self.avatar.id, target_avatar.id])
EventHelper.push_pair(feedback_event, initiator=self.avatar, target=target_avatar, to_sidebar_once=True)
self._apply_feedback(target_avatar, feedback)
"""异步执行互动动作 (deprecated, use step instead)"""
# 仅为兼容 DefineAction 接口,实际逻辑在 step 中
pass
# 实现 ActualActionMixin 接口
def can_start(self, target_avatar: "Avatar|str|None" = None) -> tuple[bool, str]:
@@ -184,10 +170,12 @@ class MutualAction(DefineAction, LLMAction, ActualActionMixin, TargetingMixin):
# 根据IS_MAJOR类变量设置事件类型
is_major = self.__class__.IS_MAJOR if hasattr(self.__class__, 'IS_MAJOR') else False
event = Event(self._start_month_stamp, f"{self.avatar.name}{target_name} 发起 {action_name}", related_avatars=rel_ids, is_major=is_major)
# 仅写入历史,避免与提交阶段重复推送到侧边栏
self.avatar.add_event(event, to_sidebar=False)
# 仅手动添加给 TargetSelf的部分由ActionMixin通过返回值处理
# 默认不推Target侧边栏因为发起事件通常只在发起者侧重要或者作为"收到发起"的通知
if target is not None:
target.add_event(event, to_sidebar=False)
return event
def step(self, target_avatar: "Avatar|str") -> ActionResult:
@@ -219,12 +207,13 @@ class MutualAction(DefineAction, LLMAction, ActualActionMixin, TargetingMixin):
target.thinking = thinking
self._settle_feedback(target, feedback)
fb_label = self.FEEDBACK_LABELS.get(str(feedback).strip(), str(feedback))
# 使用开始时间戳
month_stamp = self._start_month_stamp if self._start_month_stamp is not None else self.world.month_stamp
feedback_event = Event(month_stamp, f"{target.name}{self.avatar.name} 的反馈:{fb_label}", related_avatars=[self.avatar.id, target.id])
EventHelper.push_pair(feedback_event, initiator=self.avatar, target=target, to_sidebar_once=True)
self._apply_feedback(target, feedback)
return ActionResult(status=ActionStatus.COMPLETED, events=[])
return ActionResult(status=ActionStatus.COMPLETED, events=[feedback_event])
return ActionResult(status=ActionStatus.RUNNING, events=[])
@@ -233,5 +222,3 @@ class MutualAction(DefineAction, LLMAction, ActualActionMixin, TargetingMixin):
完成互动动作,事件已在 step 中处理,无需额外事件
"""
return []

View File

@@ -13,6 +13,7 @@ from src.classes.battle import decide_battle
from src.classes.story_teller import StoryTeller
from src.classes.death import handle_death
from src.classes.death_reason import DeathReason
from src.classes.action.event_helper import EventHelper
if TYPE_CHECKING:
from src.classes.avatar import Avatar
@@ -26,7 +27,7 @@ class Occupy(MutualAction):
占据指定的洞府。如果是无主洞府直接占据;如果是有主洞府,则发起抢夺。
对方拒绝则进入战斗,进攻方胜利则洞府易主。
"""
ACTION_NAME = "Occupy"
ACTION_NAME = "抢夺洞府"
COMMENT = "占据或抢夺洞府"
PARAMS = {"region_name": "str"}
FEEDBACK_ACTIONS = ["Yield", "Reject"]
@@ -55,7 +56,29 @@ class Occupy(MutualAction):
def start(self, region_name: str) -> Event:
region, host, _ = self._get_region_and_host(region_name)
return super().start(target_avatar=host)
# 必须初始化开始时间
self._start_month_stamp = self.world.month_stamp
target_name = host.name if host else "无主之地"
event_text = f"{self.avatar.name}{target_name}{region.name} 发起抢夺"
rel_ids = [self.avatar.id]
if host:
rel_ids.append(host.id)
event = Event(
self._start_month_stamp,
event_text,
related_avatars=rel_ids,
is_major=self.IS_MAJOR
)
# 记录到历史,侧边栏推送由 ActionMixin.commit_next_plan 统一处理
self.avatar.add_event(event, to_sidebar=False)
if host:
host.add_event(event, to_sidebar=False)
return event
def step(self, region_name: str) -> ActionResult:
region, host, _ = self._get_region_and_host(region_name)
@@ -77,8 +100,8 @@ class Occupy(MutualAction):
related_avatars=[self.avatar.id, target_avatar.id],
is_major=True
)
self.avatar.add_event(event)
target_avatar.add_event(event)
# 统一推送,避免重复
EventHelper.push_pair(event, initiator=self.avatar, target=target_avatar, to_sidebar_once=True)
self._last_result = None

View File

@@ -9,6 +9,8 @@ from src.classes.event import Event
from src.classes.story_teller import StoryTeller
from src.classes.action.cooldown import cooldown_action
from src.classes.action.event_helper import EventHelper
if TYPE_CHECKING:
from src.classes.avatar import Avatar
@@ -65,8 +67,10 @@ class Spar(MutualAction):
result_text,
related_avatars=[self.avatar.id, target_avatar.id]
)
self.avatar.add_event(event, to_sidebar=True)
target_avatar.add_event(event, to_sidebar=True)
# 使用 EventHelper.push_pair 确保只推送一次到 Global EventManager通过 to_sidebar_once=True
# 此时 Self(Initiator) 获得 to_sidebar=True, Target 获得 to_sidebar=False
EventHelper.push_pair(event, self.avatar, target_avatar, to_sidebar_once=True)
async def finish(self, target_avatar: Avatar | str) -> list[Event]:
# 获取目标
@@ -97,4 +101,5 @@ class Spar(MutualAction):
is_story=True
)
# 返回给 Self (由 ActionMixin 处理)
return [story_event]

View File

@@ -24,14 +24,6 @@ class Talk(MutualAction):
PARAMS = {"target_avatar": "AvatarName"}
FEEDBACK_ACTIONS: list[str] = ["Talk", "Reject"]
# 复用父类的所有方法:
# - _get_template_path() -> mutual_action.txt
# - _build_prompt_infos() -> 标准的双方信息和历史事件
# - can_start() -> 检查目标在交互范围内
# - _can_start() -> 无额外检查
# - start() -> 生成开始事件
# - finish() -> 返回空列表(已在父类实现)
def _can_start(self, target: "Avatar") -> tuple[bool, str]:
"""攀谈无额外检查条件"""
from src.classes.observe import is_within_observation
@@ -42,10 +34,11 @@ class Talk(MutualAction):
def _handle_feedback_result(self, target: "Avatar", result: dict) -> ActionResult:
"""
处理 LLM 返回的反馈结果。
子类可覆盖此方法来定义自己的反馈处理逻辑。
"""
feedback = str(result.get("feedback", "")).strip()
events_to_return = []
# 处理反馈
if feedback == "Talk":
# 接受攀谈,自动进入 Conversation
@@ -54,7 +47,8 @@ class Talk(MutualAction):
f"{target.name} 接受了 {self.avatar.name} 的攀谈",
related_avatars=[self.avatar.id, target.id]
)
EventHelper.push_pair(accept_event, initiator=self.avatar, target=target, to_sidebar_once=True)
events_to_return.append(accept_event)
# 将 Conversation 加入计划队列并立即提交
self.avatar.load_decide_result_chain(
@@ -66,7 +60,8 @@ class Talk(MutualAction):
# 立即提交为当前动作
start_event = self.avatar.commit_next_plan()
if start_event is not None:
EventHelper.push_pair(start_event, initiator=self.avatar, target=target, to_sidebar_once=True)
pass
else:
# 拒绝攀谈
reject_event = Event(
@@ -74,9 +69,9 @@ class Talk(MutualAction):
f"{target.name} 拒绝了 {self.avatar.name} 的攀谈",
related_avatars=[self.avatar.id, target.id]
)
EventHelper.push_pair(reject_event, initiator=self.avatar, target=target, to_sidebar_once=True)
events_to_return.append(reject_event)
return ActionResult(status=ActionStatus.COMPLETED, events=[])
return ActionResult(status=ActionStatus.COMPLETED, events=events_to_return)
def step(self, target_avatar: "Avatar|str", **kwargs) -> ActionResult:
"""调用父类的通用异步 step 逻辑"""
@@ -105,4 +100,4 @@ class Talk(MutualAction):
return self._handle_feedback_result(target, r)
return ActionResult(status=ActionStatus.RUNNING, events=[])
return ActionResult(status=ActionStatus.RUNNING, events=[])

View File

@@ -34,7 +34,7 @@ protagonist_configs = [
"desc": "《凡人修仙传》主角,韩老魔",
"params": {
"gender": "",
"age": 200,
"age": 120,
"level": 90, # 合体/大乘
"sect": 9, # 千帆城 (商会/散修流)
"technique": 33, # 青帝长生诀 (木系至高)
@@ -89,7 +89,7 @@ protagonist_configs = [
"technique": 56, # 纵地金光 (风系身法)
"weapon": 2013, # 紫薇软剑 (轻灵剑法)
"auxiliary": 2007, # 踏云靴 (身法加成)
"personas": ["霸道", "剑修", "沉思"],
"personas": ["霸道", "剑修", "刻薄"],
"appearance": 35, # 美貌御姐
}
},
@@ -123,7 +123,7 @@ protagonist_configs = [
"technique": 36, # 虚空经 (全知观测)
"weapon": 2005, # 桃花扇 (本命物)
"auxiliary": 3002, # 昆仑镜 (全知之眼)
"personas": ["心机深沉", "疑心重", "贪财"],
"personas": ["心机深沉", "疑心重", "穿越者"],
"appearance": 25,
}
},
@@ -190,7 +190,7 @@ protagonist_configs = [
"technique": 38, # 逍遥游 (身法)
"weapon": 3009, # 芭蕉扇 (术法)
"auxiliary": 2006, # 源天神眼 (明阳神通)
"personas": ["友爱", "惜命", "沉思"],
"personas": ["死宅", "惜命", "沉思"],
"appearance": 5,
}
}