diff --git a/src/classes/avatar/effects_mixin.py b/src/classes/avatar/effects_mixin.py index b290a31..445170f 100644 --- a/src/classes/avatar/effects_mixin.py +++ b/src/classes/avatar/effects_mixin.py @@ -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() - diff --git a/src/classes/battle.py b/src/classes/battle.py index 3c33c00..2cb77db 100644 --- a/src/classes/battle.py +++ b/src/classes/battle.py @@ -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] diff --git a/src/classes/mutual_action/attack.py b/src/classes/mutual_action/attack.py index d82317e..84277e7 100644 --- a/src/classes/mutual_action/attack.py +++ b/src/classes/mutual_action/attack.py @@ -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) - - diff --git a/src/classes/mutual_action/conversation.py b/src/classes/mutual_action/conversation.py index 2a2c257..ba123fe 100644 --- a/src/classes/mutual_action/conversation.py +++ b/src/classes/mutual_action/conversation.py @@ -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 逻辑""" diff --git a/src/classes/mutual_action/dual_cultivation.py b/src/classes/mutual_action/dual_cultivation.py index 1173259..2bc5059 100644 --- a/src/classes/mutual_action/dual_cultivation.py +++ b/src/classes/mutual_action/dual_cultivation.py @@ -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) + + # 仅手动添加给 Target,Self的部分由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 diff --git a/src/classes/mutual_action/mutual_action.py b/src/classes/mutual_action/mutual_action.py index a6d6863..8c0b41d 100644 --- a/src/classes/mutual_action/mutual_action.py +++ b/src/classes/mutual_action/mutual_action.py @@ -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) + + # 仅手动添加给 Target,Self的部分由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 [] - - diff --git a/src/classes/mutual_action/occupy.py b/src/classes/mutual_action/occupy.py index 5716cce..fb029c1 100644 --- a/src/classes/mutual_action/occupy.py +++ b/src/classes/mutual_action/occupy.py @@ -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 diff --git a/src/classes/mutual_action/spar.py b/src/classes/mutual_action/spar.py index aa10fcd..bbc68f3 100644 --- a/src/classes/mutual_action/spar.py +++ b/src/classes/mutual_action/spar.py @@ -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] diff --git a/src/classes/mutual_action/talk.py b/src/classes/mutual_action/talk.py index ebce573..a0f86ad 100644 --- a/src/classes/mutual_action/talk.py +++ b/src/classes/mutual_action/talk.py @@ -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=[]) \ No newline at end of file + return ActionResult(status=ActionStatus.RUNNING, events=[]) diff --git a/src/utils/protagonist.py b/src/utils/protagonist.py index ae4fb79..4a9b5e1 100644 --- a/src/utils/protagonist.py +++ b/src/utils/protagonist.py @@ -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, } } diff --git a/static/config.yml b/static/config.yml index 859c6d4..bb31fa0 100644 --- a/static/config.yml +++ b/static/config.yml @@ -20,7 +20,7 @@ ai: game: init_npc_num: 12 sect_num: 3 # init_npc_num大于sect_num时,会随机选择sect_num个宗门 - npc_birth_rate_per_month: 0.01 + npc_birth_rate_per_month: 0 fortune_probability: 0.005 df: diff --git a/static/game_configs/weapon.csv b/static/game_configs/weapon.csv index 4c88d0b..70942b4 100644 --- a/static/game_configs/weapon.csv +++ b/static/game_configs/weapon.csv @@ -26,7 +26,7 @@ id,name,weapon_type,grade,sect_id,desc,effects 3001,青竹蜂云剑,剑,法宝,1,成套飞剑,内蕴辟邪神雷,克制天下邪祟。,{extra_battle_strength_points: 3} 3002,随心铁杆兵,棍,法宝,7,六耳猕猴的兵器,随心变化,大小如意。,"{extra_battle_strength_points: 2, extra_observation_radius: 1}" 3003,五火七禽扇,扇,法宝,4,扇面有空中火、石中火、木中火等五火,一扇灰飞烟灭。,"{legal_actions: ['DevourMortals'], extra_battle_strength_points: 'avatar.weapon.special_data.get(""devoured_souls"", 0) // 100 * 0.1'}" -3004,弑神枪,枪,法宝,9,杀气滔天,曾染魔神之血,枪出无回。,"{extra_battle_strength_points: '2 + avatar.weapon.proficiency * 0.01'}" +3004,弑神枪,枪,法宝,9,杀气滔天,曾染魔神之血,枪出无回。,"{extra_battle_strength_points: '2 + avatar.weapon_proficiency * 0.01'}" 3005,赤锋矛,枪,法宝,,赤锋矛,不朽盾,斩尽仙王灭九天。,"[{extra_battle_strength_points: 2}, {when: 'avatar.cultivation.level >= 30', extra_battle_strength_points: 2, realm_suppression_bonus: 0.1}]" 3006,斩仙飞刀,暗器,法宝,5,红葫芦内藏一线毫光,有眉有目。请宝贝转身,神鬼难逃。,"{extra_battle_strength_points: 3, extra_observation_radius: 1, extra_escape_success_rate: 0.15}" 3007,诛仙剑,剑,法宝,1,非铜非铁亦非钢,曾在须弥山下藏。利气直透九重天。,{extra_battle_strength_points: 3}