fix: prevent actions on dead avatars
- Add validate_target_avatar() to TargetingMixin for unified validation. - Update Attack and Assassinate to use the new validation method. - Add comment to MutualAction.can_start() explaining why it uses inline check. - Add tests for dead target validation.
This commit is contained in:
@@ -4,10 +4,10 @@ import random
|
||||
|
||||
from src.classes.action import InstantAction
|
||||
from src.classes.action.cooldown import cooldown_action
|
||||
from src.classes.action.targeting_mixin import TargetingMixin
|
||||
from src.classes.event import Event
|
||||
from src.classes.battle import decide_battle, get_assassination_success_rate
|
||||
from src.classes.story_teller import StoryTeller
|
||||
from src.classes.normalize import normalize_avatar_name
|
||||
from src.classes.death import handle_death
|
||||
from src.classes.death_reason import DeathReason, DeathType
|
||||
from src.classes.kill_and_grab import kill_and_grab
|
||||
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
@cooldown_action
|
||||
class Assassinate(InstantAction):
|
||||
class Assassinate(InstantAction, TargetingMixin):
|
||||
ACTION_NAME = "暗杀"
|
||||
EMOJI = "🗡️"
|
||||
DESC = "暗杀目标,失败则变为战斗"
|
||||
@@ -39,15 +39,8 @@ class Assassinate(InstantAction):
|
||||
# 暗杀是大事(长期记忆)
|
||||
IS_MAJOR: bool = True
|
||||
|
||||
def _get_target(self, avatar_name: str) -> Avatar | None:
|
||||
normalized_name = normalize_avatar_name(avatar_name)
|
||||
for v in self.world.avatar_manager.avatars.values():
|
||||
if v.name == normalized_name:
|
||||
return v
|
||||
return None
|
||||
|
||||
def _execute(self, avatar_name: str) -> None:
|
||||
target = self._get_target(avatar_name)
|
||||
target = self.find_avatar_by_name(avatar_name)
|
||||
if target is None:
|
||||
return
|
||||
|
||||
@@ -77,13 +70,11 @@ class Assassinate(InstantAction):
|
||||
|
||||
def can_start(self, avatar_name: str | None = None) -> tuple[bool, str]:
|
||||
# 注意:cooldown_action 装饰器会覆盖这个方法并在调用此方法前检查 CD
|
||||
if avatar_name is None:
|
||||
return False, "缺少参数 avatar_name"
|
||||
ok = self._get_target(avatar_name) is not None
|
||||
return (ok, "" if ok else "目标不存在")
|
||||
_, ok, reason = self.validate_target_avatar(avatar_name)
|
||||
return ok, reason
|
||||
|
||||
def start(self, avatar_name: str) -> Event:
|
||||
target = self._get_target(avatar_name)
|
||||
target = self.find_avatar_by_name(avatar_name)
|
||||
target_name = target.name if target is not None else avatar_name
|
||||
|
||||
event = Event(self.world.month_stamp, f"{self.avatar.name} 潜伏在阴影中,试图暗杀 {target_name}...", related_avatars=[self.avatar.id, target.id] if target else [self.avatar.id], is_major=True)
|
||||
@@ -91,7 +82,7 @@ class Assassinate(InstantAction):
|
||||
return event
|
||||
|
||||
async def finish(self, avatar_name: str) -> list[Event]:
|
||||
target = self._get_target(avatar_name)
|
||||
target = self.find_avatar_by_name(avatar_name)
|
||||
if target is None:
|
||||
return []
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src.classes.action import InstantAction
|
||||
from src.classes.action.targeting_mixin import TargetingMixin
|
||||
from src.classes.event import Event
|
||||
from src.classes.battle import decide_battle, get_effective_strength_pair
|
||||
from src.classes.story_teller import StoryTeller
|
||||
from src.classes.normalize import normalize_avatar_name
|
||||
from src.classes.death import handle_death
|
||||
from src.classes.death_reason import DeathReason
|
||||
from src.classes.kill_and_grab import kill_and_grab
|
||||
|
||||
class Attack(InstantAction):
|
||||
class Attack(InstantAction, TargetingMixin):
|
||||
ACTION_NAME = "发起战斗"
|
||||
EMOJI = "⚔️"
|
||||
DESC = "攻击目标,进行对战"
|
||||
@@ -23,19 +23,8 @@ class Attack(InstantAction):
|
||||
# 战斗是大事(长期记忆)
|
||||
IS_MAJOR: bool = True
|
||||
|
||||
def _get_target(self, avatar_name: str):
|
||||
"""
|
||||
根据名字查找目标角色;找不到返回 None。
|
||||
会自动规范化名字(去除括号等附加信息)以提高容错性。
|
||||
"""
|
||||
normalized_name = normalize_avatar_name(avatar_name)
|
||||
for v in self.world.avatar_manager.avatars.values():
|
||||
if v.name == normalized_name:
|
||||
return v
|
||||
return None
|
||||
|
||||
def _execute(self, avatar_name: str) -> None:
|
||||
target = self._get_target(avatar_name)
|
||||
target = self.find_avatar_by_name(avatar_name)
|
||||
if target is None:
|
||||
return
|
||||
winner, loser, loser_damage, winner_damage = decide_battle(self.avatar, target)
|
||||
@@ -53,13 +42,11 @@ class Attack(InstantAction):
|
||||
self._last_result = (winner, loser, loser_damage, winner_damage)
|
||||
|
||||
def can_start(self, avatar_name: str | None = None) -> tuple[bool, str]:
|
||||
if avatar_name is None:
|
||||
return False, "缺少参数 avatar_name"
|
||||
ok = self._get_target(avatar_name) is not None
|
||||
return (ok, "" if ok else "目标不存在")
|
||||
_, ok, reason = self.validate_target_avatar(avatar_name)
|
||||
return ok, reason
|
||||
|
||||
def start(self, avatar_name: str) -> Event:
|
||||
target = self._get_target(avatar_name)
|
||||
target = self.find_avatar_by_name(avatar_name)
|
||||
target_name = target.name if target is not None else avatar_name
|
||||
# 展示双方折算战斗力(基于对手、含克制)
|
||||
s_att, s_def = get_effective_strength_pair(self.avatar, target)
|
||||
@@ -81,7 +68,7 @@ class Attack(InstantAction):
|
||||
if not (isinstance(res, tuple) and len(res) == 4):
|
||||
return []
|
||||
|
||||
target = self._get_target(avatar_name)
|
||||
target = self.find_avatar_by_name(avatar_name)
|
||||
start_text = getattr(self, '_start_event_content', "")
|
||||
|
||||
from src.classes.battle import handle_battle_finish
|
||||
|
||||
@@ -57,4 +57,26 @@ class TargetingMixin:
|
||||
avatar.load_decide_result_chain([(action_name, params)], avatar.thinking, "")
|
||||
avatar.commit_next_plan()
|
||||
|
||||
def validate_target_avatar(self, name: str | None) -> tuple["Avatar | None", bool, str]:
|
||||
"""
|
||||
验证目标角色是否有效(存在且存活)。
|
||||
|
||||
Args:
|
||||
name: 目标角色名。
|
||||
|
||||
Returns:
|
||||
(target, can_proceed, reason)
|
||||
- target: 找到的角色对象,无效时为 None。
|
||||
- can_proceed: 是否可以继续。
|
||||
- reason: 失败原因(成功时为空字符串)。
|
||||
"""
|
||||
if not name:
|
||||
return None, False, "缺少目标参数"
|
||||
target = self.find_avatar_by_name(name)
|
||||
if target is None:
|
||||
return None, False, "目标不存在"
|
||||
if target.is_dead:
|
||||
return None, False, "目标已死亡"
|
||||
return target, True, ""
|
||||
|
||||
|
||||
|
||||
@@ -140,6 +140,9 @@ class MutualAction(DefineAction, LLMAction, ActualActionMixin, TargetingMixin):
|
||||
"""
|
||||
检查互动动作能否启动:目标需在发起者的交互范围内。
|
||||
子类通过实现 _can_start 来添加额外检查。
|
||||
|
||||
注意:此方法未使用 TargetingMixin.validate_target_avatar(),
|
||||
因为需要额外检查 target == self.avatar 和调用子类的 _can_start()。
|
||||
"""
|
||||
if target_avatar is None:
|
||||
return False, "缺少参数 target_avatar"
|
||||
@@ -148,6 +151,8 @@ class MutualAction(DefineAction, LLMAction, ActualActionMixin, TargetingMixin):
|
||||
return False, "目标不存在"
|
||||
if target == self.avatar:
|
||||
return False, "不能对自己发起互动"
|
||||
if target.is_dead:
|
||||
return False, "目标已死亡"
|
||||
# 调用子类的额外检查
|
||||
return self._can_start(target)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user