fix: 装备属性面板避让逻辑 + 多项功能更新

- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
This commit is contained in:
shen
2026-09-21 16:38:59 -07:00
parent 1522a8a1a1
commit c93894313a
5498 changed files with 4558004 additions and 1442 deletions
+123 -11
View File
@@ -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;
}
}