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
+57 -33
View File
@@ -84,6 +84,61 @@ static void combat_data(const Node& root, Msa& out) {
}
}
static void motion_event(const Node& ev, MotionEvent& e) {
e.type = ev.inum("MotionEventType");
e.start_time = ev.num("StartingTime");
e.duration_time = ev.num("DuringTime");
e.effect_file = ev.str("EffectFileName");
e.sound_file = ev.str("SoundFileName");
e.attaching_bone = ev.str("AttachingBoneName");
e.attaching = ev.inum("AttachingEnable") != 0;
e.following = ev.inum("FollowingEnable") != 0;
e.independent = ev.inum("IndependentFlag") != 0;
e.fishing_effect = ev.inum("FishingEffectFlag") != 0;
vec3(ev.find("EffectPosition"), e.position);
// 40250 TMotionEventDataFly has its own resource and offset fields.
e.fly_file = ev.str("FlyFileName");
e.fly_attaching_bone = ev.str("AttachingBoneName");
e.fly_attaching = ev.inum("AttachingEnable") != 0;
vec3(ev.find("FlyPosition"), e.fly_position);
// 40250 TScreenWavingEventData defaults AffectingRange to zero and reads
// DuringTime/Power from the event itself.
e.power = ev.inum("Power");
e.affecting_range = ev.inum("AffectingRange");
// 40250 TMotionAttackingEventData defaults EnableHitProcess to TRUE.
e.enable_hit_process = ev.inum("EnableHitProcess", 1) != 0;
e.attack_type = ev.inum("AttackType", ev.inum("AttackingType"));
e.hitting_type = ev.inum("HittingType");
e.stiffen_time = ev.num("StiffenTime");
e.invisible_time = ev.num("InvisibleTime");
e.external_force = ev.num("ExternalForce");
e.hit_limit_count = ev.inum("HitLimitCount");
e.collision_type = ev.inum("CollisionType");
for (const Node& g : ev.groups) {
if (g.name.size() < 10 || g.name.compare(0, 10, "SphereData") != 0)
continue;
MotionEvent::CollisionSphere sphere;
sphere.radius = g.num("Radius");
vec3(g.find("Position"), sphere.position);
e.collision_spheres.push_back(sphere);
}
}
static void motion_events(const Node& root, Msa& out) {
const Node* med = root.group("MotionEventData");
if (!med) return;
int count = med->inum("MotionEventDataCount", int(med->groups.size()));
(void)count;
for (const Node& ev : med->groups) {
MotionEvent e;
motion_event(ev, e);
out.events.push_back(std::move(e));
}
}
bool parse_msa(const std::string& text, Msa& out, std::string* err) {
Node root;
if (!parse_textscript(text, root, err)) return false;
@@ -95,24 +150,7 @@ bool parse_msa(const std::string& text, Msa& out, std::string* err) {
loop_data(root, out);
combat_data(root, out);
if (const Node* med = root.group("MotionEventData")) {
int count = med->inum("MotionEventDataCount", int(med->groups.size()));
(void)count;
for (const Node& ev : med->groups) {
MotionEvent e;
e.type = ev.inum("MotionEventType");
e.start_time = ev.num("StartingTime");
e.effect_file = ev.str("EffectFileName");
e.sound_file = ev.str("SoundFileName");
e.attaching_bone = ev.str("AttachingBoneName");
e.attaching = ev.inum("AttachingEnable") != 0;
e.following = ev.inum("FollowingEnable") != 0;
e.independent = ev.inum("IndependentFlag") != 0;
e.fishing_effect = ev.inum("FishingEffectFlag") != 0;
vec3(ev.find("EffectPosition"), e.position);
out.events.push_back(std::move(e));
}
}
motion_events(root, out);
if (out.motion_gr2.empty()) { if (err) *err = "msa: 缺 MotionFileName"; return false; }
return true;
@@ -128,21 +166,7 @@ bool parse_msa_file(const std::string& path, Msa& out, std::string* err) {
vec3(root.find("Accumulation"), out.accumulation);
loop_data(root, out);
combat_data(root, out);
if (const Node* med = root.group("MotionEventData"))
for (const Node& ev : med->groups) {
MotionEvent e;
e.type = ev.inum("MotionEventType");
e.start_time = ev.num("StartingTime");
e.effect_file = ev.str("EffectFileName");
e.sound_file = ev.str("SoundFileName");
e.attaching_bone = ev.str("AttachingBoneName");
e.attaching = ev.inum("AttachingEnable") != 0;
e.following = ev.inum("FollowingEnable") != 0;
e.independent = ev.inum("IndependentFlag") != 0;
e.fishing_effect = ev.inum("FishingEffectFlag") != 0;
vec3(ev.find("EffectPosition"), e.position);
out.events.push_back(std::move(e));
}
motion_events(root, out);
if (out.motion_gr2.empty()) { if (err) *err = "msa: 缺 MotionFileName (" + path + ")"; return false; }
return true;
}
+33 -6
View File
@@ -6,16 +6,43 @@
namespace fmt {
struct MotionEvent {
int type = 0; // MotionEventType(10=effect, 4/5=sound, ...)
float start_time = 0; // StartingTime
std::string effect_file; // EffectFileName(type 10)
int type = 0; // MotionEventType(1=effect, 4=special attack, 6=fly, ...)
float start_time = 0; // StartingTime
float duration_time = 0; // DuringTime(SCREEN_WAVING / SPECIAL_ATTACKING)
std::string effect_file; // EffectFileName(type 1/10)
std::string sound_file; // SoundFileName
std::string attaching_bone;
bool attaching = false;
bool following = false;
bool independent = false;
bool fishing_effect = false;
float position[3] = {0, 0, 0};
bool independent = false;
bool fishing_effect = false;
float position[3] = {0, 0, 0};
// FLY 的字段不能复用 EFFECT 的 EffectFileName/EffectPosition:40250
// 在 MotionEventDataEvent.h 中使用另一套 FlyFileName/FlyPosition。
std::string fly_file;
std::string fly_attaching_bone;
bool fly_attaching = false;
float fly_position[3] = {0, 0, 0};
// SCREEN_WAVING 参数(GameEventManager::ProcessEventScreenWaving)。
int power = 0;
int affecting_range = 0;
// SPECIAL_ATTACKING 参数(TMotionAttackingEventData)。
bool enable_hit_process = true; // 40250 缺省值为 TRUE
int attack_type = 0;
int hitting_type = 0;
float stiffen_time = 0;
float invisible_time = 0;
float external_force = 0;
int hit_limit_count = 0;
int collision_type = 0;
struct CollisionSphere {
float radius = 0;
float position[3] = {0, 0, 0};
};
std::vector<CollisionSphere> collision_spheres;
};
struct Msa {
+1
View File
@@ -7,6 +7,7 @@ namespace fmt {
static bool fill(const Node& root, Msm& out, std::string* err) {
out = Msm{};
out.base_model_gr2 = root.str("BaseModelFileName");
out.motion_list_file = root.str("MotionListFileName");
if (const Node* hd = root.group("HairData")) {
out.hair_path = hd->str("PathName");
for (const Node& h : hd->groups) {
+1
View File
@@ -15,6 +15,7 @@ struct HairEntry {
struct Msm {
std::string base_model_gr2; // BaseModelFileName
std::string motion_list_file; // MotionListFileName
std::string hair_path; // HairData.PathName
std::vector<HairEntry> hairs;
// 武器 / 时装挂点 / 材质类型按需补(PoC 先到发型 + base)
+90
View File
@@ -31,6 +31,96 @@ Group MotionEventData
return 1;
}
}
{
// 40250 RaceMotionDataEvent has dedicated payloads for FLY,
// SCREEN_WAVING and SPECIAL_ATTACKING. Keep the no-payload event
// types in the same snapshot so they cannot be silently dropped.
const std::string events = R"(MotionFileName "events.gr2"
Group MotionEventData
{
Group Event00
{
MotionEventType 2
StartingTime 0.10
DuringTime 0.20
Power 300
AffectingRange 200
}
Group Event01
{
MotionEventType 4
StartingTime 0.30
DuringTime 0.40
EnableHitProcess 0
AttackType 1
HittingType 2
StiffenTime 0.05
InvisibleTime 0.10
ExternalForce 20
HitLimitCount 3
CollisionType 4
SphereDataCount 2
Group SphereData00
{
Radius 100
Position 1 2 3
}
Group SphereData01
{
Radius 200
Position 4 5 6
}
}
Group Event02
{
MotionEventType 6
StartingTime 0.50
AttachingEnable 1
AttachingBoneName "Bip01 R Hand"
FlyFileName "d:/ymir work/pc/warrior/effect/geompung.msf"
FlyPosition 7 8 9
}
Group Event03
{
MotionEventType 7
StartingTime 0.60
}
Group Event04
{
MotionEventType 8
StartingTime 0.70
}
Group Event05
{
MotionEventType 9
StartingTime 0.80
}
}
)";
fmt::Msa parsed;
std::string error;
if (!fmt::parse_msa(events, parsed, &error) || parsed.events.size() != 6) {
std::fprintf(stderr, "motion event payload parse failed: %s\n", error.c_str());
return 1;
}
const fmt::MotionEvent *wave = &parsed.events[0];
const fmt::MotionEvent *attack = &parsed.events[1];
const fmt::MotionEvent *fly = &parsed.events[2];
if (wave->type != 2 || std::fabs(wave->duration_time - 0.2f) > 1e-6f ||
wave->power != 300 || wave->affecting_range != 200 ||
attack->type != 4 || attack->enable_hit_process || attack->attack_type != 1 ||
attack->hitting_type != 2 || attack->collision_type != 4 ||
attack->collision_spheres.size() != 2 ||
std::fabs(attack->collision_spheres[1].radius - 200.0f) > 1e-6f ||
attack->collision_spheres[1].position[2] != 6.0f ||
fly->type != 6 || fly->fly_file.find("geompung.msf") == std::string::npos ||
!fly->fly_attaching || fly->fly_attaching_bone != "Bip01 R Hand" ||
fly->fly_position[0] != 7.0f || parsed.events[3].type != 7 ||
parsed.events[4].type != 8 || parsed.events[5].type != 9) {
std::fprintf(stderr, "40250 motion event payload fields were lost\n");
return 1;
}
}
const std::string text = R"(
ScriptType MotionData
MotionFileName "d:/ymir work/pc/test.gr2"
+2
View File
@@ -10,6 +10,7 @@ int main() {
ScriptType RaceDataScript
BaseModelFileName "d:/ymir work/pc2/warrior/warrior_novice.GR2"
MotionListFileName "motlist_range.txt"
Group HairData
{
@@ -56,6 +57,7 @@ Group HairData
check(m.base_model_gr2 == "d:/ymir work/pc2/warrior/warrior_novice.GR2" ||
m.base_model_gr2 == "d:/ymir work/pc2/warrior/warrior_novice.gr2",
"base model gr2");
check(m.motion_list_file == "motlist_range.txt", "motion list file");
check(m.hair_path == "d:/ymir Work/pc2/warrior/" || m.hair_path == "d:/ymir work/pc2/warrior/",
"hair PathName");
check(m.hairs.size() == 3, "3 hair entries");