fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
+123
-11
@@ -132,6 +132,8 @@ void Metin2AnimPlayer::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("get_loop"), &Metin2AnimPlayer::get_loop);
|
||||
ClassDB::bind_method(D_METHOD("set_time_scale", "s"), &Metin2AnimPlayer::set_time_scale);
|
||||
ClassDB::bind_method(D_METHOD("get_time_scale"), &Metin2AnimPlayer::get_time_scale);
|
||||
ClassDB::bind_method(D_METHOD("set_motion_loop_count", "count"), &Metin2AnimPlayer::set_motion_loop_count);
|
||||
ClassDB::bind_method(D_METHOD("get_motion_loop_count"), &Metin2AnimPlayer::get_motion_loop_count);
|
||||
ClassDB::bind_method(D_METHOD("set_blend_time", "s"), &Metin2AnimPlayer::set_blend_time);
|
||||
ClassDB::bind_method(D_METHOD("get_blend_time"), &Metin2AnimPlayer::get_blend_time);
|
||||
ClassDB::bind_method(D_METHOD("set_time", "t"), &Metin2AnimPlayer::set_time);
|
||||
@@ -167,6 +169,8 @@ void Metin2AnimPlayer::_bind_methods() {
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "get_loop");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "time_scale", PROPERTY_HINT_RANGE, "0,4,0.01"),
|
||||
"set_time_scale", "get_time_scale");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "motion_loop_count"),
|
||||
"set_motion_loop_count", "get_motion_loop_count");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "blend_time", PROPERTY_HINT_RANGE, "0,1,0.01"),
|
||||
"set_blend_time", "get_blend_time");
|
||||
}
|
||||
@@ -180,6 +184,10 @@ void Metin2AnimPlayer::set_anim_path(const String &p) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// SetMotionLoopCount belongs to the newly bound motion. Do not leak a
|
||||
// previous skill's override into a normal walk/attack/action clip.
|
||||
motion_loop_override = false;
|
||||
motion_loop_count = 0;
|
||||
// Start a crossfade from the clip that is currently playing.
|
||||
if (is_inside_tree() && anim_file && blend_time > 0.0) {
|
||||
prev_anim_file = std::move(anim_file); // anim_file now empty; reload() refills it
|
||||
@@ -204,6 +212,15 @@ void Metin2AnimPlayer::set_anim_path(const String &p) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
void Metin2AnimPlayer::set_motion_loop_count(int count) {
|
||||
if (count > 0) {
|
||||
motion_loop_override = true;
|
||||
motion_loop_count = count;
|
||||
} else {
|
||||
motion_loop_override = false;
|
||||
motion_loop_count = msa_metadata.has_loop_data ? msa_metadata.motion_loop_count : 0;
|
||||
}
|
||||
}
|
||||
void Metin2AnimPlayer::set_model_path(const NodePath &p) {
|
||||
model_path = p;
|
||||
}
|
||||
@@ -269,6 +286,7 @@ static Dictionary motion_event_dict(const fmt::MotionEvent &ev) {
|
||||
Dictionary d;
|
||||
d["type"] = ev.type;
|
||||
d["start_time"] = ev.start_time;
|
||||
d["duration_time"] = ev.duration_time;
|
||||
d["effect"] = String(ev.effect_file.c_str());
|
||||
d["sound"] = String(ev.sound_file.c_str());
|
||||
d["pos"] = Vector3(ev.position[0], ev.position[1], ev.position[2]);
|
||||
@@ -277,6 +295,28 @@ static Dictionary motion_event_dict(const fmt::MotionEvent &ev) {
|
||||
d["following"] = ev.following;
|
||||
d["independent"] = ev.independent;
|
||||
d["fishing_effect"] = ev.fishing_effect;
|
||||
d["fly_file"] = String(ev.fly_file.c_str());
|
||||
d["fly_bone"] = String(ev.fly_attaching_bone.c_str());
|
||||
d["fly_attaching"] = ev.fly_attaching;
|
||||
d["fly_pos"] = Vector3(ev.fly_position[0], ev.fly_position[1], ev.fly_position[2]);
|
||||
d["power"] = ev.power;
|
||||
d["affecting_range"] = ev.affecting_range;
|
||||
d["enable_hit_process"] = ev.enable_hit_process;
|
||||
d["attack_type"] = ev.attack_type;
|
||||
d["hitting_type"] = ev.hitting_type;
|
||||
d["stiffen_time"] = ev.stiffen_time;
|
||||
d["invisible_time"] = ev.invisible_time;
|
||||
d["external_force"] = ev.external_force;
|
||||
d["hit_limit_count"] = ev.hit_limit_count;
|
||||
d["collision_type"] = ev.collision_type;
|
||||
Array spheres;
|
||||
for (const fmt::MotionEvent::CollisionSphere &sphere : ev.collision_spheres) {
|
||||
Dictionary sd;
|
||||
sd["radius"] = sphere.radius;
|
||||
sd["pos"] = Vector3(sphere.position[0], sphere.position[1], sphere.position[2]);
|
||||
spheres.push_back(sd);
|
||||
}
|
||||
d["collision_spheres"] = spheres;
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -438,6 +478,8 @@ void Metin2AnimPlayer::reload() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!motion_loop_override)
|
||||
motion_loop_count = msa_metadata.has_loop_data ? msa_metadata.motion_loop_count : 0;
|
||||
|
||||
gr2::LoadError err;
|
||||
auto f = cached_clip(gr2_spec, &err);
|
||||
@@ -453,7 +495,12 @@ void Metin2AnimPlayer::reload() {
|
||||
}
|
||||
anim_file = std::move(f);
|
||||
const gr2::Animation &an = anim_file->file_info().animations[0];
|
||||
duration = an.duration;
|
||||
// 40250 CRaceMotionData::GetMotionDuration() is sourced from the MSA
|
||||
// MotionDuration field. GR2 contains sampled tracks, but its clip length
|
||||
// is not the motion-data authority used by actor timing, LoopData and
|
||||
// MotionEventData. Keep GR2 duration only for raw .gr2 paths or malformed
|
||||
// MSA files without a positive MotionDuration.
|
||||
duration = msa_metadata.duration > 0.0 ? msa_metadata.duration : an.duration;
|
||||
|
||||
// Diagnostic: how many anim tracks map to a model-skeleton bone by name?
|
||||
if (Metin2Model *model = diagnostics ? resolve_model() : nullptr) {
|
||||
@@ -668,6 +715,29 @@ void Metin2AnimPlayer::_process(double delta) {
|
||||
}
|
||||
}
|
||||
double next = time + delta * time_scale;
|
||||
// 40250 does not loop the whole clip. For a motion with LoopData it keeps
|
||||
// the lead-in and tail, and only rewinds LoopStartTime..LoopEndTime while
|
||||
// the actor's remaining loop count is > 1 (or -1 for infinite looping).
|
||||
// The legacy actor resets to LoopStartTime at the first frame after the
|
||||
// loop end, so the frame remainder is intentionally discarded here too.
|
||||
const bool has_segment_loop = !loop && msa_metadata.has_loop_data &&
|
||||
motion_loop_count != 0 &&
|
||||
(motion_loop_count == -1 || motion_loop_count > 1) &&
|
||||
msa_metadata.loop_end_time > msa_metadata.loop_start_time &&
|
||||
msa_metadata.loop_end_time <= duration;
|
||||
if (has_segment_loop && next > msa_metadata.loop_end_time) {
|
||||
dispatch_events(time, msa_metadata.loop_end_time);
|
||||
time = msa_metadata.loop_start_time;
|
||||
if (motion_loop_count > 0)
|
||||
--motion_loop_count;
|
||||
apply_pose(time);
|
||||
// ActorInstance resets dwcurFrame to LoopStartTime and processes
|
||||
// that frame immediately. Without an inclusive point dispatch,
|
||||
// events authored exactly at the loop boundary never fire.
|
||||
dispatch_events(time, time, true);
|
||||
prev_time = time;
|
||||
return;
|
||||
}
|
||||
if (!loop && duration > 0.0 && next >= duration) {
|
||||
next = duration;
|
||||
}
|
||||
@@ -683,29 +753,71 @@ void Metin2AnimPlayer::_process(double delta) {
|
||||
|
||||
// Emit `motion_event` for every .msa event whose start_time falls in (from, to],
|
||||
// handling loop wrap-around within one clip.
|
||||
void Metin2AnimPlayer::dispatch_events(double from, double to) {
|
||||
void Metin2AnimPlayer::dispatch_events(double from, double to, bool include_from) {
|
||||
if (events.empty() || duration <= 0.0) {
|
||||
return;
|
||||
}
|
||||
const double epsilon = 1e-9;
|
||||
if (to < from - epsilon) {
|
||||
return;
|
||||
}
|
||||
if (!loop) {
|
||||
double a = std::fmax(0.0, std::fmin(from, duration));
|
||||
double b = std::fmax(0.0, std::fmin(to, duration));
|
||||
for (const fmt::MotionEvent &ev : events) {
|
||||
if (a < ev.start_time && ev.start_time <= b) {
|
||||
const bool at_inclusive_start = include_from &&
|
||||
std::fabs(double(ev.start_time) - a) <= epsilon;
|
||||
// The first 40250 motion frame is frame 0, not frame 1. The
|
||||
// normal interval remains half-open on the left so a regular
|
||||
// frame cannot replay an event on every tick.
|
||||
const bool initial_zero = !include_from && from <= epsilon &&
|
||||
to > from + epsilon && std::fabs(double(ev.start_time)) <= epsilon;
|
||||
if (at_inclusive_start || initial_zero ||
|
||||
(a < ev.start_time && ev.start_time <= b)) {
|
||||
emit_motion_event(ev);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
double a = std::fmod(from, duration);
|
||||
if (a < 0) a += duration;
|
||||
double b = a + (to - from);
|
||||
for (const fmt::MotionEvent &ev : events) {
|
||||
double t = ev.start_time;
|
||||
bool hit = (a < t && t <= b) || (b > duration && t <= b - duration);
|
||||
if (hit) {
|
||||
emit_motion_event(ev);
|
||||
// Walk each clip segment independently. The old single-wrap expression
|
||||
// could emit at most one copy of an event when time_scale/delta crossed
|
||||
// two or more full durations; ActorInstance processes every frame and
|
||||
// therefore reaches every crossed MotionEventData entry.
|
||||
double cursor = from;
|
||||
double remaining = to - from;
|
||||
if (remaining <= epsilon) {
|
||||
if (!include_from) {
|
||||
return;
|
||||
}
|
||||
double point = std::fmod(cursor, duration);
|
||||
if (point < 0) point += duration;
|
||||
for (const fmt::MotionEvent &ev : events) {
|
||||
if (std::fabs(double(ev.start_time) - point) <= epsilon) {
|
||||
emit_motion_event(ev);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
while (remaining > epsilon) {
|
||||
double a = std::fmod(cursor, duration);
|
||||
if (a < 0) a += duration;
|
||||
double segment = std::fmin(remaining, duration - a);
|
||||
if (segment <= epsilon) {
|
||||
cursor += duration;
|
||||
continue;
|
||||
}
|
||||
double b = a + segment;
|
||||
const bool include_segment_start = a <= epsilon;
|
||||
for (const fmt::MotionEvent &ev : events) {
|
||||
const bool at_segment_start = include_segment_start &&
|
||||
std::fabs(double(ev.start_time) - a) <= epsilon;
|
||||
const bool crossed = a < ev.start_time && ev.start_time <= b;
|
||||
if (at_segment_start || crossed) {
|
||||
emit_motion_event(ev);
|
||||
}
|
||||
}
|
||||
cursor += segment;
|
||||
remaining -= segment;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user