471 lines
18 KiB
GDScript
471 lines
18 KiB
GDScript
# SkillTable (P6) —— 读 `locale/locale/<lang>/skilldesc.txt`:skill id → 名/职业/动作。
|
||
#
|
||
# 列(TSV,1-indexed,PythonSkill.cpp __LoadSkillDesc):
|
||
# 1 id · 2 JOB(WARRIOR/ASSASSIN/SURA/SHAMAN/SUPPORT/HORSE/GUILD) · 3-5 name/name2/name3
|
||
# 6-9 desc1-4 · 10 (空) · 11 attributes(ATTACK_SKILL|PASSIVE|TOGGLE|STANDING_SKILL|…)
|
||
# 12 weapon_limit(SWORD|BOW|…) · 13 motion_name · 14 motion_idx · 15 grade_count · 16+ 公式
|
||
#
|
||
# var st := preload("res://ui/skill_table.gd").new()
|
||
# st.load_file("<assets>/locale/locale/en/skilldesc.txt")
|
||
# st.entry(1) # { id, job, name, …, attrs, motion, motion_idx, grades, cooltime }
|
||
# 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 := {}
|
||
var count := 0
|
||
|
||
const JOB_DIR := {
|
||
"WARRIOR": "warrior",
|
||
"ASSASSIN": "assassin",
|
||
"SURA": "sura",
|
||
"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
|
||
if not FileAccess.file_exists(path):
|
||
return false
|
||
var f := FileAccess.open(path, FileAccess.READ)
|
||
if f == null:
|
||
return false
|
||
while not f.eof_reached():
|
||
var line := f.get_line()
|
||
if line.strip_edges() == "":
|
||
continue
|
||
var t := line.split("\t")
|
||
if t.size() < 14:
|
||
continue
|
||
var id := int(t[0])
|
||
if id == 0:
|
||
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": job,
|
||
"name": t[2].strip_edges(),
|
||
"name2": t[3].strip_edges(),
|
||
"name3": t[4].strip_edges(),
|
||
"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_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
|
||
|
||
func has(id: int) -> bool:
|
||
return _by_id.has(id)
|
||
|
||
func entry(id: int) -> Dictionary:
|
||
return _by_id.get(id, {})
|
||
|
||
func name_of(id: int) -> String:
|
||
return String(_by_id.get(id, {}).get("name", "skill %d" % id))
|
||
|
||
func motion_idx_of(id: int) -> int:
|
||
return int(_by_id.get(id, {}).get("motion_idx", 0))
|
||
|
||
func _attrs(id: int) -> String:
|
||
return String(_by_id.get(id, {}).get("attrs", ""))
|
||
|
||
# §3.8:折叠后的判定位掩码。取位即原版一众 IsXxx() 谓词(PythonSkill.cpp)。
|
||
func attr_bits(id: int) -> int:
|
||
return int(_by_id.get(id, {}).get("attr_bits", 0))
|
||
|
||
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 (need_weapon_bits(id) & (int(NEED_WEAPON["BOW"]) | int(NEED_WEAPON["ARROW"]))) != 0
|
||
|
||
# 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_guild_skill(id: int) -> bool:
|
||
return skill_type_of(id) == SKILL_TYPE_GUILD
|
||
|
||
func can_level_up(id: int) -> bool: # !CANNOT_LEVEL_UP
|
||
return _by_id.has(id) and not _has_attr(id, "CANNOT_LEVEL_UP")
|
||
|
||
# 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 == "":
|
||
return
|
||
var path := AssetRoot.path().path_join("PC/ymir work/pc/%s/skill/%s.msk" % [job_dir, motion])
|
||
if not FileAccess.file_exists(path):
|
||
return
|
||
var file := FileAccess.open(path, FileAccess.READ)
|
||
if file == null:
|
||
return
|
||
# .msk 常含 CP949/CP1252 本地化文本;只扫描 ASCII 配置行,不能把整份
|
||
# 文件当 UTF-8 逐行读,否则 Godot 会为无关文案输出解码警告。
|
||
var bytes := file.get_buffer(file.get_length())
|
||
var formula := _ascii_field(bytes, "CoolTimeFormula")
|
||
if formula != "":
|
||
entry["cooltime_formula"] = formula.trim_prefix("\"").trim_suffix("\"")
|
||
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()
|
||
if needle.is_empty():
|
||
return ""
|
||
for start in range(bytes.size() - needle.size() + 1):
|
||
var found := true
|
||
for offset in needle.size():
|
||
if bytes[start + offset] != needle[offset]:
|
||
found = false
|
||
break
|
||
if not found:
|
||
continue
|
||
var out := PackedByteArray()
|
||
var i := start + needle.size()
|
||
while i < bytes.size() and bytes[i] != 10 and bytes[i] != 13:
|
||
if bytes[i] >= 32 and bytes[i] <= 126:
|
||
out.append(bytes[i])
|
||
i += 1
|
||
return out.get_string_from_ascii().strip_edges()
|
||
return ""
|
||
|
||
func cooldown_of(id: int, level: int) -> float:
|
||
var data := entry(id)
|
||
var formula := String(data.get("cooltime_formula", ""))
|
||
if formula == "":
|
||
return 0.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(level)])
|
||
if expression.has_execute_failed() or not (value is float or value is int):
|
||
return 0.0
|
||
return maxf(0.0, float(value))
|
||
|
||
# JOB 列的分类:主动树是 WARRIOR/ASSASSIN/SURA/SHAMAN;辅助/被动树是 SUPPORT;还有 HORSE / GUILD
|
||
func category_of(id: int) -> String:
|
||
return String(_by_id.get(id, {}).get("job", ""))
|
||
|
||
func for_job(job: String) -> Array:
|
||
return for_category(job)
|
||
|
||
func for_category(cat: String) -> Array:
|
||
var out := []
|
||
for id in _by_id:
|
||
if _by_id[id].get("job", "") == cat:
|
||
out.append(id)
|
||
out.sort()
|
||
return out
|