From 17afb4c876e74d134e6d90f70379ae7cac3b5a9b Mon Sep 17 00:00:00 2001 From: bridge Date: Tue, 21 Oct 2025 23:58:39 +0800 Subject: [PATCH] update can_start --- src/classes/action/action.py | 4 ++-- src/classes/action/battle.py | 7 ++++--- src/classes/action/breakthrough.py | 5 +++-- src/classes/action/catch.py | 12 +++++++----- src/classes/action/cultivate.py | 10 +++++----- src/classes/action/devour_mortals.py | 5 +++-- src/classes/action/escape.py | 4 ++-- src/classes/action/harvest.py | 8 ++++---- src/classes/action/help_mortals.py | 10 ++++++---- src/classes/action/hunt.py | 8 ++++---- src/classes/action/move_away_from_avatar.py | 4 ++-- src/classes/action/move_away_from_region.py | 8 ++++---- src/classes/action/move_to_avatar.py | 4 ++-- src/classes/action/move_to_region.py | 16 ++++++++-------- src/classes/action/play.py | 4 ++-- src/classes/action/plunder_mortals.py | 8 +++++--- src/classes/action/self_heal.py | 12 +++++++----- src/classes/action/sold.py | 12 +++++++----- src/classes/action/talk.py | 5 +++-- src/classes/avatar.py | 7 ++++--- src/classes/mutual_action/conversation.py | 13 ++++++++----- src/classes/mutual_action/dual_cultivation.py | 10 +++++----- src/classes/mutual_action/mutual_action.py | 9 +++++---- static/game_configs/persona.csv | 1 + 24 files changed, 103 insertions(+), 83 deletions(-) diff --git a/src/classes/action/action.py b/src/classes/action/action.py index 96e76b3..80c5263 100644 --- a/src/classes/action/action.py +++ b/src/classes/action/action.py @@ -129,8 +129,8 @@ class ActualActionMixin(): """ @abstractmethod - def can_start(self, **params) -> bool: - return True + def can_start(self, **params) -> tuple[bool, str]: + return True, "" @abstractmethod def start(self, **params) -> Event | None: diff --git a/src/classes/action/battle.py b/src/classes/action/battle.py index 67486f3..35aae17 100644 --- a/src/classes/action/battle.py +++ b/src/classes/action/battle.py @@ -31,10 +31,11 @@ class Battle(InstantAction): winner.hp.reduce(winner_damage) self._last_result = (winner.name, loser.name, loser_damage, winner_damage) - def can_start(self, avatar_name: str | None = None) -> bool: + def can_start(self, avatar_name: str | None = None) -> tuple[bool, str]: if avatar_name is None: - return False - return self._get_target(avatar_name) is not None + return False, "缺少参数 avatar_name" + ok = self._get_target(avatar_name) is not None + return (ok, "" if ok else "目标不存在") def start(self, avatar_name: str) -> Event: target = self._get_target(avatar_name) diff --git a/src/classes/action/breakthrough.py b/src/classes/action/breakthrough.py index 9d8811e..eaa948a 100644 --- a/src/classes/action/breakthrough.py +++ b/src/classes/action/breakthrough.py @@ -103,8 +103,9 @@ class Breakthrough(TimedAction): self.avatar.mp.add_max(mp_increase) self.avatar.mp.recover(mp_increase) # 突破时完全恢复MP - def can_start(self) -> bool: - return self.avatar.cultivation_progress.can_break_through() + def can_start(self) -> tuple[bool, str]: + ok = self.avatar.cultivation_progress.can_break_through() + return (ok, "" if ok else "当前不处于瓶颈,无法突破") def start(self) -> Event: # 清理状态 diff --git a/src/classes/action/catch.py b/src/classes/action/catch.py index 13917cf..8af9915 100644 --- a/src/classes/action/catch.py +++ b/src/classes/action/catch.py @@ -23,7 +23,7 @@ class Catch(TimedAction): - 按动物境界映射成功率尝试捕捉,成功则成为灵兽(覆盖旧灵兽)。 """ - COMMENT = "尝试驯服一只灵兽,成为自身灵兽" + COMMENT = "尝试驯服一只灵兽,成为自身灵兽。只能有一只灵兽,但是可以高级替换低级。" DOABLES_REQUIREMENTS = "仅百兽宗;在有动物的普通区域;目标动物境界不高于角色" PARAMS = {} @@ -55,15 +55,17 @@ class Catch(TimedAction): # 覆盖为新的灵兽 self.avatar.spirit_animal = SpiritAnimal(name=target.name, realm=target.realm) - def can_start(self) -> bool: + def can_start(self) -> tuple[bool, str]: # 仅百兽宗 sect = getattr(self.avatar, "sect", None) if sect is None or getattr(sect, "name", "") != "百兽宗": - return False + return False, "仅百兽宗弟子可用" region = self.avatar.tile.region if not isinstance(region, NormalRegion): - return False - return len(self._get_available_animals()) > 0 + return False, "当前不在普通区域" + if len(self._get_available_animals()) == 0: + return False, "当前区域无可御兽的动物或其境界过高" + return True, "" def start(self) -> Event: region = self.avatar.tile.region diff --git a/src/classes/action/cultivate.py b/src/classes/action/cultivate.py index c8dfe59..d3c1b56 100644 --- a/src/classes/action/cultivate.py +++ b/src/classes/action/cultivate.py @@ -45,17 +45,17 @@ class Cultivate(TimedAction): base = 100 return base * essence_density - def can_start(self) -> bool: + def can_start(self) -> tuple[bool, str]: root = self.avatar.root region = self.avatar.tile.region essence_types = get_essence_types_for_root(root) if not self.avatar.cultivation_progress.can_cultivate(): - return False + return False, "修为已达瓶颈,无法继续修炼" if not isinstance(region, CultivateRegion): - return False + return False, "当前不在修炼区域" if all(region.essence.get_density(et) == 0 for et in essence_types): - return False - return True + return False, "当前区域无与灵根相符的灵气" + return True, "" def start(self) -> Event: return Event(self.world.month_stamp, f"{self.avatar.name} 在 {self.avatar.tile.region.name} 开始修炼", related_avatars=[self.avatar.id]) diff --git a/src/classes/action/devour_mortals.py b/src/classes/action/devour_mortals.py index 1805eda..fbaabf6 100644 --- a/src/classes/action/devour_mortals.py +++ b/src/classes/action/devour_mortals.py @@ -23,9 +23,10 @@ class DevourMortals(TimedAction): gain = random.randint(10, 100) tr.devoured_souls = min(10000, int(tr.devoured_souls) + gain) - def can_start(self) -> bool: + def can_start(self) -> tuple[bool, str]: legal = self.avatar.effects.get("legal_actions", []) - return "DevourMortals" in legal + ok = "DevourMortals" in legal + return (ok, "" if ok else "未被允许的非法动作(缺少万魂幡或权限)") def start(self) -> Event: return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇开始吞噬凡人", related_avatars=[self.avatar.id]) diff --git a/src/classes/action/escape.py b/src/classes/action/escape.py index c2a1178..8ddf0f8 100644 --- a/src/classes/action/escape.py +++ b/src/classes/action/escape.py @@ -56,8 +56,8 @@ class Escape(InstantAction): if start_event is not None: EventHelper.push_pair(start_event, initiator=self.avatar, target=target, to_sidebar_once=True) - def can_start(self, avatar_name: str | None = None) -> bool: - return True + def can_start(self, avatar_name: str | None = None) -> tuple[bool, str]: + return True, "" def start(self, avatar_name: str) -> Event: target = self._find_avatar_by_name(avatar_name) diff --git a/src/classes/action/harvest.py b/src/classes/action/harvest.py index 6e71a53..a4b9919 100644 --- a/src/classes/action/harvest.py +++ b/src/classes/action/harvest.py @@ -52,14 +52,14 @@ class Harvest(TimedAction): """ return 1.0 # 100%成功率 - def can_start(self) -> bool: + def can_start(self) -> tuple[bool, str]: region = self.avatar.tile.region if not isinstance(region, NormalRegion): - return False + return False, "当前不在普通区域" avaliable_plants = self.get_available_plants() if len(avaliable_plants) == 0: - return False - return True + return False, "当前区域无可采集的植物或其境界过高" + return True, "" def start(self) -> Event: region = self.avatar.tile.region diff --git a/src/classes/action/help_mortals.py b/src/classes/action/help_mortals.py index f715c41..38137a2 100644 --- a/src/classes/action/help_mortals.py +++ b/src/classes/action/help_mortals.py @@ -27,14 +27,16 @@ class HelpMortals(TimedAction): if getattr(self.avatar.magic_stone, "value", 0) >= cost: self.avatar.magic_stone = self.avatar.magic_stone - cost - def can_start(self) -> bool: + def can_start(self) -> tuple[bool, str]: region = self.avatar.tile.region if not isinstance(region, CityRegion): - return False + return False, "仅能在城市区域执行" if self.avatar.alignment != Alignment.RIGHTEOUS: - return False + return False, "仅正阵营可执行" cost = self.COST - return self.avatar.magic_stone >= cost + if not (self.avatar.magic_stone >= cost): + return False, "灵石不足" + return True, "" def start(self) -> Event: return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇开始帮助凡人") diff --git a/src/classes/action/hunt.py b/src/classes/action/hunt.py index 27d648e..249d45f 100644 --- a/src/classes/action/hunt.py +++ b/src/classes/action/hunt.py @@ -52,14 +52,14 @@ class Hunt(TimedAction): """ return 1.0 # 100%成功率 - def can_start(self) -> bool: + def can_start(self) -> tuple[bool, str]: region = self.avatar.tile.region if not isinstance(region, NormalRegion): - return False + return False, "当前不在普通区域" available_animals = self.get_available_animals() if len(available_animals) == 0: - return False - return True + return False, "当前区域无可狩猎的动物或其境界过高" + return True, "" def start(self) -> Event: region = self.avatar.tile.region diff --git a/src/classes/action/move_away_from_avatar.py b/src/classes/action/move_away_from_avatar.py index 9838cd2..c243059 100644 --- a/src/classes/action/move_away_from_avatar.py +++ b/src/classes/action/move_away_from_avatar.py @@ -39,8 +39,8 @@ class MoveAwayFromAvatar(TimedAction): dx, dy = clamp_manhattan_with_diagonal_priority(raw_dx, raw_dy, step) Move(self.avatar, self.world).execute(dx, dy) - def can_start(self, avatar_name: str | None = None) -> bool: - return True + def can_start(self, avatar_name: str | None = None) -> tuple[bool, str]: + return True, "" def start(self, avatar_name: str) -> Event: target_name = avatar_name diff --git a/src/classes/action/move_away_from_region.py b/src/classes/action/move_away_from_region.py index 4737e13..a093e3f 100644 --- a/src/classes/action/move_away_from_region.py +++ b/src/classes/action/move_away_from_region.py @@ -32,14 +32,14 @@ class MoveAwayFromRegion(InstantAction): dx, dy = clamp_manhattan_with_diagonal_priority(away_dx, away_dy, step) Move(self.avatar, self.world).execute(dx, dy) - def can_start(self, region: str | None = None) -> bool: + def can_start(self, region: str | None = None) -> tuple[bool, str]: if region is None: - return True + return True, "" try: resolve_region(self.world, region) - return True + return True, "" except Exception: - return False + return False, f"无法解析区域: {region}" def start(self, region: str) -> Event: r = resolve_region(self.world, region) diff --git a/src/classes/action/move_to_avatar.py b/src/classes/action/move_to_avatar.py index b8a08ed..95de781 100644 --- a/src/classes/action/move_to_avatar.py +++ b/src/classes/action/move_to_avatar.py @@ -37,8 +37,8 @@ class MoveToAvatar(DefineAction, ActualActionMixin): dx, dy = clamp_manhattan_with_diagonal_priority(raw_dx, raw_dy, step) Move(self.avatar, self.world).execute(dx, dy) - def can_start(self, avatar_name: str | None = None) -> bool: - return True + def can_start(self, avatar_name: str | None = None) -> tuple[bool, str]: + return True, "" def start(self, avatar_name: str) -> Event: target = self._get_target(avatar_name) diff --git a/src/classes/action/move_to_region.py b/src/classes/action/move_to_region.py index 11eac73..fa7f254 100644 --- a/src/classes/action/move_to_region.py +++ b/src/classes/action/move_to_region.py @@ -30,24 +30,24 @@ class MoveToRegion(DefineAction, ActualActionMixin): dx, dy = clamp_manhattan_with_diagonal_priority(raw_dx, raw_dy, step) Move(self.avatar, self.world).execute(dx, dy) - def can_start(self, region: Region | str | None = None) -> bool: + def can_start(self, region: Region | str | None = None) -> tuple[bool, str]: if region is None: - return False + return False, "缺少参数 region" try: - self._resolve_region(region) - return True - except (ValueError, TypeError): - return False + resolve_region(self.world, region) + return True, "" + except Exception: + return False, f"无法解析区域: {region}" def start(self, region: Region | str) -> Event: - r = self._resolve_region(region) + r = resolve_region(self.world, region) region_name = r.name # 仅使用规范化后的区域名 return Event(self.world.month_stamp, f"{self.avatar.name} 开始移动向 {region_name}", related_avatars=[self.avatar.id]) def step(self, region: Region | str) -> ActionResult: self.execute(region=region) # 完成条件:到达目标区域 - r = resolve_region(region) + r = resolve_region(self.world, region) # 常规:基于 tile.region 精确判定;兜底:当前位置坐标属于目标区域的格点集合 done = self.avatar.is_in_region(r) or ((self.avatar.pos_x, self.avatar.pos_y) in getattr(r, "cors", ())) return ActionResult(status=(ActionStatus.COMPLETED if done else ActionStatus.RUNNING), events=[]) diff --git a/src/classes/action/play.py b/src/classes/action/play.py index 611e276..96f4e82 100644 --- a/src/classes/action/play.py +++ b/src/classes/action/play.py @@ -23,8 +23,8 @@ class Play(TimedAction): # 比如增加心情值、减少压力等 pass - def can_start(self) -> bool: - return True + def can_start(self) -> tuple[bool, str]: + return True, "" def start(self) -> Event: return Event(self.world.month_stamp, f"{self.avatar.name} 开始玩耍") diff --git a/src/classes/action/plunder_mortals.py b/src/classes/action/plunder_mortals.py index d2355cd..135f262 100644 --- a/src/classes/action/plunder_mortals.py +++ b/src/classes/action/plunder_mortals.py @@ -26,11 +26,13 @@ class PlunderMortals(TimedAction): gain = self.GAIN self.avatar.magic_stone = self.avatar.magic_stone + gain - def can_start(self) -> bool: + def can_start(self) -> tuple[bool, str]: region = self.avatar.tile.region if not isinstance(region, CityRegion): - return False - return self.avatar.alignment == Alignment.EVIL + return False, "仅能在城市区域执行" + if self.avatar.alignment != Alignment.EVIL: + return False, "仅邪阵营可执行" + return True, "" def start(self) -> Event: return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇开始搜刮凡人", related_avatars=[self.avatar.id]) diff --git a/src/classes/action/self_heal.py b/src/classes/action/self_heal.py index 3d6b781..de31c01 100644 --- a/src/classes/action/self_heal.py +++ b/src/classes/action/self_heal.py @@ -37,16 +37,18 @@ class SelfHeal(TimedAction): hq_name = getattr(getattr(sect, "headquarter", None), "name", None) or getattr(sect, "name", None) return bool(hq_name) and region.name == hq_name - def can_start(self) -> bool: + def can_start(self) -> tuple[bool, str]: # 必须是宗门弟子且在自身宗门总部,且当前HP未满 if getattr(self.avatar, "sect", None) is None: - return False + return False, "仅宗门弟子可用" if not self._is_in_own_sect_headquarter(): - return False + return False, "需要位于自身宗门总部" hp_obj = getattr(self.avatar, "hp", None) if hp_obj is None: - return False - return hp_obj.cur < hp_obj.max + return False, "缺少HP信息" + if not (hp_obj.cur < hp_obj.max): + return False, "当前HP已满" + return True, "" def start(self) -> Event: region = getattr(getattr(self.avatar, "tile", None), "region", None) diff --git a/src/classes/action/sold.py b/src/classes/action/sold.py index 372808b..27317e0 100644 --- a/src/classes/action/sold.py +++ b/src/classes/action/sold.py @@ -43,17 +43,19 @@ class SellItems(InstantAction): self.avatar.magic_stone = self.avatar.magic_stone + total_gain - def can_start(self, item_name: str | None = None) -> bool: + def can_start(self, item_name: str | None = None) -> tuple[bool, str]: region = self.avatar.tile.region if not isinstance(region, CityRegion): - return False + return False, "仅能在城市区域执行" if item_name is None: # 用于动作空间:只要背包非空即可 - return bool(self.avatar.items) + ok = bool(self.avatar.items) + return (ok, "" if ok else "背包为空,无可出售物品") item = items_by_name.get(item_name) if item is None: - return False - return self.avatar.get_item_quantity(item) > 0 + return False, f"未知物品: {item_name}" + ok = self.avatar.get_item_quantity(item) > 0 + return (ok, "" if ok else "该物品数量为0") def start(self, item_name: str) -> Event: return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇出售 {item_name}") diff --git a/src/classes/action/talk.py b/src/classes/action/talk.py index 2a1cfe4..a93c9c3 100644 --- a/src/classes/action/talk.py +++ b/src/classes/action/talk.py @@ -26,9 +26,10 @@ class Talk(InstantAction): # Talk 本身不做长期效果,主要在 step 中驱动 Conversation return - def can_start(self, **kwargs) -> bool: + def can_start(self, **kwargs) -> tuple[bool, str]: # 感知范围内是否存在其他NPC(用于展示在动作空间) - return len(self._get_observed_others()) > 0 + ok = len(self._get_observed_others()) > 0 + return (ok, "" if ok else "感知范围内没有可交谈对象") def start(self) -> Event: self.observed_others = self._get_observed_others() diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 40a80cb..2f6c3ea 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -271,11 +271,11 @@ class Avatar: action = self.create_action(plan.action_name) # 再验证 params_for_can_start = filter_kwargs_for_callable(action.can_start, plan.params) - can_start = bool(action.can_start(**params_for_can_start)) + can_start, reason = action.can_start(**params_for_can_start) if not can_start: # 记录不合法动作 logger = get_logger().logger - logger.warning("非法动作: Avatar(name=%s,id=%s) 的动作 %s 参数=%s 无法启动", self.name, self.id, plan.action_name, plan.params) + logger.warning("非法动作: Avatar(name=%s,id=%s) 的动作 %s 参数=%s 无法启动,原因=%s", self.name, self.id, plan.action_name, plan.params, reason) continue # 启动 params_for_start = filter_kwargs_for_callable(action.start, plan.params) @@ -471,7 +471,8 @@ class Avatar: doable_actions: list[Action] = [] for action in actual_actions: # 用 can_start 的无参形式,用于“是否在动作空间中显示” - if action.can_start(): + ok, _reason = action.can_start() + if ok: doable_actions.append(action) action_space = [action.name for action in doable_actions] return action_space diff --git a/src/classes/mutual_action/conversation.py b/src/classes/mutual_action/conversation.py index b4406e6..343c0c7 100644 --- a/src/classes/mutual_action/conversation.py +++ b/src/classes/mutual_action/conversation.py @@ -52,13 +52,16 @@ class Conversation(MutualAction): "possible_relations": possible_relations, } - def can_start(self, target_avatar: "Avatar|str|None" = None, **kwargs) -> bool: + def can_start(self, target_avatar: "Avatar|str|None" = None, **kwargs) -> tuple[bool, str]: if target_avatar is None: - return False + return False, "缺少参数 target_avatar" target = self._get_target_avatar(target_avatar) - if target is None or target.tile is None or self.avatar.tile is None: - return False - return target.tile.region == self.avatar.tile.region + if target is None: + return False, "目标不存在" + if target.tile is None or self.avatar.tile is None: + return False, "任一角色未处于有效区域" + ok = target.tile.region == self.avatar.tile.region + return (ok, "" if ok else "目标不在同一区域") def start(self, target_avatar: "Avatar|str", **kwargs) -> Event: target = self._get_target_avatar(target_avatar) diff --git a/src/classes/mutual_action/dual_cultivation.py b/src/classes/mutual_action/dual_cultivation.py index 7fce363..cb489bb 100644 --- a/src/classes/mutual_action/dual_cultivation.py +++ b/src/classes/mutual_action/dual_cultivation.py @@ -35,18 +35,18 @@ class DualCultivation(MutualAction): # 复用 mutual_action 模板,仅需返回 Accept/Reject return CONFIG.paths.templates / "mutual_action.txt" - def can_start(self, target_avatar: "Avatar|str|None" = None) -> bool: + def can_start(self, target_avatar: "Avatar|str|None" = None) -> tuple[bool, str]: if target_avatar is None: - return False + return False, "缺少参数 target_avatar" # 基于 effects 判断是否允许 effects = self.avatar.effects legal_actions = effects.get("legal_actions", []) if not isinstance(legal_actions, list) or "DualCultivation" not in legal_actions: - return False + return False, "仅合欢宗或未被允许" target = self._get_target_avatar(target_avatar) if target is None: - return False - return True + return False, "目标不存在" + return True, "" def start(self, target_avatar: "Avatar|str") -> Event: target = self._get_target_avatar(target_avatar) diff --git a/src/classes/mutual_action/mutual_action.py b/src/classes/mutual_action/mutual_action.py index af218a9..ae6ab1d 100644 --- a/src/classes/mutual_action/mutual_action.py +++ b/src/classes/mutual_action/mutual_action.py @@ -141,17 +141,18 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): self._apply_feedback(target_avatar, feedback) # 实现 ActualActionMixin 接口 - def can_start(self, target_avatar: "Avatar|str|None" = None) -> bool: + def can_start(self, target_avatar: "Avatar|str|None" = None) -> tuple[bool, str]: """ 检查互动动作能否启动:目标需在发起者的感知范围内。 """ if target_avatar is None: - return False + return False, "缺少参数 target_avatar" target = self._get_target_avatar(target_avatar) if target is None: - return False + return False, "目标不存在" from src.classes.observe import is_within_observation - return is_within_observation(self.avatar, target) + ok = is_within_observation(self.avatar, target) + return (ok, "" if ok else "目标不在感知范围内") def start(self, target_avatar: "Avatar|str") -> Event: """ diff --git a/static/game_configs/persona.csv b/static/game_configs/persona.csv index 97cc124..5d49eee 100644 --- a/static/game_configs/persona.csv +++ b/static/game_configs/persona.csv @@ -29,3 +29,4 @@ id,name,exclusion_ids,desc,weight,condition 27,腼腆,26,你对待和他人结为道侣或者双修比较谨慎,1, 28,舔狗,13;14;22;27,你对异性中外貌出众者格外友善,倾向主动接近、帮助与合作。,1, 29,嫉妒,11;23,你对在修为、外貌或财富等方面远超于你的人容易产生敌意,更倾向对其冷淡、挑衅或打压。,1, +30,穿越者,,你来自现代社会,怀念现代社会的一切,希望调查清楚你来的原因,早日回到现代,你的思考方式都是现代化的,1, \ No newline at end of file