- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
207 lines
6.6 KiB
GDScript
207 lines
6.6 KiB
GDScript
# MotionRegistry —— 40250 playersettingmodule.py 的 RaceData motion 注册表适配。
|
|
#
|
|
# 40250 不按文件名等权挑动作:RegisterCacheMotionData/SetMotionRandomWeight
|
|
# 先把 (mode, index, sub-index, weight) 注册到 CRaceData,随后
|
|
# CActorInstance::GetRandomMotionKey 使用 random() % 100 和注册顺序选择。
|
|
# 这里读取随客户端资源发布的 playersettingmodule.py,保留相同的注册顺序、
|
|
# 重复项和权重;这样资源表才是唯一数据源,不能在 PlayerView 中复制一份常量。
|
|
extends RefCounted
|
|
|
|
const MODE_VALUES := {
|
|
"MOTION_MODE_GENERAL": 1,
|
|
"MOTION_MODE_ONEHAND_SWORD": 2,
|
|
"MOTION_MODE_TWOHAND_SWORD": 3,
|
|
"MOTION_MODE_DUALHAND_SWORD": 4,
|
|
"MOTION_MODE_BOW": 5,
|
|
"MOTION_MODE_FAN": 6,
|
|
"MOTION_MODE_BELL": 7,
|
|
"MOTION_MODE_FISHING": 8,
|
|
"MOTION_MODE_HORSE": 9,
|
|
"MOTION_MODE_HORSE_ONEHAND_SWORD": 10,
|
|
"MOTION_MODE_HORSE_TWOHAND_SWORD": 11,
|
|
"MOTION_MODE_HORSE_DUALHAND_SWORD": 12,
|
|
"MOTION_MODE_HORSE_BOW": 13,
|
|
"MOTION_MODE_HORSE_FAN": 14,
|
|
"MOTION_MODE_HORSE_BELL": 15,
|
|
"MOTION_MODE_WEDDING_DRESS": 16,
|
|
}
|
|
|
|
# CRaceMotionData::NAME_* in GameLib/RaceMotionData.h. Only the indices used by
|
|
# PlayerView are required here; unresolved skill/guild expressions are skipped,
|
|
# exactly as an unregistered motion key is rejected by GetMotionKey.
|
|
const MOTION_VALUES := {
|
|
"MOTION_WAIT": 1,
|
|
"MOTION_WALK": 2,
|
|
"MOTION_RUN": 3,
|
|
"MOTION_DAMAGE": 5,
|
|
"MOTION_DAMAGE_FLYING": 6,
|
|
"MOTION_STAND_UP": 7,
|
|
"MOTION_DAMAGE_BACK": 8,
|
|
"MOTION_DAMAGE_FLYING_BACK": 9,
|
|
"MOTION_STAND_UP_BACK": 10,
|
|
"MOTION_DEAD": 11,
|
|
"MOTION_DIG": 12,
|
|
"MOTION_NORMAL_ATTACK": 13,
|
|
"MOTION_COMBO_ATTACK_1": 14,
|
|
"MOTION_COMBO_ATTACK_2": 15,
|
|
"MOTION_COMBO_ATTACK_3": 16,
|
|
"MOTION_COMBO_ATTACK_4": 17,
|
|
"MOTION_COMBO_ATTACK_5": 18,
|
|
"MOTION_COMBO_ATTACK_6": 19,
|
|
"MOTION_COMBO_ATTACK_7": 20,
|
|
"MOTION_COMBO_ATTACK_8": 21,
|
|
}
|
|
|
|
const RACE_FUNCTIONS := [
|
|
"__LoadGameWarriorEx",
|
|
"__LoadGameAssassinEx",
|
|
"__LoadGameSuraEx",
|
|
"__LoadGameShamanEx",
|
|
]
|
|
|
|
var source_path := ""
|
|
var source_loaded := false
|
|
var _entries: Dictionary = {}
|
|
|
|
static func load_for(assets_root: String, race: int) -> RefCounted:
|
|
var result := new()
|
|
result.load_from(assets_root, race)
|
|
return result
|
|
|
|
func load_from(assets_root: String, race: int) -> void:
|
|
source_path = assets_root.path_join("root/playersettingmodule.py")
|
|
_entries.clear()
|
|
source_loaded = false
|
|
if not FileAccess.file_exists(source_path):
|
|
return
|
|
var file := FileAccess.open(source_path, FileAccess.READ)
|
|
if file == null:
|
|
return
|
|
var lines: Array[String] = []
|
|
while not file.eof_reached():
|
|
lines.append(file.get_line())
|
|
file.close()
|
|
|
|
# SetGeneralMotions is a Python helper invoked by each PC race function.
|
|
# Its `mode` parameter is the selected general mode in all four PC loaders.
|
|
_parse_lines(_function_block(lines, "SetGeneralMotions"), 1)
|
|
var function_name: String = RACE_FUNCTIONS[clampi(race & 3, 0, RACE_FUNCTIONS.size() - 1)]
|
|
_parse_lines(_function_block(lines, function_name), -1)
|
|
source_loaded = not _entries.is_empty()
|
|
|
|
func variants(mode: int, index: int) -> Array:
|
|
var value: Variant = _entries.get(_key(mode, index), [])
|
|
return value.duplicate(true) if value is Array else []
|
|
|
|
func has_variants(mode: int, index: int) -> bool:
|
|
var value: Variant = _entries.get(_key(mode, index), [])
|
|
return value is Array and not value.is_empty()
|
|
|
|
func has_mode(mode: int) -> bool:
|
|
var prefix := "%d:" % mode
|
|
for key in _entries.keys():
|
|
if String(key).begins_with(prefix):
|
|
return true
|
|
return false
|
|
|
|
func _key(mode: int, index: int) -> String:
|
|
return "%d:%d" % [mode, index]
|
|
|
|
func _function_block(lines: Array[String], function_name: String) -> Array[String]:
|
|
var result: Array[String] = []
|
|
var started := false
|
|
var prefix := "def " + function_name + "("
|
|
for line in lines:
|
|
if not started:
|
|
if line.begins_with(prefix):
|
|
started = true
|
|
continue
|
|
if line.begins_with("def "):
|
|
break
|
|
result.append(line)
|
|
return result
|
|
|
|
func _parse_lines(lines: Array[String], default_mode: int) -> void:
|
|
for raw in lines:
|
|
var line := raw
|
|
var comment := line.find("#")
|
|
if comment >= 0:
|
|
line = line.substr(0, comment)
|
|
line = line.strip_edges()
|
|
if line.is_empty():
|
|
continue
|
|
_parse_registration(line, default_mode)
|
|
_parse_weight_update(line, default_mode)
|
|
|
|
func _parse_registration(line: String, default_mode: int) -> void:
|
|
var marker := "RegisterCacheMotionData("
|
|
var start := line.find(marker)
|
|
if start < 0:
|
|
return
|
|
var close := line.rfind(")")
|
|
if close <= start + marker.length():
|
|
return
|
|
var args := line.substr(start + marker.length(), close - start - marker.length()).split(",")
|
|
if args.size() < 3:
|
|
return
|
|
var mode := _resolve(args[0], default_mode, MODE_VALUES)
|
|
var index := _resolve(args[1], -1, MOTION_VALUES)
|
|
var filename := _literal_filename(args[2])
|
|
if mode < 0 or index < 0 or filename == "":
|
|
return
|
|
var weight := 100
|
|
if args.size() >= 4:
|
|
weight = _integer_literal(args[3], 100)
|
|
var key := _key(mode, index)
|
|
var entries: Array = _entries.get(key, [])
|
|
entries.append({"name": filename, "weight": weight})
|
|
_entries[key] = entries
|
|
|
|
func _parse_weight_update(line: String, default_mode: int) -> void:
|
|
var marker := "SetMotionRandomWeight("
|
|
var start := line.find(marker)
|
|
if start < 0:
|
|
return
|
|
var close := line.rfind(")")
|
|
if close <= start + marker.length():
|
|
return
|
|
var args := line.substr(start + marker.length(), close - start - marker.length()).split(",")
|
|
if args.size() < 4:
|
|
return
|
|
var mode := _resolve(args[0], default_mode, MODE_VALUES)
|
|
var index := _resolve(args[1], -1, MOTION_VALUES)
|
|
var sub_index := _integer_literal(args[2], -1)
|
|
var weight := _integer_literal(args[3], -1)
|
|
if mode < 0 or index < 0 or sub_index < 0 or weight < 0:
|
|
return
|
|
var key := _key(mode, index)
|
|
var entries: Array = _entries.get(key, [])
|
|
if sub_index >= entries.size():
|
|
return
|
|
var entry: Dictionary = entries[sub_index]
|
|
entry["weight"] = weight
|
|
entries[sub_index] = entry
|
|
_entries[key] = entries
|
|
|
|
func _resolve(token: String, fallback: int, values: Dictionary) -> int:
|
|
var text := token.strip_edges()
|
|
if text == "mode":
|
|
return fallback if fallback >= 0 else 1
|
|
if text.is_valid_int():
|
|
return int(text)
|
|
var name := text.get_slice(".", text.get_slice_count(".") - 1)
|
|
return int(values.get(name, fallback))
|
|
|
|
func _integer_literal(token: String, fallback: int) -> int:
|
|
var text := token.strip_edges()
|
|
return int(text) if text.is_valid_int() else fallback
|
|
|
|
func _literal_filename(token: String) -> String:
|
|
var text := token.strip_edges()
|
|
if text.length() < 2:
|
|
return ""
|
|
if (text.begins_with("\"") and text.ends_with("\"")) or \
|
|
(text.begins_with("'") and text.ends_with("'")):
|
|
return text.substr(1, text.length() - 2)
|
|
return ""
|