完善客户端功能并同步差距文档
This commit is contained in:
+324
-22
@@ -11,6 +11,12 @@
|
||||
# st.for_job("WARRIOR") # 主动职业技能 [id, …]
|
||||
# st.for_category("SUPPORT") # 辅助 / 被动树
|
||||
# st.is_passive(121) # true
|
||||
#
|
||||
# §3.8 TSkillData 判定位:第 11 列 attributes / 第 12 列 weapon_limit 是 `|` 分隔的
|
||||
# 记号串,原客户端 RegisterSkillDesc(PythonSkill.cpp:280)把它们经
|
||||
# m_SkillAttributeIndexMap / m_SkillNeedWeaponIndexMap 折成 dwSkillAttribute /
|
||||
# dwNeedWeapon 两个位掩码,CPythonSkill 的一众 IsXxx() 谓词就是对这两个掩码取位。
|
||||
# 这里在 load_file 时做同样的折叠,谓词逐条对齐 PythonSkill.cpp。
|
||||
extends RefCounted
|
||||
|
||||
var _by_id := {}
|
||||
@@ -23,6 +29,87 @@ const JOB_DIR := {
|
||||
"SHAMAN": "shaman",
|
||||
}
|
||||
|
||||
# --- SKILL_ATTRIBUTE_*(PythonSkill.h:92-115,记号名同 m_SkillAttributeIndexMap)---
|
||||
const ATTR := {
|
||||
"NEED_TARGET": 1 << 0,
|
||||
"TOGGLE": 1 << 1,
|
||||
"WEAPON_LIMITATION": 1 << 2,
|
||||
"MELEE_ATTACK": 1 << 3,
|
||||
"USE_HP": 1 << 4,
|
||||
"CAN_CHANGE_DIRECTION": 1 << 5,
|
||||
"STANDING_SKILL": 1 << 6,
|
||||
"ONLY_FOR_ALLIANCE": 1 << 7,
|
||||
"CAN_USE_FOR_ME": 1 << 8,
|
||||
"NEED_CORPSE": 1 << 9,
|
||||
"FAN_RANGE": 1 << 10,
|
||||
"CAN_USE_IF_NOT_ENOUGH": 1 << 11,
|
||||
"NEED_EMPTY_BOTTLE": 1 << 12,
|
||||
"NEED_POISON_BOTTLE": 1 << 13,
|
||||
"ATTACK_SKILL": 1 << 14,
|
||||
"TIME_INCREASE_SKILL": 1 << 15,
|
||||
"CHARGE_ATTACK": 1 << 16,
|
||||
"PASSIVE": 1 << 17,
|
||||
"CANNOT_LEVEL_UP": 1 << 18,
|
||||
"ONLY_FOR_GUILD_WAR": 1 << 19,
|
||||
"MOVING_SKILL": 1 << 20,
|
||||
"HORSE_SKILL": 1 << 21,
|
||||
"CIRCLE_RANGE": 1 << 22,
|
||||
"SEARCH_TARGET": 1 << 23,
|
||||
}
|
||||
|
||||
# --- CItemData::EWeaponSubTypes(GameLib/ItemData.h:63-75)---
|
||||
const WEAPON_SWORD := 0
|
||||
const WEAPON_DAGGER := 1
|
||||
const WEAPON_BOW := 2
|
||||
const WEAPON_TWO_HANDED := 3
|
||||
const WEAPON_BELL := 4
|
||||
const WEAPON_FAN := 5
|
||||
const WEAPON_ARROW := 6
|
||||
const WEAPON_NONE := 8 # WEAPON_NUM_TYPES(7) + 1
|
||||
|
||||
# --- SKILL_NEED_WEAPON_*(PythonSkill.h:120-128,位 = 1 << CItemData::WEAPON_*)---
|
||||
# 记号名同 m_SkillNeedWeaponIndexMap;DOUBLE_SWORD 复用 DAGGER 位。
|
||||
const NEED_WEAPON := {
|
||||
"SWORD": 1 << WEAPON_SWORD,
|
||||
"DAGGER": 1 << WEAPON_DAGGER,
|
||||
"BOW": 1 << WEAPON_BOW,
|
||||
"TWO_HANDED": 1 << WEAPON_TWO_HANDED,
|
||||
"DOUBLE_SWORD": 1 << WEAPON_DAGGER,
|
||||
"BELL": 1 << WEAPON_BELL,
|
||||
"FAN": 1 << WEAPON_FAN,
|
||||
"ARROW": 1 << WEAPON_ARROW,
|
||||
"EMPTY_HAND": 1 << WEAPON_NONE,
|
||||
}
|
||||
|
||||
# --- CPythonSkill::ESkillType(PythonSkill.h)+ m_SkillTypeIndexMap(PythonSkill.cpp:909-918)---
|
||||
const SKILL_TYPE_NONE := 0
|
||||
const SKILL_TYPE_ACTIVE := 1
|
||||
const SKILL_TYPE_SUPPORT := 2
|
||||
const SKILL_TYPE_GUILD := 3
|
||||
const SKILL_TYPE_HORSE := 4
|
||||
const _SKILL_TYPE_MAP := {
|
||||
"none": SKILL_TYPE_NONE,
|
||||
"active": SKILL_TYPE_ACTIVE,
|
||||
"support": SKILL_TYPE_SUPPORT,
|
||||
"guild": SKILL_TYPE_GUILD,
|
||||
"horse": SKILL_TYPE_HORSE,
|
||||
"warrior": SKILL_TYPE_ACTIVE,
|
||||
"assassin": SKILL_TYPE_ACTIVE,
|
||||
"sura": SKILL_TYPE_ACTIVE,
|
||||
"shaman": SKILL_TYPE_ACTIVE,
|
||||
"passive": SKILL_TYPE_ACTIVE,
|
||||
}
|
||||
|
||||
# 把 `|`(或旧数据里的空格 / 逗号)分隔的记号串折成位掩码。未知记号忽略,
|
||||
# 对齐原版“map 里查不到就跳过该记号”的行为(PythonSkill.cpp:374-378 / 393-397)。
|
||||
static func _fold_bits(tokens: String, name_to_bit: Dictionary) -> int:
|
||||
var bits := 0
|
||||
for raw in tokens.replace(",", "|").replace(" ", "|").split("|", false):
|
||||
var key := raw.strip_edges().to_upper()
|
||||
if name_to_bit.has(key):
|
||||
bits |= int(name_to_bit[key])
|
||||
return bits
|
||||
|
||||
func load_file(path: String) -> bool:
|
||||
_by_id.clear()
|
||||
count = 0
|
||||
@@ -43,19 +130,34 @@ func load_file(path: String) -> bool:
|
||||
continue
|
||||
# 0-indexed: id=0 job=1 name=2..4 desc=5..8 (9 空) attrs=10 weapon=11
|
||||
# motion_name=12 motion_idx=13 grades=14
|
||||
var job := t[1].strip_edges()
|
||||
var attrs := t[10].strip_edges() if t.size() > 10 else ""
|
||||
var weapon := t[11].strip_edges() if t.size() > 11 else ""
|
||||
var entry := {
|
||||
"id": id,
|
||||
"job": t[1].strip_edges(),
|
||||
"job": job,
|
||||
"name": t[2].strip_edges(),
|
||||
"name2": t[3].strip_edges(),
|
||||
"name3": t[4].strip_edges(),
|
||||
"attrs": t[10].strip_edges() if t.size() > 10 else "",
|
||||
"weapon": t[11].strip_edges() if t.size() > 11 else "",
|
||||
"attrs": attrs,
|
||||
"weapon": weapon,
|
||||
"motion": t[12].strip_edges() if t.size() > 12 else "",
|
||||
"motion_idx": int(t[13]) if t.size() > 13 else 0,
|
||||
"grades": int(t[14]) if t.size() > 14 else 1,
|
||||
# §3.8:预折叠的 TSkillData 判定位(对齐 dwSkillAttribute / dwNeedWeapon)
|
||||
"attr_bits": _fold_bits(attrs, ATTR),
|
||||
"need_weapon_bits": _fold_bits(weapon, NEED_WEAPON),
|
||||
"skill_type": int(_SKILL_TYPE_MAP.get(job.to_lower(), SKILL_TYPE_NONE)),
|
||||
}
|
||||
_load_cooltime(entry)
|
||||
_load_msk(entry)
|
||||
# RegisterSkillDesc(PythonSkill.cpp:280):DESC_TOKEN_TYPE LEVEL_LIMIT=26 /
|
||||
# MAX_LEVEL=27 只在 TSV 列数够时解析。本 40250 shipped skilldesc.txt 只有 22 列,
|
||||
# 这两列都缺,逻辑保留以对齐参考端、并兼容带这两列的第三方数据。
|
||||
# .msk 的 maxlevel(_load_msk 已处理)优先于 skilldesc MAX_LEVEL。
|
||||
if not entry.has("max_level") and t.size() > 27 and int(t[27]) > 0:
|
||||
entry["max_level"] = max(1, int(t[27]))
|
||||
if t.size() > 26 and int(t[26]) > 0:
|
||||
entry["level_limit"] = int(t[26])
|
||||
_by_id[id] = entry
|
||||
count = _by_id.size()
|
||||
return count > 0
|
||||
@@ -75,27 +177,171 @@ func motion_idx_of(id: int) -> int:
|
||||
func _attrs(id: int) -> String:
|
||||
return String(_by_id.get(id, {}).get("attrs", ""))
|
||||
|
||||
func is_attack(id: int) -> bool:
|
||||
return _attrs(id).contains("ATTACK_SKILL")
|
||||
# §3.8:折叠后的判定位掩码。取位即原版一众 IsXxx() 谓词(PythonSkill.cpp)。
|
||||
func attr_bits(id: int) -> int:
|
||||
return int(_by_id.get(id, {}).get("attr_bits", 0))
|
||||
|
||||
# 原版只有弓类飞行攻击在动作事件中发送 CG_SHOOT;把 weapon_limit 保留下来,
|
||||
# 让上层可以只为远程技能安排 shoot,而不会给近战技能误发该包。
|
||||
func need_weapon_bits(id: int) -> int:
|
||||
return int(_by_id.get(id, {}).get("need_weapon_bits", 0))
|
||||
|
||||
func _has_attr(id: int, name: String) -> bool:
|
||||
return (attr_bits(id) & int(ATTR[name])) != 0
|
||||
|
||||
# --- 逐条对齐 CPythonSkill 谓词(PythonSkill.cpp)---
|
||||
func is_attack(id: int) -> bool: # IsAttackSkill
|
||||
return _has_attr(id, "ATTACK_SKILL")
|
||||
|
||||
func is_passive(id: int) -> bool: # SKILL_ATTRIBUTE_PASSIVE
|
||||
return _has_attr(id, "PASSIVE")
|
||||
|
||||
func is_toggle(id: int) -> bool: # IsToggleSkill
|
||||
return _has_attr(id, "TOGGLE")
|
||||
|
||||
func is_standing(id: int) -> bool: # IsStandingSkill
|
||||
return _has_attr(id, "STANDING_SKILL")
|
||||
|
||||
func is_need_target(id: int) -> bool: # IsNeedTarget
|
||||
return _has_attr(id, "NEED_TARGET")
|
||||
|
||||
func is_need_corpse(id: int) -> bool: # IsNeedCorpse
|
||||
return _has_attr(id, "NEED_CORPSE")
|
||||
|
||||
func is_auto_search_target(id: int) -> bool: # IsAutoSearchTarget
|
||||
return _has_attr(id, "SEARCH_TARGET")
|
||||
|
||||
func can_change_direction(id: int) -> bool: # CanChangeDirection
|
||||
return _has_attr(id, "CAN_CHANGE_DIRECTION")
|
||||
|
||||
func is_horse_skill(id: int) -> bool: # IsHorseSkill
|
||||
return _has_attr(id, "HORSE_SKILL")
|
||||
|
||||
func is_moving_skill(id: int) -> bool: # IsMovingSkill
|
||||
return _has_attr(id, "MOVING_SKILL")
|
||||
|
||||
func is_melee(id: int) -> bool: # IsMeleeSkill
|
||||
return _has_attr(id, "MELEE_ATTACK")
|
||||
|
||||
func is_charge_skill(id: int) -> bool: # IsChargeSkill
|
||||
return _has_attr(id, "CHARGE_ATTACK")
|
||||
|
||||
func is_use_hp(id: int) -> bool: # IsUseHPSkill
|
||||
return _has_attr(id, "USE_HP")
|
||||
|
||||
func is_time_increase(id: int) -> bool: # IsTimeIncreaseSkill
|
||||
return _has_attr(id, "TIME_INCREASE_SKILL")
|
||||
|
||||
func is_need_empty_bottle(id: int) -> bool: # IsNeedEmptyBottle
|
||||
return _has_attr(id, "NEED_EMPTY_BOTTLE")
|
||||
|
||||
func is_need_poison_bottle(id: int) -> bool: # IsNeedPoisonBottle
|
||||
return _has_attr(id, "NEED_POISON_BOTTLE")
|
||||
|
||||
func can_use_for_me(id: int) -> bool: # CanUseForMe
|
||||
return _has_attr(id, "CAN_USE_FOR_ME")
|
||||
|
||||
func is_only_for_alliance(id: int) -> bool: # IsOnlyForAlliance
|
||||
return _has_attr(id, "ONLY_FOR_ALLIANCE")
|
||||
|
||||
func can_use_if_not_enough(id: int) -> bool: # CanUseIfNotEnough
|
||||
return _has_attr(id, "CAN_USE_IF_NOT_ENOUGH")
|
||||
|
||||
func is_only_for_guild_war(id: int) -> bool: # IsOnlyForGuildWar
|
||||
return _has_attr(id, "ONLY_FOR_GUILD_WAR")
|
||||
|
||||
func is_fan_range(id: int) -> bool: # IsFanRange
|
||||
return _has_attr(id, "FAN_RANGE")
|
||||
|
||||
func is_circle_range(id: int) -> bool: # IsCircleRange
|
||||
return _has_attr(id, "CIRCLE_RANGE")
|
||||
|
||||
# GetTargetCount(fSkillPoint)(PythonSkill.cpp:1361):strTargetCountFormula 为空 → 0,
|
||||
# 否则把 SkillPoint(= LocaleService_GetSkillPower/100)代入公式后向下取整。
|
||||
# 扇形/圆形补目标(PythonPlayerSkill.cpp:684 `if (dwTargetMaxCount>0 …)`)就靠这个 > 0。
|
||||
func target_count(id: int, level: int) -> int:
|
||||
var formula := String(_by_id.get(id, {}).get("target_count_formula", ""))
|
||||
if formula == "":
|
||||
return 0
|
||||
var expression := Expression.new()
|
||||
if expression.parse(formula, ["SkillPoint"]) != OK:
|
||||
return 0
|
||||
var value: Variant = expression.execute([_skill_point(level)])
|
||||
if expression.has_execute_failed() or not (value is float or value is int):
|
||||
return 0
|
||||
return int(floor(float(value)))
|
||||
|
||||
# __GetSkillTargetRange(PythonPlayerSkill.cpp:391)= rkSkillData.GetTargetRange()
|
||||
# + GetStatus(POINT_BOW_DISTANCE)*100。这里只给 .msk 的基础射程(cm);主角弓距加成
|
||||
# 由 net_play(有玩家状态)叠加。
|
||||
func target_range(id: int) -> int:
|
||||
return int(_by_id.get(id, {}).get("target_range", 0))
|
||||
|
||||
# skilldesc / .msk 的 FAN_RANGE / CIRCLE_RANGE → NetPlay.FlyShape(SINGLE/FAN/CIRCLE)。
|
||||
func fly_shape(id: int) -> int:
|
||||
if is_fan_range(id):
|
||||
return 1
|
||||
if is_circle_range(id):
|
||||
return 2
|
||||
return 0
|
||||
|
||||
func has_weapon_limitation(id: int) -> bool: # SKILL_ATTRIBUTE_WEAPON_LIMITATION
|
||||
return _has_attr(id, "WEAPON_LIMITATION")
|
||||
|
||||
# IsCanUseSkill()(PythonSkill.cpp:1407):被动技一律不可主动施放,其余可。
|
||||
func is_can_use_skill(id: int) -> bool:
|
||||
return not _has_attr(id, "PASSIVE")
|
||||
|
||||
# CanUseWeaponType(dwWeaponType)(PythonSkill.cpp:1143):无武器限制恒真,
|
||||
# 否则 dwNeedWeapon & (1 << dwWeaponType)。
|
||||
func can_use_weapon_type(id: int, weapon_type: int) -> bool:
|
||||
if not has_weapon_limitation(id):
|
||||
return true
|
||||
return (need_weapon_bits(id) & (1 << weapon_type)) != 0
|
||||
|
||||
# IsNeedBow():dwNeedWeapon & SKILL_NEED_WEAPON_BOW。
|
||||
func is_need_bow(id: int) -> bool:
|
||||
return (need_weapon_bits(id) & int(NEED_WEAPON["BOW"])) != 0
|
||||
|
||||
# 保留旧名:原本靠 weapon 列字符串判远程,现改为查 need-weapon 位(等价,含 ARROW)。
|
||||
func is_ranged(id: int) -> bool:
|
||||
return String(_by_id.get(id, {}).get("weapon", "")).to_upper().contains("BOW")
|
||||
return (need_weapon_bits(id) & (int(NEED_WEAPON["BOW"]) | int(NEED_WEAPON["ARROW"]))) != 0
|
||||
|
||||
func is_passive(id: int) -> bool:
|
||||
return _attrs(id).contains("PASSIVE")
|
||||
# CPythonSkill::ESkillType:3=SKILL_TYPE_GUILD,4=SKILL_TYPE_HORSE,1=主动,2=辅助。
|
||||
func skill_type_of(id: int) -> int:
|
||||
return int(_by_id.get(id, {}).get("skill_type", SKILL_TYPE_NONE))
|
||||
|
||||
func is_toggle(id: int) -> bool:
|
||||
return _attrs(id).contains("TOGGLE")
|
||||
func is_guild_skill(id: int) -> bool:
|
||||
return skill_type_of(id) == SKILL_TYPE_GUILD
|
||||
|
||||
func can_level_up(id: int) -> bool:
|
||||
return _by_id.has(id) and not _attrs(id).contains("CANNOT_LEVEL_UP")
|
||||
func can_level_up(id: int) -> bool: # !CANNOT_LEVEL_UP
|
||||
return _by_id.has(id) and not _has_attr(id, "CANNOT_LEVEL_UP")
|
||||
|
||||
# 读取对应 .msk 中的 CoolTimeFormula 和 MaxLevel。原客户端用同一份公式
|
||||
# 按技能效率百分比计算本地冷却;这里在发送后进行同样的本地预测,GC_SKILL_COOLTIME_END
|
||||
# 仍是服务器的提前解锁信号。
|
||||
func _load_cooltime(entry: Dictionary) -> void:
|
||||
# SSkillData::GetMaxLevel()(PythonSkill.cpp:1392)= byMaxLevel。优先 .msk 的
|
||||
# maxlevel(_load_msk),其次 skilldesc MAX_LEVEL 列,都没有则构造默认 20
|
||||
# (SSkillData 构造,PythonSkill.cpp:1462)。
|
||||
func max_level_of(id: int) -> int:
|
||||
return int(_by_id.get(id, {}).get("max_level", 20))
|
||||
|
||||
# skillGetSkillLevelLimit(PythonSkill.cpp:1741)= byLevelLimit。本 40250 数据里
|
||||
# skilldesc 无 LEVEL_LIMIT 列、.msk 也无 levellimit 记号,恒 0;参考端马术页门
|
||||
# `GetStatus(LEVEL) >= GetSkillLevelLimit()` 因而恒真。
|
||||
func skill_level_limit(id: int) -> int:
|
||||
return int(_by_id.get(id, {}).get("level_limit", 0))
|
||||
|
||||
# skillCanLevelUpSkill(PythonSkill.cpp:1833)。本 build 里 isRequirement /
|
||||
# strRequireSkillName / byRequireSkillLevel / RequireStatDataVector 从不被填充
|
||||
# (构造默认 isRequirement=FALSE、空 vector,PythonSkill.cpp:1471),
|
||||
# .msk / skilldesc 也没有任何前置技能 / 前置属性字段,故 1:1 化简为:
|
||||
# 未满级(level < GetMaxLevel())且 非 SKILL_ATTRIBUTE_CANNOT_LEVEL_UP。
|
||||
func can_level_up_skill(id: int, level: int) -> bool:
|
||||
return _by_id.has(id) and level < max_level_of(id) and not _has_attr(id, "CANNOT_LEVEL_UP")
|
||||
|
||||
# 读取对应 .msk 中的 CoolTimeFormula / MaxLevel / NeedSPFormula。原客户端用同一份公式
|
||||
# 按技能效率百分比计算本地冷却与消耗;这里在发送前 / 后做同样的本地预测,
|
||||
# GC_SKILL_COOLTIME_END 仍是服务器的提前解锁信号。
|
||||
# NeedSPFormula 供 §3.8 修改 3 的 __CheckShortMana / __CheckShortLife
|
||||
# (参考 GetNeedSP,PythonSkill.cpp:1426;__CheckShortLife 同样取 GetNeedSP,
|
||||
# PythonPlayerSkill.cpp:359)。
|
||||
func _load_msk(entry: Dictionary) -> void:
|
||||
var job_dir := String(JOB_DIR.get(String(entry.get("job", "")), ""))
|
||||
var motion := String(entry.get("motion", ""))
|
||||
if job_dir == "" or motion == "":
|
||||
@@ -115,6 +361,62 @@ func _load_cooltime(entry: Dictionary) -> void:
|
||||
var max_level := _ascii_field(bytes, "MaxLevel")
|
||||
if max_level != "":
|
||||
entry["max_level"] = max(1, int(max_level.trim_prefix("\"").trim_suffix("\"")))
|
||||
var need_sp := _ascii_field(bytes, "NeedSPFormula")
|
||||
if need_sp != "":
|
||||
entry["need_sp_formula"] = need_sp.trim_prefix("\"").trim_suffix("\"")
|
||||
# GetTargetCount / __GetSkillTargetRange 的数据源(.msk 的 TargetCountFormula / Range,
|
||||
# CMotionSkillData 里对应 strTargetCountFormula / dwTargetRange)。
|
||||
var tcf := _ascii_field(bytes, "TargetCountFormula")
|
||||
if tcf != "":
|
||||
entry["target_count_formula"] = tcf.trim_prefix("\"").trim_suffix("\"")
|
||||
var trange := _ascii_field(bytes, "Range")
|
||||
if trange != "":
|
||||
entry["target_range"] = max(0, int(trange.trim_prefix("\"").trim_suffix("\"")))
|
||||
|
||||
# LocaleService_GetSkillPower(Locale.cpp:126,INTERNATIONAL 表)——
|
||||
# 原客户端 fcurEfficientPercentage = GetSkillPower(level) / 100,公式里的
|
||||
# SkillPoint / k 变量取这个值(ProcessFormula,PythonSkill.cpp:1258)。
|
||||
const _SKILL_POWER := [
|
||||
0,
|
||||
5, 6, 8, 10, 12,
|
||||
14, 16, 18, 20, 22,
|
||||
24, 26, 28, 30, 32,
|
||||
34, 36, 38, 40, 50, # 20 = master
|
||||
52, 54, 56, 58, 60,
|
||||
63, 66, 69, 72, 82, # 30 = grand master
|
||||
85, 88, 91, 94, 98,
|
||||
102, 106, 110, 115, 125, # 40 = perfect master
|
||||
125,
|
||||
]
|
||||
|
||||
# fSkillPoint(= fcurEfficientPercentage):level 越界(>= 50,参考 SKILL_POWER_NUM)→ 0。
|
||||
func _skill_point(level: int) -> float:
|
||||
if level < 0 or level >= 50:
|
||||
return 0.0
|
||||
var idx: int = min(level, _SKILL_POWER.size() - 1)
|
||||
return float(_SKILL_POWER[idx]) / 100.0
|
||||
|
||||
# GetNeedSP(fSkillPoint)(PythonSkill.cpp:1426):无公式 → 返回 -1(POC 约定:
|
||||
# 「未知」,调用方按参考里的 `if (-1 != iNeedSP)` 分支不拦截;参考端此处返回 0,
|
||||
# 但 POC 的 skilldesc/.msk 里不少主动技确实没有 NeedSPFormula,硬套 0 会误挡,
|
||||
# 故这里显式区分「解析不出」= -1)。公式引用了非 SkillPoint 的状态变量时同样退化 -1。
|
||||
func need_sp(id: int, level: int) -> int:
|
||||
var data := entry(id)
|
||||
var formula := String(data.get("need_sp_formula", ""))
|
||||
if formula == "":
|
||||
return -1
|
||||
var expression := Expression.new()
|
||||
if expression.parse(formula, ["SkillPoint"]) != OK:
|
||||
return -1
|
||||
var value: Variant = expression.execute([_skill_point(level)])
|
||||
if expression.has_execute_failed() or not (value is float or value is int):
|
||||
return -1
|
||||
return int(value)
|
||||
|
||||
# __CheckShortLife(PythonPlayerSkill.cpp:359)里 dwNeedHP 也是取 GetNeedSP,
|
||||
# 不是单独的 HP 公式——照抄这个怪癖。
|
||||
func need_hp(id: int, level: int) -> int:
|
||||
return need_sp(id, level)
|
||||
|
||||
func _ascii_field(bytes: PackedByteArray, field: String) -> String:
|
||||
var needle := field.to_ascii_buffer()
|
||||
@@ -142,12 +444,12 @@ func cooldown_of(id: int, level: int) -> float:
|
||||
var formula := String(data.get("cooltime_formula", ""))
|
||||
if formula == "":
|
||||
return 0.0
|
||||
var max_level: int = max(1, int(data.get("max_level", 20)))
|
||||
var skill_point: float = clampf(float(level) / float(max_level), 0.0, 1.0)
|
||||
# §3.8 修改 3:SkillPoint 统一走 LocaleService_GetSkillPower(与 GetNeedSP 同一份
|
||||
# fcurEfficientPercentage),不再用 level/max_level 近似。
|
||||
var expression := Expression.new()
|
||||
if expression.parse(formula, ["SkillPoint"]) != OK:
|
||||
return 0.0
|
||||
var value: Variant = expression.execute([skill_point])
|
||||
var value: Variant = expression.execute([_skill_point(level)])
|
||||
if expression.has_execute_failed() or not (value is float or value is int):
|
||||
return 0.0
|
||||
return maxf(0.0, float(value))
|
||||
|
||||
Reference in New Issue
Block a user