diff --git a/src/classes/action/self_heal.py b/src/classes/action/self_heal.py index 7bc8f1b..2393cdd 100644 --- a/src/classes/action/self_heal.py +++ b/src/classes/action/self_heal.py @@ -7,26 +7,53 @@ from src.classes.sect_region import SectRegion class SelfHeal(TimedAction): """ - 在宗门总部静养疗伤(仅宗门弟子可用,且必须位于自身宗门总部)。 - 单月动作,执行后HP直接回满。 + 静养疗伤。 + 单月动作。非宗门总部恢复一定比例HP,在宗门总部则回满HP。 """ ACTION_NAME = "疗伤" EMOJI = "💚" - DESC = "在宗门总部静养疗伤,回满HP" - DOABLES_REQUIREMENTS = "自己是宗门弟子,且位于本宗门总部区域,且当前HP未满" + DESC = "运功疗伤,宗门总部可完全恢复" + 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 + + # 基础回复比例 (10%) + base_ratio = 0.1 + + # 特质/效果加成 + # extra_self_heal_efficiency 为小数,例如 0.5 代表 +50% 效率 + effect_bonus = float(self.avatar.effects.get("extra_self_heal_efficiency", 0.0)) + + # 地点加成 + # 宗门总部:直接回满 (覆盖基础值,视为极大加成) + is_hq = self._is_in_own_sect_headquarter() + + if is_hq: + # 宗门总部:直接回满 + heal_amount = max(0, hp_obj.max - hp_obj.cur) + else: + # 普通区域:基础 + 加成 + # 计算总比例:基础 * (1 + 效率加成) + total_ratio = base_ratio * (1.0 + effect_bonus) + heal_amount = int(hp_obj.max * total_ratio) + + # 确保不溢出且至少为1(如果HP不满) + heal_amount = min(heal_amount, hp_obj.max - hp_obj.cur) + if hp_obj.cur < hp_obj.max: + heal_amount = max(1, heal_amount) + else: + heal_amount = 0 + + if heal_amount > 0: + hp_obj.recover(heal_amount) + + self._healed_total = heal_amount def _is_in_own_sect_headquarter(self) -> bool: sect = getattr(self.avatar, "sect", None) @@ -40,11 +67,8 @@ class SelfHeal(TimedAction): return bool(hq_name) and region and region.name == hq_name def can_start(self) -> tuple[bool, str]: - # 必须是宗门弟子且在自身宗门总部,且当前HP未满 - if getattr(self.avatar, "sect", None) is None: - return False, "仅宗门弟子可用" - if not self._is_in_own_sect_headquarter(): - return False, "需要位于自身宗门总部" + # 任何人任何地方都可疗伤,只要HP未满 + hp_obj = getattr(self.avatar, "hp", None) if hp_obj is None: return False, "缺少HP信息" @@ -54,7 +78,7 @@ class SelfHeal(TimedAction): def start(self) -> Event: region = getattr(getattr(self.avatar, "tile", None), "region", None) - region_name = getattr(region, "name", "宗门总部") + region_name = getattr(region, "name", "荒郊野外") # 重置累计量 self._healed_total = 0 return Event(self.world.month_stamp, f"{self.avatar.name} 在 {region_name} 开始静养疗伤", related_avatars=[self.avatar.id]) @@ -64,6 +88,4 @@ class SelfHeal(TimedAction): async 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})", related_avatars=[self.avatar.id])] - - + return [Event(self.world.month_stamp, f"{self.avatar.name} 疗伤完成(本次恢复{healed_total}点,当前HP {self.avatar.hp})", related_avatars=[self.avatar.id])] diff --git a/src/classes/effect/consts.py b/src/classes/effect/consts.py index bcb5cfe..414f9b9 100644 --- a/src/classes/effect/consts.py +++ b/src/classes/effect/consts.py @@ -280,7 +280,7 @@ EXTRA_MAX_LIFESPAN = "extra_max_lifespan" EXTRA_HP_RECOVERY_RATE = "extra_hp_recovery_rate" """ -额外HP恢复速率 +额外HP恢复速率。同时影响动作SelfHeal和Simulator中的自然回复。 类型: float 结算: src/classes/action/self_heal.py 说明: 疗伤时的HP恢复效率倍率。 diff --git a/src/classes/map.py b/src/classes/map.py index 37cb99d..7fe7b95 100644 --- a/src/classes/map.py +++ b/src/classes/map.py @@ -113,5 +113,5 @@ class Map(): "修炼区域(可以修炼以增进修为)": build_regions_info(filter_regions(CultivateRegion)), "普通区域(可以狩猎、采集、挖矿)": build_regions_info(filter_regions(NormalRegion)), "城市区域(可以交易)": build_regions_info(filter_regions(CityRegion)), - "宗门总部(宗门弟子可在此进行疗伤等操作)": build_regions_info(filter_regions(SectRegion)), + "宗门总部(宗门弟子在此疗伤事半功倍)": build_regions_info(filter_regions(SectRegion)), } diff --git a/src/utils/protagonist.py b/src/utils/protagonist.py index b07964d..59a5739 100644 --- a/src/utils/protagonist.py +++ b/src/utils/protagonist.py @@ -40,7 +40,7 @@ protagonist_configs = [ "technique": 33, # 青帝长生诀 (木系至高) "weapon": 3001, # 青竹蜂云剑 (本命法宝) "auxiliary": 3003, # 掌天瓶 (催熟万物) - "personas": ["惜命", "心机深沉", "药师"], + "personas": ["惜命", "心机深沉", "苟"], "appearance": 15, } }, diff --git a/static/game_configs/persona.csv b/static/game_configs/persona.csv index 4b7f005..785fdbd 100644 --- a/static/game_configs/persona.csv +++ b/static/game_configs/persona.csv @@ -63,3 +63,4 @@ id,name,exclusion_names,desc,rarity,condition,effects 61,炼器师,好斗,精通炼器之道,对材料敏锐,擅长铸造法宝。你认为法宝是修行的关键,战斗并非你的专长。,R,,{extra_cast_success_rate: 0.15} 62,情绪化,理性;淡漠,你的情绪波动很大,极易受外界事件影响而改变心情,做事也更随心所欲。,N,, 63,矿工,怠惰,擅长勘探挖掘,对矿脉有着独特的直觉。你认为地下的宝藏才是最实在的财富。,R,,{extra_mine_materials: 1} +64,苟,冒险;鲁莽;好斗,你躲避风险和风头,尽力不担上任何风险。虽然别人议论嘲笑你,但是你只自称“稳健”。,R,,{extra_self_heal_efficiency: 0.5, extra_escape_success_rate: 0.1} diff --git a/tests/test_action_self_heal.py b/tests/test_action_self_heal.py new file mode 100644 index 0000000..1fea1f4 --- /dev/null +++ b/tests/test_action_self_heal.py @@ -0,0 +1,140 @@ +import pytest +from unittest.mock import MagicMock, patch + +from src.classes.action.self_heal import SelfHeal +from src.classes.sect_region import SectRegion +from src.classes.region import NormalRegion +from src.classes.tile import Tile, TileType +from src.classes.sect import Sect +from src.classes.hp import HP + +class TestSelfHealAction: + + @pytest.fixture + def healing_avatar(self, dummy_avatar): + """ + 基于 dummy_avatar 扩展, + 设置 HP 为半血,以便可以进行疗伤。 + """ + dummy_avatar.hp = HP(100, 50) # 50/100 HP + # effects 是 property,无法直接赋值,需要 mock 或者通过 effects mixin 覆盖 + # 这里 dummy_avatar 使用了 Real Avatar 类,所以 effects property 会去读 self._effects 或者计算 + return dummy_avatar + + @pytest.fixture + def sect_region(self): + return SectRegion(id=999, name="青云门总部", desc="测试宗门总部") + + @pytest.fixture + def normal_region(self): + return NormalRegion(id=101, name="荒野", desc="测试荒野") + + @pytest.fixture + def mock_sect(self, sect_region): + sect = MagicMock(spec=Sect) + sect.name = "青云门" + # 确保 headquarter.name 和 region.name 一致 + sect.headquarter = MagicMock() + sect.headquarter.name = sect_region.name + return sect + + def test_can_start_basic(self, healing_avatar): + """测试基本启动条件:HP不满即可""" + # Action 类通常需要 (avatar, world) 参数 + action = SelfHeal(healing_avatar, healing_avatar.world) + can, reason = action.can_start() + assert can is True + assert reason == "" + + def test_cannot_start_full_hp(self, healing_avatar): + """测试满血不能启动""" + healing_avatar.hp.cur = 100 + action = SelfHeal(healing_avatar, healing_avatar.world) + can, reason = action.can_start() + assert can is False + assert "HP已满" in reason + + def test_execute_in_wild_no_bonus(self, healing_avatar, normal_region): + """测试在野外(非宗门)的基础回复(10%)""" + # 设置位置 + healing_avatar.tile = Tile(0, 0, TileType.PLAIN) + healing_avatar.tile.region = normal_region + healing_avatar.sect = None # 散修 + + # Mock effects 为空 + with patch.object(type(healing_avatar), 'effects', new_callable=lambda: {}) as mock_effects: + action = SelfHeal(healing_avatar, healing_avatar.world) + action._execute() + + # 预期:基础回复 10% * 100 = 10 + # 初始 50 -> 60 + assert healing_avatar.hp.cur == 60 + assert action._healed_total == 10 + + def test_execute_in_wild_with_persona_bonus(self, healing_avatar, normal_region): + """测试在野外带有 '苟' 特质加成(+50% efficiency)""" + # 设置位置 + healing_avatar.tile = Tile(0, 0, TileType.PLAIN) + healing_avatar.tile.region = normal_region + + # Mock effects 带有加成 + with patch.object(type(healing_avatar), 'effects', new_callable=lambda: {"extra_self_heal_efficiency": 0.5}): + action = SelfHeal(healing_avatar, healing_avatar.world) + action._execute() + + # 预期:基础 0.1 * (1 + 0.5) = 0.15 + # 回复 15 点 -> 50 + 15 = 65 + assert healing_avatar.hp.cur == 65 + assert action._healed_total == 15 + + def test_execute_in_sect_hq_as_member(self, healing_avatar, sect_region, mock_sect): + """测试宗门弟子在总部回复(直接回满)""" + # 设置位置 + # TileType.SECT 可能不存在,检查源码通常用 TileType.CITY 或 PLAIN,关键是 region + # 如果需要区分 TileType,请检查 src/classes/tile.py,这里先用 PLAIN 并确保 region 是 SectRegion + # 不过为了保险,我们可以查看 TileType 定义。 + # 暂时用 PLAIN,关键是 region 类型。 + healing_avatar.tile = Tile(0, 0, TileType.PLAIN) + healing_avatar.tile.region = sect_region + + # 设置宗门身份 + healing_avatar.sect = mock_sect + + with patch.object(type(healing_avatar), 'effects', new_callable=lambda: {}): + action = SelfHeal(healing_avatar, healing_avatar.world) + action._execute() + + # 预期:直接回满 -> 100 + assert healing_avatar.hp.cur == 100 + assert action._healed_total == 50 + + def test_execute_in_sect_hq_not_member(self, healing_avatar, sect_region): + """测试非本门弟子在某宗门总部(视为普通区域回复)""" + # 设置位置 + healing_avatar.tile = Tile(0, 0, TileType.PLAIN) + healing_avatar.tile.region = sect_region + + # 散修(或无匹配宗门) + healing_avatar.sect = None + + with patch.object(type(healing_avatar), 'effects', new_callable=lambda: {}): + action = SelfHeal(healing_avatar, healing_avatar.world) + action._execute() + + # 预期:基础回复 10% = 10 + assert healing_avatar.hp.cur == 60 + assert action._healed_total == 10 + + def test_heal_overflow_clamp(self, healing_avatar, normal_region): + """测试回复溢出处理(不超过 MaxHP)""" + healing_avatar.hp.cur = 95 # 只差5点 + healing_avatar.tile = Tile(0, 0, TileType.PLAIN) + healing_avatar.tile.region = normal_region + + with patch.object(type(healing_avatar), 'effects', new_callable=lambda: {}): + action = SelfHeal(healing_avatar, healing_avatar.world) + action._execute() + + # 预期:基础回复 10点,但只缺5点 -> 回复5点,当前100 + assert healing_avatar.hp.cur == 100 + assert action._healed_total == 5