Files
mtgodot-poc/project/ui/skill_table.gd
T
shenleiandClaude Opus 5 1933a5ceda fix(combat): 战斗逐项对齐 40250,修复移动 / 技能释放与武器挂点
- 连击类型由 combo_changed 驱动(SetComboSkillFlag,技能 122 等级)
- 命中判定改为 .msa 刀尖 Z 圆柱 vs .msm 防御球(hit_collision.gd 逐行移植)
- _register_hit 改为 map::insert 语义,修正命中上限
- 攻速同时缩放 GetAttackingElapsedTime 与攻击动作速率;按武器动作模式目录绑定攻击段
- 去掉本地连击超时,motion_bound 清命中表 / 连击段号
- __ProcessDataAttackSuccess:InsertDelay 硬直、physics_push.gd 击退 + GetBlendingPosition 同步、
  命中特效、__HitGood / __HitGreate / __HitStone 受击动作链与抖动(ui/hit_reaction.gd)
- ClassicSession::send_use_skill 不再夹带 CG_FLY_TARGETING
- mac 前进 / 后退方向与技能释放修复;武器按职业骨骼挂到手上
- 新增 hit_collision / physics_push / hit_view / net_world_push / weapon_attach 测试;
  gpu_pose_bounds / race_motion_assembly 适配 damage 随机变体
- 文档:CLIENT-GAP.md、CLIENT-GAP-FIX.md §3.5 / §3.7 与 C.5 增量 130

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
2026-09-12 13:15:28 +09:00

530 lines
20 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SkillTable (P6) —— 读 `locale/locale/<lang>/skilldesc.txt`skill id → 名/职业/动作。
#
# 列(TSV1-indexedPythonSkill.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 是 `|` 分隔的
# 记号串,原客户端 RegisterSkillDescPythonSkill.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::EWeaponSubTypesGameLib/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_SkillNeedWeaponIndexMapDOUBLE_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::ESkillTypePythonSkill.h+ m_SkillTypeIndexMapPythonSkill.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)
# RegisterSkillDescPythonSkill.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
# RegisterSkillTablePythonSkill.cpp:90):SkillTable.txt 按 TAB 切分,列数不等于
# TABLE_TOKEN_TYPE_MAX_NUM 的行、skilldesc 里没有的 vnum 都跳过。参考端 RegisterSkill(.msk)
# 没有调用点,TSkillData 构造时 dwTargetRange = 0,只由这里非空的 TARGET_RANGE 列覆盖;
# 所以表生效后不再沿用 .msk 的 Range。第 2 列是 CP949 技能名,按字节切分避免解码告警。
const TABLE_TOKEN_TYPE_VNUM := 0
const TABLE_TOKEN_TYPE_TARGET_RANGE := 25
const TABLE_TOKEN_TYPE_MAX_NUM := 27
func load_table(path: String) -> bool:
if _by_id.is_empty() or not FileAccess.file_exists(path):
return false
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
return false
var bytes := file.get_buffer(file.get_length())
var ranges := {}
var start := 0
while start < bytes.size():
var stop := bytes.find(10, start)
if stop < 0:
stop = bytes.size()
var fields := _tab_fields(bytes.slice(start, stop))
start = stop + 1
if fields.size() != TABLE_TOKEN_TYPE_MAX_NUM:
continue
var vnum := int(_ascii_token(fields[TABLE_TOKEN_TYPE_VNUM]))
if not _by_id.has(vnum):
continue
ranges[vnum] = _ascii_token(fields[TABLE_TOKEN_TYPE_TARGET_RANGE])
if ranges.is_empty():
return false
for id in _by_id:
_by_id[id]["target_range"] = 0
for vnum in ranges:
if String(ranges[vnum]) != "":
_by_id[vnum]["target_range"] = int(ranges[vnum])
return true
static func _tab_fields(line: PackedByteArray) -> Array:
var fields := []
var begin := 0
for i in line.size() + 1:
if i == line.size() or line[i] == 9:
fields.append(line.slice(begin, i))
begin = i + 1
return fields
static func _ascii_token(field: PackedByteArray) -> String:
var out := PackedByteArray()
for b in field:
if b >= 32 and b <= 126:
out.append(b)
return out.get_string_from_ascii().strip_edges()
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)))
# __GetSkillTargetRangePythonPlayerSkill.cpp:391= rkSkillData.GetTargetRange()
# + GetStatus(POINT_BOW_DISTANCE)*100。这里是 GetTargetRangePythonSkill.cpp:1086):
# MELEE_ATTACK / CHARGE_ATTACK 固定 MELEE_SKILL_TARGET_RANGE,否则 dwTargetRange
# load_table 之后取 SkillTable.txt TARGET_RANGE);主角弓距加成由 player_skill 叠加。
const MELEE_SKILL_TARGET_RANGE := 170
func target_range(id: int) -> int:
if is_melee(id) or is_charge_skill(id):
return MELEE_SKILL_TARGET_RANGE
return int(_by_id.get(id, {}).get("target_range", 0))
# skilldesc / .msk 的 FAN_RANGE / CIRCLE_RANGE → NetPlay.FlyShapeSINGLE/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::ESkillType3=SKILL_TYPE_GUILD4=SKILL_TYPE_HORSE1=主动,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))
# skillGetSkillLevelLimitPythonSkill.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))
# skillCanLevelUpSkillPythonSkill.cpp:1833)。本 build 里 isRequirement /
# strRequireSkillName / byRequireSkillLevel / RequireStatDataVector 从不被填充
# (构造默认 isRequirement=FALSE、空 vectorPythonSkill.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
# (参考 GetNeedSPPythonSkill.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_GetSkillPowerLocale.cpp:126INTERNATIONAL 表)——
# 原客户端 fcurEfficientPercentage = GetSkillPower(level) / 100,公式里的
# SkillPoint / k 变量取这个值(ProcessFormulaPythonSkill.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)
# __CheckShortLifePythonPlayerSkill.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 修改 3SkillPoint 统一走 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