fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
+478
-21
@@ -31,31 +31,48 @@
|
||||
# OnExplodingAtAnotherTarget(skill,vid) / OnExplodingAtTarget(skill)
|
||||
extends Node
|
||||
|
||||
const AssetRoot = preload("res://asset_root.gd")
|
||||
const FlyTargetAnchor = preload("res://fx/target_effect_anchor.gd")
|
||||
|
||||
# CFlyingManager::EIndexFlyType(REF/GameLib/FlyingObjectManager.h)
|
||||
const INDEX_FLY_TYPE_NORMAL := 0
|
||||
const INDEX_FLY_TYPE_FIRE_CRACKER := 1
|
||||
const INDEX_FLY_TYPE_AUTO_FIRE := 2
|
||||
|
||||
# 40250 playersettingmodule.py / char.h 索引飞行物类型 (effect.FLY_*)
|
||||
const FLY_EXP := 0
|
||||
const FLY_HP_SMALL := 1
|
||||
# 40250 PythonEffectModule.cpp / playersettingmodule.py / char.h 索引飞行物类型。
|
||||
# FLY_NONE 占用 0;经验球从 1 开始,不能把 0 当成 FLY_EXP。
|
||||
const FLY_NONE := 0
|
||||
const FLY_EXP := 1
|
||||
const FLY_HP_MEDIUM := 2
|
||||
const FLY_HP_BIG := 3
|
||||
const FLY_SP_SMALL := 4
|
||||
const FLY_SP_MEDIUM := 5
|
||||
const FLY_SP_BIG := 6
|
||||
const FLY_FIREWORK1 := 7
|
||||
const FLY_FIREWORK2 := 8
|
||||
const FLY_FIREWORK3 := 9
|
||||
const FLY_FIREWORK4 := 10
|
||||
const FLY_FIREWORK5 := 11
|
||||
const FLY_FIREWORK6 := 12
|
||||
const FLY_FIREWORK_XMAS := 13
|
||||
const FLY_CHAIN_LIGHTNING := 14
|
||||
const FLY_HP_SMALL := 15
|
||||
const FLY_SKILL_MUYEONG := 16
|
||||
|
||||
# CFlyingData —— .fly 脚本字段。POC 无 .fly 加载器,默认值逐项对齐 CFlyingData::__Initialize()
|
||||
# (长度单位:原版是 pixel==cm,这里换算成米后传进来,故 init_vel / range / bomb_range 都按米)。
|
||||
# CFlyingData —— .fly 脚本字段。原版文件使用 pixel==cm;加载器在边界处
|
||||
# 统一换算为 Godot 米制,因此 init_vel / range / bomb_range / acceleration 等
|
||||
# 长度量都按 0.01 转换,角度和时间保持原单位。
|
||||
class FlyData extends RefCounted:
|
||||
var init_vel := 2.0 # m_fInitVel=200(cm/s)→ 2 m/s(占位;net_world 用弓箭值覆盖)
|
||||
var resource_path := ""
|
||||
var init_vel := 2.0 # m_fInitVel=200(cm/s)→ 2 m/s
|
||||
var cone_angle := 0.0 # m_fConeAngle
|
||||
var roll_angle := 0.0 # m_fRollAngle
|
||||
var gravity := 0.0 # m_fGravity(原版加到速度 .z(=up),本实现映射到 -Y = 向下)
|
||||
var accel := Vector3.ZERO # m_v3Accel
|
||||
var angular_velocity := Vector3.ZERO # m_v3AngVel,度/秒
|
||||
var flat_range := 5.0 # m_fRange=500(cm)→ 5 m
|
||||
var bomb_range := 0.1 # m_fBombRange=10(cm)→ 0.1 m
|
||||
var collision_sphere_radius := 0.0 # m_fCollisionSphereRadius,米
|
||||
var pierce_count := 0 # m_iPierceCount
|
||||
var spreading := false # m_bSpreading
|
||||
var maintain_parallel := false # m_bMaintainParallel(对象目标点 y 抬 0.5 m ≈ 原版 +50cm)
|
||||
@@ -63,6 +80,147 @@ class FlyData extends RefCounted:
|
||||
var homing_start_time := 0.0 # m_fHomingStartTime
|
||||
var homing_max_angle := 0.0 # m_fHomingMaxAngle(度 / 每次修正)
|
||||
var hit_on_background := false # m_bHitOnBackground
|
||||
var hit_on_another_monster := false # m_bHitOnAnotherMonster
|
||||
var bomb_effect := "" # m_strBombEffectName
|
||||
var attach_data: Array[Dictionary] = [] # CFlyingData::m_AttachDataVector
|
||||
|
||||
static func load_msf(path: String, assets_root: String = ""):
|
||||
var resolved := _resolve_msf_path(path, assets_root)
|
||||
if resolved == "":
|
||||
return null
|
||||
var file := FileAccess.open(resolved, FileAccess.READ)
|
||||
if file == null:
|
||||
return null
|
||||
var data := FlyData.new()
|
||||
data.resource_path = resolved
|
||||
var saw_initial := false
|
||||
var saw_range := false
|
||||
var in_attach := false
|
||||
var attach: Dictionary = {}
|
||||
while not file.eof_reached():
|
||||
var line := file.get_line().strip_edges()
|
||||
if line == "" or line.begins_with("#") or line.begins_with("//"):
|
||||
continue
|
||||
if in_attach and line == "}":
|
||||
data.attach_data.append(attach)
|
||||
in_attach = false
|
||||
attach = {}
|
||||
continue
|
||||
if line.to_lower().begins_with("group attachdata"):
|
||||
in_attach = true
|
||||
attach = {}
|
||||
continue
|
||||
var pair := _split_assignment(line)
|
||||
if pair.is_empty():
|
||||
continue
|
||||
var key: String = String(pair[0]).to_lower()
|
||||
var value: String = String(pair[1]).strip_edges()
|
||||
if in_attach:
|
||||
_match_attach_value(attach, key, value, resolved, assets_root)
|
||||
continue
|
||||
if line == "{" or line == "}":
|
||||
continue
|
||||
match key:
|
||||
"spreadingflag": data.spreading = _as_int(value) != 0
|
||||
"maintainparallelflag": data.maintain_parallel = _as_int(value) != 0
|
||||
"initialvelocity":
|
||||
data.init_vel = _as_float(value) * 0.01
|
||||
saw_initial = true
|
||||
"coneangle": data.cone_angle = _as_float(value)
|
||||
"rollangle": data.roll_angle = _as_float(value)
|
||||
"angularvelocity": data.angular_velocity = _as_vec3(value)
|
||||
"gravity": data.gravity = _as_float(value) * 0.01
|
||||
"hitonbackground": data.hit_on_background = _as_int(value) != 0
|
||||
"hitonanothermonster": data.hit_on_another_monster = _as_int(value) != 0
|
||||
"piercecount": data.pierce_count = _as_int(value)
|
||||
"collisionsphereradius": data.collision_sphere_radius = _as_float(value) * 0.01
|
||||
"bombrange": data.bomb_range = _as_float(value) * 0.01
|
||||
"bombeffect": data.bomb_effect = _resolve_related_path(resolved, _unquote(value), assets_root)
|
||||
"homingflag": data.is_homing = _as_int(value) != 0
|
||||
"homingstarttime": data.homing_start_time = _as_float(value)
|
||||
"homingmaxangle": data.homing_max_angle = _as_float(value)
|
||||
"range":
|
||||
data.flat_range = _as_float(value) * 0.01
|
||||
saw_range = true
|
||||
"acceleration": data.accel = _as_vec3(value) * 0.01
|
||||
if not saw_initial or not saw_range:
|
||||
return null
|
||||
return data
|
||||
|
||||
static func _split_assignment(line: String) -> Array:
|
||||
var normalized := line.replace("\t", " ").strip_edges()
|
||||
var sep := normalized.find(" ")
|
||||
if sep < 0:
|
||||
return []
|
||||
return [normalized.substr(0, sep), normalized.substr(sep + 1).strip_edges()]
|
||||
|
||||
static func _unquote(value: String) -> String:
|
||||
var out := value.strip_edges()
|
||||
if out.length() >= 2 and out.begins_with("\"") and out.ends_with("\""):
|
||||
return out.substr(1, out.length() - 2)
|
||||
return out
|
||||
|
||||
static func _as_float(value: String) -> float:
|
||||
return value.strip_edges().to_float()
|
||||
|
||||
static func _as_int(value: String) -> int:
|
||||
return value.strip_edges().to_int()
|
||||
|
||||
static func _as_vec3(value: String) -> Vector3:
|
||||
var parts := value.replace("\t", " ").split(" ", false)
|
||||
if parts.size() < 3:
|
||||
return Vector3.ZERO
|
||||
return Vector3(parts[0].to_float(), parts[1].to_float(), parts[2].to_float())
|
||||
|
||||
static func _match_attach_value(attach: Dictionary, key: String, value: String,
|
||||
resolved: String, assets_root: String) -> void:
|
||||
match key:
|
||||
"type": attach["type"] = _as_int(value)
|
||||
"flytype": attach["fly_type"] = _as_int(value)
|
||||
"attachfile": attach["file"] = _resolve_related_path(resolved, _unquote(value), assets_root)
|
||||
"tailflag": attach["has_tail"] = _as_int(value) != 0
|
||||
"tailcolor": attach["tail_color"] = _unquote(value).trim_suffix("d").to_int()
|
||||
# CFlyingData keeps TailLength as seconds; TailSize is a world
|
||||
# length stored in centimetres and is the only one converted here.
|
||||
"taillength": attach["tail_length"] = _as_float(value)
|
||||
"tailsize": attach["tail_size"] = _as_float(value) * 0.01
|
||||
"tailshaperect": attach["rect_shape"] = _as_int(value) != 0
|
||||
"roll": attach["roll"] = _as_float(value)
|
||||
"distance": attach["distance"] = _as_float(value) * 0.01
|
||||
"period": attach["period"] = _as_float(value)
|
||||
"amplitude": attach["amplitude"] = _as_float(value) * 0.01
|
||||
|
||||
static func _resolve_related_path(source_file: String, value: String, assets_root: String) -> String:
|
||||
if value == "":
|
||||
return ""
|
||||
var sibling := source_file.get_base_dir().path_join(value)
|
||||
if FileAccess.file_exists(sibling):
|
||||
return sibling
|
||||
var resolved := _resolve_msf_path(value, assets_root)
|
||||
return resolved if resolved != "" else value
|
||||
|
||||
static func _resolve_msf_path(path: String, assets_root: String) -> String:
|
||||
var root := assets_root
|
||||
if root == "":
|
||||
root = AssetRoot.path()
|
||||
var source := path.replace("\\", "/")
|
||||
if source.length() >= 2 and source[1] == ":":
|
||||
source = source.substr(2).lstrip("/")
|
||||
var candidates: Array[String] = [source]
|
||||
var lower := source.to_lower()
|
||||
if lower.begins_with("ymir work/"):
|
||||
candidates.append("Effect/" + source)
|
||||
candidates.append("PC/" + source)
|
||||
var marker := lower.find("ymir work/")
|
||||
if marker >= 0:
|
||||
var tail := source.substr(marker)
|
||||
candidates.append("Effect/" + tail)
|
||||
candidates.append("PC/" + tail)
|
||||
for rel in candidates:
|
||||
var candidate := root.path_join(String(rel))
|
||||
if FileAccess.file_exists(candidate):
|
||||
return candidate
|
||||
return ""
|
||||
|
||||
# 单颗飞行物 —— 对齐 CFlyingInstance。target 可为 Node3D(对象目标,跟随移动)、
|
||||
# Vector3(位置目标)、或 { "vid": int, "pos": Vector3 }。
|
||||
@@ -80,25 +238,45 @@ class FlyInstance extends RefCounted:
|
||||
var skill_index := 0
|
||||
var target_vid := 0
|
||||
var target_hitted := false
|
||||
var hitted_objects := {} # CFlyingInstance::m_HittedObjectSet
|
||||
var _target_obj: Node3D
|
||||
var _target_pos := Vector3.ZERO
|
||||
var _is_object := false
|
||||
var _elapsed := 0.0
|
||||
var _world: Object # 可空:有 sample_height 时做触地
|
||||
var _actor_provider := Callable() # func() -> Array[{vid,node,center,radius}]
|
||||
var _owner_node: Node3D # FCheckAnotherMonsterDuringFlying 排除射手
|
||||
var _owner_vid := 0
|
||||
var attachments: Array = [] # TAttachEffectInstanceVector
|
||||
var _attach_rotation := Quaternion.IDENTITY
|
||||
# handler.call(event: String, world_pos: Vector3, vid: int) —— 对齐 IFlyEventHandler
|
||||
var handler := Callable()
|
||||
|
||||
func _node_fly_target_position(node: Node3D) -> Vector3:
|
||||
if not is_instance_valid(node):
|
||||
return Vector3.ZERO
|
||||
if node.is_inside_tree():
|
||||
return FlyTargetAnchor.fly_target_position(node)
|
||||
return node.position
|
||||
|
||||
func target_position() -> Vector3:
|
||||
if _is_object and is_instance_valid(_target_obj):
|
||||
if _target_obj.is_inside_tree():
|
||||
_target_pos = _target_obj.global_position
|
||||
else:
|
||||
_target_pos = _target_obj.position
|
||||
_target_pos = _node_fly_target_position(_target_obj)
|
||||
var tp := _target_pos
|
||||
if data and data.maintain_parallel:
|
||||
tp.y += 0.5
|
||||
return tp
|
||||
|
||||
func notify_target_despawn(node: Node3D) -> void:
|
||||
if not _is_object or _target_obj != node:
|
||||
return
|
||||
# CFlyTarget::NotifyTargetClear changes TYPE_OBJECT to TYPE_POSITION
|
||||
# while retaining the last cached target position. Read it once before
|
||||
# dropping the node reference so delayed fade-out cannot move the target.
|
||||
_target_pos = target_position()
|
||||
_target_obj = null
|
||||
_is_object = false
|
||||
|
||||
# CFlyingInstance::Create → __SetDataPointer + __SetTargetDirection。
|
||||
func create(d: FlyData, start_world: Vector3, target, attack: bool) -> void:
|
||||
data = d
|
||||
@@ -108,12 +286,13 @@ class FlyInstance extends RefCounted:
|
||||
alive = true
|
||||
_elapsed = 0.0
|
||||
target_hitted = false
|
||||
hitted_objects.clear()
|
||||
pierce = d.pierce_count
|
||||
remain_range = d.flat_range
|
||||
if target is Node3D:
|
||||
_is_object = true
|
||||
_target_obj = target
|
||||
_target_pos = (target as Node3D).global_position if (target as Node3D).is_inside_tree() else (target as Node3D).position
|
||||
_target_pos = _node_fly_target_position(target as Node3D)
|
||||
elif target is Vector3:
|
||||
_is_object = false
|
||||
_target_pos = target
|
||||
@@ -126,7 +305,7 @@ class FlyInstance extends RefCounted:
|
||||
if tn is Node3D:
|
||||
_is_object = true
|
||||
_target_obj = tn
|
||||
_target_pos = (tn as Node3D).global_position
|
||||
_target_pos = _node_fly_target_position(tn as Node3D)
|
||||
# __SetTargetDirection:方向 = normalize(target - pos),局部速度 (0,-init_vel,0)
|
||||
# 旋到该方向即 dir*init_vel;加速度同样旋到该方向。
|
||||
var dir := (target_position() - pos)
|
||||
@@ -139,6 +318,142 @@ class FlyInstance extends RefCounted:
|
||||
# 局部帧 -Y → dir 的最小弧旋转(对齐 __SetTargetNormalizedDirection)。
|
||||
accel = Quaternion(Vector3(0, -1, 0), dir).normalized() * accel
|
||||
|
||||
func build_attachments(effect_registry, visual_parent: Node3D) -> void:
|
||||
attachments.clear()
|
||||
_attach_rotation = Quaternion.IDENTITY
|
||||
if data == null:
|
||||
return
|
||||
for attach_variant in data.attach_data:
|
||||
if not attach_variant is Dictionary or int(attach_variant.get("type", 0)) != 1:
|
||||
continue # CFlyingData::FLY_ATTACH_OBJECT is also not implemented in 40250.
|
||||
var attach: Dictionary = attach_variant
|
||||
var effect: Node3D = null
|
||||
var effect_file := String(attach.get("file", ""))
|
||||
if effect_registry != null and effect_file != "" \
|
||||
and effect_registry.has_method("spawn") and visual_parent != null:
|
||||
effect = effect_registry.spawn(effect_file, visual_parent, false)
|
||||
var tail: MeshInstance3D = null
|
||||
if bool(attach.get("has_tail", false)) and visual_parent != null:
|
||||
tail = _create_tail(visual_parent, attach)
|
||||
attachments.append({"data": attach, "effect": effect, "tail": tail, "history": []})
|
||||
_update_attachments(0.0)
|
||||
|
||||
func clear_attachments() -> void:
|
||||
for item in attachments:
|
||||
for key in ["effect", "tail"]:
|
||||
var node: Variant = item.get(key, null)
|
||||
if is_instance_valid(node):
|
||||
node.free()
|
||||
attachments.clear()
|
||||
|
||||
func _create_tail(visual_parent: Node3D, attach: Dictionary) -> MeshInstance3D:
|
||||
var tail := MeshInstance3D.new()
|
||||
tail.name = "FlyTrace"
|
||||
var mesh := ImmediateMesh.new()
|
||||
tail.mesh = mesh
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
material.vertex_color_use_as_albedo = true
|
||||
material.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
tail.material_override = material
|
||||
visual_parent.add_child(tail)
|
||||
return tail
|
||||
|
||||
func _fly_orientation() -> Quaternion:
|
||||
var direction := vel.normalized()
|
||||
if direction.length_squared() < 0.000001:
|
||||
direction = Vector3(0.0, 0.0, -1.0)
|
||||
return Quaternion(Vector3(0.0, -1.0, 0.0), direction).normalized()
|
||||
|
||||
func _orientation(include_attach: bool = true) -> Quaternion:
|
||||
var base := _fly_orientation()
|
||||
return (_attach_rotation * base).normalized() if include_attach else base
|
||||
|
||||
func _attachment_position(attach: Dictionary) -> Vector3:
|
||||
var fly_type := int(attach.get("fly_type", 1))
|
||||
var roll := deg_to_rad(float(attach.get("roll", 0.0)))
|
||||
var distance := float(attach.get("distance", 0.0))
|
||||
var amplitude := float(attach.get("amplitude", 0.0))
|
||||
var period := float(attach.get("period", 1.0))
|
||||
var offset_amount := distance
|
||||
match fly_type:
|
||||
3: # FLY_ATTACH_TYPE_SINE
|
||||
var angle := _elapsed * TAU / maxf(period, 0.0001)
|
||||
offset_amount = amplitude * sin(angle)
|
||||
4: # FLY_ATTACH_TYPE_EXP
|
||||
var angle := _elapsed / maxf(period, 0.0001)
|
||||
offset_amount = amplitude * exp(-angle) * angle
|
||||
2: # FLY_ATTACH_TYPE_MULTI_LINE
|
||||
pass
|
||||
_: # FLY_ATTACH_TYPE_NONE / LINE
|
||||
return pos
|
||||
# EffectLib's source horizontal plane maps to Godot x/z; apply the
|
||||
# current fly orientation after the local roll offset.
|
||||
# 40250's source plane uses -cos(roll) on the local Z axis. Keeping
|
||||
# this sign is important for MULTI_LINE/SINE/EXP trace placement.
|
||||
var local := Vector3(-sin(roll) * offset_amount, 0.0, -cos(roll) * offset_amount)
|
||||
return pos + (_orientation() * local)
|
||||
|
||||
func _tail_color(value: int) -> Color:
|
||||
return Color8((value >> 16) & 0xff, (value >> 8) & 0xff,
|
||||
value & 0xff, (value >> 24) & 0xff)
|
||||
|
||||
func _update_tail(tail: MeshInstance3D, history: Array, attach: Dictionary) -> void:
|
||||
if tail == null or not is_instance_valid(tail) or history.size() < 2:
|
||||
return
|
||||
var mesh := tail.mesh as ImmediateMesh
|
||||
if mesh == null:
|
||||
return
|
||||
mesh.clear_surfaces()
|
||||
var material := tail.material_override as Material
|
||||
mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP, material)
|
||||
var length := maxf(float(attach.get("tail_length", 0.0)), 0.0001)
|
||||
var width := maxf(float(attach.get("tail_size", 0.0)), 0.001)
|
||||
var base_color := _tail_color(int(attach.get("tail_color", 0xffffffff)))
|
||||
for i in range(history.size()):
|
||||
var item: Dictionary = history[i]
|
||||
var previous: Vector3 = history[maxi(i - 1, 0)].position
|
||||
var next: Vector3 = history[mini(i + 1, history.size() - 1)].position
|
||||
var tangent := (next - previous).normalized()
|
||||
var side := tangent.cross(Vector3.UP)
|
||||
if side.length_squared() < 0.000001:
|
||||
side = tangent.cross(Vector3.RIGHT)
|
||||
side = side.normalized() * width
|
||||
var age := maxf(0.0, _elapsed - float(item.time))
|
||||
var alpha := clampf(1.0 - age / length, 0.0, 1.0)
|
||||
mesh.surface_set_color(Color(base_color, base_color.a * alpha))
|
||||
mesh.surface_add_vertex(item.position - side)
|
||||
mesh.surface_set_color(Color(base_color, base_color.a * alpha))
|
||||
mesh.surface_add_vertex(item.position + side)
|
||||
mesh.surface_end()
|
||||
|
||||
func _update_attachments(dt: float) -> void:
|
||||
if data == null:
|
||||
return
|
||||
var delta_q := Quaternion(Vector3.UP, deg_to_rad(data.angular_velocity.y) * dt)
|
||||
delta_q = delta_q * Quaternion(Vector3.RIGHT, deg_to_rad(data.angular_velocity.x) * dt)
|
||||
delta_q = delta_q * Quaternion(Vector3.BACK, deg_to_rad(data.angular_velocity.z) * dt)
|
||||
_attach_rotation = (_attach_rotation * delta_q).normalized()
|
||||
for item_variant in attachments:
|
||||
var item: Dictionary = item_variant
|
||||
var attach: Dictionary = item.data
|
||||
var at := _attachment_position(attach)
|
||||
var effect: Node3D = item.get("effect", null)
|
||||
if is_instance_valid(effect):
|
||||
# UpdateAttachInstance uses m_qRot for LINE and
|
||||
# m_qAttachRotation * m_qRot for the offset trace types.
|
||||
var attach_orientation := _orientation(
|
||||
int(attach.get("fly_type", 1)) != 1)
|
||||
effect.global_transform = Transform3D(Basis(attach_orientation), at)
|
||||
var history: Array = item.history
|
||||
if bool(attach.get("has_tail", false)):
|
||||
history.push_front({"time": _elapsed, "position": at})
|
||||
var tail_length := maxf(float(attach.get("tail_length", 0.0)), 0.0)
|
||||
while not history.is_empty() and _elapsed - float(history.back().time) > tail_length:
|
||||
history.pop_back()
|
||||
_update_tail(item.get("tail", null), history, attach)
|
||||
|
||||
# CFlyingInstance::AdjustDirectionForHoming —— 把速度朝目标方向转,单次夹在
|
||||
# homing_max_angle 度以内。
|
||||
func _adjust_homing() -> void:
|
||||
@@ -169,6 +484,7 @@ class FlyInstance extends RefCounted:
|
||||
var move_dist := movement.length()
|
||||
remain_range -= move_dist
|
||||
pos += movement
|
||||
_update_attachments(dt)
|
||||
|
||||
if remain_range < 0.0:
|
||||
_emit("out_of_range")
|
||||
@@ -196,6 +512,30 @@ class FlyInstance extends RefCounted:
|
||||
_explode()
|
||||
return false
|
||||
|
||||
# FlyingInstance.cpp checks another Actor after the primary target branch
|
||||
# and before background collision. The sphere is expanded by twice this
|
||||
# frame's movement distance, or the resource-configured radius, whichever
|
||||
# is larger. HittedObjectSet is pointer identity in 40250; the provider's
|
||||
# VID is only the observable callback value, so a new node with a reused
|
||||
# VID is not accidentally treated as the old object.
|
||||
if data.hit_on_another_monster and _actor_provider.is_valid():
|
||||
var collision_radius := maxf(move_dist * 2.0, data.collision_sphere_radius)
|
||||
var another := _find_another_actor(collision_radius)
|
||||
if not another.is_empty():
|
||||
var actor_node: Node3D = another.get("node", null)
|
||||
var object_key := actor_node.get_instance_id() if is_instance_valid(actor_node) else int(another.get("vid", 0))
|
||||
if not hitted_objects.has(object_key):
|
||||
hitted_objects[object_key] = true
|
||||
var another_vid := int(another.get("vid", 0))
|
||||
_emit("at_another", pos, another_vid)
|
||||
if pierce > 0:
|
||||
pierce -= 1
|
||||
_emit("bomb")
|
||||
else:
|
||||
_explode()
|
||||
return false
|
||||
return true
|
||||
|
||||
if data.hit_on_background:
|
||||
if _world and _world.has_method("sample_height"):
|
||||
var gh := float(_world.call("sample_height", pos.x, pos.z))
|
||||
@@ -224,9 +564,36 @@ class FlyInstance extends RefCounted:
|
||||
if bomb:
|
||||
_emit("bomb")
|
||||
|
||||
func _emit(ev: String) -> void:
|
||||
func _find_another_actor(projectile_radius: float) -> Dictionary:
|
||||
var candidates: Variant = _actor_provider.call()
|
||||
if not candidates is Array:
|
||||
return {}
|
||||
for candidate_variant in candidates:
|
||||
if not candidate_variant is Dictionary:
|
||||
continue
|
||||
var candidate: Dictionary = candidate_variant
|
||||
var node_variant: Variant = candidate.get("node", null)
|
||||
if not node_variant is Node3D or not is_instance_valid(node_variant):
|
||||
continue
|
||||
var node: Node3D = node_variant
|
||||
if node == _owner_node or int(candidate.get("vid", 0)) == _owner_vid:
|
||||
continue
|
||||
if bool(node.get_meta("dead", false)):
|
||||
continue
|
||||
var center_variant: Variant = candidate.get("center", null)
|
||||
var center := _node_fly_target_position(node)
|
||||
if center_variant is Vector3:
|
||||
center = center_variant
|
||||
var actor_radius := maxf(0.0, float(candidate.get("radius", 0.0)))
|
||||
if _seg_point_sq_dist(pos, last_pos, center) < (projectile_radius + actor_radius) * (projectile_radius + actor_radius):
|
||||
return candidate
|
||||
return {}
|
||||
|
||||
func _emit(ev: String, event_pos: Variant = null, event_vid: int = -1) -> void:
|
||||
var world_pos := pos if event_pos == null else (event_pos as Vector3)
|
||||
var vid := target_vid if event_vid < 0 else event_vid
|
||||
if handler.is_valid():
|
||||
handler.call(ev, pos, target_vid)
|
||||
handler.call(ev, world_pos, vid)
|
||||
|
||||
# REF/EterLib/GrpMath.h:80 square_distance_between_linesegment_and_point
|
||||
static func _seg_point_sq_dist(p1: Vector3, p2: Vector3, x: Vector3) -> float:
|
||||
@@ -242,33 +609,65 @@ class FlyInstance extends RefCounted:
|
||||
|
||||
var parent: Node3D # 弹道 / 爆点可视节点挂这下面(可空 = 纯逻辑,仅回调)
|
||||
var world: Object # Metin2World(可空,用于 hit_on_background 贴地)
|
||||
var _assets_root := "" # 与 NetWorld 的资源根保持同一解析上下文
|
||||
var manual_step := false # true 时 _process 不自动推进(headless 测试用 step() 手动驱动)
|
||||
var actor_provider := Callable() # func() -> Array[{vid,node,center,radius}]
|
||||
var effect_registry = null # EffectRegistry;可空时保留 headless fallback flash
|
||||
var _instances: Array = [] # Array[FlyInstance]
|
||||
var _visuals := {} # FlyInstance -> MeshInstance3D
|
||||
var _flash_nodes: Array = [] # 短生命周期爆点特效,Loading 时必须一并回收
|
||||
var _effect_nodes: Array = [] # EffectRegistry bomb/attach nodes owned by this manager
|
||||
var _id_counter := 1
|
||||
|
||||
signal shoot_damage(target_vid: int)
|
||||
signal exploded(world_pos: Vector3, cause: String)
|
||||
signal exp_absorbed(world_pos: Vector3)
|
||||
|
||||
func setup(mount: Node3D, w: Object = null) -> void:
|
||||
func setup(mount: Node3D, w: Object = null, assets_root_path: String = "") -> void:
|
||||
parent = mount
|
||||
world = w
|
||||
_assets_root = assets_root_path
|
||||
|
||||
func set_actor_provider(provider: Callable) -> void:
|
||||
actor_provider = provider
|
||||
|
||||
func set_effect_registry(registry) -> void:
|
||||
effect_registry = registry
|
||||
|
||||
func notify_target_despawn(node: Node3D) -> void:
|
||||
if not is_instance_valid(node):
|
||||
return
|
||||
for inst_variant in _instances:
|
||||
if inst_variant is FlyInstance:
|
||||
(inst_variant as FlyInstance).notify_target_despawn(node)
|
||||
|
||||
# CFlyingManager::CreateFlyingInstanceFlyTarget —— 建一颗飞行物并入表。
|
||||
func spawn(start_world: Vector3, target, can_attack: bool, data: FlyData = null,
|
||||
skill_index := 0) -> FlyInstance:
|
||||
skill_index := 0, owner_node: Node3D = null, owner_vid: int = 0) -> FlyInstance:
|
||||
var d := data if data != null else FlyData.new()
|
||||
var inst := FlyInstance.new()
|
||||
inst.id = _id_counter
|
||||
_id_counter += 1
|
||||
inst.skill_index = skill_index
|
||||
inst._owner_node = owner_node
|
||||
inst._owner_vid = owner_vid
|
||||
inst._actor_provider = actor_provider
|
||||
inst._world = world
|
||||
inst.handler = func(ev: String, wp: Vector3, vid: int) -> void:
|
||||
_on_instance_event(inst, ev, wp, vid)
|
||||
inst.create(d, start_world, target, can_attack)
|
||||
inst.build_attachments(effect_registry, parent)
|
||||
_instances.append(inst)
|
||||
if parent != null:
|
||||
var has_resource_visual := false
|
||||
for attachment_variant in inst.attachments:
|
||||
var attached_effect: Variant = attachment_variant.get("effect", null)
|
||||
if is_instance_valid(attached_effect):
|
||||
has_resource_visual = true
|
||||
break
|
||||
# CFlyingInstance renders its AttachFile/EffectManager instance. The
|
||||
# SphereMesh is only a headless/asset-missing fallback; do not draw both
|
||||
# representations when the 40250 resource visual was created successfully.
|
||||
if parent != null and not has_resource_visual:
|
||||
var m := MeshInstance3D.new()
|
||||
var sm := SphereMesh.new()
|
||||
var mat := StandardMaterial3D.new()
|
||||
@@ -312,8 +711,50 @@ func spawn(start_world: Vector3, target, can_attack: bool, data: FlyData = null,
|
||||
func active_count() -> int:
|
||||
return _instances.size()
|
||||
|
||||
func _release_instance(inst: FlyInstance) -> void:
|
||||
if not is_instance_valid(inst):
|
||||
return
|
||||
inst.alive = false
|
||||
# The event Callable closes over `inst` and is itself owned by the
|
||||
# RefCounted instance. Break that cycle before dropping the manager's
|
||||
# arrays, matching CFlyingInstance destruction at Loading or expiry.
|
||||
inst.handler = Callable()
|
||||
inst.clear_attachments()
|
||||
inst._target_obj = null
|
||||
inst._owner_node = null
|
||||
inst._actor_provider = Callable()
|
||||
inst._world = null
|
||||
inst.data = null
|
||||
|
||||
# ClientVS22 的 CFlyingManager::DeleteAllInstances / CEffectManager::DeleteAllInstances。
|
||||
# 地图切换发生在下一批 GC_MAIN_CHARACTER / GC_CHARACTER_ADD 到达之前,旧地图的
|
||||
# 弹道和爆点不能继续挂在新地图上,否则会产生跨图命中、残留特效和旧目标回调。
|
||||
func clear_for_map_change() -> void:
|
||||
for inst in _instances:
|
||||
if inst is FlyInstance:
|
||||
_release_instance(inst)
|
||||
var vis: MeshInstance3D = _visuals.get(inst, null)
|
||||
if is_instance_valid(vis):
|
||||
# ClientVS22's DeleteAllInstances is synchronous at the Loading
|
||||
# boundary. Free the old visual now so its mesh/material RIDs cannot
|
||||
# survive into the replacement world or a short-process Destroy.
|
||||
vis.free()
|
||||
_instances.clear()
|
||||
_visuals.clear()
|
||||
for flash in _flash_nodes:
|
||||
if is_instance_valid(flash):
|
||||
flash.free()
|
||||
_flash_nodes.clear()
|
||||
for effect in _effect_nodes:
|
||||
if is_instance_valid(effect):
|
||||
effect.free()
|
||||
_effect_nodes.clear()
|
||||
|
||||
# CFlyingManager::Update() —— 遍历实例,Update() 返 false 即删除。
|
||||
func step(dt: float) -> void:
|
||||
for cleanup_index in range(_effect_nodes.size() - 1, -1, -1):
|
||||
if not is_instance_valid(_effect_nodes[cleanup_index]):
|
||||
_effect_nodes.remove_at(cleanup_index)
|
||||
var i := 0
|
||||
while i < _instances.size():
|
||||
var inst: FlyInstance = _instances[i]
|
||||
@@ -325,6 +766,7 @@ func step(dt: float) -> void:
|
||||
else:
|
||||
vis.position = inst.pos
|
||||
if not keep:
|
||||
_release_instance(inst)
|
||||
if is_instance_valid(vis):
|
||||
vis.queue_free()
|
||||
_visuals.erase(inst)
|
||||
@@ -344,11 +786,21 @@ func _on_instance_event(inst: FlyInstance, ev: String, wp: Vector3, vid: int) ->
|
||||
exploded.emit(wp, ev)
|
||||
if ev == "at_target" and inst.skill_index == FLY_EXP:
|
||||
exp_absorbed.emit(wp)
|
||||
if parent != null and ev != "out_of_range":
|
||||
_spawn_flash(wp, inst.skill_index)
|
||||
# 40250 emits the hit callback first, then __Explode() emits exactly
|
||||
# one __Bomb() effect. Do not create a compatibility flash here: the
|
||||
# following "bomb" event owns both the real BombEffect and its fallback.
|
||||
"bomb":
|
||||
if parent != null:
|
||||
_spawn_flash(wp, inst.skill_index)
|
||||
var bomb_spawned := false
|
||||
if effect_registry != null and inst.data != null:
|
||||
var bomb_file := String(inst.data.bomb_effect)
|
||||
if bomb_file != "" and effect_registry.has_method("spawn_at"):
|
||||
var bomb_fx = effect_registry.spawn_at(bomb_file, parent, wp, true)
|
||||
bomb_spawned = bomb_fx != null
|
||||
if bomb_spawned:
|
||||
_effect_nodes.append(bomb_fx)
|
||||
if not bomb_spawned:
|
||||
_spawn_flash(wp, inst.skill_index)
|
||||
|
||||
func _spawn_flash(wp: Vector3, skill_index: int = -1) -> void:
|
||||
var f := MeshInstance3D.new()
|
||||
@@ -379,6 +831,7 @@ func _spawn_flash(wp: Vector3, skill_index: int = -1) -> void:
|
||||
|
||||
f.material_override = mat
|
||||
parent.add_child(f)
|
||||
_flash_nodes.append(f)
|
||||
if f.is_inside_tree():
|
||||
f.global_position = wp
|
||||
else:
|
||||
@@ -387,4 +840,8 @@ func _spawn_flash(wp: Vector3, skill_index: int = -1) -> void:
|
||||
tw.set_parallel(true)
|
||||
tw.tween_property(f, "scale", final_scale, 0.28)
|
||||
tw.tween_property(mat, "albedo_color:a", 0.0, 0.28)
|
||||
tw.chain().tween_callback(f.queue_free)
|
||||
tw.chain().tween_callback(func() -> void:
|
||||
_flash_nodes.erase(f)
|
||||
if is_instance_valid(f):
|
||||
f.queue_free()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user