Files
mtgodot-poc/project/ui/skill_table.gd
T

471 lines
18 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
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。这里只给 .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.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