refactor self heal system

This commit is contained in:
bridge
2026-01-08 00:33:41 +08:00
parent b53f428cbb
commit 40d8a0425b
6 changed files with 184 additions and 21 deletions

View File

@@ -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])]

View File

@@ -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恢复效率倍率。

View File

@@ -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)),
}

View File

@@ -40,7 +40,7 @@ protagonist_configs = [
"technique": 33, # 青帝长生诀 (木系至高)
"weapon": 3001, # 青竹蜂云剑 (本命法宝)
"auxiliary": 3003, # 掌天瓶 (催熟万物)
"personas": ["惜命", "心机深沉", "药师"],
"personas": ["惜命", "心机深沉", ""],
"appearance": 15,
}
},

View File

@@ -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}
1 id name exclusion_names desc rarity condition effects
63 61 炼器师 好斗 精通炼器之道,对材料敏锐,擅长铸造法宝。你认为法宝是修行的关键,战斗并非你的专长。 R {extra_cast_success_rate: 0.15}
64 62 情绪化 理性;淡漠 你的情绪波动很大,极易受外界事件影响而改变心情,做事也更随心所欲。 N
65 63 矿工 怠惰 擅长勘探挖掘,对矿脉有着独特的直觉。你认为地下的宝藏才是最实在的财富。 R {extra_mine_materials: 1}
66 64 冒险;鲁莽;好斗 你躲避风险和风头,尽力不担上任何风险。虽然别人议论嘲笑你,但是你只自称“稳健”。 R {extra_self_heal_efficiency: 0.5

View File

@@ -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