update can_start
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
# 清理状态
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} 在城镇开始帮助凡人")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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=[])
|
||||
|
||||
@@ -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} 开始玩耍")
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user