fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
+358
-25
@@ -19,15 +19,18 @@ const WEAPON_BONE := ["equip_right_hand", "equip_right", "equip_right", "equip_r
|
||||
const WEAPON_LEFT_BONE := ["", "equip_left", "", "equip_left"]
|
||||
const HitReaction = preload("res://ui/hit_reaction.gd")
|
||||
const MsaMotion = preload("res://ui/msa_motion.gd")
|
||||
const MotionRegistry = preload("res://ui/motion_registry.gd")
|
||||
const MOVE_STATES := ["walk", "run"]
|
||||
const HitCollision = preload("res://hit_collision.gd")
|
||||
const WeaponTrace = preload("res://ui/weapon_trace.gd")
|
||||
# CRaceMotionData::MOTION_MODE_* -> playersettingmodule 注册动作用的目录(与 general 同级)
|
||||
const MOTION_MODE_DIRS := {1: "general", 2: "onehand_sword", 3: "twohand_sword", 4: "dualhand_sword",
|
||||
5: "bow", 6: "fan", 7: "bell", 8: "fishing", 9: "horse", 10: "horse_onehand_sword",
|
||||
11: "horse_twohand_sword", 12: "horse_dualhand_sword", 13: "horse_bow", 14: "horse_fan",
|
||||
15: "horse_bell", 16: "wedding"}
|
||||
const MOTION_NORMAL_ATTACK := 13 # CRaceMotionData::NAME_NORMAL_ATTACK;COMBO_ATTACK_1 = 14
|
||||
# 受击动作步 -> playersettingmodule RegisterMotionData 的文件(同名多份按权重随机,这里等权)
|
||||
# 受击动作步 -> playersettingmodule RegisterMotionData 的文件名回退。
|
||||
# 实际选择优先消费 MotionRegistry 中的 (mode,index) 注册项及其权重。
|
||||
const REACTION_FILES := {
|
||||
"damage": ["damage", "damage_1"], # NAME_DAMAGE
|
||||
"damage_back": ["damage_2", "damage_3"], # NAME_DAMAGE_BACK
|
||||
@@ -36,6 +39,28 @@ const REACTION_FILES := {
|
||||
"standup": ["falling_stand"], # NAME_STAND_UP
|
||||
"standup_back": ["back_falling_stand"], # NAME_STAND_UP_BACK
|
||||
}
|
||||
const MOTION_INDEX_BY_STATE := {
|
||||
"wait": 1,
|
||||
"walk": 2,
|
||||
"run": 3,
|
||||
"damage": 5,
|
||||
"knockdown": 6,
|
||||
"standup": 7,
|
||||
"damage_back": 8,
|
||||
"knockdown_back": 9,
|
||||
"standup_back": 10,
|
||||
"dead": 11,
|
||||
"attack": 14, # PC playersettingmodule stores normal/first swing as COMBO_ATTACK_1
|
||||
"combo": 14,
|
||||
}
|
||||
const REACTION_INDEX := {
|
||||
"damage": 5,
|
||||
"damage_back": 8,
|
||||
"knockdown": 6,
|
||||
"knockdown_back": 9,
|
||||
"standup": 7,
|
||||
"standup_back": 10,
|
||||
}
|
||||
|
||||
# CActorInstance::__SetMotion 尾:动作绑定完成(net_play 据此清 m_HitDataMap / 连击段号)。
|
||||
signal motion_bound(state: String)
|
||||
@@ -47,14 +72,22 @@ var action_dir := ""
|
||||
var _state := ""
|
||||
var _move_speed_ratio := 1.0
|
||||
var _assets_root := ""
|
||||
var _motion_registry: RefCounted
|
||||
var _audio: Node
|
||||
var _sound_instances: Array = []
|
||||
var _sound_frame := -1
|
||||
var _forward := ["weapon_gr2", "shield_gr2", "gr2_path", "hair_gr2", "hair_skin", "specular_power"]
|
||||
var _left_bone := ""
|
||||
var _race := 0
|
||||
var _skill_loop_count := 0
|
||||
var _skill_moving := false
|
||||
var _motion_hidden := false
|
||||
var _hit := HitReaction.new()
|
||||
var _motion_mode := 1 # CRaceMotionData::MODE_GENERAL
|
||||
var _weapon_trace: Node
|
||||
var _weapon_traces: Array = [] # 40250 m_WeaponTraceVector:右手/左手逐部件保存
|
||||
var _weapon_trace_allowed := true
|
||||
var _weapon_trace_reach_scale := 1.0 # CActorInstance::m_fReachScale
|
||||
|
||||
# pump 可空:每个重活(gr2 解析 / LOD 扫描 / hair 折叠 / .msa 解析)之间调一下
|
||||
# 它(= M2Client.net_poll),免得整段阻塞几秒漏 PONG 被服务器踢。
|
||||
@@ -71,6 +104,7 @@ func build(assets_root: String, race: int, pump := Callable()) -> bool:
|
||||
return false
|
||||
motion_dir = base.path_join("general")
|
||||
action_dir = base.path_join("action")
|
||||
_motion_registry = MotionRegistry.load_for(_assets_root, _race)
|
||||
|
||||
model = ClassDB.instantiate("Metin2Model")
|
||||
model.name = "Metin2Model"
|
||||
@@ -116,6 +150,7 @@ func build(assets_root: String, race: int, pump := Callable()) -> bool:
|
||||
if pump.is_valid(): pump.call()
|
||||
set_anim_state("wait") # 重:解 .msa
|
||||
if pump.is_valid(): pump.call()
|
||||
call_deferred("_refresh_weapon_trace")
|
||||
CharShadow.attach(self, 1.3) # 脚下接触阴影 + 强制 cast_shadow
|
||||
return true
|
||||
|
||||
@@ -123,8 +158,93 @@ func set_audio(audio_node: Node) -> void:
|
||||
_audio = audio_node
|
||||
_refresh_sound_script(String(anim.get("anim_path")) if anim else "")
|
||||
|
||||
# 40250 CActorInstance::__HideEvent / __ShowEvent. Keep the state on the
|
||||
# actor view so a type-8 event cannot be undone by an unrelated child refresh.
|
||||
func set_motion_hidden(hidden: bool) -> void:
|
||||
_motion_hidden = hidden
|
||||
if is_instance_valid(model) and model is Node3D:
|
||||
(model as Node3D).visible = not hidden
|
||||
|
||||
func is_motion_hidden() -> bool:
|
||||
return _motion_hidden
|
||||
|
||||
# 40250 CActorInstance::ProcessMotionEventFly 起点:FlyPosition 先按角色
|
||||
# 资源坐标转换,AttachingBoneName 再叠加当前骨骼平移。复用 motion effect
|
||||
# 的同一坐标变换,确保本地箭矢与挂骨特效从同一个点出发。
|
||||
func motion_event_world_position(event: Dictionary) -> Vector3:
|
||||
var source_variant: Variant = event.get("fly_pos", Vector3.ZERO)
|
||||
var source: Vector3 = source_variant if source_variant is Vector3 else Vector3.ZERO
|
||||
if is_instance_valid(model):
|
||||
var bone := Transform3D.IDENTITY
|
||||
if bool(event.get("fly_attaching", false)) and anim != null \
|
||||
and anim.has_method("get_effect_bone_pose"):
|
||||
var bone_name := String(event.get("fly_bone", ""))
|
||||
if bone_name != "":
|
||||
var pose: Dictionary = anim.get_effect_bone_pose(bone_name)
|
||||
if pose.has("transform"):
|
||||
bone = pose["transform"]
|
||||
var source_to_metres := Transform3D(
|
||||
Basis(Vector3(0.01, 0, 0), Vector3(0, 0, -0.01), Vector3(0, 0.01, 0)),
|
||||
Vector3.ZERO)
|
||||
var offset := Transform3D(Basis.IDENTITY, source)
|
||||
return (model.global_transform * offset * bone * source_to_metres.affine_inverse()).origin
|
||||
return global_transform * Vector3(source.x * 0.01, source.z * 0.01, -source.y * 0.01)
|
||||
|
||||
# ActorInstanceAttach::__IsWeaponTrace / __DestroyWeaponTrace seam. EquipModel
|
||||
# calls this after it knows the item subtype; direct PlayerView callers keep the
|
||||
# default enabled behavior for ordinary sword-like weapon meshes.
|
||||
func set_weapon_trace_enabled(enabled: bool) -> void:
|
||||
_weapon_trace_allowed = enabled
|
||||
if not enabled:
|
||||
_destroy_weapon_trace()
|
||||
call_deferred("_refresh_weapon_trace")
|
||||
|
||||
func weapon_trace_state() -> Dictionary:
|
||||
if _weapon_traces.is_empty():
|
||||
return {"present": false, "enabled": _weapon_trace_allowed}
|
||||
var primary: Node = _weapon_traces[0]
|
||||
var state: Dictionary = primary.debug_state() if is_instance_valid(primary) and primary.has_method("debug_state") else {}
|
||||
var any_playing := false
|
||||
for trace in _weapon_traces:
|
||||
if is_instance_valid(trace) and trace.has_method("is_playing") and trace.is_playing():
|
||||
any_playing = true
|
||||
state["count"] = _weapon_traces.size()
|
||||
state["playing"] = any_playing
|
||||
state["present"] = true
|
||||
state["enabled"] = _weapon_trace_allowed
|
||||
return state
|
||||
|
||||
func weapon_reach_scale() -> float:
|
||||
return _weapon_trace_reach_scale
|
||||
|
||||
# 40250 PythonCharacterModule::chrWeaponTraceSetTexture / UseTextureWeaponTrace /
|
||||
# UseAlphaWeaponTrace。当前端按 m_WeaponTraceVector 广播到每个武器部件。
|
||||
func weapon_trace_set_texture(path: String) -> void:
|
||||
for trace in _weapon_traces:
|
||||
if is_instance_valid(trace) and trace.has_method("set_texture"):
|
||||
trace.set_texture(path)
|
||||
|
||||
func weapon_trace_use_texture() -> void:
|
||||
for trace in _weapon_traces:
|
||||
if is_instance_valid(trace) and trace.has_method("use_texture_mode"):
|
||||
trace.use_texture_mode()
|
||||
|
||||
func weapon_trace_use_alpha() -> void:
|
||||
for trace in _weapon_traces:
|
||||
if is_instance_valid(trace) and trace.has_method("use_alpha"):
|
||||
trace.use_alpha()
|
||||
|
||||
func _set_weapon_trace_reach_scale(value: float) -> void:
|
||||
_weapon_trace_reach_scale = maxf(value, 0.0)
|
||||
for trace in _weapon_traces:
|
||||
if is_instance_valid(trace) and trace.has_method("set_reach_scale"):
|
||||
trace.set_reach_scale(_weapon_trace_reach_scale)
|
||||
|
||||
func set_anim_state(s: String) -> void:
|
||||
if s == _state or anim == null or motion_dir == "":
|
||||
# 40250 SetLoopMotion still reaches __SetMotion for the current motion;
|
||||
# that path calls __ShowEvent() when a previous CHARACTER_HIDE event left
|
||||
# the actor hidden. Do not let the same-state fast path keep it invisible.
|
||||
if (s == _state and not _motion_hidden) or anim == null or motion_dir == "":
|
||||
return
|
||||
if HitReaction.is_reaction(s):
|
||||
_hit.start([[s]], _state, _bind_reaction)
|
||||
@@ -142,11 +262,15 @@ func set_anim_state(s: String) -> void:
|
||||
return
|
||||
else:
|
||||
return
|
||||
var msa := _motion_file(s)
|
||||
# CActorInstance::SetLoopMotion/InterceptMotion resolve GetMotionKey
|
||||
# before mutating the current motion node. A missing registered key must
|
||||
# leave the old animation/state in place, rather than becoming a dead label.
|
||||
if msa == "":
|
||||
return
|
||||
_hit.cancel()
|
||||
_state = s
|
||||
var msa := _motion_file(s)
|
||||
if msa != "":
|
||||
_bind(s, msa, s in HitReaction.LOOP_STATES, _loop_speed(s))
|
||||
_bind(s, msa, s in HitReaction.LOOP_STATES, _loop_speed(s))
|
||||
|
||||
# CActorInstance::SetMoveSpeed:m_fMovSpd = movSpd / 100,走 / 跑循环动作按它播放
|
||||
# (Move -> SetLoopMotion(WALK/RUN, 0.15, m_fMovSpd))。>1100 时参考端不走动作累计,
|
||||
@@ -163,11 +287,11 @@ func _loop_speed(state: String) -> float:
|
||||
## 走 / 跑动作的根运动速度(cm/s,movSpd 100):x = walk,y = run。0 = 取不到。
|
||||
## net_world 推给 EntityStore,远端移动按动作累计量推进(CActorInstance::AccumulationMovement)。
|
||||
func get_move_motion_speeds() -> Vector2:
|
||||
var dir := _mode_motion_dir() if motion_dir != "" else ""
|
||||
if dir == "":
|
||||
if motion_dir == "":
|
||||
return Vector2.ZERO
|
||||
return Vector2(MsaMotion.move_speed(dir.path_join("walk.msa")),
|
||||
MsaMotion.move_speed(dir.path_join("run.msa")))
|
||||
var walk := _motion_file("walk")
|
||||
var run := _motion_file("run")
|
||||
return Vector2(MsaMotion.move_speed(walk), MsaMotion.move_speed(run))
|
||||
|
||||
# CInstanceBase::RefreshState selects every motion through m_wcurMotionMode,
|
||||
# not only attack/combo. A two-handed weapon therefore uses twohand_sword for
|
||||
@@ -257,18 +381,28 @@ func set_motion_id(motion: int, target_race: int = -1) -> bool:
|
||||
return true
|
||||
|
||||
# §3.7 CActorInstance::__SetMotion(SSetMotionData{ MAKE_MOTION_KEY(mode, index), fSpeedRatio }):
|
||||
# 攻击段动作按武器动作模式目录绑定(NORMAL_ATTACK -> attack(_1).msa,COMBO_ATTACK_N -> combo_0N.msa),
|
||||
# 模式目录缺该段时退回 general 的 attack(_1).msa(GENERAL 模式的 COMBO_ATTACK_* 也注册这两份)。
|
||||
# 攻击段动作按武器动作模式目录绑定(NORMAL_ATTACK/COMBO_ATTACK_1 使用
|
||||
# playersettingmodule 注册的 index 14,后续段使用 combo_0N.msa);普通武器模式
|
||||
# 缺 key 时由 CRaceData 只回退 GENERAL,HORSE_* 只回退 HORSE。
|
||||
func play_attack_motion(mode: int, index: int, speed_ratio: float) -> bool:
|
||||
if anim == null or motion_dir == "":
|
||||
return false
|
||||
var mode_dir := motion_dir.get_base_dir().path_join(String(MOTION_MODE_DIRS.get(mode, "general")))
|
||||
var names: Array = ["attack", "attack_1"]
|
||||
if index > MOTION_NORMAL_ATTACK:
|
||||
names = ["combo_%02d" % (index - MOTION_NORMAL_ATTACK)]
|
||||
var msa := _pick_msa(mode_dir, names)
|
||||
if msa == "":
|
||||
msa = _pick_msa(motion_dir, ["attack", "attack_1"])
|
||||
var registry_index := index if index > MOTION_NORMAL_ATTACK else MOTION_NORMAL_ATTACK + 1
|
||||
var msa := ""
|
||||
for dir in _motion_dirs_for_motion(mode, registry_index):
|
||||
var registry_mode := _motion_mode_for_dir(dir)
|
||||
# PC playersettingmodule registers the normal swing under
|
||||
# COMBO_ATTACK_1 (14), including the GENERAL attack/attack_1 pair.
|
||||
# The network's NAME_NORMAL_ATTACK (13) therefore resolves to the same
|
||||
# registered resource when the mode has no dedicated normal entry.
|
||||
msa = _pick_registered_msa(dir, registry_mode, registry_index, names)
|
||||
if msa == "" and (_motion_registry == null or not _motion_registry.source_loaded):
|
||||
msa = _pick_msa(dir, names)
|
||||
if msa != "":
|
||||
break
|
||||
if msa == "":
|
||||
return false
|
||||
_bind("attack", msa, false, speed_ratio if speed_ratio > 0.0 else 1.0)
|
||||
@@ -276,7 +410,8 @@ func play_attack_motion(mode: int, index: int, speed_ratio: float) -> bool:
|
||||
|
||||
# 40250 CInstanceBase::NEW_UseSkill (InstanceBaseBattle.cpp:310)
|
||||
# 播放指定技能动作(.msa),依据 grade (0=普, 1=M, 2=G, 3=P) 读取对应阶位动作文件。
|
||||
func play_skill_motion(motion_name: String, grade: int = 0) -> bool:
|
||||
func play_skill_motion(motion_name: String, grade: int = 0, loop_count: int = 0,
|
||||
moving_skill: bool = false) -> bool:
|
||||
if anim == null:
|
||||
return false
|
||||
var cls: String = CLASS_OF[_race & 3]
|
||||
@@ -292,9 +427,24 @@ func play_skill_motion(motion_name: String, grade: int = 0) -> bool:
|
||||
msa = sdir.path_join(motion_name + ".msa")
|
||||
if not FileAccess.file_exists(msa):
|
||||
return false
|
||||
_skill_loop_count = maxi(0, loop_count)
|
||||
_skill_moving = moving_skill
|
||||
_bind("__skill", msa, false, 1.0)
|
||||
# 40250 applies the network/local uMotLoopCount after binding the motion;
|
||||
# the native player then uses the MSA LoopData segment, not a whole-clip
|
||||
# loop. Zero leaves the resource-defined LoopData count intact.
|
||||
if anim.has_method("set_motion_loop_count"):
|
||||
anim.call("set_motion_loop_count", _skill_loop_count)
|
||||
# Keep the 40250 OnUseSkill payload visible to the animation/effect bridge;
|
||||
# the native player still owns clip timing, while callers can distinguish a
|
||||
# finite loop override from an ordinary one-shot and moving-skill motion.
|
||||
anim.set_meta("skill_loop_count", _skill_loop_count)
|
||||
anim.set_meta("skill_moving", _skill_moving)
|
||||
return true
|
||||
|
||||
func skill_motion_state() -> Dictionary:
|
||||
return {"loop_count": _skill_loop_count, "moving": _skill_moving}
|
||||
|
||||
const AFFECT_EFFECT_MAP := {
|
||||
15: {"file": "d:/ymir work/pc/warrior/effect/geom_sword_loop.mse", "bone": "equip_right_hand"}, # AFFECT_GEOMGYEONG
|
||||
16: {"file": "d:/ymir work/pc/warrior/effect/gyeokgongjang_loop.mse", "bone": ""}, # AFFECT_CHEONGEUN
|
||||
@@ -323,6 +473,11 @@ func set_fx(fx_registry: RefCounted) -> void:
|
||||
|
||||
func set_affect(affect_type: int, visible: bool, fx_registry: RefCounted = null) -> void:
|
||||
var reg: RefCounted = fx_registry if fx_registry != null else _fx
|
||||
# InstanceBaseEffect.cpp::__Warrior_SetGeomgyeongAffect changes the actor's
|
||||
# reach scale independently of whether the visual MSE can be spawned. The
|
||||
# same value is consumed by both WeaponTrace::Update and attack collision.
|
||||
if affect_type == 15:
|
||||
_set_weapon_trace_reach_scale(1.5 if visible else 1.0)
|
||||
if not visible:
|
||||
if _affect_nodes.has(affect_type):
|
||||
var n: Node = _affect_nodes[affect_type]
|
||||
@@ -365,6 +520,7 @@ func clear_affects() -> void:
|
||||
if is_instance_valid(n):
|
||||
n.queue_free()
|
||||
_affect_nodes.clear()
|
||||
_set_weapon_trace_reach_scale(1.0)
|
||||
|
||||
# CGraphicThingInstance::InsertDelay(fStiffenTime):动作冻结,结束后恢复 fSpeedRatio。
|
||||
func insert_delay(d: float) -> void:
|
||||
@@ -373,14 +529,30 @@ func insert_delay(d: float) -> void:
|
||||
# CActorInstance::__HitGood / __HitGreate / __HitStone(ActorInstanceBattle.cpp)。
|
||||
# scalar = dot(攻击方朝向, 受击方朝向):< 0 面对面 -> 正面受击动作。
|
||||
func hit_good(scalar: float, stunned: bool) -> void:
|
||||
# ActorInstanceBattle::__HitGood returns while NAME_DAMAGE_FLYING is
|
||||
# active; the semantic resume state is still "run"/"wait", so checking
|
||||
# _state alone would incorrectly replace the knockdown chain with DAMAGE.
|
||||
if _hit.current_step in HitReaction.KNOCKDOWN_STATES:
|
||||
return
|
||||
_hit.start(_hit.good(_state, scalar, stunned), _state, _bind_reaction)
|
||||
|
||||
func hit_greate(scalar: float, stunned: bool) -> void:
|
||||
# __HitGreate has both IsKnockDown and __IsStandUpMotion early returns.
|
||||
if _hit.current_step in HitReaction.KNOCKDOWN_STATES or \
|
||||
_hit.current_step in HitReaction.STANDUP_STATES:
|
||||
return
|
||||
_hit.start(_hit.greate(_state, scalar, stunned), _state, _bind_reaction, stunned)
|
||||
|
||||
func hit_stone(stunned: bool) -> void:
|
||||
_hit.stone(stunned)
|
||||
|
||||
# CActorInstance::TEMP_Push large-correction tail:
|
||||
# InterceptOnceMotion(DAMAGE_FLYING) + PushOnceMotion(STAND_UP).
|
||||
func sync_push_motion() -> bool:
|
||||
if anim == null:
|
||||
return false
|
||||
return _hit.start([["knockdown", "standup"], ["standup"]], _state, _bind_reaction)
|
||||
|
||||
# 40250 CActorInstance::__Shake(100)
|
||||
func shake() -> void:
|
||||
_hit.shake()
|
||||
@@ -401,6 +573,8 @@ func _on_playback_finished() -> void:
|
||||
set_anim_state(_hit.resume)
|
||||
return
|
||||
if _state in ["__motion", "__skill", "attack", "combo"]:
|
||||
_skill_loop_count = 0
|
||||
_skill_moving = false
|
||||
_state = ""
|
||||
set_anim_state("wait")
|
||||
motion_bound.emit("wait")
|
||||
@@ -408,6 +582,8 @@ func _on_playback_finished() -> void:
|
||||
|
||||
func reset_skill_motion() -> void:
|
||||
if _state in ["__skill", "__motion"]:
|
||||
_skill_loop_count = 0
|
||||
_skill_moving = false
|
||||
_state = ""
|
||||
set_anim_state("wait")
|
||||
motion_bound.emit("wait")
|
||||
@@ -420,7 +596,7 @@ func is_in_hit_reaction() -> bool:
|
||||
func _bind_reaction(step: String) -> bool:
|
||||
if anim == null or motion_dir == "":
|
||||
return false
|
||||
var msa := _pick_mode_msa(REACTION_FILES.get(step, []))
|
||||
var msa := _pick_mode_msa(REACTION_FILES.get(step, []), int(REACTION_INDEX.get(step, -1)))
|
||||
if msa == "":
|
||||
return false
|
||||
_bind(step, msa, false, 1.0)
|
||||
@@ -429,7 +605,16 @@ func _bind_reaction(step: String) -> bool:
|
||||
func _bind(state: String, path: String, loop: bool, speed_ratio: float) -> void:
|
||||
if not HitReaction.is_reaction(state):
|
||||
_hit.cancel()
|
||||
_state = state
|
||||
_state = state
|
||||
# CActorInstance::__SetMotion calls __ShowEvent() after a valid motion key
|
||||
# is resolved, before the new motion starts.
|
||||
if _motion_hidden:
|
||||
set_motion_hidden(false)
|
||||
# 40250 __SetMotion always hides the old trace before binding a new motion;
|
||||
# only a successfully bound attack motion shows it again below.
|
||||
for trace in _weapon_traces:
|
||||
if is_instance_valid(trace):
|
||||
trace.turn_off()
|
||||
anim.set("loop", loop)
|
||||
# A repeated one-shot can name the same clip; clear first because the native
|
||||
# player treats assigning the current anim_path as a no-op.
|
||||
@@ -439,6 +624,11 @@ func _bind(state: String, path: String, loop: bool, speed_ratio: float) -> void:
|
||||
anim.set("playing", true)
|
||||
anim.set("time_scale", _hit.scale_for(speed_ratio))
|
||||
_refresh_sound_script(path)
|
||||
if state in ["attack", "combo"] and _weapon_trace_allowed:
|
||||
_refresh_weapon_trace()
|
||||
for trace in _weapon_traces:
|
||||
if is_instance_valid(trace):
|
||||
trace.turn_on()
|
||||
motion_bound.emit(state)
|
||||
|
||||
func _pick_msa(dir: String, names: Array) -> String:
|
||||
@@ -449,25 +639,124 @@ func _pick_msa(dir: String, names: Array) -> String:
|
||||
found.append(p)
|
||||
return "" if found.is_empty() else String(found[randi() % found.size()])
|
||||
|
||||
func _motion_mode_for_dir(dir: String) -> int:
|
||||
var name := String(dir).get_file()
|
||||
for mode in MOTION_MODE_DIRS:
|
||||
if String(MOTION_MODE_DIRS[mode]) == name:
|
||||
return int(mode)
|
||||
return 1
|
||||
|
||||
func _pick_registered_msa(dir: String, mode: int, index: int, fallback_names: Array) -> String:
|
||||
if _motion_registry == null or index < 0:
|
||||
return ""
|
||||
var registered: Array = _motion_registry.variants(mode, index)
|
||||
if registered.is_empty():
|
||||
return ""
|
||||
var available: Array = []
|
||||
for entry in registered:
|
||||
var name := String(entry.get("name", ""))
|
||||
if name == "":
|
||||
continue
|
||||
var path := String(dir).path_join(name)
|
||||
if FileAccess.file_exists(path):
|
||||
available.append({"path": path, "weight": int(entry.get("weight", 100))})
|
||||
if available.is_empty():
|
||||
return ""
|
||||
if available.size() == 1:
|
||||
return String(available[0]["path"])
|
||||
# CActorInstance::GetRandomMotionKey uses random() % 100, subtracts
|
||||
# byPercentage in registration order, and leaves sub-index 0 on overflow.
|
||||
var percentage := randi() % 100
|
||||
for entry in available:
|
||||
percentage -= int(entry["weight"])
|
||||
if percentage < 0:
|
||||
return String(entry["path"])
|
||||
return String(available[0]["path"])
|
||||
|
||||
func _mode_motion_dir() -> String:
|
||||
var dirs := _motion_dirs_for_mode(_motion_mode)
|
||||
return String(dirs[0]) if not dirs.is_empty() else ""
|
||||
|
||||
# 40250 CRaceData::GetMotionKey:先确认 mode 已注册,再按 motion index 查找。
|
||||
# 普通模式缺 key 回退 GENERAL;HORSE_* 武器模式只回退 HORSE,HORSE 也缺失时失败。
|
||||
# 这条顺序不能只用“具体目录 -> general”,否则骑马武器的未注册动作会错误
|
||||
# 使用普通姿态,掩盖 InterceptMotion/PushMotion 的失败分支。
|
||||
func _motion_dir_for_mode(mode: int) -> String:
|
||||
if motion_dir == "":
|
||||
return ""
|
||||
var candidate := motion_dir.get_base_dir().path_join(String(MOTION_MODE_DIRS.get(_motion_mode, "general")))
|
||||
return candidate if DirAccess.dir_exists_absolute(candidate) else motion_dir
|
||||
var base := motion_dir.get_base_dir()
|
||||
var path := base.path_join(String(MOTION_MODE_DIRS.get(mode, "")))
|
||||
return path if path != "" and DirAccess.dir_exists_absolute(path) else ""
|
||||
|
||||
func _motion_dirs_for_motion(mode: int, index: int) -> Array[String]:
|
||||
if _motion_registry == null or not _motion_registry.source_loaded:
|
||||
return _motion_dirs_for_mode(mode)
|
||||
if not _motion_registry.has_mode(mode):
|
||||
return []
|
||||
var selected := mode
|
||||
if not _motion_registry.has_variants(mode, index):
|
||||
selected = 9 if mode >= 10 and mode <= 15 else 1
|
||||
if not _motion_registry.has_variants(selected, index):
|
||||
return []
|
||||
var path := _motion_dir_for_mode(selected)
|
||||
return [path] if path != "" else []
|
||||
|
||||
func _motion_dirs_for_mode(mode: int) -> Array[String]:
|
||||
if motion_dir == "":
|
||||
return []
|
||||
var dirs: Array[String] = []
|
||||
var base := motion_dir.get_base_dir()
|
||||
var specific := base.path_join(String(MOTION_MODE_DIRS.get(mode, "general")))
|
||||
if DirAccess.dir_exists_absolute(specific):
|
||||
dirs.append(specific)
|
||||
if mode >= 10 and mode <= 15:
|
||||
var horse := base.path_join(String(MOTION_MODE_DIRS[9]))
|
||||
if DirAccess.dir_exists_absolute(horse) and not dirs.has(horse):
|
||||
dirs.append(horse)
|
||||
if not dirs.has(motion_dir):
|
||||
dirs.append(motion_dir)
|
||||
return dirs
|
||||
|
||||
func _motion_file(state: String) -> String:
|
||||
for dir in [_mode_motion_dir(), motion_dir]:
|
||||
var index := int(MOTION_INDEX_BY_STATE.get(state, -1))
|
||||
var fallback_names: Array = [state]
|
||||
if state == "damage":
|
||||
fallback_names = ["damage", "damage_1"]
|
||||
elif state == "damage_back":
|
||||
fallback_names = ["damage_2", "damage_3"]
|
||||
elif state == "knockdown":
|
||||
fallback_names = ["damage_flying"]
|
||||
elif state == "knockdown_back":
|
||||
fallback_names = ["back_damage_flying"]
|
||||
elif state == "standup":
|
||||
fallback_names = ["falling_stand"]
|
||||
elif state == "standup_back":
|
||||
fallback_names = ["back_falling_stand"]
|
||||
elif state in ["attack", "combo"]:
|
||||
fallback_names = ["attack", "attack_1"] if state == "attack" else ["combo_01"]
|
||||
for dir in _motion_dirs_for_motion(_motion_mode, index):
|
||||
if dir == "":
|
||||
continue
|
||||
if index >= 0:
|
||||
var registered := _pick_registered_msa(dir, _motion_mode_for_dir(dir), index, fallback_names)
|
||||
if registered != "":
|
||||
return registered
|
||||
for ext in [".msa", ".gr2"]:
|
||||
var path: String = String(dir).path_join(state + String(ext))
|
||||
if _motion_registry != null and _motion_registry.source_loaded:
|
||||
continue
|
||||
var path: String = String(dir).path_join(String(fallback_names[0]) + String(ext))
|
||||
if FileAccess.file_exists(path):
|
||||
return path
|
||||
return ""
|
||||
|
||||
func _pick_mode_msa(names: Array) -> String:
|
||||
var picked := _pick_msa(_mode_motion_dir(), names)
|
||||
return picked if picked != "" else _pick_msa(motion_dir, names)
|
||||
func _pick_mode_msa(names: Array, motion_index: int = -1) -> String:
|
||||
for dir in _motion_dirs_for_motion(_motion_mode, motion_index):
|
||||
var picked := _pick_registered_msa(dir, _motion_mode_for_dir(dir), motion_index, names)
|
||||
if picked == "" and (_motion_registry == null or not _motion_registry.source_loaded):
|
||||
picked = _pick_msa(dir, names)
|
||||
if picked != "":
|
||||
return picked
|
||||
return ""
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_hit.tick(delta, anim, model as Node3D) # InsertDelay + ShakeProcess
|
||||
@@ -498,9 +787,53 @@ func _set(prop: StringName, val: Variant) -> bool:
|
||||
model.set(prop, val)
|
||||
if String(prop) == "gr2_path":
|
||||
call_deferred("_ground_model")
|
||||
if String(prop) == "weapon_gr2":
|
||||
if String(val) == "":
|
||||
_destroy_weapon_trace()
|
||||
else:
|
||||
call_deferred("_refresh_weapon_trace")
|
||||
return true
|
||||
return false
|
||||
|
||||
func _refresh_weapon_trace() -> void:
|
||||
if not _weapon_trace_allowed or not is_instance_valid(model):
|
||||
return
|
||||
# ActorInstanceAttach.cpp creates one CWeaponTrace for each attached weapon
|
||||
# part. Dagger and mounted fan can populate both PART_WEAPON and
|
||||
# PART_WEAPON_LEFT; keep the same part order and never collapse them into a
|
||||
# single trace.
|
||||
var weapons: Array[Node3D] = []
|
||||
for part_name in ["Weapon", "Shield"]:
|
||||
var part := model.find_child(part_name, true, false) as Node3D
|
||||
if part != null and is_instance_valid(part):
|
||||
weapons.append(part)
|
||||
if weapons.is_empty():
|
||||
_destroy_weapon_trace()
|
||||
return
|
||||
while _weapon_traces.size() > weapons.size():
|
||||
var extra: Node = _weapon_traces.pop_back()
|
||||
if is_instance_valid(extra):
|
||||
extra.detach_weapon()
|
||||
extra.free()
|
||||
while _weapon_traces.size() < weapons.size():
|
||||
var trace: Node = WeaponTrace.new()
|
||||
trace.name = "WeaponTrace_%d" % _weapon_traces.size()
|
||||
add_child(trace)
|
||||
_weapon_traces.append(trace)
|
||||
for i in weapons.size():
|
||||
var trace: Node = _weapon_traces[i]
|
||||
trace.attach_to_weapon(weapons[i])
|
||||
trace.set_reach_scale(_weapon_trace_reach_scale)
|
||||
_weapon_trace = _weapon_traces[0]
|
||||
|
||||
func _destroy_weapon_trace() -> void:
|
||||
for trace in _weapon_traces:
|
||||
if is_instance_valid(trace):
|
||||
trace.detach_weapon()
|
||||
trace.free()
|
||||
_weapon_traces.clear()
|
||||
_weapon_trace = null
|
||||
|
||||
# EquipModel operates on the outer PlayerView, while the material palette lives
|
||||
# on the native Metin2Model child. Keep these as explicit methods so MSM
|
||||
# SourceSkin -> TargetSkin remaps reach both character select and the game.
|
||||
|
||||
Reference in New Issue
Block a user