no message
This commit is contained in:
@@ -5,9 +5,11 @@ extends SceneTree
|
||||
|
||||
const MobView = preload("res://ui/mob_view.gd")
|
||||
const Audio = preload("res://audio.gd")
|
||||
const MsaMotion = preload("res://ui/msa_motion.gd")
|
||||
|
||||
var _fail := 0
|
||||
var _resolution_done := false
|
||||
var _move_speed_done := false
|
||||
func _ck(c: bool, m: String) -> void:
|
||||
if not c:
|
||||
_fail += 1
|
||||
@@ -26,6 +28,7 @@ class FakeAnim:
|
||||
extends Node
|
||||
var anim_path := ""
|
||||
var loop := false
|
||||
var time_scale := 1.0
|
||||
|
||||
# 纯解析:不需要扩展和资产。旧客户端 CRaceData::GetMotionKey 找不到动作时
|
||||
# SetLoopMotion/InterceptMotion 直接返回、保留当前动作;不能静默换成 WAIT。
|
||||
@@ -57,8 +60,49 @@ func _check_motion_resolution() -> void:
|
||||
mv.free()
|
||||
_resolution_done = true
|
||||
|
||||
# CActorInstance::Move:走 / 跑按 m_fMovSpd = movSpd/100 播放;根运动速度
|
||||
# = |.msa Accumulation| / MotionDuration(wolf 03.msa:255.45 / 0.6 = 425.75 cm/s)。
|
||||
func _write_msa(path: String, duration: float, ay: float) -> void:
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
f.store_string("ScriptType MotionData\n\nMotionFileName \"x.gr2\"\n" +
|
||||
"MotionDuration %f\nAccumulation 0.00\t%.2f\t0.00\n" % [duration, ay])
|
||||
f.close()
|
||||
|
||||
func _check_move_speed() -> void:
|
||||
var walk_p := "user://__mob_view_test_walk.msa"
|
||||
var run_p := "user://__mob_view_test_run.msa"
|
||||
_write_msa(walk_p, 0.866667, -75.81)
|
||||
_write_msa(run_p, 0.6, -255.45)
|
||||
_ck(absf(MsaMotion.move_speed(run_p) - 425.75) < 0.01, "run msa 速度 425.75(%f)" % MsaMotion.move_speed(run_p))
|
||||
_ck(absf(MsaMotion.move_speed(walk_p) - 87.47) < 0.01, "walk msa 速度 87.47(%f)" % MsaMotion.move_speed(walk_p))
|
||||
_ck(MsaMotion.move_speed("user://__no_such.msa") == 0.0, "缺文件 -> 0")
|
||||
var mv: Node3D = MobView.new()
|
||||
mv._dir = "res://__no_such_mob_dir__"
|
||||
mv._motions = {"WAIT": "wait.msa", "WALK": walk_p, "RUN": run_p}
|
||||
var sp: Vector2 = mv.get_move_motion_speeds()
|
||||
_ck(absf(sp.x - 87.47) < 0.01 and absf(sp.y - 425.75) < 0.01, "get_move_motion_speeds(%s)" % sp)
|
||||
var anim := FakeAnim.new()
|
||||
mv.anim = anim
|
||||
mv.set_move_speed(200)
|
||||
mv.set_anim_state("wait")
|
||||
_ck(anim.time_scale == 1.0, "wait 不受移动速度影响")
|
||||
mv.set_anim_state("run")
|
||||
_ck(anim.anim_path == run_p and anim.time_scale == 2.0, "run 按 movSpd/100 播放(%f)" % anim.time_scale)
|
||||
mv.set_move_speed(150)
|
||||
_ck(anim.time_scale == 1.5, "跑动中改速立即生效(%f)" % anim.time_scale)
|
||||
mv.set_move_speed(1200)
|
||||
_ck(anim.time_scale == 1.0, ">1100 保持原速(%f)" % anim.time_scale)
|
||||
mv.anim = null
|
||||
anim.free()
|
||||
mv.free()
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(walk_p))
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(run_p))
|
||||
_move_speed_done = true
|
||||
|
||||
func _run() -> void:
|
||||
_check_motion_resolution()
|
||||
_check_move_speed()
|
||||
_ck(_move_speed_done, "移动速度断言全部执行")
|
||||
# 脚本运行时错误只会中止函数、不计失败;用哨兵保证整段断言都执行过。
|
||||
_ck(_resolution_done, "动作解析断言全部执行")
|
||||
if not ClassDB.class_exists("Metin2Model") or not ClassDB.class_exists("Metin2Proto"):
|
||||
|
||||
+27
-4
@@ -48,6 +48,12 @@ const FUNC_COMBO := 3
|
||||
const FUNC_MOB_SKILL := 4
|
||||
const FUNC_EMOTION := 5
|
||||
const FUNC_SKILL := 0x80
|
||||
# packet.h EWalkModes(GC_WALK_MODE / GC_CHARACTER_ADD_INFO.bWalkMode)
|
||||
const WALKMODE_RUN := 0
|
||||
const WALKMODE_WALK := 1
|
||||
# CActorInstance c_fDefaultRotationSpeed / c_fDefaultHorseRotationSpeed(度/秒)
|
||||
const ROTATION_SPEED := 1200.0
|
||||
const ROTATION_SPEED_HORSE := 300.0
|
||||
|
||||
# EDamageFlag / IsShowDamage / ProcessDamage 分类见 damage_effect.gd(DamageEffect.DAMAGE_*)。
|
||||
|
||||
@@ -423,12 +429,26 @@ func _on_spawn(d: Dictionary) -> void:
|
||||
_refresh_shop_sign(node, str(d.get("shop_sign", "")))
|
||||
_refresh_pvp_tag(vid)
|
||||
_apply_field_updates(node, d)
|
||||
_push_motion_speeds(vid, node)
|
||||
_apply_name_color(vid)
|
||||
_apply_text_tail(vid)
|
||||
entity_added.emit(node, vid)
|
||||
if vid == _main_vid:
|
||||
main_entity_ready.emit(node, vid)
|
||||
|
||||
# CActorInstance::Move -> SetLoopMotion(WALK/RUN, .., m_fMovSpd):远端位移按动作根运动
|
||||
# 累计(.msa Accumulation / MotionDuration × movSpd/100)推进。模型建好后把走 / 跑
|
||||
# 速度交给 EntityStore;占位胶囊 / 骑马(马的动作不在 general 目录)不推,EntityStore
|
||||
# 退回按服务端 duration 线性插值。
|
||||
func _push_motion_speeds(vid: int, n: Node3D) -> void:
|
||||
if not n.has_method("get_move_motion_speeds") or not client.has_method("set_entity_motion_speed"):
|
||||
return
|
||||
if int(n.get_meta("mount_vnum", 0)) != 0:
|
||||
return
|
||||
var sp: Vector2 = n.call("get_move_motion_speeds")
|
||||
if sp.x > 0.0 or sp.y > 0.0:
|
||||
client.call("set_entity_motion_speed", vid, sp.x, sp.y)
|
||||
|
||||
# GC_CHAR_ADD_INFO 到了:刷新头顶名字 / HP 上限(节点已存在)。
|
||||
func _on_info(vid: int, d: Dictionary) -> void:
|
||||
var n: Node3D = _by_vid.get(vid, null)
|
||||
@@ -1287,12 +1307,15 @@ func _process(dt: float) -> void:
|
||||
want = _pushed_position(vid, e, dt, want)
|
||||
n.position = want
|
||||
# 朝向:Metin2 heading(度) -> Godot yaw。经验换算,真机再校准。
|
||||
# CActorInstance::RotationProcess:匀角速度转向 c_fDefaultRotationSpeed 1200°/s,
|
||||
# 骑马 c_fDefaultHorseRotationSpeed 300°/s(而不是指数逼近)。
|
||||
var yaw := MapCoord.heading_to_yaw(float(e.get("angle_deg", 0.0)))
|
||||
n.rotation.y = lerp_angle(n.rotation.y, yaw, clampf(10.0 * dt, 0.0, 1.0))
|
||||
var turn_rate := ROTATION_SPEED_HORSE if int(n.get_meta("mount_vnum", 0)) != 0 else ROTATION_SPEED
|
||||
n.rotation.y = rotate_toward(n.rotation.y, yaw, deg_to_rad(turn_rate) * dt)
|
||||
# 动画状态
|
||||
var f := int(e.get("func", FUNC_WAIT))
|
||||
var moving := bool(e.get("moving", false))
|
||||
var walk_mode := int(e.get("walk_mode", 1))
|
||||
var walk_mode := int(e.get("walk_mode", WALKMODE_RUN))
|
||||
var dead := bool(e.get("dead", false))
|
||||
if f != int(n.get_meta("func", -1)) or moving != bool(n.get_meta("moving", false)) \
|
||||
or walk_mode != int(n.get_meta("walk_mode", -1)) or dead != bool(n.get_meta("dead", false)):
|
||||
@@ -1303,13 +1326,13 @@ func _process(dt: float) -> void:
|
||||
_apply_anim(n, f, moving, walk_mode, dead)
|
||||
_update_hp_bars()
|
||||
|
||||
func _apply_anim(n: Node3D, f: int, moving: bool, walk_mode := 1, dead := false) -> void:
|
||||
func _apply_anim(n: Node3D, f: int, moving: bool, walk_mode := WALKMODE_RUN, dead := false) -> void:
|
||||
var state := "wait"
|
||||
if dead:
|
||||
# CActorInstance::__SetMotion:IsDead() 时拒绝 DEAD/DAMAGE_FLYING 以外的动作。
|
||||
state = "dead"
|
||||
elif f == FUNC_MOVE or (f == FUNC_WAIT and moving):
|
||||
state = "walk" if walk_mode == 0 else "run"
|
||||
state = "walk" if walk_mode == WALKMODE_WALK else "run"
|
||||
elif f == FUNC_ATTACK or f == FUNC_COMBO:
|
||||
state = "attack"
|
||||
elif f == FUNC_MOB_SKILL or (f & FUNC_SKILL) != 0:
|
||||
|
||||
@@ -24,6 +24,9 @@ class FakeClient extends Node:
|
||||
signal entity_dead(vid: int)
|
||||
signal damage(vid: int, amount: int, flag: int)
|
||||
var ents := {}
|
||||
var motion_speeds := {} # vid -> Vector2(walk, run)
|
||||
func set_entity_motion_speed(vid: int, walk: float, run: float) -> void:
|
||||
motion_speeds[vid] = Vector2(walk, run)
|
||||
func get_entity(vid: int) -> Dictionary: return ents.get(vid, {})
|
||||
func get_entities() -> Array: return ents.values()
|
||||
func get_pvp_relations() -> Array: return []
|
||||
@@ -126,6 +129,7 @@ func _run() -> void:
|
||||
_ck(int(added.get(2, 0)) == 2, "entity 2 rebuilt exactly once on re-entry")
|
||||
|
||||
await _dead_motion(fc, nw)
|
||||
await _walk_mode_motion(fc, nw)
|
||||
|
||||
class AnimView extends Node3D:
|
||||
var states: Array[String] = []
|
||||
@@ -159,3 +163,38 @@ func _dead_motion(fc: FakeClient, nw: Node) -> void:
|
||||
fc.entity_dead.emit(7)
|
||||
await create_tween().tween_interval(0.4).finished
|
||||
_ck(placeholder != null and placeholder.rotation.x < -1.0, "placeholder capsule (no motions) still falls over as its only death cue")
|
||||
|
||||
class MoveView extends AnimView:
|
||||
func get_move_motion_speeds() -> Vector2:
|
||||
return Vector2(87.5, 425.75)
|
||||
|
||||
# packet.h WALKMODE_RUN = 0 / WALKMODE_WALK = 1;CActorInstance::Move 按 m_isWalking
|
||||
# 选 WALK / RUN。模型建好后把走 / 跑根运动速度推给 EntityStore(骑马不推)。
|
||||
func _walk_mode_motion(fc: FakeClient, nw: Node) -> void:
|
||||
nw.set_model_factory(func(_d: Dictionary) -> Node3D: return MoveView.new())
|
||||
var walker := fc.add(8, NEAR_CM + Vector2(900, 0), {"race": 101, "ch_type": 2, "func": 1, "walk_mode": 1})
|
||||
fc.entity_spawned.emit(walker)
|
||||
var runner := fc.add(9, NEAR_CM + Vector2(1200, 0), {"race": 101, "ch_type": 2, "func": 1, "walk_mode": 0})
|
||||
fc.entity_spawned.emit(runner)
|
||||
var rider := fc.add(10, NEAR_CM + Vector2(1500, 0), {"race": 101, "ch_type": 0, "mount_vnum": 20030})
|
||||
fc.entity_spawned.emit(rider)
|
||||
await process_frame
|
||||
await process_frame # process_frame 在节点 _process 之前发
|
||||
var wv := nw.node_for(8) as AnimView
|
||||
var rv := nw.node_for(9) as AnimView
|
||||
_ck(wv != null and wv.states.back() == "walk", "walk_mode 1 (WALKMODE_WALK) -> walk: %s" % [wv.states if wv else []])
|
||||
_ck(rv != null and rv.states.back() == "run", "walk_mode 0 (WALKMODE_RUN) -> run: %s" % [rv.states if rv else []])
|
||||
_ck(fc.motion_speeds.get(8, Vector2.ZERO) == Vector2(87.5, 425.75), "spawn pushes walk/run motion speeds: %s" % [fc.motion_speeds])
|
||||
_ck(not fc.motion_speeds.has(10), "mounted actor keeps the duration lerp (no push)")
|
||||
walker["walk_mode"] = 0
|
||||
await process_frame
|
||||
await process_frame
|
||||
_ck(wv.states.back() == "run", "GC_WALK_MODE flip to run re-selects RUN: %s" % [wv.states])
|
||||
# 匀角速度转向:一帧转角不超过 1200°/s × dt。
|
||||
var before := wv.rotation.y
|
||||
walker["angle_deg"] = 180.0
|
||||
await process_frame
|
||||
var t0 := Time.get_ticks_usec()
|
||||
await process_frame
|
||||
var dt := float(Time.get_ticks_usec() - t0) / 1e6
|
||||
_ck(absf(angle_difference(before, wv.rotation.y)) <= deg_to_rad(1200.0) * (dt + 0.05), "turn limited to 1200deg/s")
|
||||
|
||||
+22
-1
@@ -30,6 +30,8 @@ const STATE_MOTIONS := {
|
||||
"standup_back": ["BACK_STANDUP"],
|
||||
}
|
||||
const HitReaction = preload("res://ui/hit_reaction.gd")
|
||||
const MsaMotion = preload("res://ui/msa_motion.gd")
|
||||
const MOVE_STATES := ["walk", "run"]
|
||||
const HitCollision = preload("res://hit_collision.gd")
|
||||
|
||||
# CActorInstance::__SetMotion 尾:动作绑定完成。
|
||||
@@ -42,6 +44,7 @@ var _mesh_stem := ""
|
||||
var _hit := HitReaction.new()
|
||||
var _motions := {} # MOTION_NAME(String) -> 绝对 .msa 路径
|
||||
var _state := ""
|
||||
var _move_speed_ratio := 1.0
|
||||
var _last_resolution := {}
|
||||
var _audio: Node
|
||||
var _sound_instances: Array = []
|
||||
@@ -156,7 +159,25 @@ func set_anim_state(s: String) -> void:
|
||||
if msa == "":
|
||||
# 旧客户端 GetMotionKey 失败时 SetLoopMotion/InterceptMotion 直接返回:保留当前动作。
|
||||
return
|
||||
_bind(s, msa, s in HitReaction.LOOP_STATES, 1.0)
|
||||
_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 时参考端不走动作累计,
|
||||
# EntityStore 退回按 duration 插值,这里保持原速播放。
|
||||
func set_move_speed(moving_speed: int) -> void:
|
||||
_move_speed_ratio = 1.0 if moving_speed <= 0 or moving_speed > 1100 \
|
||||
else float(moving_speed) / 100.0
|
||||
if anim != null and _state in MOVE_STATES and not _hit.active:
|
||||
anim.set("time_scale", _hit.scale_for(_move_speed_ratio))
|
||||
|
||||
func _loop_speed(state: String) -> float:
|
||||
return _move_speed_ratio if state in MOVE_STATES else 1.0
|
||||
|
||||
## 走 / 跑动作的根运动速度(cm/s,movSpd 100):x = walk,y = run。0 = 取不到。
|
||||
## net_world 推给 EntityStore,远端移动按动作累计量推进(CActorInstance::AccumulationMovement)。
|
||||
func get_move_motion_speeds() -> Vector2:
|
||||
return Vector2(MsaMotion.move_speed(String(resolve_motion("walk").path)),
|
||||
MsaMotion.move_speed(String(resolve_motion("run").path)))
|
||||
|
||||
# CGraphicThingInstance::InsertDelay(fStiffenTime)
|
||||
func insert_delay(d: float) -> void:
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# MsaMotion —— 读 .msa 文本里的根运动:移动动作每秒位移(cm/s,movSpd 100 时)。
|
||||
#
|
||||
# MotionDuration 0.600000
|
||||
# Accumulation 0.00 -255.45 0.00
|
||||
#
|
||||
# 服务端 CHARACTER::GetMoveMotionSpeed = -Accumulation.y / MotionDuration;
|
||||
# 客户端 CActorInstance::AccumulationMovement 按同一累计量推进。这里取 xy 长度。
|
||||
extends RefCounted
|
||||
|
||||
static var _cache := {} # path -> float
|
||||
|
||||
static func move_speed(path: String) -> float:
|
||||
if path == "" or not path.to_lower().ends_with(".msa"):
|
||||
return 0.0
|
||||
if _cache.has(path):
|
||||
return float(_cache[path])
|
||||
var speed := 0.0
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if f:
|
||||
var duration := 0.0
|
||||
var accum := Vector2.ZERO
|
||||
while not f.eof_reached():
|
||||
var parts := _fields(f.get_line())
|
||||
if parts.size() < 2:
|
||||
continue
|
||||
match String(parts[0]):
|
||||
"MotionDuration":
|
||||
duration = String(parts[1]).to_float()
|
||||
"Accumulation":
|
||||
if parts.size() >= 3:
|
||||
accum = Vector2(String(parts[1]).to_float(), String(parts[2]).to_float())
|
||||
if duration > 0.0:
|
||||
speed = accum.length() / duration
|
||||
_cache[path] = speed
|
||||
return speed
|
||||
|
||||
static func _fields(line: String) -> PackedStringArray:
|
||||
return line.strip_edges().replace("\t", " ").split(" ", false)
|
||||
@@ -18,6 +18,8 @@ const FEMALE_RACES := [1, 3, 4, 6]
|
||||
const WEAPON_BONE := ["equip_right_hand", "equip_right", "equip_right", "equip_right"]
|
||||
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 MOVE_STATES := ["walk", "run"]
|
||||
const HitCollision = preload("res://hit_collision.gd")
|
||||
# CRaceMotionData::MOTION_MODE_* -> playersettingmodule 注册动作用的目录(与 general 同级)
|
||||
const MOTION_MODE_DIRS := {1: "general", 2: "onehand_sword", 3: "twohand_sword", 4: "dualhand_sword",
|
||||
@@ -43,6 +45,7 @@ var anim: Node # Metin2AnimPlayer
|
||||
var motion_dir := ""
|
||||
var action_dir := ""
|
||||
var _state := ""
|
||||
var _move_speed_ratio := 1.0
|
||||
var _assets_root := ""
|
||||
var _audio: Node
|
||||
var _sound_instances: Array = []
|
||||
@@ -135,7 +138,27 @@ func set_anim_state(s: String) -> void:
|
||||
if not FileAccess.file_exists(msa):
|
||||
msa = motion_dir.path_join(s + ".gr2")
|
||||
if FileAccess.file_exists(msa):
|
||||
_bind(s, msa, s in HitReaction.LOOP_STATES, 1.0)
|
||||
_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 时参考端不走动作累计,
|
||||
# EntityStore 退回按 duration 插值,这里保持原速播放。
|
||||
func set_move_speed(moving_speed: int) -> void:
|
||||
_move_speed_ratio = 1.0 if moving_speed <= 0 or moving_speed > 1100 \
|
||||
else float(moving_speed) / 100.0
|
||||
if anim != null and _state in MOVE_STATES and not _hit.active:
|
||||
anim.set("time_scale", _hit.scale_for(_move_speed_ratio))
|
||||
|
||||
func _loop_speed(state: String) -> float:
|
||||
return _move_speed_ratio if state in MOVE_STATES else 1.0
|
||||
|
||||
## 走 / 跑动作的根运动速度(cm/s,movSpd 100):x = walk,y = run。0 = 取不到。
|
||||
## net_world 推给 EntityStore,远端移动按动作累计量推进(CActorInstance::AccumulationMovement)。
|
||||
func get_move_motion_speeds() -> Vector2:
|
||||
if motion_dir == "":
|
||||
return Vector2.ZERO
|
||||
return Vector2(MsaMotion.move_speed(motion_dir.path_join("walk.msa")),
|
||||
MsaMotion.move_speed(motion_dir.path_join("run.msa")))
|
||||
|
||||
# Play one of the 40250 CRaceMotionData one-shot motions. The protocol sends
|
||||
# the numeric motion id; paired emotions use the other entity's race to select
|
||||
|
||||
Reference in New Issue
Block a user