- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
175 lines
6.4 KiB
C++
175 lines
6.4 KiB
C++
// M2 T1 —— .msa 解析(照 EterGrnLib/Util.cpp CGrannyMotion::LoadMotionData 的字段)。
|
|
#include "msa.h"
|
|
#include "textscript.h"
|
|
#include <cstdlib>
|
|
|
|
namespace fmt {
|
|
|
|
static void vec3(const std::vector<std::string>* l, float* out) {
|
|
if (!l) return;
|
|
for (int k = 0; k < 3 && k + 1 < (int)l->size(); ++k) out[k] = float(std::atof((*l)[k + 1].c_str()));
|
|
}
|
|
|
|
static void loop_data(const Node& root, Msa& out) {
|
|
if (const Node* loop = root.group("LoopData")) {
|
|
out.has_loop_data = true;
|
|
out.motion_loop_count = loop->inum("MotionLoopCount", -1);
|
|
out.loop_cancel_enable = loop->inum("LoopCancelEnable", 0) != 0;
|
|
out.loop_start_time = loop->num("LoopStartTime");
|
|
out.loop_end_time = loop->num("LoopEndTime");
|
|
}
|
|
}
|
|
|
|
// `List HitPosition { t lastXYZ XYZ t lastXYZ XYZ ... }` —— 7 个一组切成采样点。
|
|
static void hit_positions(const Node& win, std::vector<Msa::HitSample>& out) {
|
|
const Node* lst = win.group("HitPosition");
|
|
if (!lst || lst->lines.empty()) return;
|
|
const std::vector<std::string>& t = lst->lines[0];
|
|
for (size_t i = 0; i + 7 <= t.size(); i += 7) {
|
|
Msa::HitSample s;
|
|
s.time = float(std::atof(t[i].c_str()));
|
|
for (int k = 0; k < 3; ++k) s.last_pos[k] = float(std::atof(t[i + 1 + k].c_str()));
|
|
for (int k = 0; k < 3; ++k) s.pos[k] = float(std::atof(t[i + 4 + k].c_str()));
|
|
out.push_back(s);
|
|
}
|
|
}
|
|
|
|
// 一个命中窗(NRaceData::THitData)。老式写法里 win == AttackingData 组本身。
|
|
static Msa::HitWindow hit_window(const Node& win) {
|
|
Msa::HitWindow w;
|
|
w.start_time = win.num("AttackingStartTime");
|
|
w.end_time = win.num("AttackingEndTime");
|
|
w.bone_name = win.str("AttackingBone");
|
|
w.weapon_length = win.num("WeaponLength");
|
|
hit_positions(win, w.samples);
|
|
return w;
|
|
}
|
|
|
|
// Group ComboInputData / Group AttackingData —— 照 EterGrnLib CGrannyMotion::LoadMotionData
|
|
// 与 GameLib NRaceData::LoadMotionAttackData 的字段名读取;缺组时保持默认值。
|
|
static void combat_data(const Node& root, Msa& out) {
|
|
if (const Node* c = root.group("ComboInputData")) {
|
|
out.has_combo_input = true;
|
|
out.combo_pre_input_time = c->num("PreInputTime");
|
|
out.combo_direct_input_time = c->num("DirectInputTime");
|
|
out.combo_input_limit_time = c->num("InputLimitTime");
|
|
out.combo_link_time = c->num("LinkTime");
|
|
}
|
|
if (const Node* a = root.group("AttackingData")) {
|
|
out.has_attacking_data = true;
|
|
out.attacking_type = a->inum("AttackType", a->inum("AttackingType", 0));
|
|
out.motion_type = a->inum("MotionType", out.attacking_type);
|
|
out.hitting_type = a->inum("HittingType", 0);
|
|
out.stiffen_time = a->num("StiffenTime");
|
|
out.invisible_time = a->num("InvisibleTime");
|
|
out.external_force = a->num("ExternalForce");
|
|
out.hit_limit_count = a->inum("HitLimitCount", 0);
|
|
|
|
// 新式:HitDataCount N + Group HitData00..0N;老式:无 HitDataCount,组本身即窗。
|
|
bool has_children = false;
|
|
for (const Node& g : a->groups) {
|
|
const std::string& n = g.name;
|
|
if (n.size() >= 7 && (n.compare(0, 7, "HitData") == 0 || n.compare(0, 7, "hitdata") == 0)) {
|
|
out.hit_windows.push_back(hit_window(g));
|
|
has_children = true;
|
|
}
|
|
}
|
|
if (!has_children)
|
|
out.hit_windows.push_back(hit_window(*a));
|
|
|
|
if (!out.hit_windows.empty()) {
|
|
out.attack_start_time = out.hit_windows.front().start_time;
|
|
out.attack_end_time = out.hit_windows.front().end_time;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
out = Msa{};
|
|
|
|
out.motion_gr2 = root.str("MotionFileName");
|
|
out.duration = root.num("MotionDuration");
|
|
vec3(root.find("Accumulation"), out.accumulation);
|
|
loop_data(root, out);
|
|
combat_data(root, out);
|
|
|
|
motion_events(root, out);
|
|
|
|
if (out.motion_gr2.empty()) { if (err) *err = "msa: 缺 MotionFileName"; return false; }
|
|
return true;
|
|
}
|
|
|
|
bool parse_msa_file(const std::string& path, Msa& out, std::string* err) {
|
|
Node root;
|
|
if (!parse_textscript_file(path, root, err)) return false;
|
|
// 复用上面逻辑:重解析文本更简单,这里直接从 root 走一遍
|
|
out = Msa{};
|
|
out.motion_gr2 = root.str("MotionFileName");
|
|
out.duration = root.num("MotionDuration");
|
|
vec3(root.find("Accumulation"), out.accumulation);
|
|
loop_data(root, out);
|
|
combat_data(root, out);
|
|
motion_events(root, out);
|
|
if (out.motion_gr2.empty()) { if (err) *err = "msa: 缺 MotionFileName (" + path + ")"; return false; }
|
|
return true;
|
|
}
|
|
|
|
} // namespace fmt
|