diff --git a/src/classes/avatar/info_presenter.py b/src/classes/avatar/info_presenter.py index 3ed1aae..97aa9fc 100644 --- a/src/classes/avatar/info_presenter.py +++ b/src/classes/avatar/info_presenter.py @@ -120,6 +120,7 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict: "hp": {"cur": avatar.hp.cur, "max": avatar.hp.max}, "alignment": str(avatar.alignment) if avatar.alignment else "未知", "magic_stone": avatar.magic_stone.value, + "base_battle_strength": int(get_base_strength(avatar)), "thinking": avatar.thinking, "short_term_objective": avatar.short_term_objective, "long_term_objective": avatar.long_term_objective.content if avatar.long_term_objective else "", diff --git a/src/classes/battle.py b/src/classes/battle.py index 3e435d5..434264b 100644 --- a/src/classes/battle.py +++ b/src/classes/battle.py @@ -12,7 +12,20 @@ if TYPE_CHECKING: # 战斗力参数(参考文明6思想,但适配本项目数值体系) -_STRENGTH_LOG_SCALE: float = 10.0 # 修为强度的对数缩放:10×ln(1+level) +# 境界基础战力 +_REALM_BASE_STRENGTH = { + "Qi_Refinement": 10.0, + "Foundation_Establishment": 20.0, + "Core_Formation": 30.0, + "Nascent_Soul": 40.0, +} +# 小境界(阶段)额外加成 +_STAGE_BONUS_STRENGTH = { + "Early_Stage": 0.0, + "Middle_Stage": 2.5, + "Late_Stage": 5.0, +} + _SUPPRESSION_POINTS: float = 3.0 # 属性克制即加固定战斗力点数 _CIV6_K: float = 0.04 # 伤害指数系数:e^(K×差值) _WIN_BETA: float = 0.15 # 胜率逻辑函数斜率 @@ -26,11 +39,18 @@ _PAIR_BIAS: float = 1.1 # 成对偏置:让败者再多一 def get_base_strength(self_avatar: "Avatar") -> float: """ - 基础战斗力:与对手无关。 - = 10×ln(1+修为等级) + 额外效果点数 + 基础战斗力(重构版): + = 境界基准值 + 小境界加成 + 额外效果 """ - level = max(1, self_avatar.cultivation_progress.level) - strength_from_level = _STRENGTH_LOG_SCALE * math.log1p(level) + # 1. 获取当前境界的基础值 + realm_name = self_avatar.cultivation_progress.realm.name + base_val = _REALM_BASE_STRENGTH.get(realm_name, 10.0) + + # 2. 获取小境界(阶段)加成 + stage_name = self_avatar.cultivation_progress.stage.name + stage_bonus = _STAGE_BONUS_STRENGTH.get(stage_name, 0.0) + + strength_from_level = base_val + stage_bonus # 来自效果的额外战斗力点数(例如功法、法宝带来的被动加成) extra_raw = self_avatar.effects.get("extra_battle_strength_points", 0) @@ -40,7 +60,7 @@ def get_base_strength(self_avatar: "Avatar") -> float: def _combat_strength_vs(opponent: "Avatar", self_avatar: "Avatar") -> float: """ - 相对战斗力:= 基础战斗力 + 克制点数(若克制则+3) + 境界压制点数 + 相对战斗力:= 基础战斗力 + 克制点数(若克制则+3) """ base = get_base_strength(self_avatar) @@ -50,29 +70,7 @@ def _combat_strength_vs(opponent: "Avatar", self_avatar: "Avatar") -> float: if get_suppression_bonus(self_avatar.technique.attribute, opponent.technique.attribute) > 0.0: suppression_points = _SUPPRESSION_POINTS - # 境界压制加成 - realm_bonus_points = 0.0 - realm_suppression_bonus_raw = self_avatar.effects.get("realm_suppression_bonus", 0.0) - if realm_suppression_bonus_raw: - realm_suppression_bonus = float(realm_suppression_bonus_raw or 0.0) - # 计算境界差(大境界) - from src.classes.cultivation import Realm - realm_order = { - Realm.Qi_Refinement: 1, - Realm.Foundation_Establishment: 2, - Realm.Core_Formation: 3, - Realm.Nascent_Soul: 4, - } - self_realm_rank = realm_order.get(self_avatar.cultivation_progress.realm, 1) - opponent_realm_rank = realm_order.get(opponent.cultivation_progress.realm, 1) - realm_diff = self_realm_rank - opponent_realm_rank - - # 如果境界更高,则获得加成 - if realm_diff > 0: - # 按基础战斗力的百分比计算加成点数 - realm_bonus_points = base * realm_suppression_bonus * realm_diff - - return base + suppression_points + realm_bonus_points + return base + suppression_points def _strength_diff(attacker: "Avatar", defender: "Avatar") -> float: diff --git a/tests/test_battle.py b/tests/test_battle.py new file mode 100644 index 0000000..c344f64 --- /dev/null +++ b/tests/test_battle.py @@ -0,0 +1,150 @@ +import pytest +import math +from unittest.mock import MagicMock +from src.classes.battle import ( + get_base_strength, + _combat_strength_vs, + _strength_diff, + calc_win_rate, + _REALM_BASE_STRENGTH, + _STAGE_BONUS_STRENGTH, + _SUPPRESSION_POINTS +) +from src.classes.cultivation import Realm, Stage +from src.classes.technique import TechniqueAttribute + +# Helper to create a mock avatar +def create_mock_avatar(level, realm=None, stage=None, effects=None, technique_attr=None): + avatar = MagicMock() + + # Setup cultivation progress + cp = MagicMock() + cp.level = level + + # Setup Realm Enum Mock or Real Enum + if realm: + cp.realm = realm + else: + # Fallback to Qi Refinement if not specified + cp.realm = Realm.Qi_Refinement + + if stage: + cp.stage = stage + else: + # Fallback to Early Stage + cp.stage = Stage.Early_Stage + + avatar.cultivation_progress = cp + + # Setup effects + avatar.effects = effects or {} + + # Setup technique + if technique_attr: + tech = MagicMock() + tech.attribute = technique_attr + avatar.technique = tech + else: + avatar.technique = None + + return avatar + +class TestBattleStrength: + def test_base_strength_qi_early_min(self): + # 练气前期 1级 + # Base: 10, Stage: 0 + avatar = create_mock_avatar(1, Realm.Qi_Refinement, Stage.Early_Stage) + strength = get_base_strength(avatar) + expected = 10.0 + 0.0 + assert strength == expected + + def test_base_strength_qi_late_max(self): + # 练气后期 30级 + # Base: 10, Stage: 5 + avatar = create_mock_avatar(30, Realm.Qi_Refinement, Stage.Late_Stage) + strength = get_base_strength(avatar) + expected = 10.0 + 5.0 + assert strength == pytest.approx(expected) + + def test_base_strength_foundation_early_min(self): + # 筑基前期 31级 + # Base: 20, Stage: 0 + avatar = create_mock_avatar(31, Realm.Foundation_Establishment, Stage.Early_Stage) + strength = get_base_strength(avatar) + expected = 20.0 + 0.0 + assert strength == expected + + def test_base_strength_nascent_middle(self): + # 元婴中期 105级 + # Base: 40, Stage: 2.5 + avatar = create_mock_avatar(105, Realm.Nascent_Soul, Stage.Middle_Stage) + strength = get_base_strength(avatar) + expected = 40.0 + 2.5 + assert strength == pytest.approx(expected) + + def test_extra_effects(self): + # Test extra strength points from effects + avatar = create_mock_avatar(1, Realm.Qi_Refinement, Stage.Early_Stage, effects={"extra_battle_strength_points": 5.0}) + strength = get_base_strength(avatar) + assert strength == 15.0 + +class TestCombatMechanics: + def test_realm_gap_win_rate(self): + # 筑基前期 vs 练气巅峰 + # 筑基前期: 20.0 + # 练气巅峰: 15.0 (10 + 5) + # Diff: 5.0 + p1 = create_mock_avatar(31, Realm.Foundation_Establishment, Stage.Early_Stage) + p2 = create_mock_avatar(30, Realm.Qi_Refinement, Stage.Late_Stage) + + # Win rate check + # p = 1 / (1 + exp(-0.15 * 5.0)) = 1 / (1 + exp(-0.75)) = 1 / (1 + 0.472) = 1 / 1.472 = 0.679 + rate = calc_win_rate(p1, p2) + assert rate > 0.67 + assert rate < 0.69 + + def test_massive_gap_win_rate(self): + # 元婴 vs 练气 + # 元婴: 40+ + # 练气: 10+ + # Diff > 20 -> should be close to max win rate + p1 = create_mock_avatar(91, Realm.Nascent_Soul, Stage.Early_Stage) + p2 = create_mock_avatar(1, Realm.Qi_Refinement, Stage.Early_Stage) + + rate = calc_win_rate(p1, p2) + # With cap at 0.99, but actually calculation might be slightly below 0.99 if diff isn't huge enough + # Diff = 30, p = 1/(1+exp(-4.5)) = 0.989 + assert rate > 0.98 + + def test_technique_suppression(self): + # Test attribute suppression bonus (Metal > Wood) + # GOLD suppresses WOOD + p1 = create_mock_avatar(10, Realm.Qi_Refinement, Stage.Early_Stage, technique_attr=TechniqueAttribute.GOLD) + p2 = create_mock_avatar(10, Realm.Qi_Refinement, Stage.Early_Stage, technique_attr=TechniqueAttribute.WOOD) + + # Base strengths are equal (same level/realm/stage) + # P1 attacks P2: Gold vs Wood -> Bonus + s1 = _combat_strength_vs(p2, p1) + + # P2 attacks P1: Wood vs Gold -> No Bonus + s2 = _combat_strength_vs(p1, p2) + + base = get_base_strength(p1) + + assert s1 == base + _SUPPRESSION_POINTS + assert s2 == base + + diff = s1 - s2 + assert diff == _SUPPRESSION_POINTS + + def test_intra_stage_diff(self): + # Test same stage same strength + # Level 1 vs Level 10 (Early Stage) + # Diff = 0 + p1 = create_mock_avatar(10, Realm.Qi_Refinement, Stage.Early_Stage) + p2 = create_mock_avatar(1, Realm.Qi_Refinement, Stage.Early_Stage) + + diff = _strength_diff(p1, p2) + expected_diff = 0.0 + assert diff == pytest.approx(expected_diff) + diff --git a/web/src/components/game/panels/info/AvatarDetail.vue b/web/src/components/game/panels/info/AvatarDetail.vue index 32aed44..65f8d80 100644 --- a/web/src/components/game/panels/info/AvatarDetail.vue +++ b/web/src/components/game/panels/info/AvatarDetail.vue @@ -108,6 +108,7 @@ async function handleClearObjective() { /> + diff --git a/web/src/types/core.ts b/web/src/types/core.ts index 14dd81c..995b085 100644 --- a/web/src/types/core.ts +++ b/web/src/types/core.ts @@ -62,6 +62,7 @@ export interface AvatarDetail extends EntityBase { level: number; hp: { cur: number; max: number }; magic_stone: number; + base_battle_strength: number; // 属性与资质 alignment: string;