diff --git a/src/classes/action/__init__.py b/src/classes/action/__init__.py index f67429b..6001f7e 100644 --- a/src/classes/action/__init__.py +++ b/src/classes/action/__init__.py @@ -31,6 +31,7 @@ from .plunder_mortals import PlunderMortals from .help_mortals import HelpMortals from .talk import Talk from .devour_mortals import DevourMortals +from .self_heal import SelfHeal # 注册到 ActionRegistry(标注是否为实际可执行动作) register_action(actual=False)(Action) @@ -58,6 +59,7 @@ register_action(actual=True)(PlunderMortals) register_action(actual=True)(HelpMortals) register_action(actual=True)(Talk) register_action(actual=True)(DevourMortals) +register_action(actual=True)(SelfHeal) __all__ = [ # 基类 @@ -87,6 +89,7 @@ __all__ = [ "HelpMortals", "Talk", "DevourMortals", + "SelfHeal", ] diff --git a/src/classes/action/battle.py b/src/classes/action/battle.py index 9e42a12..ad5271e 100644 --- a/src/classes/action/battle.py +++ b/src/classes/action/battle.py @@ -21,9 +21,11 @@ class Battle(InstantAction): target = self._get_target(avatar_name) if target is None: return - winner, loser, damage = decide_battle(self.avatar, target) - loser.hp.reduce(damage) - self._last_result = (winner.name, loser.name, damage) + winner, loser, loser_damage, winner_damage = decide_battle(self.avatar, target) + # 应用双方伤害 + loser.hp.reduce(loser_damage) + 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: if avatar_name is None: @@ -42,22 +44,19 @@ class Battle(InstantAction): def finish(self, avatar_name: str) -> list[Event]: res = self._last_result - if isinstance(res, tuple) and len(res) in (2, 3): - winner, loser = res[0], res[1] - damage = res[2] if len(res) == 3 else None - if damage is not None: - result_text = f"{winner} 战胜了 {loser},造成{damage}点伤害" - else: - result_text = f"{winner} 战胜了 {loser}" - result_event = Event(self.world.month_stamp, result_text) + if not (isinstance(res, tuple) and len(res) == 4): + return [] + winner, loser = res[0], res[1] + loser_damage, winner_damage = res[2], res[3] + result_text = f"{winner} 战胜了 {loser},{loser} 受伤{loser_damage}点,{winner} 也受伤{winner_damage}点" + result_event = Event(self.world.month_stamp, result_text) - # 生成战斗小故事:直接复用已生成的事件文本 - target = self._get_target(avatar_name) - avatar_infos = StoryTeller.build_avatar_infos(self.avatar, target) - start_text = getattr(self, "_start_event_content", "") or result_event.content - story = StoryTeller.tell_story(avatar_infos, start_text, result_event.content) - story_event = Event(self.world.month_stamp, story) - return [result_event, story_event] - return [] + # 生成战斗小故事:直接复用已生成的事件文本 + target = self._get_target(avatar_name) + avatar_infos = StoryTeller.build_avatar_infos(self.avatar, target) + start_text = getattr(self, "_start_event_content", "") or result_event.content + story = StoryTeller.tell_story(avatar_infos, start_text, result_event.content) + story_event = Event(self.world.month_stamp, story) + return [result_event, story_event] diff --git a/src/classes/action/self_heal.py b/src/classes/action/self_heal.py new file mode 100644 index 0000000..b0f0d75 --- /dev/null +++ b/src/classes/action/self_heal.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from src.classes.action import TimedAction +from src.classes.event import Event +from src.classes.sect_region import SectRegion + + +class SelfHeal(TimedAction): + """ + 在宗门总部静养疗伤(仅宗门弟子可用,且必须位于自身宗门总部)。 + 单月动作,执行后HP直接回满。 + """ + + COMMENT = "在宗门总部静养疗伤(单月回满HP)" + DOABLES_REQUIREMENTS = "自己是宗门弟子,且位于本宗门总部区域,且当前HP未满" + PARAMS = {} + + # 单月动作 + duration_months = 1 + + def _execute(self) -> None: + # 单月直接回满HP + hp_obj = self.avatar.hp + delta = int(max(0, hp_obj.max - hp_obj.cur)) + if delta > 0: + hp_obj.recover(delta) + self._healed_total = delta + + def _is_in_own_sect_headquarter(self) -> bool: + sect = getattr(self.avatar, "sect", None) + if sect is None: + return False + tile = getattr(self.avatar, "tile", None) + region = getattr(tile, "region", None) + if not isinstance(region, SectRegion): + return False + 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: + # 必须是宗门弟子且在自身宗门总部,且当前HP未满 + if getattr(self.avatar, "sect", None) is None: + return False + if not self._is_in_own_sect_headquarter(): + return False + hp_obj = getattr(self.avatar, "hp", None) + if hp_obj is None: + return False + return hp_obj.cur < hp_obj.max + + def start(self) -> Event: + region = getattr(getattr(self.avatar, "tile", None), "region", None) + region_name = getattr(region, "name", "宗门总部") + # 重置累计量 + self._healed_total = 0 + return Event(self.world.month_stamp, f"{self.avatar.name} 在 {region_name} 开始静养疗伤") + + # TimedAction 已统一 step 逻辑 + + def finish(self) -> list[Event]: + healed_total = int(getattr(self, "_healed_total", 0)) + # 统一用一次事件简要反馈 + return [Event(self.world.month_stamp, f"{self.avatar.name} 疗伤完成,HP已回满(本次恢复{healed_total}点,当前HP {self.avatar.hp})")] + + diff --git a/src/classes/battle.py b/src/classes/battle.py index 7c11283..71b21ef 100644 --- a/src/classes/battle.py +++ b/src/classes/battle.py @@ -49,10 +49,13 @@ def calc_win_rate(attacker: "Avatar", defender: "Avatar") -> float: return max(0.1, min(0.9, base)) -def decide_battle(attacker: "Avatar", defender: "Avatar") -> Tuple["Avatar", "Avatar", int]: +def decide_battle(attacker: "Avatar", defender: "Avatar") -> Tuple["Avatar", "Avatar", int, int]: """ - 结算一场战斗,返回(胜者, 败者, 伤害值)。 - 伤害值根据胜负双方境界差距给出,范围约 [30, 80]。 + 结算一场战斗,返回(胜者, 败者, 败者掉血, 赢家掉血)。 + 规则: + - 先按 calc_win_rate 判定胜负; + - 以 get_damage 计算基准伤害,再让败者“多掉一点血”(适度上调,例如 +15%); + - 赢家也会受伤,但伤害不超过败者伤害的一半(随机 15%~40% 区间)。 """ p = calc_win_rate(attacker, defender) if random.random() < p: @@ -60,8 +63,16 @@ def decide_battle(attacker: "Avatar", defender: "Avatar") -> Tuple["Avatar", "Av else: winner, loser = defender, attacker - damage = get_damage(winner, loser) - return winner, loser, damage + base_damage = get_damage(winner, loser) + # 败者多掉一点血:适度上调,保持上限由 HP.reduce 自然处理 + loser_damage = max(1, int(base_damage * 1.15)) + + # 赢家也掉血,但不超过败者的一半:在 15%~40% 的范围取随机值 + rnd_ratio = random.uniform(0.15, 0.40) + winner_damage = int(loser_damage * rnd_ratio) + winner_damage = max(0, min(winner_damage, loser_damage // 2)) + + return winner, loser, loser_damage, winner_damage def get_escape_success_rate(attacker: "Avatar", defender: "Avatar") -> float: """ diff --git a/src/classes/map.py b/src/classes/map.py index 51d42b8..1755750 100644 --- a/src/classes/map.py +++ b/src/classes/map.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, Optional from src.classes.tile import Tile, TileType +from src.classes.sect_region import SectRegion if TYPE_CHECKING: from src.classes.region import Region @@ -34,6 +35,9 @@ class Map(): self.cultivate_region_names = cultivate_regions_by_name self.city_regions = city_regions_by_id self.city_region_names = city_regions_by_name + # 宗门总部区域集合(由地图生成阶段注入) + # 若外部未注入 SectRegion,这里仍可通过 regions 过滤得到 + self.sect_regions = {rid: r for rid, r in self.regions.items() if isinstance(r, SectRegion)} def is_in_bounds(self, x: int, y: int) -> bool: """ @@ -95,6 +99,12 @@ class Map(): # 城市区域 parts.append("城市区域(可以交易):") parts.extend([f"- {str(region)}" for region in self.city_regions.values()]) + parts.append("") + + # 宗门总部区域 + if getattr(self, "sect_regions", None): + parts.append("宗门总部:") + parts.extend([f"- {region.name} - {region.desc}" for region in self.sect_regions.values()]) return "\n".join(parts)