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;