From 16e72b122b93e0ae6903886179747e634cf8ccf1 Mon Sep 17 00:00:00 2001 From: bridge Date: Thu, 23 Oct 2025 00:19:35 +0800 Subject: [PATCH] add cooldown for given actions --- README.md | 2 +- src/classes/action/breakthrough.py | 6 +- src/classes/action/cooldown.py | 58 +++++++++++++++++++ src/classes/action/devour_mortals.py | 4 +- src/classes/action/self_heal.py | 2 +- src/classes/action/talk.py | 18 +++--- src/classes/actions.py | 1 + src/classes/avatar.py | 2 + src/classes/avatar_manager.py | 2 +- src/classes/mutual_action/attack.py | 10 +++- src/classes/mutual_action/conversation.py | 4 +- src/classes/mutual_action/drive_away.py | 10 +++- src/classes/mutual_action/dual_cultivation.py | 10 +++- src/classes/mutual_action/mutual_action.py | 6 +- src/classes/observe.py | 4 +- 15 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 src/classes/action/cooldown.py diff --git a/README.md b/README.md index f7e1057..72b3330 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ - ✅ 角色性格 - ✅ 境界突破机制 - ✅ 角色间的相互关系 -- ✅ 角色感知范围 +- ✅ 角色交互范围 - ✅ 角色Buffs系统:增益/减益效果 - ✅ 法宝 - [ ] 角色特殊能力 diff --git a/src/classes/action/breakthrough.py b/src/classes/action/breakthrough.py index eaa948a..b3f8b06 100644 --- a/src/classes/action/breakthrough.py +++ b/src/classes/action/breakthrough.py @@ -2,6 +2,7 @@ from __future__ import annotations import random from src.classes.action import TimedAction +from src.classes.action.cooldown import cooldown_action from src.classes.event import Event from src.classes.cultivation import Realm from src.classes.story_teller import StoryTeller @@ -28,6 +29,7 @@ from src.classes.hp_and_mp import HP_MAX_BY_REALM, MP_MAX_BY_REALM from src.classes.effect import _merge_effects +@cooldown_action class Breakthrough(TimedAction): """ 突破境界。 @@ -36,8 +38,10 @@ class Breakthrough(TimedAction): """ COMMENT = "尝试突破境界(成功增加寿元上限,失败折损寿元上限;境界越高,成功率越低。)" - DOABLES_REQUIREMENTS = "角色处于瓶颈时" + DOABLES_REQUIREMENTS = "角色处于瓶颈时;不能连续执行" PARAMS = {} + # 冷却:突破应当有CD,避免连刷 + ACTION_CD_MONTHS: int = 3 # 保留类级常量声明,实际读取模块级配置 def calc_success_rate(self) -> float: diff --git a/src/classes/action/cooldown.py b/src/classes/action/cooldown.py new file mode 100644 index 0000000..559f717 --- /dev/null +++ b/src/classes/action/cooldown.py @@ -0,0 +1,58 @@ +from __future__ import annotations + + +def cooldown_action(cls: type) -> type: + """ + 冷却类装饰器: + - 仅当类定义了 ACTION_CD_MONTHS 且 >0 时生效 + - 在 can_start 前置检查冷却;在 finish 后记录冷却开始月戳 + - 冷却记录存放于 avatar._action_cd_last_months[ClassName] + - 同时在 COMMENT 中追加“(冷却:X月)”便于 UI 显示 + """ + + cd = int(getattr(cls, "ACTION_CD_MONTHS", 0) or 0) + if cd <= 0: + return cls + + # 追加提示到 COMMENT(若存在) + try: + comment = getattr(cls, "COMMENT", "") + if isinstance(comment, str) and comment.strip(): + if f"冷却:{cd}月" not in comment: + setattr(cls, "COMMENT", f"{comment}(冷却:{cd}月)") + except Exception: + # 避免 COMMENT 异常影响核心逻辑 + pass + + # 包装 can_start + if hasattr(cls, "can_start"): + original_can_start = cls.can_start + + def can_start(self, **params): # type: ignore[no-redef] + last_map = getattr(self.avatar, "_action_cd_last_months", {}) + last = last_map.get(self.__class__.__name__) + if last is not None: + elapsed = self.world.month_stamp - last + if elapsed < cd: + remain = cd - elapsed + return False, f"冷却中,还需 {remain} 个月" + return original_can_start(self, **params) + + cls.can_start = can_start # type: ignore[assignment] + + # 包装 finish:调用原逻辑后记录冷却 + if hasattr(cls, "finish"): + original_finish = cls.finish + + def finish(self, **params): # type: ignore[no-redef] + events = original_finish(self, **params) + last_map = getattr(self.avatar, "_action_cd_last_months", None) + if last_map is not None: + last_map[self.__class__.__name__] = self.world.month_stamp + return events + + cls.finish = finish # type: ignore[assignment] + + return cls + + diff --git a/src/classes/action/devour_mortals.py b/src/classes/action/devour_mortals.py index fbaabf6..16aabb9 100644 --- a/src/classes/action/devour_mortals.py +++ b/src/classes/action/devour_mortals.py @@ -7,10 +7,10 @@ import random class DevourMortals(TimedAction): """ - 吞噬凡人:需持有万魂幡,吞噬魂魄可大大增加战力。 + 吞噬凡人:需持有万魂幡,吞噬魂魄可较多增加战力。 """ - COMMENT = "吞噬凡人,大大增加战力" + COMMENT = "吞噬凡人,较多增加战力" DOABLES_REQUIREMENTS = "持有万魂幡" PARAMS = {} diff --git a/src/classes/action/self_heal.py b/src/classes/action/self_heal.py index de31c01..0668d86 100644 --- a/src/classes/action/self_heal.py +++ b/src/classes/action/self_heal.py @@ -11,7 +11,7 @@ class SelfHeal(TimedAction): 单月动作,执行后HP直接回满。 """ - COMMENT = "在宗门总部静养疗伤(单月回满HP)" + COMMENT = "在宗门总部静养疗伤,回满HP" DOABLES_REQUIREMENTS = "自己是宗门弟子,且位于本宗门总部区域,且当前HP未满" PARAMS = {} diff --git a/src/classes/action/talk.py b/src/classes/action/talk.py index a93c9c3..e18e6b1 100644 --- a/src/classes/action/talk.py +++ b/src/classes/action/talk.py @@ -8,33 +8,33 @@ from src.classes.event import Event class Talk(InstantAction): """ - 攀谈:尝试与感知范围内的某个NPC进行交谈。 - - can_start:感知范围内存在其他NPC - - 发起后:从感知范围内随机选择一个目标,进入 MutualAction: Conversation(允许建立关系) + 攀谈:尝试与交互范围内的某个NPC进行交谈。 + - can_start:交互范围内存在其他NPC + - 发起后:从交互范围内随机选择一个目标,进入 MutualAction: Conversation(允许建立关系) """ - COMMENT = "与感知范围内的NPC发起攀谈" - DOABLES_REQUIREMENTS = "感知范围内存在其他NPC" + COMMENT = "与交互范围内的NPC发起攀谈" + DOABLES_REQUIREMENTS = "交互范围内存在其他NPC" PARAMS = {} def _get_observed_others(self) -> list["Avatar"]: return self.world.avatar_manager.get_observable_avatars(self.avatar) - # 不再限定必须同一 tile,由感知范围统一约束 + # 不再限定必须同一 tile,由交互范围统一约束 def _execute(self) -> None: # Talk 本身不做长期效果,主要在 step 中驱动 Conversation return def can_start(self, **kwargs) -> tuple[bool, str]: - # 感知范围内是否存在其他NPC(用于展示在动作空间) + # 交互范围内是否存在其他NPC(用于展示在动作空间) ok = len(self._get_observed_others()) > 0 - return (ok, "" if ok else "感知范围内没有可交谈对象") + return (ok, "" if ok else "交互范围内没有可交谈对象") def start(self) -> Event: self.observed_others = self._get_observed_others() # 记录开始事件 - return Event(self.world.month_stamp, f"{self.avatar.name} 尝试与感知范围内的他人攀谈", related_avatars=[self.avatar.id]) + return Event(self.world.month_stamp, f"{self.avatar.name} 尝试与交互范围内的他人攀谈", related_avatars=[self.avatar.id]) def step(self) -> ActionResult: import random diff --git a/src/classes/actions.py b/src/classes/actions.py index b4b5358..13f9d71 100644 --- a/src/classes/actions.py +++ b/src/classes/actions.py @@ -18,6 +18,7 @@ ACTION_INFOS = { "comment": getattr(action, "COMMENT", ""), "doable_requirements": getattr(action, "DOABLES_REQUIREMENTS", ""), "params": getattr(action, "PARAMS", {}), + "cd_months": int(getattr(action, "ACTION_CD_MONTHS", 0) or 0), } for action in ALL_ACTUAL_ACTION_CLASSES } diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 2f6c3ea..a8d3a79 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -96,6 +96,8 @@ class Avatar: spirit_animal: Optional[SpiritAnimal] = None # 当月/当步新设动作标记:在 commit_next_plan 设为 True,首次 tick_action 后清为 False _new_action_set_this_step: bool = False + # 动作冷却:记录动作类名 -> 上次完成月戳 + _action_cd_last_months: dict[str, int] = field(default_factory=dict) # 不缓存 effects;实时从宗门与功法合并 def __post_init__(self): diff --git a/src/classes/avatar_manager.py b/src/classes/avatar_manager.py index a0663fa..b16bcf9 100644 --- a/src/classes/avatar_manager.py +++ b/src/classes/avatar_manager.py @@ -28,7 +28,7 @@ class AvatarManager: def get_observable_avatars(self, avatar: "Avatar") -> List["Avatar"]: """ - 返回处于 avatar 感知范围内的其他角色列表(不含自己)。 + 返回处于 avatar 交互范围内的其他角色列表(不含自己)。 基于曼哈顿距离与境界映射的感知半径过滤。 """ return get_observable_avatars(avatar, self.avatars.values()) diff --git a/src/classes/mutual_action/attack.py b/src/classes/mutual_action/attack.py index c18f765..94dde8e 100644 --- a/src/classes/mutual_action/attack.py +++ b/src/classes/mutual_action/attack.py @@ -1,17 +1,25 @@ from __future__ import annotations from .mutual_action import MutualAction +from src.classes.action.cooldown import cooldown_action +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from src.classes.avatar import Avatar +@cooldown_action class Attack(MutualAction): """攻击另一个NPC""" ACTION_NAME = "攻击" COMMENT = "对目标进行攻击。" - DOABLES_REQUIREMENTS = "与目标处于同一区域" + DOABLES_REQUIREMENTS = "目标在交互范围内;不能连续执行" PARAMS = {"target_avatar": "AvatarName"} FEEDBACK_ACTIONS = ["Escape", "Battle"] STORY_PROMPT: str = "" + # 攻击冷却:避免同月连刷攻击 + ACTION_CD_MONTHS: int = 3 def _settle_feedback(self, target_avatar: "Avatar", feedback_name: str) -> None: fb = str(feedback_name).strip() diff --git a/src/classes/mutual_action/conversation.py b/src/classes/mutual_action/conversation.py index 343c0c7..d748a27 100644 --- a/src/classes/mutual_action/conversation.py +++ b/src/classes/mutual_action/conversation.py @@ -24,8 +24,8 @@ class Conversation(MutualAction): """ ACTION_NAME = "交谈" - COMMENT = "两人需在同一地区,进行一段交流对话" - DOABLES_REQUIREMENTS = "与目标处于同一区域" + COMMENT = "与对方进行一段交流对话" + DOABLES_REQUIREMENTS = "目标在交互范围内" PARAMS = {"target_avatar": "AvatarName"} FEEDBACK_ACTIONS: list[str] = ["Talk", "Reject"] STORY_PROMPT: str = "" diff --git a/src/classes/mutual_action/drive_away.py b/src/classes/mutual_action/drive_away.py index 673acbb..e98a92b 100644 --- a/src/classes/mutual_action/drive_away.py +++ b/src/classes/mutual_action/drive_away.py @@ -1,17 +1,25 @@ from __future__ import annotations from .mutual_action import MutualAction +from src.classes.action.cooldown import cooldown_action +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from src.classes.avatar import Avatar +@cooldown_action class DriveAway(MutualAction): """驱赶:试图让对方离开当前区域。""" ACTION_NAME = "驱赶" COMMENT = "以武力威慑对方离开此地。" - DOABLES_REQUIREMENTS = "与目标处于同一区域" + DOABLES_REQUIREMENTS = "目标在交互范围内;不能连续执行" PARAMS = {"target_avatar": "AvatarName"} FEEDBACK_ACTIONS = ["MoveAwayFromRegion", "Battle"] STORY_PROMPT: str = "" + # 驱赶冷却:避免反复驱赶刷屏 + ACTION_CD_MONTHS: int = 3 def _settle_feedback(self, target_avatar: "Avatar", feedback_name: str) -> None: fb = str(feedback_name).strip() diff --git a/src/classes/mutual_action/dual_cultivation.py b/src/classes/mutual_action/dual_cultivation.py index cb489bb..cafc44a 100644 --- a/src/classes/mutual_action/dual_cultivation.py +++ b/src/classes/mutual_action/dual_cultivation.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import TYPE_CHECKING from .mutual_action import MutualAction +from src.classes.action.cooldown import cooldown_action from src.classes.event import Event from src.classes.story_teller import StoryTeller from src.utils.config import CONFIG @@ -13,11 +14,12 @@ if TYPE_CHECKING: from src.classes.avatar import Avatar +@cooldown_action class DualCultivation(MutualAction): - """双修:合欢宗弟子可与感知范围内的修士尝试双修。 + """双修:合欢宗弟子可与交互范围内的修士尝试双修。 - 仅限发起方为合欢宗成员 - - 仅当目标在感知范围内 + - 仅当目标在交互范围内 - 目标可以选择 接受 或 拒绝 - 若接受:发起者获得大量修为(约为修炼的 3~5 倍,随对方等级浮动),目标不获得修为 - 成功进入后生成一段“恋爱/双修”的小故事 @@ -25,11 +27,13 @@ class DualCultivation(MutualAction): ACTION_NAME = "双修" COMMENT = "以情入道的双修之术,仅合欢宗弟子可发起,对象可接受或拒绝" - DOABLES_REQUIREMENTS = "发起者为合欢宗;目标在感知范围内" + DOABLES_REQUIREMENTS = "发起者为合欢宗;目标在交互范围内;不能连续执行" PARAMS = {"target_avatar": "AvatarName"} FEEDBACK_ACTIONS = ["Accept", "Reject"] # 提供用于故事生成的提示词,供 StoryTeller 模板参考 STORY_PROMPT: str | None = "两位修士在双修过程中情愫暗生,以含蓄、雅致的文字描绘一段暧昧而不露骨的双修体验,体现彼此性格、境界差异与甜蜜的恋爱时光。不要体现经验的数值。" + # 双修的社交冷却:避免频繁请求 + ACTION_CD_MONTHS: int = 3 def _get_template_path(self) -> Path: # 复用 mutual_action 模板,仅需返回 Accept/Reject diff --git a/src/classes/mutual_action/mutual_action.py b/src/classes/mutual_action/mutual_action.py index ae6ab1d..bf5cb12 100644 --- a/src/classes/mutual_action/mutual_action.py +++ b/src/classes/mutual_action/mutual_action.py @@ -30,7 +30,7 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): ACTION_NAME: str = "MutualAction" COMMENT: str = "" - DOABLES_REQUIREMENTS: str = "感知范围内可互动" + DOABLES_REQUIREMENTS: str = "交互范围内可互动" PARAMS: dict = {"target_avatar": "Avatar"} FEEDBACK_ACTIONS: list[str] = [] # 反馈动作 -> 中文标签 的映射,供事件展示复用 @@ -143,7 +143,7 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): # 实现 ActualActionMixin 接口 def can_start(self, target_avatar: "Avatar|str|None" = None) -> tuple[bool, str]: """ - 检查互动动作能否启动:目标需在发起者的感知范围内。 + 检查互动动作能否启动:目标需在发起者的交互范围内。 """ if target_avatar is None: return False, "缺少参数 target_avatar" @@ -152,7 +152,7 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): return False, "目标不存在" from src.classes.observe import is_within_observation ok = is_within_observation(self.avatar, target) - return (ok, "" if ok else "目标不在感知范围内") + return (ok, "" if ok else "目标不在交互范围内") def start(self, target_avatar: "Avatar|str") -> Event: """ diff --git a/src/classes/observe.py b/src/classes/observe.py index b6a2b87..0d33758 100644 --- a/src/classes/observe.py +++ b/src/classes/observe.py @@ -36,7 +36,7 @@ def get_avatar_observation_radius(avatar: "Avatar") -> int: def is_within_observation(initiator: "Avatar", other: "Avatar") -> bool: """ - 判断 other 是否处于 initiator 的感知范围内: + 判断 other 是否处于 initiator 的交互范围内: 汉明距离(曼哈顿距离) <= initiator 的感知半径。 """ return get_avatar_distance(initiator, other) <= get_avatar_observation_radius(initiator) @@ -44,7 +44,7 @@ def is_within_observation(initiator: "Avatar", other: "Avatar") -> bool: def get_observable_avatars(initiator: "Avatar", avatars: Iterable["Avatar"]) -> List["Avatar"]: """ - 从给定集合中过滤出处于 initiator 感知范围内的角色(不包含 initiator 本人)。 + 从给定集合中过滤出处于 initiator 交互范围内的角色(不包含 initiator 本人)。 算法:线性扫描 O(N),与现有管理器遍历复杂度一致。 """ result: list["Avatar"] = []