add cooldown for given actions

This commit is contained in:
bridge
2025-10-23 00:19:35 +08:00
parent 5b4b1b1ff8
commit 16e72b122b
15 changed files with 112 additions and 27 deletions

View File

@@ -64,7 +64,7 @@
- ✅ 角色性格
- ✅ 境界突破机制
- ✅ 角色间的相互关系
- ✅ 角色感知范围
- ✅ 角色交互范围
- ✅ 角色Buffs系统增益/减益效果
- ✅ 法宝
- [ ] 角色特殊能力

View File

@@ -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:

View File

@@ -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

View File

@@ -7,10 +7,10 @@ import random
class DevourMortals(TimedAction):
"""
吞噬凡人:需持有万魂幡,吞噬魂魄可大大增加战力。
吞噬凡人:需持有万魂幡,吞噬魂魄可较多增加战力。
"""
COMMENT = "吞噬凡人,大大增加战力"
COMMENT = "吞噬凡人,较多增加战力"
DOABLES_REQUIREMENTS = "持有万魂幡"
PARAMS = {}

View File

@@ -11,7 +11,7 @@ class SelfHeal(TimedAction):
单月动作执行后HP直接回满。
"""
COMMENT = "在宗门总部静养疗伤(单月回满HP"
COMMENT = "在宗门总部静养疗伤回满HP"
DOABLES_REQUIREMENTS = "自己是宗门弟子且位于本宗门总部区域且当前HP未满"
PARAMS = {}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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):

View File

@@ -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())

View File

@@ -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()

View File

@@ -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 = ""

View File

@@ -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()

View File

@@ -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

View File

@@ -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:
"""

View File

@@ -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"] = []