add technique
This commit is contained in:
@@ -30,6 +30,7 @@ from .battle import Battle
|
||||
from .plunder_mortals import PlunderMortals
|
||||
from .help_mortals import HelpMortals
|
||||
from .talk import Talk
|
||||
from .devour_mortals import DevourMortals
|
||||
|
||||
# 注册到 ActionRegistry(标注是否为实际可执行动作)
|
||||
register_action(actual=False)(Action)
|
||||
@@ -56,6 +57,7 @@ register_action(actual=False)(Battle)
|
||||
register_action(actual=True)(PlunderMortals)
|
||||
register_action(actual=True)(HelpMortals)
|
||||
register_action(actual=True)(Talk)
|
||||
register_action(actual=True)(DevourMortals)
|
||||
|
||||
__all__ = [
|
||||
# 基类
|
||||
@@ -84,6 +86,7 @@ __all__ = [
|
||||
"PlunderMortals",
|
||||
"HelpMortals",
|
||||
"Talk",
|
||||
"DevourMortals",
|
||||
]
|
||||
|
||||
|
||||
|
||||
44
src/classes/action/devour_mortals.py
Normal file
44
src/classes/action/devour_mortals.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from src.classes.action import TimedAction
|
||||
from src.classes.event import Event
|
||||
from src.classes.region import CityRegion
|
||||
from src.classes.alignment import Alignment
|
||||
|
||||
|
||||
class DevourMortals(TimedAction):
|
||||
"""
|
||||
吞噬凡人:仅邪阵营可在城市区域执行,获得大量修炼经验。
|
||||
与普通修炼相比,经验获取显著更高。
|
||||
"""
|
||||
|
||||
COMMENT = "在城镇吞噬凡人,获得大量修行经验(邪修)"
|
||||
DOABLES_REQUIREMENTS = "仅限城市区域,且角色阵营为‘邪’,且未处于瓶颈"
|
||||
PARAMS = {}
|
||||
|
||||
duration_months = 2
|
||||
EXP_GAIN = 2000
|
||||
|
||||
def _execute(self) -> None:
|
||||
region = self.avatar.tile.region
|
||||
if not isinstance(region, CityRegion):
|
||||
return
|
||||
if self.avatar.cultivation_progress.is_in_bottleneck():
|
||||
return
|
||||
self.avatar.cultivation_progress.add_exp(self.EXP_GAIN)
|
||||
|
||||
def can_start(self) -> bool:
|
||||
region = self.avatar.tile.region
|
||||
if not isinstance(region, CityRegion):
|
||||
return False
|
||||
if self.avatar.alignment != Alignment.EVIL:
|
||||
return False
|
||||
return not self.avatar.cultivation_progress.is_in_bottleneck()
|
||||
|
||||
def start(self) -> Event:
|
||||
return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇开始吞噬凡人")
|
||||
|
||||
def finish(self) -> list[Event]:
|
||||
return []
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from src.classes.tile import Tile
|
||||
from src.classes.region import Region
|
||||
from src.classes.cultivation import CultivationProgress
|
||||
from src.classes.root import Root
|
||||
from src.classes.technique import Technique, get_random_technique_for_avatar
|
||||
from src.classes.age import Age
|
||||
from src.classes.event import NULL_EVENT, Event
|
||||
from src.classes.typings import ACTION_NAME, ACTION_PARAMS, ACTION_NAME_PARAMS_PAIRS, ACTION_NAME_PARAMS_PAIR
|
||||
@@ -64,6 +65,7 @@ class Avatar:
|
||||
|
||||
root: Root = field(default_factory=lambda: random.choice(list(Root)))
|
||||
personas: List[Persona] = field(default_factory=list)
|
||||
technique: Technique | None = None
|
||||
history_events: List[Event] = field(default_factory=list)
|
||||
_pending_events: List[Event] = field(default_factory=list)
|
||||
current_action: Optional[ActionInstance] = None
|
||||
@@ -97,6 +99,10 @@ class Avatar:
|
||||
if not self.personas:
|
||||
self.personas = get_random_compatible_personas(persona_num, avatar=self)
|
||||
|
||||
# 出生即随机赋予功法(与灵根/阵营/条件兼容)
|
||||
if self.technique is None:
|
||||
self.technique = get_random_technique_for_avatar(self)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
|
||||
@@ -106,7 +112,8 @@ class Avatar:
|
||||
尽量多打一些,因为会用来给LLM进行决策
|
||||
"""
|
||||
personas_str = ", ".join([persona.name for persona in self.personas])
|
||||
return f"Avatar(id={self.id}, 性别={self.gender}, 年龄={self.age}, name={self.name}, 阵营={self.alignment.get_info()}, 区域={self.tile.region.name}, 灵根={str(self.root)}, 境界={self.cultivation_progress}, HP={self.hp}, MP={self.mp}, 个性={personas_str})"
|
||||
technique_str = self.technique.name if self.technique is not None else "无"
|
||||
return f"Avatar(id={self.id}, 性别={self.gender}, 年龄={self.age}, name={self.name}, 阵营={self.alignment.get_info()}, 区域={self.tile.region.name}, 灵根={str(self.root)}, 功法={technique_str}, 境界={self.cultivation_progress}, HP={self.hp}, MP={self.mp}, 个性={personas_str})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.get_info()
|
||||
|
||||
@@ -2,8 +2,10 @@ from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Tuple, TYPE_CHECKING
|
||||
import random
|
||||
|
||||
from src.classes.cultivation import Realm
|
||||
from src.classes.technique import get_suppression_bonus, get_grade_advantage_bonus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
@@ -24,13 +26,26 @@ def _realm_order(realm: Realm) -> int:
|
||||
|
||||
def calc_win_rate(attacker: "Avatar", defender: "Avatar") -> float:
|
||||
"""
|
||||
根据双方境界粗略计算进攻方胜率。
|
||||
基准50%,每高一个大境界+15%,限制在[0.1, 0.9]。
|
||||
胜率计算(返回进攻方胜率 p ∈ [0.1, 0.9]):
|
||||
- 基准:50%
|
||||
- 境界差:每高一大境界 +15%
|
||||
- 功法品阶差:按品阶差的相对加成(可正可负)
|
||||
- 属性克制:若进攻方克制防守方,再 +10%
|
||||
最后夹紧到 [0.1, 0.9]
|
||||
"""
|
||||
atk_order = _realm_order(attacker.cultivation_progress.realm)
|
||||
def_order = _realm_order(defender.cultivation_progress.realm)
|
||||
delta = atk_order - def_order
|
||||
base = 0.5 + 0.15 * delta
|
||||
# 功法品阶差相对加成
|
||||
atk_grade = getattr(getattr(attacker, "technique", None), "grade", None)
|
||||
def_grade = getattr(getattr(defender, "technique", None), "grade", None)
|
||||
base += get_grade_advantage_bonus(atk_grade, def_grade)
|
||||
# 属性克制:若进攻方克制防守方,再+10%
|
||||
atk_attr = getattr(getattr(attacker, "technique", None), "attribute", None)
|
||||
def_attr = getattr(getattr(defender, "technique", None), "attribute", None)
|
||||
if atk_attr is not None and def_attr is not None:
|
||||
base += get_suppression_bonus(atk_attr, def_attr)
|
||||
return max(0.1, min(0.9, base))
|
||||
|
||||
|
||||
@@ -58,8 +73,37 @@ def get_escape_success_rate(attacker: "Avatar", defender: "Avatar") -> float:
|
||||
|
||||
def get_damage(winner: "Avatar", loser: "Avatar") -> int:
|
||||
"""
|
||||
根据胜负双方境界差距估算伤害:基础100,差一大境界+100,上限500。
|
||||
伤害计算(返回单次战斗伤害值,整数):
|
||||
1) 先计算“期望伤害” expected:
|
||||
- 境界差:base = 100 + 80 × gap,其中 gap = max(0, winnerRealmOrder - loserRealmOrder)
|
||||
- 功法品阶差:按品阶差 bonus 调整 expected *= (1 + bonus)
|
||||
- 属性克制:若胜者克制败者,再乘 1.15
|
||||
- 夹紧:期望伤害最终限制在 [30, 500]
|
||||
2) 再生成随机区间:[low, high] = [0.85×expected, 1.15×expected]
|
||||
3) 下限与比例保护:不低于败者最大HP的 10%,并至少为 15 的硬下限
|
||||
4) 返回区间内的随机整数
|
||||
"""
|
||||
gap = max(0, _realm_order(winner.cultivation_progress.realm) - _realm_order(loser.cultivation_progress.realm))
|
||||
# return min(500, 100 + 100 * gap)
|
||||
return 500
|
||||
expected = 100 + 80 * gap
|
||||
win_grade = getattr(getattr(winner, "technique", None), "grade", None)
|
||||
lose_grade = getattr(getattr(loser, "technique", None), "grade", None)
|
||||
expected *= (1.0 + get_grade_advantage_bonus(win_grade, lose_grade))
|
||||
win_attr = getattr(getattr(winner, "technique", None), "attribute", None)
|
||||
lose_attr = getattr(getattr(loser, "technique", None), "attribute", None)
|
||||
if win_attr is not None and lose_attr is not None:
|
||||
if get_suppression_bonus(win_attr, lose_attr) > 0:
|
||||
expected *= 1.15
|
||||
# 期望伤害夹紧
|
||||
expected = max(30.0, min(500.0, expected))
|
||||
|
||||
# 设定伤害区间并随机
|
||||
low = int(expected * 0.85)
|
||||
high = int(expected * 1.15)
|
||||
|
||||
# 与最大HP挂钩的最低保护
|
||||
loser_max_hp = getattr(getattr(loser, "hp", None), "max", 0) or 0
|
||||
hp_floor = int(max(15, loser_max_hp * 0.10))
|
||||
low = max(low, hp_floor)
|
||||
high = max(high, low + 1)
|
||||
|
||||
return random.randint(low, high)
|
||||
@@ -2,15 +2,13 @@
|
||||
灵根
|
||||
五行元素与灵根组合:
|
||||
- RootElement:金、木、水、火、土(恒定不变)
|
||||
- Root:从 CSV 配置加载(单/双/天灵根等),每个成员包含(中文名, 元素列表)
|
||||
- Root:硬编码定义(单/双/天灵根等),每个成员包含(中文名, 元素列表)
|
||||
"""
|
||||
from enum import Enum
|
||||
from typing import List, Tuple, Dict
|
||||
from collections import defaultdict
|
||||
|
||||
from src.classes.essence import EssenceType
|
||||
from src.utils.df import game_configs
|
||||
from src.utils.config import CONFIG
|
||||
|
||||
|
||||
class RootElement(Enum):
|
||||
@@ -44,30 +42,41 @@ class _RootMixin:
|
||||
return f"{self.value}({', '.join(str(e) for e in self.elements)})"
|
||||
|
||||
|
||||
def _build_root_members_from_csv() -> Dict[str, tuple]:
|
||||
class Root(_RootMixin, Enum):
|
||||
"""
|
||||
从 CSV 读取 Root 定义,返回用于创建枚举的 members 映射。
|
||||
CSV 列:id,key,name,element_list,其中 element_list 用分号分隔中文名(如:金;木)。
|
||||
灵根(硬编码):成员值为 (中文名, 元素元组)。
|
||||
数据来源原为 CSV,现改为内置:
|
||||
- GOLD: 金灵根 -> 金
|
||||
- WOOD: 木灵根 -> 木
|
||||
- WATER: 水灵根 -> 水
|
||||
- FIRE: 火灵根 -> 火
|
||||
- EARTH: 土灵根 -> 土
|
||||
- THUNDER: 雷灵根 -> 水;土
|
||||
- ICE: 冰灵根 -> 金;水
|
||||
- WIND: 风灵根 -> 木;水
|
||||
- DARK: 暗灵根 -> 火;土
|
||||
- HEAVEN: 天灵根 -> 金;木;水;火;土(额外突破+0.1)
|
||||
"""
|
||||
df = game_configs.get("root")
|
||||
sep = CONFIG.df.ids_separator
|
||||
members: Dict[str, tuple] = {}
|
||||
for _, row in df.iterrows():
|
||||
key = str(row["key"]).strip()
|
||||
cn_name = str(row["name"]).strip()
|
||||
elements_field = str(row["element_list"]).strip()
|
||||
element_names = [s.strip() for s in elements_field.split(sep) if str(s).strip()]
|
||||
element_members: List[RootElement] = []
|
||||
for en in element_names:
|
||||
element_members.append(RootElement(en))
|
||||
members[key] = (cn_name, tuple(element_members))
|
||||
return members
|
||||
|
||||
|
||||
# 动态创建 Root 枚举(使用 mixin 作为 type,使 __new__ 生效)
|
||||
Root = Enum("Root", _build_root_members_from_csv(), type=_RootMixin)
|
||||
# 某些环境下函数式创建的 Enum 可能未正确采用 mixin 的 __str__,这里显式绑定确保生效
|
||||
Root.__str__ = _RootMixin.__str__
|
||||
GOLD = ("金灵根", (RootElement.GOLD,))
|
||||
WOOD = ("木灵根", (RootElement.WOOD,))
|
||||
WATER = ("水灵根", (RootElement.WATER,))
|
||||
FIRE = ("火灵根", (RootElement.FIRE,))
|
||||
EARTH = ("土灵根", (RootElement.EARTH,))
|
||||
THUNDER = ("雷灵根", (RootElement.WATER, RootElement.EARTH))
|
||||
ICE = ("冰灵根", (RootElement.GOLD, RootElement.WATER))
|
||||
WIND = ("风灵根", (RootElement.WOOD, RootElement.WATER))
|
||||
DARK = ("暗灵根", (RootElement.FIRE, RootElement.EARTH))
|
||||
HEAVEN = (
|
||||
"天灵根",
|
||||
(
|
||||
RootElement.GOLD,
|
||||
RootElement.WOOD,
|
||||
RootElement.WATER,
|
||||
RootElement.FIRE,
|
||||
RootElement.EARTH,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# 元素到灵气类型的一一对应
|
||||
@@ -87,23 +96,10 @@ def get_essence_types_for_root(root: "Root") -> List[EssenceType]:
|
||||
return [_essence_by_element[e] for e in root.elements]
|
||||
|
||||
|
||||
def _load_extra_breakthrough_success_rate_from_csv() -> Dict["Root", float]:
|
||||
"""
|
||||
从 root.csv 载入各灵根的额外突破成功率,默认0。
|
||||
列名:extra_breakthrough_success_rate
|
||||
"""
|
||||
df = game_configs["root"]
|
||||
bonuses: Dict["Root", float] = {}
|
||||
for _, row in df.iterrows():
|
||||
key = str(row["key"]).strip()
|
||||
root_member = getattr(Root, key)
|
||||
bonus = float(row.get("extra_breakthrough_success_rate", 0) or 0)
|
||||
bonuses[root_member] = bonus
|
||||
return bonuses
|
||||
|
||||
|
||||
# 从配置构造带默认值的加成表
|
||||
# 额外突破成功率(默认 0.0),根据原 CSV 保留天灵根 0.1
|
||||
extra_breakthrough_success_rate = defaultdict(
|
||||
lambda: 0.0,
|
||||
_load_extra_breakthrough_success_rate_from_csv(),
|
||||
{
|
||||
Root.HEAVEN: 0.1,
|
||||
},
|
||||
)
|
||||
200
src/classes/technique.py
Normal file
200
src/classes/technique.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional, Dict, List
|
||||
|
||||
from src.utils.df import game_configs
|
||||
from src.classes.alignment import Alignment
|
||||
from src.classes.root import Root, RootElement
|
||||
|
||||
|
||||
class TechniqueAttribute(Enum):
|
||||
GOLD = "金"
|
||||
WOOD = "木"
|
||||
WATER = "水"
|
||||
FIRE = "火"
|
||||
EARTH = "土"
|
||||
ICE = "冰"
|
||||
WIND = "风"
|
||||
DARK = "暗"
|
||||
THUNDER = "雷"
|
||||
EVIL = "邪"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class TechniqueGrade(Enum):
|
||||
LOWER = "下品"
|
||||
MIDDLE = "中品"
|
||||
UPPER = "上品"
|
||||
|
||||
@staticmethod
|
||||
def from_str(s: str) -> "TechniqueGrade":
|
||||
s = str(s).strip()
|
||||
if s == "上品":
|
||||
return TechniqueGrade.UPPER
|
||||
if s == "中品":
|
||||
return TechniqueGrade.MIDDLE
|
||||
return TechniqueGrade.LOWER
|
||||
|
||||
|
||||
@dataclass
|
||||
class Technique:
|
||||
id: int
|
||||
name: str
|
||||
attribute: TechniqueAttribute
|
||||
grade: TechniqueGrade
|
||||
prompt: str
|
||||
weight: float
|
||||
condition: str
|
||||
|
||||
def is_allowed_for(self, avatar) -> bool:
|
||||
if not self.condition:
|
||||
return True
|
||||
return bool(eval(self.condition, {"__builtins__": {}}, {"avatar": avatar, "Alignment": Alignment}))
|
||||
|
||||
|
||||
# 五行与扩展属性的克制关系
|
||||
# - 五行:金克木,木克土,土克水,水克火,火克金
|
||||
# - 雷克邪;邪、冰、风、暗不克任何人
|
||||
SUPPRESSION: dict[TechniqueAttribute, set[TechniqueAttribute]] = {
|
||||
TechniqueAttribute.GOLD: {TechniqueAttribute.WOOD},
|
||||
TechniqueAttribute.WOOD: {TechniqueAttribute.EARTH},
|
||||
TechniqueAttribute.EARTH: {TechniqueAttribute.WATER},
|
||||
TechniqueAttribute.WATER: {TechniqueAttribute.FIRE},
|
||||
TechniqueAttribute.FIRE: {TechniqueAttribute.GOLD},
|
||||
TechniqueAttribute.THUNDER: {TechniqueAttribute.EVIL},
|
||||
TechniqueAttribute.ICE: set(),
|
||||
TechniqueAttribute.WIND: set(),
|
||||
TechniqueAttribute.DARK: set(),
|
||||
TechniqueAttribute.EVIL: set(),
|
||||
}
|
||||
|
||||
|
||||
def loads() -> tuple[dict[int, Technique], dict[str, Technique]]:
|
||||
techniques_by_id: dict[int, Technique] = {}
|
||||
techniques_by_name: dict[str, Technique] = {}
|
||||
df = game_configs["technique"]
|
||||
for _, row in df.iterrows():
|
||||
attr = TechniqueAttribute(str(row["technique_root"]).strip())
|
||||
name = str(row["name"]).strip()
|
||||
grade = TechniqueGrade.from_str(row.get("grade", "下品"))
|
||||
cond_val = row.get("condition", "")
|
||||
condition = "" if str(cond_val) == "nan" else str(cond_val).strip()
|
||||
weight_val = row.get("weight", 1)
|
||||
weight = float(str(weight_val)) if str(weight_val) != "nan" else 1.0
|
||||
t = Technique(
|
||||
id=int(row["id"]),
|
||||
name=name,
|
||||
attribute=attr,
|
||||
grade=grade,
|
||||
prompt=str(row.get("prompt", "")),
|
||||
weight=weight,
|
||||
condition=condition,
|
||||
)
|
||||
techniques_by_id[t.id] = t
|
||||
techniques_by_name[t.name] = t
|
||||
return techniques_by_id, techniques_by_name
|
||||
|
||||
|
||||
techniques_by_id, techniques_by_name = loads()
|
||||
|
||||
|
||||
def is_attribute_compatible_with_root(attr: TechniqueAttribute, root: Root) -> bool:
|
||||
if attr == TechniqueAttribute.EVIL:
|
||||
# 邪功法仅由阵营约束,这里视为与灵根无关
|
||||
return True
|
||||
|
||||
# 天灵根:除邪外全系可修
|
||||
if root == Root.HEAVEN:
|
||||
return attr != TechniqueAttribute.EVIL
|
||||
|
||||
# 单属性灵根:只能修行对应属性
|
||||
single_map = {
|
||||
Root.GOLD: TechniqueAttribute.GOLD,
|
||||
Root.WOOD: TechniqueAttribute.WOOD,
|
||||
Root.WATER: TechniqueAttribute.WATER,
|
||||
Root.FIRE: TechniqueAttribute.FIRE,
|
||||
Root.EARTH: TechniqueAttribute.EARTH,
|
||||
}
|
||||
if root in single_map:
|
||||
return attr == single_map[root]
|
||||
|
||||
# 复合/扩展灵根:根名属性 + 其元素列表中的属性
|
||||
complex_map: dict[Root, set[TechniqueAttribute]] = {
|
||||
Root.ICE: {TechniqueAttribute.ICE, TechniqueAttribute.GOLD, TechniqueAttribute.WATER},
|
||||
Root.WIND: {TechniqueAttribute.WIND, TechniqueAttribute.WOOD, TechniqueAttribute.WATER},
|
||||
Root.DARK: {TechniqueAttribute.DARK, TechniqueAttribute.FIRE, TechniqueAttribute.EARTH},
|
||||
Root.THUNDER: {TechniqueAttribute.THUNDER, TechniqueAttribute.WATER, TechniqueAttribute.EARTH},
|
||||
}
|
||||
if root in complex_map:
|
||||
return attr in complex_map[root]
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_random_technique_for_avatar(avatar) -> Technique:
|
||||
import random
|
||||
candidates: List[Technique] = []
|
||||
for t in techniques_by_id.values():
|
||||
if not t.is_allowed_for(avatar):
|
||||
continue
|
||||
if t.attribute == TechniqueAttribute.EVIL and avatar.alignment != Alignment.EVIL:
|
||||
continue
|
||||
if not is_attribute_compatible_with_root(t.attribute, avatar.root):
|
||||
continue
|
||||
candidates.append(t)
|
||||
if not candidates:
|
||||
# 回退:不考虑条件,仅按灵根兼容挑选(若仍为空,则全量)
|
||||
fallback = [
|
||||
t for t in techniques_by_id.values()
|
||||
if (t.attribute != TechniqueAttribute.EVIL) and is_attribute_compatible_with_root(t.attribute, avatar.root)
|
||||
]
|
||||
candidates = fallback or list(techniques_by_id.values())
|
||||
weights = [max(0.0, t.weight) for t in candidates]
|
||||
return random.choices(candidates, weights=weights, k=1)[0]
|
||||
|
||||
|
||||
def get_grade_bonus(grade: TechniqueGrade) -> float:
|
||||
if grade is TechniqueGrade.UPPER:
|
||||
return 0.10
|
||||
if grade is TechniqueGrade.MIDDLE:
|
||||
return 0.05
|
||||
return 0.0
|
||||
|
||||
|
||||
def get_suppression_bonus(att_attr: TechniqueAttribute, def_attr: TechniqueAttribute) -> float:
|
||||
return 0.10 if def_attr in SUPPRESSION.get(att_attr, set()) else 0.0
|
||||
|
||||
|
||||
# 相对品阶优势加成:按“品阶差×步进”的方式计算
|
||||
# - 品阶映射:下品=0,中品=1,上品=2
|
||||
# - 每级差距步进:5%(最大±10%)
|
||||
_GRADE_RANK: dict[TechniqueGrade, int] = {
|
||||
TechniqueGrade.LOWER: 0,
|
||||
TechniqueGrade.MIDDLE: 1,
|
||||
TechniqueGrade.UPPER: 2,
|
||||
}
|
||||
|
||||
|
||||
def get_grade_advantage_bonus(attacker_grade: Optional[TechniqueGrade], defender_grade: Optional[TechniqueGrade]) -> float:
|
||||
"""
|
||||
根据双方品阶差计算进攻方的相对加成:
|
||||
- diff = rank(att) - rank(def)
|
||||
- bonus = 0.05 × diff,夹紧到 [-0.10, 0.10]
|
||||
- 任一为空则视为无加成
|
||||
返回:进攻方概率或伤害的相对加成(可为负)。
|
||||
"""
|
||||
if attacker_grade is None or defender_grade is None:
|
||||
return 0.0
|
||||
diff = _GRADE_RANK[attacker_grade] - _GRADE_RANK[defender_grade]
|
||||
bonus = 0.05 * diff
|
||||
if bonus > 0.10:
|
||||
bonus = 0.10
|
||||
if bonus < -0.10:
|
||||
bonus = -0.10
|
||||
return bonus
|
||||
|
||||
|
||||
Reference in New Issue
Block a user