add cooldown for given actions
This commit is contained in:
@@ -64,7 +64,7 @@
|
||||
- ✅ 角色性格
|
||||
- ✅ 境界突破机制
|
||||
- ✅ 角色间的相互关系
|
||||
- ✅ 角色感知范围
|
||||
- ✅ 角色交互范围
|
||||
- ✅ 角色Buffs系统:增益/减益效果
|
||||
- ✅ 法宝
|
||||
- [ ] 角色特殊能力
|
||||
|
||||
@@ -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:
|
||||
|
||||
58
src/classes/action/cooldown.py
Normal file
58
src/classes/action/cooldown.py
Normal 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
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import random
|
||||
|
||||
class DevourMortals(TimedAction):
|
||||
"""
|
||||
吞噬凡人:需持有万魂幡,吞噬魂魄可大大增加战力。
|
||||
吞噬凡人:需持有万魂幡,吞噬魂魄可较多增加战力。
|
||||
"""
|
||||
|
||||
COMMENT = "吞噬凡人,大大增加战力"
|
||||
COMMENT = "吞噬凡人,较多增加战力"
|
||||
DOABLES_REQUIREMENTS = "持有万魂幡"
|
||||
PARAMS = {}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class SelfHeal(TimedAction):
|
||||
单月动作,执行后HP直接回满。
|
||||
"""
|
||||
|
||||
COMMENT = "在宗门总部静养疗伤(单月回满HP)"
|
||||
COMMENT = "在宗门总部静养疗伤,回满HP"
|
||||
DOABLES_REQUIREMENTS = "自己是宗门弟子,且位于本宗门总部区域,且当前HP未满"
|
||||
PARAMS = {}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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"] = []
|
||||
|
||||
Reference in New Issue
Block a user