311 lines
12 KiB
Python
311 lines
12 KiB
Python
from __future__ import annotations
|
||
|
||
import math
|
||
import random
|
||
from typing import Tuple, TYPE_CHECKING, Callable, Awaitable, Optional
|
||
|
||
from src.classes.technique import TechniqueGrade, get_suppression_bonus
|
||
|
||
if TYPE_CHECKING:
|
||
from src.classes.avatar import Avatar
|
||
from src.classes.event import Event
|
||
|
||
|
||
# 战斗力参数(参考文明6思想,但适配本项目数值体系)
|
||
_STRENGTH_LOG_SCALE: float = 10.0 # 修为强度的对数缩放:10×ln(1+level)
|
||
_SUPPRESSION_POINTS: float = 3.0 # 属性克制即加固定战斗力点数
|
||
_CIV6_K: float = 0.04 # 伤害指数系数:e^(K×差值)
|
||
_WIN_BETA: float = 0.15 # 胜率逻辑函数斜率
|
||
_MIN_WIN_RATE: float = 0.01 # 最小胜率
|
||
_MAX_WIN_RATE: float = 0.99 # 最大胜率
|
||
_BASE_DAMAGE_LOW: int = 24 # 基础伤害下限(按 defender.maxHP/100 缩放)
|
||
_BASE_DAMAGE_HIGH: int = 36 # 基础伤害上限(按 defender.maxHP/100 缩放)
|
||
_MIN_RATIO: float = 1.05 # 最小相对优势比,确保赢家伤害严格更低
|
||
_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)
|
||
|
||
# 来自效果的额外战斗力点数(例如功法、法宝带来的被动加成)
|
||
extra_raw = self_avatar.effects.get("extra_battle_strength_points", 0)
|
||
extra_points = float(extra_raw or 0.0)
|
||
return strength_from_level + extra_points
|
||
|
||
|
||
def _combat_strength_vs(opponent: "Avatar", self_avatar: "Avatar") -> float:
|
||
"""
|
||
相对战斗力:= 基础战斗力 + 克制点数(若克制则+3) + 境界压制点数
|
||
"""
|
||
base = get_base_strength(self_avatar)
|
||
|
||
# 属性克制加成
|
||
suppression_points = 0.0
|
||
if self_avatar.technique is not None and opponent.technique is not None:
|
||
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
|
||
|
||
|
||
def _strength_diff(attacker: "Avatar", defender: "Avatar") -> float:
|
||
return _combat_strength_vs(defender, attacker) - _combat_strength_vs(attacker, defender)
|
||
|
||
|
||
def get_effective_strength(self_avatar: "Avatar", opponent: "Avatar") -> float:
|
||
"""
|
||
对外公开:返回 self_avatar 面对 opponent 时的折算战斗力。
|
||
用于事件展示与调试,不参与状态修改。
|
||
"""
|
||
return _combat_strength_vs(opponent, self_avatar)
|
||
|
||
|
||
def get_effective_strength_pair(a: "Avatar", b: "Avatar") -> tuple[float, float]:
|
||
"""
|
||
一次性返回双方(a 面对 b,b 面对 a)的折算战斗力。
|
||
顺序:(a_strength, b_strength)
|
||
"""
|
||
return _combat_strength_vs(b, a), _combat_strength_vs(a, b)
|
||
|
||
|
||
def calc_win_rate(attacker: "Avatar", defender: "Avatar") -> float:
|
||
"""
|
||
胜率 = sigmoid(β×战斗力差),并夹紧到 [0.1, 0.9]
|
||
- 战斗力差 = 有效战斗力(att) - 有效战斗力(def)
|
||
- β 默认 0.15,使差值≈10时胜率≈0.82
|
||
"""
|
||
diff = _strength_diff(attacker, defender)
|
||
p = 1.0 / (1.0 + math.exp(-_WIN_BETA * diff))
|
||
if p < _MIN_WIN_RATE:
|
||
return _MIN_WIN_RATE
|
||
if p > _MAX_WIN_RATE:
|
||
return _MAX_WIN_RATE
|
||
return p
|
||
|
||
|
||
def _base_damage_scale(defender: "Avatar") -> float:
|
||
# 以 100 HP 为基准,将 Civ6 的 24~36 损伤映射到不同境界的 HP 档位
|
||
max_hp = defender.hp.max
|
||
return max(1.0, max_hp / 100.0)
|
||
|
||
def _damage_pair(winner: "Avatar", loser: "Avatar") -> tuple[int, int]:
|
||
"""
|
||
成对伤害:使用同一基础与对称比值,保证赢家伤害严格小于败者伤害。
|
||
- ratio = max(exp(K×|diff|), MIN_RATIO)
|
||
- 中间尺度 = 几何均值 sqrt(scale_winner × scale_loser)
|
||
- 败者伤害 = base × 中间尺度 × ratio × (1 - loser的伤害减免)
|
||
- 赢家伤害 = base × 中间尺度 ÷ ratio × (1 - winner的伤害减免)
|
||
"""
|
||
abs_diff = abs(_strength_diff(winner, loser))
|
||
ratio = math.exp(_CIV6_K * abs_diff)
|
||
ratio *= _PAIR_BIAS
|
||
if ratio < _MIN_RATIO:
|
||
ratio = _MIN_RATIO
|
||
|
||
base = random.randint(_BASE_DAMAGE_LOW, _BASE_DAMAGE_HIGH)
|
||
scale_w = _base_damage_scale(winner)
|
||
scale_l = _base_damage_scale(loser)
|
||
mid_scale = math.sqrt(scale_w * scale_l)
|
||
|
||
# 计算原始伤害
|
||
loser_damage_raw = base * mid_scale * ratio
|
||
winner_damage_raw = base * mid_scale / ratio
|
||
|
||
# 应用伤害减免效果
|
||
loser_reduction_raw = loser.effects.get("damage_reduction", 0.0)
|
||
loser_reduction = max(0.0, min(1.0, float(loser_reduction_raw or 0.0)))
|
||
winner_reduction_raw = winner.effects.get("damage_reduction", 0.0)
|
||
winner_reduction = max(0.0, min(1.0, float(winner_reduction_raw or 0.0)))
|
||
|
||
loser_damage = max(1, int(loser_damage_raw * (1.0 - loser_reduction)))
|
||
winner_damage = max(1, int(winner_damage_raw * (1.0 - winner_reduction)))
|
||
|
||
return loser_damage, winner_damage
|
||
|
||
|
||
def decide_battle(attacker: "Avatar", defender: "Avatar") -> Tuple["Avatar", "Avatar", int, int]:
|
||
"""
|
||
结算战斗,返回 (胜者, 败者, 败者掉血, 赢家掉血)。
|
||
- 胜率由战斗力差的逻辑函数给出;
|
||
- 双方伤害均按 Civ6 风格由同一差值决定(对称公式),HP 与战斗力独立。
|
||
"""
|
||
p = calc_win_rate(attacker, defender)
|
||
if random.random() < p:
|
||
winner, loser = attacker, defender
|
||
else:
|
||
winner, loser = defender, attacker
|
||
|
||
loser_damage, winner_damage = _damage_pair(winner, loser)
|
||
return winner, loser, loser_damage, winner_damage
|
||
|
||
|
||
def get_escape_success_rate(attacker: "Avatar", defender: "Avatar") -> float:
|
||
"""
|
||
逃跑成功率:defender 试图从 attacker 身边逃离
|
||
基础成功率 0.1,可通过 defender 的 effects 提升
|
||
"""
|
||
base_rate = 0.1
|
||
bonus = float(defender.effects.get("extra_escape_success_rate", 0.0))
|
||
return max(0.0, min(1.0, base_rate + bonus))
|
||
|
||
|
||
def get_assassination_success_rate(attacker: "Avatar", defender: "Avatar") -> float:
|
||
"""
|
||
暗杀成功率:
|
||
- 基础 10%
|
||
- 同境界 10%
|
||
- 每高一个大境界 +5%,每低一个大境界 -5%
|
||
- 范围 [1%, 100%]
|
||
"""
|
||
from src.classes.cultivation import Realm
|
||
realm_order = {
|
||
Realm.Qi_Refinement: 1,
|
||
Realm.Foundation_Establishment: 2,
|
||
Realm.Core_Formation: 3,
|
||
Realm.Nascent_Soul: 4,
|
||
}
|
||
|
||
base_rate = 0.10
|
||
|
||
attacker_rank = realm_order.get(attacker.cultivation_progress.realm, 1)
|
||
defender_rank = realm_order.get(defender.cultivation_progress.realm, 1)
|
||
|
||
diff = attacker_rank - defender_rank
|
||
rate = base_rate + diff * 0.05
|
||
|
||
# 应用额外暗杀成功率加成
|
||
extra = float(attacker.effects.get("extra_assassinate_success_rate", 0.0))
|
||
rate += extra
|
||
|
||
return max(0.01, min(1.0, rate))
|
||
|
||
|
||
async def gen_battle_result_text(
|
||
winner: "Avatar",
|
||
loser: "Avatar",
|
||
l_dmg: int,
|
||
w_dmg: int,
|
||
is_fatal: bool,
|
||
prefix: str = "",
|
||
action_desc: str = "战胜了",
|
||
postfix: str = "",
|
||
check_loot: bool = False
|
||
) -> str:
|
||
"""
|
||
生成标准战斗结果文本。
|
||
"""
|
||
text_prefix = f"{prefix} " if prefix else ""
|
||
if is_fatal:
|
||
text = f"{text_prefix}{winner.name} {action_desc} {loser.name}{postfix},造成 {l_dmg} 点伤害。{loser.name} 遭受重创,当场陨落。"
|
||
if check_loot:
|
||
from src.classes.kill_and_grab import kill_and_grab
|
||
text += await kill_and_grab(winner, loser)
|
||
return text
|
||
else:
|
||
return f"{text_prefix}{winner.name} {action_desc} {loser.name}{postfix},{loser.name} 受伤 {l_dmg} 点,{winner.name} 也受伤 {w_dmg} 点。"
|
||
|
||
|
||
async def handle_battle_finish(
|
||
world,
|
||
attacker: "Avatar",
|
||
target: "Avatar",
|
||
res: Tuple["Avatar", "Avatar", int, int],
|
||
start_event_content: str,
|
||
story_prompt: str,
|
||
outcome_text_func: Optional[Callable[["Avatar", "Avatar", int, int, bool], Awaitable[str]]] = None,
|
||
check_loot: bool = False,
|
||
prefix: str = "",
|
||
action_desc: str = "战胜了",
|
||
postfix: str = ""
|
||
) -> list["Event"]:
|
||
"""
|
||
处理战斗结果的通用逻辑(生成事件、故事、处理死亡)。
|
||
|
||
Args:
|
||
world: 世界对象
|
||
attacker: 发起者
|
||
target: 目标
|
||
res: decide_battle 的结果 (winner, loser, loser_damage, winner_damage)
|
||
start_event_content: 开始事件的内容(用于故事生成)
|
||
story_prompt: 故事生成的提示词
|
||
outcome_text_func: (可选) 异步回调函数,用于生成结果文本。
|
||
如果为 None,则使用标准生成逻辑。
|
||
参数: (winner, loser, loser_damage, winner_damage, is_fatal)
|
||
返回: 结果文本字符串
|
||
check_loot: 是否检查杀人夺宝(仅当 is_fatal 为 True 且使用默认文本生成时生效,或者 outcome_text_func 自己处理)
|
||
prefix: 默认文本生成的前缀(仅当 outcome_text_func 为 None 时生效)
|
||
action_desc: 默认文本生成的动作描述(仅当 outcome_text_func 为 None 时生效)
|
||
postfix: 默认文本生成的后缀(仅当 outcome_text_func 为 None 时生效)
|
||
"""
|
||
from src.classes.event import Event
|
||
from src.classes.story_teller import StoryTeller
|
||
from src.classes.death import handle_death
|
||
from src.classes.death_reason import DeathReason
|
||
|
||
winner, loser, loser_damage, winner_damage = res
|
||
is_fatal = loser.hp <= 0
|
||
|
||
# 生成结果文本
|
||
if outcome_text_func:
|
||
result_text = await outcome_text_func(winner, loser, loser_damage, winner_damage, is_fatal)
|
||
else:
|
||
result_text = await gen_battle_result_text(
|
||
winner, loser, loser_damage, winner_damage, is_fatal,
|
||
prefix=prefix, action_desc=action_desc, postfix=postfix, check_loot=check_loot
|
||
)
|
||
|
||
# 构造事件
|
||
rel_ids = [attacker.id]
|
||
if target:
|
||
rel_ids.append(target.id)
|
||
|
||
result_event = Event(world.month_stamp, result_text, related_avatars=rel_ids, is_major=True)
|
||
|
||
# 确定故事生成的起始文本(如果为空则使用结果文本作为兜底)
|
||
start_content = start_event_content
|
||
if not start_content:
|
||
start_content = result_text
|
||
|
||
# 生成故事
|
||
story = await StoryTeller.tell_story(
|
||
start_content,
|
||
result_event.content,
|
||
attacker,
|
||
target,
|
||
prompt=story_prompt,
|
||
allow_relation_changes=True
|
||
)
|
||
story_event = Event(world.month_stamp, story, related_avatars=rel_ids, is_story=True)
|
||
|
||
# 处理死亡
|
||
if is_fatal:
|
||
handle_death(world, loser, DeathReason.BATTLE)
|
||
return [result_event, story_event]
|